分布式锁:Redis、Zookeeper

1.基于Redis实现分布式锁

Redis分布式锁原理如上图所示,当有多个Set命令发送到Redis时,Redis会串行处理,最终只有一个Set命令执行成功,从而只有一个线程加锁成功

2.SetNx命令加锁

利用Redis的setNx命令在Redis数据库中创建一个<Key,Value>记录,这条命令只有当Redis中没有这个Key的时候才执行成功,当已经有这个Key的时候会返回失败。

可以借助于redis中的命令setnx(key, value),key不存在就新增,存在就什么都不做。同时有多个客户端发送setnx命令,只有一个客户端可以成功,返回1(true);其他的客户端返回0(false),流程图如下图所示:

  • 多个客户端同时尝试获取锁(setnx)
  • 获取成功,执行业务逻辑,执行完成释放锁(del)
  • 其他客户端等待重试

利用如上的setNx命令便可以简单的实现加锁功能,当多个线程去执行这个加锁命令时,只有一个线程执行成功,然后执行业务逻辑,其他线程加锁失败返回或者重试

public void testLock() {// 1. 从redis中获取锁,setnxBoolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", "111");if (lock) {// 查询redis中的num值String value = this.redisTemplate.opsForValue().get("num");// 没有该值returnif (StringUtils.isBlank(value)){return ;}// 有值就转成成intint num = Integer.parseInt(value);// 把redis中的num值+1this.redisTemplate.opsForValue().set("num", String.valueOf(++num));// 2. 释放锁 delthis.redisTemplate.delete("lock");} else {// 3. 每隔1秒钟回调一次,再次尝试获取锁try {Thread.sleep(1000);testLock();} catch (InterruptedException e) {e.printStackTrace();}}
}

3.优化分布式锁_设置过期时间

设置过期有俩种方式可以选择:

  • 通过expire设置过期时间(缺乏原子性:如果在setnx和expire之间出现异常,锁也无法释放)
  • 在set时指定过期时间(推荐)

代码实现优化就是在设置锁的时候设置过期时间:

public void testLock() {// 1. 从redis中获取锁,setnxBoolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", "111",3, TimeUnit.MINUTES);if (lock) {//与之前相同代码略过...}
}

那么还会不会存在问题呢?
场景:如果业务逻辑的执行时间是7s。执行流程如下:

  • index1业务逻辑没执行完,3秒后锁被自动释放。
  • index2获取到锁,执行业务逻辑,3秒后锁被自动释放。
  • index3获取到锁,执行业务逻辑
  • index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只执行1s就被别人释放。
  • 最终等于没锁的情况。

解决:setnx获取锁时,设置一个指定的唯一值(例如:uuid);释放前获取这个值,判断是否自己的锁。

4.错误删除锁问题

上面直接删除key来解锁方式会存在一个问题,考虑下面这种情况:

(1)线程1执行业务时间过长导致自己加的锁过期

(2)这时线程2进来加锁成功

(3)然后线程1业务逻辑执行完毕开始执行del key命令

(4)这时就会出现错误删除线程2加的锁

(5)错误删除线程2的锁后,线程3又可以加锁成功,导致有两个线程执行业务代码

5.优化分布式锁_防止误删除

public void testLock() {// 1. 从redis中获取锁,setnxString uuid = UUID.randomUUID().toString();Boolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", uuid,3, TimeUnit.MINUTES);if (lock) {//与之前相同代码略过...// 2. 释放锁 delif (StringUtils.equals(redisTemplate.opsForValue().get("lock"),uuid)){this.redisTemplate.delete("lock");}}
}

场景:

  • index1执行删除时,查询到的lock值确实和uuid相等
  • index1执行删除前,lock刚好过期时间已到,被redis自动释放
  • index2获取了lock
  • index1执行删除,此时会把index2的lock删除
  • 问题:缺乏原子性

上面的setNx命令实现了基本的加锁功能,但存在一个致命的问题是,当程序在执行业务代码崩溃时,无法再执行到下面的解锁指令,从而导致出现死锁问题

为了解决死锁问题,这里就需要引入过期时间的概念,过期时间是给当前这个key设置一定的存活时间,当存活时间到期后,Redis就会自动删除这个过期的Key,从而使得程序在崩溃时也能到期自动释放锁

如上图所示,使用Redis的expire命令来为锁设置过期时间,从而实现到期自动解锁的功能,但这里仍然还存在一个问题就是加锁与给锁设置过期时间这两个操作命令并不是原子命令

考虑下面这种情况:

当程序在加锁完成后,在设置过期时间前崩溃,这时仍然会造成锁无法自动释放,从而产生死锁现象。

6.优化分布式锁_LUA脚本保证删除的原子性

  • 首先我们先简单介绍一下lua脚本的基本知识(lua脚本是c语言) 定义变量:
    全局变量:a = 11局部变量:local b = 22redis不允许lua脚本创建全局变量,只能声明局部变量 流程控制:if(exp) then业务逻辑elseif(exp) then业务逻辑else业务逻辑end redis中执行lua脚本: eval script numkeys keys[] args[] : eval指令的输出不是lua脚本的打印而是lua脚本的返回值 script:lua脚本字符串,定义动态变量:KEYS[1] ARGV[1] numkeys:key数组的元素个数 keys:keys数组 args:argv数组 redis集群执行lua脚本可能会报错:如果所有keys不在同一个分片上,lua脚本就会报错:解决方案是: keys只传一个 可以使用CLUSTER KEYSLOT bb{xx}
  • 删除LUA脚本:
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
public void testLock() {// 1. 从redis中获取锁,setnxString uuid = UUID.randomUUID().toString();Boolean lock = this.redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);if (lock) {//与之前相同代码略过...// 2. 释放锁 delString script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";this.redisTemplate.execute(new DefaultRedisScript<>(script), Arrays.asList("lock"), uuid);} else {// 3. 每隔1秒钟回调一次,再次尝试获取锁try {Thread.sleep(1000);testLock();} catch (InterruptedException e) {e.printStackTrace();}}
}

7.优化分布式锁_可以重入

上述加锁命令使用了 SETNX ,一旦键存在就无法再设置成功,这就导致后续同一线程内继续加锁,将会加锁失败。当一个线程执行一段代码成功获取锁之后,继续执行时,又遇到加锁的子任务代码,可重入性就保证线程能继续执行,而不可重入就是需要等待锁释放之后,再次获取锁成功,才能继续往下执行。
可重入锁最大特性就是计数,计算加锁的次数。所以当可重入锁需要在分布式环境实现时,我们也就需要统计加锁次数。
我们基于Redis Hash 实现方案
Redis 提供了 Hash (哈希表)这种可以存储键值对数据结构。所以我们可以使用 Redis Hash 存储的锁的重入次数,然后利用 lua 脚本判断逻辑。

  • 加锁
if (redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1) 
thenredis.call('hincrby', KEYS[1], ARGV[1], 1);redis.call('expire', KEYS[1], ARGV[2]);return 1;
elsereturn 0;
end

假设值为:KEYS:[lock], ARGV[uuid, expire]

如果锁不存在或者这是自己的锁,就通过hincrby(不存在新增,存在就加1)获取锁或者锁次数加1。 代码实例如下:

private Boolean tryLock(String lockName, String uuid, Long expire){String script = "if (redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1) " +"then" +"    redis.call('hincrby', KEYS[1], ARGV[1], 1);" +"    redis.call('expire', KEYS[1], ARGV[2]);" +"    return 1;" +"else" +"   return 0;" +"end";if (!this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, expire.toString())){try {// 没有获取到锁,重试Thread.sleep(200);tryLock(lockName, uuid, expire);} catch (InterruptedException e) {e.printStackTrace();}}// 获取到锁,返回truereturn true;
}
  • 解锁
lua复制代码-- 判断 hash set 可重入 key 的值是否等于 0
-- 如果为 nil 代表 自己的锁已不存在,在尝试解其他线程的锁,解锁失败
-- 如果为 0 代表 可重入次数被减 1
-- 如果为 1 代表 该可重入 key 解锁成功
if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) thenreturn nil;
end;
-- 小于等于 0 代表可以解锁
if (redis.call('hincrby', KEYS[1], ARGV[1], -1) > 0) thenreturn 0;
elseredis.call('del', KEYS[1]);return 1;
end;

这里之所以没有跟加锁一样使用 Boolean ,这是因为解锁 lua 脚本中,三个返回值含义如下:

  • 1 代表解锁成功,锁被释放
  • 0 代表可重入次数被减 1
  • null 代表其他线程尝试解锁,解锁失败
    如果返回值使用 Boolean,Spring-data-redis 进行类型转换时将会把 null 转为 false,这就会影响我们逻辑判断,所以返回类型只好使用 Long。
private void unlock(String lockName, String uuid){String script = "if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then" +"    return nil;" +"end;" +"if (redis.call('hincrby', KEYS[1], ARGV[1], -1) > 0) then" +"    return 0;" +"else" +"    redis.call('del', KEYS[1]);" +"    return 1;" +"end;";// 这里之所以没有跟加锁一样使用 Boolean ,这是因为解锁 lua 脚本中,三个返回值含义如下:// 1 代表解锁成功,锁被释放// 0 代表可重入次数被减 1// null 代表其他线程尝试解锁,解锁失败Long result = this.redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Lists.newArrayList(lockName), uuid);// 如果未返回值,代表尝试解其他线程的锁if (result == null) {throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName: "+ lockName + " with request: "  + uuid);}
}
  • 使用
public void testLock() {// 加锁String uuid = UUID.randomUUID().toString();Boolean lock = this.tryLock("lock", uuid, 300l);if (lock) {// 读取redis中的num值String numString = this.redisTemplate.opsForValue().get("num");if (StringUtils.isBlank(numString)) {return;}// ++操作Integer num = Integer.parseInt(numString);num++;// 放入redisthis.redisTemplate.opsForValue().set("num", String.valueOf(num));// 测试可重入性this.testSubLock(uuid);// 释放锁this.unlock("lock", uuid);}
}
// 测试可重入性
private void testSubLock(String uuid){// 加锁Boolean lock = this.tryLock("lock", uuid, 300l);if (lock) {System.out.println("分布式可重入锁。。。");this.unlock("lock", uuid);}
}

8.优化分布式锁_自动续期

A线程超时时间设为10s(为了解决死锁问题),但代码执行时间可能需要30s,然后redis服务端10s后将锁删除。 此时,B线程恰好申请锁,redis服务端不存在该锁,可以申请,也执行了代码。那么问题来了, A、B线程都同时获取到锁并执行业务逻辑,这与分布式锁最基本的性质相违背:在任意一个时刻,只有一个客户端持有锁(即独享排他)。

对于上述的这种情况,原因是由于设置的过期时间太短或者业务执行时间太长导致锁过期,但是为了避免死锁问题又必须设置过期时间,那这就需要引入自动续期的功能,即在加锁成功时,开启一个定时任务,自动刷新Redis加锁key的超时时间,从而避免上诉情况发生,如下图所示:

锁延期方法:开启子线程执行延期。在加锁成功后可以启动一个定时任务来对锁进行自动续期,定时任务的执行逻辑是:

(1)判断Redis中的锁是否是自己的

(2)如果存在的话就使用expire命令重新设置过期时间

这里由于需要两个Redis的命令,所以也需要使用lua脚本来实现原子操作,代码如下所示:

/*** 锁延期* 线程等待超时时间的2/3时间后,执行锁延时代码,直到业务逻辑执行完毕,因此在此过程中,其他线程无法获取到锁,保证了线程安全性* @param lockName* @param expire 单位:毫秒*/
private void renewTime(String lockName, String uuid, Long expire){String script = "if(redis.call('hexists', KEYS[1], ARGV[1]) == 1) then redis.call('expire', KEYS[1], ARGV[2]); return 1; else return 0; end";new Thread(() -> {while (this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Lists.newArrayList(lockName), uuid, expire.toString())){try {// 到达过期时间的2/3时间,自动续期Thread.sleep(expire / 3);} catch (InterruptedException e) {e.printStackTrace();}}}).start();
}

获取锁成功后,调用延期方法给锁 定时延期:

private Boolean tryLock(String lockName, String uuid, Long expire){String script = "if (redis.call('exists', KEYS[1]) == 0 or redis.call('hexists', KEYS[1], ARGV[1]) == 1) " +"then" +"    redis.call('hincrby', KEYS[1], ARGV[1], 1);" +"    redis.call('expire', KEYS[1], ARGV[2]);" +"    return 1;" +"else" +"   return 0;" +"end";if (!this.redisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class), Arrays.asList(lockName), uuid, expire.toString())){try {// 没有获取到锁,重试Thread.sleep(200);tryLock(lockName, uuid, expire);} catch (InterruptedException e) {e.printStackTrace();}}// 锁续期this.renewTime(lockName, uuid, expire * 1000);// 获取到锁,返回truereturn true;
}

9.优化分布式锁_Redlock算法

redis集群状态下的问题:

  • 客户端A从master获取到锁
  • 在master将锁同步到slave之前,master宕掉了。
  • slave节点被晋级为master节点
  • 客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。
    安全失效 解决集群下锁失效,参照redis官方网站针对redlock文档:redis.io/topics/dist…

10.本地锁会出现的问题

我们知道java中有synchronized、lock锁、读写锁ReadWriteLock,众所周知这些锁都是本地锁。
提到锁就不得不提JUC:java.util.concurrent包,又称concurrent包。jdk1.5提供,为多线程高并发编程而提供的包,但此文章的场景是分布式场景,后续会出JUC的文章。

  • 简单的介绍一下synchronized及lock锁 synchronized是一个关键字,lock是一个接口,ReentrantLock是实现了lock接口的一个类 ReentrantLock:悲观的独占的互斥的排他的可公平可不公平的可重入锁 synchronized:悲观的独占的互斥的排他的非公平的可重入锁

11.准备

redis、ab工具(压测)

11.1不使用任何锁的情况下

  • 我们首先创建一个测试方法,testNoLock
@GetMapping("/test")
public void testNoLock(){String count = (String) this.redisTemplate.opsForValue().get("count");if (count == null){//没有值直接返回return;}// 有值就转成成intint number = Integer.parseInt(count);// 把redis中的num值+1this.redisTemplate.opsForValue().set("count", String.valueOf(++number));
}

测试之前的查看值为1

@GetMapping("/getCount")
public String getCount(){String count = String.valueOf(this.redisTemplate.opsForValue().get("count"));return count; //1
}
  • 接下来使用ab压力测试工具
cmd复制代码// ab  -n(一次发送的请求数)  -c(请求的并发数) 访问路径
ab -n100 -c50 http://127.0.0.1:8080/test/test

再次查询结果为6。

11.2使用本地锁

public synchronized void testNoLock(){String count = String.valueOf(this.redisTemplate.opsForValue().get("count"));if ("null".equals(count)){//没有值直接返回return;}// 有值就转成成intint number = Integer.parseInt(count);// 把redis中的num值+1this.redisTemplate.opsForValue().set("count", String.valueOf(++number));}
  • 再次使用ab压力测试工具
ab -n100 -c50 http://127.0.0.1:8080/test/test

此次结果为106,说明结果是正确的,看样子结果是非常完美的,但是真的很完美吗?

 11.3使用集群+本地锁

  • 我们只需要在idea中在启动俩个服务,修改端口号,三个运行实例的名称是相同的,并且网关的配置就是通过服务名在负载均衡,所以我们只需要访问网关,网关就会给我们做负载均衡了。

  • 再次使用ab压力测试工具(将count重置为1)
cmd复制代码ab -n100 -c50 http://127.0.0.1:8080/test/test

此次的结果为58!!!

到此说明了,本地锁是有局限性的。

12.可重入锁

对于一个功能完整的锁来说,可重入功能是必不可少的特性,所谓的锁可重入就是同一个线程,第一次加锁成功后,在第二次加锁时,无需进行排队等待,只需要判断是否是自己的锁就行了,可以直接再次获取锁来执行业务逻辑,如下图所示:

 实现可重入机制的原理就是在加锁的时候记录加锁次数,在释放锁的时候减少加锁次数,这个加锁的次数记录可以存在Redis中,如下图所示:

 如上图所示,加入可重入功能后,加锁的步骤就变为如下步骤:

(1)判断锁是否存在

(2)判断锁是否是自己的

(3)增加加锁的次数

由于增加次数以及减少次数是多个操作,这里需要再次使用lua脚本来实现,同时由于这里需要在Redis中存入加锁的次数,所以需要使用到Redis中的Map数据结构Map(key,uuid,lockCount),加锁lua脚本如下:

//锁不存在
if (redis.call('exists', key) == 0) thenredis.call('hset', key, uuid, 1); redis.call('expire', key, time); return 1;
end;
//锁存在,判断是否是自己的锁
if (redis.call('hexists', key, uuid) == 1) thenredis.call('hincrby', key, uuid, 1); redis.call('expire', key, uuid);return 1; 
end; 
//锁不是自己的,返回加锁失败
return 0;

加入可重入功能后的解锁逻辑就变为:

(1)判断锁是否是自己的

(2)如果是自己的则减少加锁次数,否则返回解锁失败

//判断锁是否是自己的,不是自己的直接返回错误
if (redis.call('hexists', key,uuid) == 0) thenreturn 0;
end;
//锁是自己的,则对加锁次数-1
local counter = redis.call('hincrby', key, uuid, -1);
if (counter > 0) then //剩余加锁次数大于0,则不能释放锁,重新设置过期时间redis.call('expire', key, uuid); return 1;
else
//等于0,代表可以释放锁了redis.call('del', key); return 1; 
end; 

到此,在实现基本的加锁与解锁的逻辑上,又加入了可重入和自动续期的功能

13.Zookeeper实现分布式锁

Zookeeper是一个分布式协调服务,分布式协调主要是来解决分布式系统中多个应用之间的数据一致性,Zookeeper内部的数据存储方式类似于文件目录形式的存储结构,它的内存结果如下图所示:

14.Zookeeper加锁原理

在Zookeeper中的指定路径下创建节点,然后客户端根据当前路径下的节点状态来判断是否加锁成功,如下图一种情况为例,线程1创建节点成功后,线程2再去创建节点就会创建失败

15.Zookeeper节点类型

持久节点:在Zookeeper中创建后会进行持久储存,直到客户端主动删除

临时节点:以客户端会话Session维度创建节点,一旦客户端会话断开,节点就会自动删除

临时/持久顺序节点:在同一个路径下创建的节点会对每个节点按创建先后顺序编号

zookeeper.exists("/watchpath",new Watcher() {@Overridepublic void process(WatchedEvent event) {System.out.println("进入监听器");System.out.println("监听路径Path:"+event.getPath());System.out.println("监听事件类型EventType:"+event.getType());				}			
});	

16.利用临时顺序节点和监听机制来实现分布式锁

实现分布式锁的方式有多种,我们可以使用临时节点和顺序节点这种方案来实现分布式锁:

1:使用临时节点可以在客户端程序崩溃时自动释放锁,避免死锁问题

2:使用顺序节点的好处是,可以利用锁释放的事件监听机制,来实现阻塞监听式的分布式锁

下面将基于这两个特性来实现分布式锁

17.加锁原理

1:首先在Zookeeper上创建临时顺序节点Node01、Node02等

2:第二步客户端拿到加锁路径下所有创建的节点

3:判断自己的序号是否最小,如果最小的话,代表加锁成功,如果不是最小的话,就对前一个节点创建监听器

4:如果前一个节点删除,监听器就会通知客户端来准备重新获取锁

加锁原理和代码入下图所示:

//加锁路径
String lockPath;
//用来阻塞线程
CountDownLatch cc = new CountDownLatch(1);
//创建锁节点的路径
Sting LOCK_ROOT_PATH = "/locks"//先创建锁
public void createLock(){//lockPath = /locks/lock_01 lockPath = zkClient.create(LOCK_ROOT_PATH+"/lock_", CreateMode.EPHEMERAL_SEQUENTIAL);
}//获取锁
public boolean acquireLock(){//获取当前加锁路径下所有的节点allLocks = zkClient.getChildren("/locks");//按节点顺序大小排序Collections.sort(allLocks);//判断自己是否是第一个节点int index = allLocks.indexOf(lockPath.substring(LOCK_ROOT_PATH.length() + 1));//如果是第一个节点,则加锁成功if (index == 0) {System.out.println(Thread.currentThread().getName() + "获得锁成功, lockPath: " + lockPath);return true;} else {//不是序号最小的节点,则监听前一个节点String preLock = allLocks.get(index - 1);//创建监听器Stat status = zkClient.exists(LOCK_ROOT_PATH + "/" + preLockPath, watcher);// 前一个节点不存在了,则重新获取锁if (status == null) {return acquireLock();} else { //阻塞当前进程,直到前一个节点释放锁System.out.println(" 等待前一个节点锁释放,prelocakPath:"+preLockPath);//唤醒当前线程,继续尝试获取锁cc.await();return acquireLock();}}
}private Watcher watcher = new Watcher() {@Overridepublic void process(WatchedEvent event) {//监听到前一个节点释放锁,唤醒当前线程cc.countDown();}
}

18.可重入锁实现

Zookeeper实现可重入分布式锁的机制是在本地维护一个Map记录,因为如果在Zookeeper节点维护数据的话,Zookeeper的写操作是很慢,集群内部需要进行投票同步数据,所以在本地维护一个Map记录来记录当前加锁的次数和加锁状态,在释放锁的时候减少加锁的次数,原理如下图所示:

//利用Map记录线程持有的锁
ConcurrentMap<Thread, LockData> lockMap = Maps.newConcurrentMap();
public Boolean lock(){Thread currentThread = Thread.currentThread();LockData lockData = lockMap.get(currentThread);//LockData不为空则说明已经有锁if (lockData != null) {//加锁次数加一lockData.lockCount.increment();return true;}//没有锁则尝试获取锁Boolean lockResult = acquireLock();//获取到锁if (lockResult) {LockData newLockData = new LockData(currentThread,1);lockMap.put(currentThread, newLockData);return true;}//获取锁失败return false;
}

19.解锁原理

解锁的步骤如下:

(1)判断锁是不是自己的

(2)如果是则减少加锁次数

(3)如果加锁次数等于0,则释放锁,删除掉创建的临时节点,下一个监听这个节点的客户端会感知到节点删除事件,从而重新去获取锁

public Boolean releaseLock(){LockData lockData = lockMap.get(currentThread);//没有锁if(lockData == null){return false; }//有锁则加锁次数减一lockCount = lockData.lockCount.decrement();if(lockCount > 0){return true;} //加锁次数为0try{//删除节点zkClient.delete(lockPath);//断开连接zkClient.close();finally{//删除加锁记录lockMap.remove(currentThread);}return true;
}

20.Redis和Zookeeper锁对比

Redis

Zookeeper

读性能

基于内存

基于内存

加锁性能

直接写内存加锁

Master节点创建好后与其他Follower节点进行同步,半数成功后才能返回写入成功

数据一致性

AP架构Redis集群之间的数据同步是存在一定的延迟的,当主节点宕机后,数据如果还没有同步到从节点上,就会导致分布式锁失效,会造成数据的不一致

CP架构当Leader节点宕机后,会进行集群重新选举,如果此时只有一部分节点收到了数据的话,会在集群内进行数据同步,保证集群数据的一致性

21.总结

使用Redis还是Zookeeper来实现分布式锁,最终还是要基于业务来决定,可以参考以下两种情况:

(1)如果业务并发量很大,Redis分布式锁高效的读写性能更能支持高并发。

(2)如果业务要求锁的强一致性,那么使用Zookeeper可能是更好的选择。

(3)在做技术选型的时候,也应该酌情考虑团队成员技能及现有资源情况,如果部署有Redsi集群克优先考虑使用Redis。

  • 性能角度:redis > zk > mysql
  • 安全角度:zk > redis == mysql
  • 难易程度:zk > redis > mysql

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/10507.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MySQL数据库关于表的一系列操作

MySQL中的数据类型 varchar 动态字符串类型&#xff08;最长255位&#xff09;&#xff0c;可以根据实际长度来动态分配空间&#xff0c;例如&#xff1a;varchar(100) char 定长字符串&#xff08;最长255位&#xff09;&#xff0c;存储空间是固定的&#xff0c;例如&#…

系统架构设计师-软件架构设计(4)

目录 一、软件架构评估 1、敏感点 2、权衡点 3、风险点 4、非风险点 5、架构评估方法 5.1 基于调查问卷或检查表的方式 5.2 基于度量的方式 5.3 基于场景的方式 6、基于场景的评估方法 6.1 软件架构分析法&#xff08;SAAM&#xff09; 6.2 架构权衡分析法&#xff08;ATAM&am…

J2EE通用分页02

目录 一.重构-提取公用方法 1.为了进行公共方法的抽取&#xff0c;需要找出上面实习中的可通用部分&#xff0c;和差异化部分 2.公用方法封装思路 3. 具体实现 二.分页标签 2.1 准备一个Servlet 3.2 结果展示页面 三. 过滤器解决中文乱码问题 四.加入分页功能 四…

Clion开发STM32之W5500系列(NTP服务封装)

概述 在w5500基础库中进行封装&#xff0c;获取服务端的时间&#xff0c;来校准本地时间。本次使用的方案是通过ntp获取时间定时器更新保证时间准确。 NTP封装 头文件 /*******************************************************************************Copyright (c) [sc…

2:SpringIOC

文章目录 一&#xff1a;Spring_IOC概念引入_重要1&#xff1a;Spring解耦合的原理2&#xff1a;创建一个spring项目并实现IOC基本功能 二&#xff1a;Spring_IOC原理分析 ***1&#xff1a;XML解析技术读取配置文件**2**&#xff1a;反射技术实例化对象,放到容器中3&#xff1a…

【算法训练营】字符串转成整数

字符串转成整数 题目题解代码 题目 点击跳转: 把字符串转换为整数 题解 【题目解析】&#xff1a; 本题本质是模拟实现实现C库函数atoi&#xff0c;不过参数给的string对象 【解题思路】&#xff1a; 解题思路非常简单&#xff0c;就是上次计算的结果10&#xff0c;相当于10…

【大数据之Flume】三、Flume进阶之Flume Agent 内部原理和拓扑结构

1 Flume事务 2 Flume Agent 内部原理 重要组件&#xff1a; 1、ChannelSelector&#xff08;选择器&#xff09;   ChannelSelector 的作用就是选出 Event 将要被发往哪个 Channel。   &#xff08;1&#xff09;Replicating ChannelSelector&#xff08;复制或副本&#x…

格式工厂5.10.0版本安装

目前格式工厂有很多&#xff0c;大多都可以进行视频转换 之前遇到一个用ffmpeg拉流保存的MP4在vlc和迅雷都无法正常播放的问题&#xff0c;发现视频长度不对&#xff0c;声音也不对&#xff0c;最后换到了格式工厂的格式播放器是可以正常播放的 格式工厂下载之家的地址 http…

每天五分钟计算机视觉:单卷积层的前向传播过程

什么是单卷积层? 一张图片(输入)经过多个卷积核卷积就会得到一个输出,而这多个卷积核的组合就是一个单卷积层。 这些卷积核可能大小是不一样的,但是他们接收同样大小是输入,他们的输出必须是一般大小,所以不同的卷积核需要具备不同的步长和填充值。 单层卷积网络前向传…

你们公司的【前端项目】是如何做测试的?字节10年测试经验的我这样做的...

前端项目也叫web端项目&#xff08;通俗讲就是网页上的功能&#xff09;是我们能够在屏幕上看到并产生交互的体验。 前端项目如何做测试&#xff1f; 要讲清楚这个问题&#xff0c;先需要你对测试流程现有一个全局的了解&#xff0c;先上一张测试流程图&#xff1a; 测试流程…

旧版Xcode文件较大导致下载总是失败但又不能断点续传重新开始的解决方法

问题&#xff1a; 旧版mac下载旧版Xcode时需要进入https://developer.apple.com/download/all/?qxcode下载&#xff0c;但是下载这些文件需要登录。登录后下载中途很容易失败&#xff0c;失败后又必须重新下载。 解决方案&#xff1a; 下载这里面的内容都需要登录&#xff0…

开发一个RISC-V上的操作系统(二)—— 系统引导程序(Bootloader)

目录 文章传送门 一、什么是Bootloader 二、简单的启动程序 三、上板测试 文章传送门 开发一个RISC-V上的操作系统&#xff08;一&#xff09;—— 环境搭建_riscv开发环境_Patarw_Li的博客-CSDN博客 开发一个RISC-V上的操作系统&#xff08;二&#xff09;—— 系统引导…

机器学习深度学习——多层感知机

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——感知机 &#x1f4da;订阅专栏&#xff1a;机器学习&&深度学习 希望文章对你们有所帮助 上一节…

Git时间:版本控制工具进阶

Git时间&#xff1a;版本控制工具进阶 忽略文件 Git允许用户将指定的文件或目录排除在版本控制之外&#xff0c;它会检查代码仓库的目录下是否存在一个名为.gitignore的文件&#xff0c;如果存在&#xff0c;就去一行行读取这个文件中的内容&#xff0c;并把每一行指定的文件…

MySQL 读写分离

目录 一、什么是读写分离&#xff1f; 二、为什么要读写分离呢&#xff1f; 三、什么时候要读写分离&#xff1f; 四、主从复制与读写分离 五、MySQL 读写分离原理 六、企业 使用MySQL 读写分离场景 1&#xff09;基于程序代码内部实现 2&#xff09;基于中间代理层实现…

你说你会Java手动锁,但你会这道题吗???

按照这个格式输出你会吗&#xff1f;&#xff1f;&#xff1f; 你说你不会&#xff0c;接下来认真看认真学了。 1.首先引入原子类。AtomicInteger num new AtomicInteger(0); 什么是原子类&#xff1f; 就是可以保证线程安全的原子操作的数据类型。 有什么作用&#xff1f;…

在Debian 12 上安装 PHP 5.6, 7.4

环境&#xff1a;Debian 12 Debian 12 默认的PHP版本为 8.2 如果直接安装php7.4就出现下面的报错&#xff1a; sudo apt-get install libapache2-mod-php7.4 php7.4 php7.4-gd php7.4-opcache php7.4-mbstring php7.4-xml php7.4-json php7.4-zip php7.4-curl php7.4-imap p…

导出为PDF加封面且分页处理dom元素分割

文章目录 正常展示页面导出后效果代码 正常展示页面 导出后效果 代码 组件内 <template><div><div><div class"content" id"content" style"padding: 0px 20px"><div class"item"><divstyle"…

Ubuntu Server版 之 mysql 系列

Ubuntu 分 桌面版 和 服务版 桌面版 &#xff1a;有额外的简易界面 服务版&#xff1a;是纯黑框的。没有任何UI界面的可言 安装mysql 安装位置 一般按照的位置存放在 /usr/bin 中 sudo apt-get install mysql-server查看mysql的状态 service mysql status mysql 安全设置…

使用xtcp映射穿透指定服务

使用xtcp映射穿透指定服务 管理员Ubuntu配置公网服务端frps配置service自启(可选) 配置内网服务端frpc配置service自启(可选) 使用者配置service自启(可选) 通过frp实现内网client访问另外一个内网服务器 管理员 1&#xff09;配置公网服务端frps2&#xff09;配置内网服务端…