分布式锁—1.原理算法和使用建议

news/2025/2/28 23:48:55/文章来源:https://www.cnblogs.com/mjunz/p/18744347

大纲

1.Redis分布式锁的8大问题

2.Redis的RedLock算法分析

3.基于Redis和zk的分布式锁实现原理

4.Redis分布式锁的问题以及使用建议

 

1.Redis分布式锁的8大问题

(1)非原子操作(set+lua)

(2)忘了释放锁(手动+超时)

(3)释放了其他线程的锁(lua+唯一值)

(4)加锁失败的处理(自旋+睡眠)

(5)锁重入问题(key是锁名+field是请求ID+值加1)

(6)锁竞争问题(读写锁+分段锁)

(7)锁超时失效或锁提前过期问题(自动续期)

(8)主从复制问题(RedLock算法)

 

(1)非原子操作(set+lua)

使用Redis实现分布式锁,首先想到的可能是setnx命令,而且通过设置超时时间可以避免死锁。如下这段代码确实可以加锁成功,但是存在一个问题。

if (jedis.setnx(lockKey, val) == 1) {jedis.expire(lockKey, timeout);
}

一.存在的问题

加锁操作和设置锁的超时时间是分开的,并非原子操作。假如加锁成功,但设置超时时间失败了,该lockKey就变成永不失效。极端情况下,获取锁的客户端如果宕机了,那么就没法释放锁了。那么应该如何保证原子性的加锁命令呢?

 

二.优化措施

由于Redis的setnx命令加锁和设置超时时间是分开的,并非原子操作。所以可以通过Redis的set命令来实现原子操作,该命令可指定多个参数。

//lockKey:锁的标识
//requestId:请求ID
//NX:只在键不存在时,才对键进行设置操作
//PX:设置键的过期时间为millisecond毫秒
//expireTime:过期时间
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("OK".equals(result)) {return true;
}
return false;

(2)忘了释放锁(手动+超时)

一.存在的问题

使用Redis的set命令加锁,表面上看起来没有问题。但如果加锁后,每次都要达到超时时间才释放锁,就有点不合理了。加锁后,如果不及时释放锁,会有很多问题。

 

二.优化措施

所以分布式锁更合理的用法是:

步骤一:手动加锁

步骤二:执行业务操作

步骤三:手动释放锁

步骤四:如果手动释放锁失败了,那么达到超时时间Redis才自动释放锁

 

如何释放锁的代码如下:捕获业务代码的异常,然后在finally中释放锁,保证无论代码执行成功或失败,都执行释放锁。

try {String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);if ("OK".equals(result)) {return true;}return false;
} finally {unlock(lockKey);
} 

(3)释放了其他线程的锁(lua+唯一值)

一.存在的问题

只在finally中释放锁还是会存在问题,因为在多线程场景中可能会出现释放了其他线程的锁的情况。

 

假如线程A和线程B,都使用lockKey加锁。线程A加锁成功,但是由于业务处理耗时很长,超过设置的超时时间。这时Redis会自动释放lockKey锁,此时线程B就能给lockKey加锁成功。接下来当线程B进行业务处理时,线程A刚好执行完业务处理。于是线程A就在finally方法中释放lockKey锁。从而导致:线程B的锁被线程A给释放了。

 

二.优化措施

在使用set命令加锁时,给lockKey锁设置一个唯一值如requestId。requestId的作用就是在释放锁时,防止释放其他线程的锁。

 

在释放锁的时候,先获取到该锁的值(之前设置值就是requestId),然后判断跟之前设置的值是否相同。如果相同才允许删除锁,返回成功。如果不同,则直接返回失败。也就是每个线程只能释放自己加的锁,不允许释放其他线程加的锁。

if (jedis.get(lockKey).equals(requestId)) {jedis.del(lockKey);return true;
}
return false;

也可以使用lua脚本来实现释放锁的原子操作:

String script = 
"if redis.call('get', KEYS[1]) == ARGV[1] then" +"return redis.call('del', KEYS[1]);" +
"else" +"return 0;" +
"end;";

(4)加锁失败的处理(自旋+睡眠)

一.存在的问题

如果有两个线程同时上传文件到sftp,上传文件前先要创建目录,假设两个线程需要创建的目录名都是当天的日期。如果不做任何控制,直接并发创建目录,第二个线程必然会失败。如果加一个Redis分布式锁后在目录不存在时才进行创建,那么第二个请求加锁失败时,是返回失败,还是返回成功?第二个请求加锁失败肯定不能返回成功,因为可能还没创建文件。但也不能直接返回失败,因为锁释放后,还是可以处理成功的。

try {String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);if ("OK".equals(result)) {if (!exists(path)) {mkdir(path);}return true;}
} finally {unlock(lockKey, requestId);
}
return false;

二.优化措施

在规定的时间内,通过自旋 + 睡眠去尝试加锁。比如在规定的500毫秒内,不断自旋尝试加锁。如果成功,则直接返回。如果失败,则睡眠50毫秒,再发起新一轮加锁的尝试。如果到了超时时间还未成功加锁,则直接返回失败。

try {Long timeout = 500L;Long start = System.currentTimeMillis();while (true) {String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);if ("OK".equals(result)) {if (!exists(path)) {mkdir(path);}return true;}long time = System.currentTimeMillis() - start;if (time >= timeout) {return false;}try {Thread.sleep(50);} catch (InterruptedException e) {e.printStackTrace();}}
} finally {unlock(lockKey, requestId);
}
return false;

(5)锁重入问题(key是锁名+field是请求ID+值加1)

一.存在的问题

假设在某个请求中,需要获取一颗满足条件的菜单树或者分类树。以菜单树为例,需要在接口中从根节点开始,递归遍历出所有满足条件的子节点,然后组装成一颗菜单树。

 

需要注意的是菜单不是一成不变的,在后台中运营可以动态添加、修改和删除菜单。

 

为了保证在并发情况下,每次都能获取最新数据,可以加Redis分布式锁。但接着问题来了,在递归方法中会递归遍历多次,每次都加同一把锁。

 

递归第一层当然是可以加锁成功的,但递归第二层、第三层、第N层,不就会加锁失败了吗?

 

递归方法中加锁的伪代码如下:看起来好像没有问题,但最终执行程序之后发现出现了异常。因为第一层递归加锁成功,还没释放锁,就直接进入第二层递归。由于锁名为lockKey且值为requestId的锁已经存在,所以第二层递归会加锁失败,然后返回到第一层,第一层接下来正常释放锁,然后整个递归方法直接返回了。所以这个递归方法其实只执行了第一层递归就返回了,其他层的递归由于加锁失败而根本没法执行。

private int expireTime = 1000;
public void fun(int level, String lockKey, String requestId) {try {String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);if ("OK".equals(result)) {if (level <= 10) {this.fun(++level, lockKey, requestId);} else {return;}}return;} finally {unlock(lockKey, requestId);}
}

二.改进措施

使用可重入锁,也就是使用Redisson框架内部实现的可重入锁功能。

private int expireTime = 1000;
public void run(String lockKey) {RLock lock = redisson.getLock(lockKey);this.fun(lock, 1);
}
public void fun(RLock lock, int level) {try {lock.lock(5, TimeUnit.SECONDS);if (level <= 10) {this.fun(lock, ++level);} else {return;}} finally {lock.unlock();}
}

如下是Redisson框架的可重入锁lua加锁代码:如果锁名不存在,则使用hset命令+pexpire命令加锁。如果锁名和requestId都存在,则使用hincrby命令给锁名的requestId值加1。这就是重入锁的关键,锁重入一次那么锁名的requestId值就加1。如果锁名存在,但值不是requestId,则返回过期时间。

//KEYS[1]:锁名
//ARGV[1]:过期时间
//ARGV[2]:uuid + ":" + threadId,可认为是requestId
//HINCRBY key field increment,会为哈希表key中的域field的值加上增量increment
String script = 
"if (redis.call('exists', KEYS[1]) == 0) then" +"redis.call('hset', KEYS[1], ARGV[2], 1);" +"redis.call('pexpire', KEYS[1], ARGV[1]);" +"return nil;" +
"endif (redis.call('hexists', KEYS[1], ARGV[2]) == 1)" +"redis.call('hincrby', KEYS[1], ARGV[2], 1);" +"redis.call('pexpire', KEYS[1], ARGV[1]);" +"return nil;" +
"end;" +
"return redis.call('pttl', KEYS[1]);";

如下是Redisson框架的可重入锁lua释放锁代码:如果锁名和requestId不存在,则直接返回。如果锁名和requestId存在,则使用hincrby命令给锁名的requestId值减1。如果减1后锁名的requestId值大于0,说明还有引用,则重设过期时间。如果减1后锁名的requestId值还等于0,则可以删除锁名的key。然后发消息通知给等待的线程抢锁。

String script =
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then" +"return nil;" +
"end" +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);" +
"if (counter > 0) then" +"redis.call('pexpire', KEYS[1], ARGV[2]);" +"return 0;" +
"else" +"redis.call('del', KEYS[1]);" +"redis.call('publish', KEYS[2], ARGV[1]);" +"return 1;" +
"end;" +
"return nil;";

(6)锁竞争问题(读写锁+分段锁)

如果有大量写的业务场景,使用上述Redis分布式锁是没有问题的。但如果有些业务场景,写操作比较少,而有大量读操作。这时使用上述的Redis分布式锁,就会有点浪费性能了。

 

锁的粒度越粗,多个线程对锁的竞争就越激烈,从而造成多个线程锁等待的时间也就越长,性能也就越差。所以提升Redis分布式锁性能的第一步,就是要把锁的粒度变细。

 

一.读写锁

加锁的目的是为了保证在并发环境中读写数据的安全性,也就是保证不会出现数据错误或者不一致的情况。但在绝大多数实际业务场景中,一般读操作的频率远远大于写操作。

 

由于并发读操作是并不涉及并发安全问题,所以没必要给读操作加互斥锁。只要保证读写、写写并发操作是互斥即可,这样可以提升系统性能。

 

将读锁和写锁分开,最大的好处是可以提升读操作性能。因为读和读之间是共享的,不存在互斥性。而我们的实际业务场景中,绝大多数的数据操作都是读操作。所以如果提升了读操作的性能,也就提升了整个锁的性能。

 

Redisson框架内部已经实现了读写锁的功能。

 

使用Redisson读锁的伪代码如下:

RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
RLock rLock = readWriteLock.readLock();
try {rLock.lock();//业务操作
} catch (Exception e) {log.error(e);
} finally {rLock.unlock();
}

使用Redisson写锁的伪代码如下:

RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
RLock rLock = readWriteLock.writeLock();
try {rLock.lock();//业务操作
} catch (InterruptedException e) {log.error(e);
} finally {rLock.unlock();
}

二.锁分段

把锁的粒度变细的另一种常见做法就是锁分段。ConcurrentHashMap就是将数据分为16段,每一段都有单独的锁,并且处于不同锁段的数据互不干扰,以此来提升锁的性能。

 

在秒杀场景中,假设库存有2000个商品可以供用户秒杀。为了防止出现超卖,通常会对库存加锁。如果有1W的用户竞争同一把锁,显然系统吞吐量会非常的低。

 

所以为了提升系统性能,可以将库存分段。比如:分为100段,每段有20个商品可以参与秒杀。在秒杀过程中,先获取用户ID的Hash值,然后除以100取模。模为1的用户访问第1段库存,模为2的用户访问第2段库存。以此类推,最后模为100的用户访问第100段库存。这样在多线程环境中,就可以大大减少锁的冲突。

 

需要注意的是:将锁分段虽可以提升系统的性能,但它也会让系统的复杂度提升不少。因为它需要引入额外的路由算法,跨段统计等功能。

 

(7)锁超时失效或锁提前过期问题(自动续期)

一.存在的问题

如果线程A加锁成功,但由于某些原因执行耗时过长,超过锁的超时时间,这时Redis会自动释放线程A加的锁。但线程A还没执行完,还在对共享数据进行访问。如果此时线程B尝试加锁,那么也可以加锁成功,并对共享数据进行访问。这样就出现了多个线程对共享数据进行操作的问题。

 

二.改进措施

如果达到了超时时间,但业务代码还没执行完,则需要给锁自动续期。可以使用TimerTask类来实现自动续期的功能,比如获取锁之后自动开启一个定时任务,每隔10秒自动刷新一次过期时间。

 

这种机制在Redisson框架中叫Watch Dog,即看门狗。其实自动续期除了可以解决锁超时导致的锁失效问题之外,还可以解决不好预估锁过期时间而导致的锁提前过期问题。

Timer timer = new Timer();
timer.schedule(new TimerTask() {@Overridepublic void run(Timeout timeout) throws Exception {//自动续期逻辑String script ="if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then" +"redis.call('pexpire', KEYS[1], ARGV[1]);" +"return 1;" +"end;" +"return 0;";...}
}, 10000, TimeUnit.MILLISECONDS);

需要注意:

在实现自动续期功能时,还要设置一个总过期时间,比如设置成30秒。如果业务代码到了这个总过期时间,还没有执行完,就不再自动续期。

 

具体就是:

获取锁之后开启一个定时任务,每隔10秒判断锁是否存在。如果存在,则刷新过期时间。如果续期3次(30秒后),业务方法还是没执行完,则不再续期。

 

(8)主从复制问题(RedLock算法)

一.存在的问题

如果Redis存在多个节点,比如做了主从,或者使用了哨兵模式。如果线程A刚好从Redis的主节点成功加锁,结果主节点还没同步就挂了,这样就会导致新的主节点的锁丢失了。如果有新的线程B过来尝试加锁就会成功,最后导致分布式锁失效。

 

二.改进措施

为了解决这个问题,Redis官网提供了一个RedLock算法。Redisson框架也提供了一个专门的类RedissonRedLock,实现了该算法。

 

2.Redis的RedLock算法分析

(1)RedLock算法的两个前提

(2)RedLock算法的具体流程

(3)RedLock算法为什么要这么做

(4)分布式专家Martin对于RedLock的质疑

(6)基于zk的分布式锁是否安全

(7)是否要用RedLock

(8)如何正确使用分布式锁

 

(1)RedLock算法的两个前提

一.不再需要部署从库和哨兵节点,只部署主库

二.主库要部署多个,官方推荐至少5个节点

 

也就是说,想要使用Redlock,至少要部署5个Redis节点,而且这5个节点都是主库,它们之间没有任何关系。注意:不是部署Redis Cluster,而是部署5个简单的Redis节点。

 

(2)RedLock算法的具体流程

步骤一:客户端先获取当前时间戳T1。

步骤二:客户端依次向这5个节点发起加锁请求,且每个请求都会设置超时时间。超时时间是毫秒级的,要远小于锁的有效时间,而且一般是几十毫秒。如果某一个节点加锁失败,包括网络超时、锁被其它线程持有等各种情况,那么就立即向下一个Redis节点申请加锁。

步骤三:如果客户端从3个以上(过半)节点加锁成功,则再次获取当前时间戳T2。如果T2 - T1 < 锁的过期时间,则认为客户端加锁成功,否则加锁失败。

步骤四:如果加锁失败,要向全部节点发起释放锁的请求。如果加锁成功,则去操作共享资源。

 

RedLock算法的四个要点总结:

一.客户端在多个Redis节点上申请加锁

二.必须保证大多数节点加锁成功

三.大多数节点加锁的总耗时要小于锁的过期时间

四.释放锁时要向全部节点发起释放锁的请求

 

(3)RedLock算法为什么要这么做

一.为什么要在多个节点上加锁

二.为什么大多数加锁成功才算成功

三.为什么加锁成功后还要计算加锁的累计耗时

四.为什么释放锁要操作所有节点

 

一.为什么要在多个节点上加锁

本质上是为了容错,部分节点异常宕机,剩余节点加锁成功,整个锁服务依旧可用。

 

二.为什么大多数加锁成功才算成功

使用的多个Redis节点,其实就组成了一个分布式系统。在分布式系统中,总会出现异常节点。所以分布式系统要考虑,异常节点达到多少个也不影响整个系统的正确性。这是一个分布式系统容错问题,这个问题的结论是:如果存在故障节点,只要大多数节点正常,则整个系统依旧可以提供服务。

 

三.为什么加锁成功后还要计算加锁的累计耗时

因为操作的是多个节点,所以耗时肯定会比操作单个节点耗时更久,而且网络请求可能会存在延迟、丢包、超时等情况。所以网络请求越多,异常发生的概率就越大。即使大多数节点加锁成功,但如果加锁的累计耗时已超过锁的过期时间,那此时有些节点上的锁可能已经失效了,这个锁就没有意义了。

 

四.为什么释放锁要操作所有节点

在向某一个Redis节点加锁时,可能因为网络原因导致加锁失败。例如客户端在一个节点上加锁成功,但在读取响应结果时,可能因为网络问题导致读取响应失败,那么这把锁其实已经在Redis上加锁成功。所以释放锁时,不管之前有没有加锁成功,都需要释放所有节点的锁。

 

RedLock是否解决了Redis节点异常宕机锁失效的问题,保证了锁的安全?

 

(4)分布式专家Martin对于RedLock的质疑

他的文章主要阐述了4个论点:

一.分布式锁的目的是什么

二.锁在分布式系统中会遇到的问题

三.RedLock假设时钟正确是不合理的

四.提出fecing token的方案保证正确性

 

一.分布式锁的目的是什么

Martin认为使用分布式锁有两个目的:

目的一:效率

使用分布式锁的互斥能力,是避免做同样的没必要的两次工作。如果锁失效,并不会带来恶性的后果,如发了2次邮件无伤大雅。

 

目的二:正确性

使用锁用来防止并发进程互相干扰,如果锁失效,会造成多个进程同时操作同一条数据(数据库 + ES + 缓存)。从而导致数据严重错误、永久性不一致、数据丢失等恶性问题。

 

如果是为了效率,那么使用单机版Redis就可以了。即使偶尔发生锁失效(宕机、主从切换),都不会产生严重的后果。而此时使用RedLock去提升效率,则显得太重了,没有这个必要。

 

如果是为了正确性,那么RedLock又达不到安全性要求,因为RedLock依旧存在锁失效的问题。

 

二.锁在分布式系统中会遇到的问题

Martin表示一个分布式系统的主要异常是NPC。

N:Network Delay,网络延迟

P:Process Pause,进程暂停(GC)

C:Clock Drift,时钟漂移

 

Martin用一个进程暂停(GC)的例子,指出了RedLock的安全性问题。

时间点一:客户端1请求锁定节点A、B、C、D、E

时间点二:客户端1拿到锁后,进入GC,时间比较久

时间点三:所有Redis节点上的锁都过期了

时间点四:客户端2获取到A、B、C、D、E上的锁

时间点五:客户端1结束GC,认为成功获取锁

时间点六:客户端2也认为获取到锁,于是发生冲突

 

Martin认为,GC可能发生在程序的任意时刻,而且执行时间是不可控的。即使使用没有GC的编程语言,当发生网络延迟、时钟漂移时,也都有可能导致RedLock出现问题,这里Martin只是拿GC举例。

 

三.RedLock假设时钟正确是不合理的

又或者当多个Redis节点的时钟发生问题时,也会导致RedLock锁失效。

时间点一:客户端1获取节点到A、B、C上的锁,但由于网络无法访问D和E

时间点二:节点C上的时钟向前跳跃,导致锁到期

时间点三:客户端2获取节点C、D、E上的锁,但由于网络无法访问A和B

时间点四:客户端1和2现在都相信它们持有了锁,产生冲突

 

Martin觉得,RedLock必须强依赖多个节点的时钟保持同步。一旦有节点时钟发生错误,那这个算法模型就失效了。即使C不是时钟跳跃,而是崩溃后立即重启,也会发生类似的问题。

 

Martin继续阐述,机器的时钟发生错误,是很有可能发生的。比如,系统管理员手动修改了机器时钟。比如,机器时钟在同步NTP时间时,发生了大的跳跃。

 

总之,Martin认为RedLock算法是建立在同步模型基础上的。但大量资料研究表明,同步模型的假设,在分布式系统中是有问题的。在分布式系统的中不能假设系统时钟是对的,所以必须非常小心假设。

 

四.提出fecing token的方案保证正确性

Martin提出了一种被叫作fecing token的方案,保证分布式锁的正确性,这个模型的流程步骤如下:

步骤一:客户端在获取锁时,锁服务可以向客户端提供一个递增的token

步骤二:客户端拿着这个token去操作共享资源

步骤三:共享资源可以根据token拒绝后来者(不是递增token的客户端)的请求

 

这样无论发生哪种NPC异常情况,都可以保证分布式锁的安全性,因为这个fecing token方案是建立在异步模型上的。而RedLock无法提供类似fecing token的方案,所以它无法保证安全性。

 

Martin还表示:

一个好的分布式锁,无论NPC怎么发生。可以不在规定时间内给出结果,但并不会给出一个错误的结果。也就是只会影响锁的性能(或称为活性),而不会影响锁的正确性。

 

Martin的结论:

结论一:RedLock不伦不类

如果使用RedLock是为了效率,那么RedLock又比较重,没必要这么做。如果使用RedLock是为了正确性,那么RedLock又不够安全。

结论二:时钟假设不合理

RedLock对系统时钟做出了危险的假设,假设节点机器时钟都是一致。如果不满足这些假设,锁就会失效。例如各节点的锁过期时间不一致导致过半节点提早过期,高并发下锁失效。又如时钟大幅跳跃还没执行到自动续期就过期,那么锁就可能失效。

结论三:无法保证正确性

Redlock不能提供类似fencing token的方案,所以解决不了正确性的问题。为了正确性,请使用有共识系统的软件,例如zk。

 

此外,还有一些场景无法保证锁的正确性(当然RedLock要求都是主库)。

 

比如客户端A尝试向5个Master实例加锁,但是仅仅在3个Maste中加锁成功。不幸的是此时3个Master中有1个Master突然宕机了,而且锁key还没同步到该宕机Master的Slave上,此时Salve切换为Master。于是在这5个Master中,由于其中有一个是新切换过来的Master,所以只有2个Master是有客户端A加锁的数据,另外3个Master是没有锁的。但继续不幸的是,此时客户端B来加锁。那么客户端B就很有可能成功在没有锁数据的3个Master上加到锁,从而满足了过半数加锁的要求,最后也完成了加锁,依然发生重复加锁。

 

(5)Redis作者Antirez的反驳

在Redis作者的反驳文章中,重点有3个:

一.解释时钟问题

二.解释网络延迟、GC问题

三.质疑fencing token机制

四.总结Redis作者的结论

 

一.解释时钟问题

首先Redis作者一眼就看穿了Martin提出的最为核心的问题:时钟问题。Redis作者认为:RedLock并不需要完全一致的时钟。RedLock只需要大体一致的时钟即可,允许有误差。例如要计时5s,但实际可能记了4.5s,之后又记了5.5s,有一定误差,但只要不超过误差范围锁失效时间即可。这对于时钟的精度要求并不高,而且这也符合现实环境。

 

对于时钟修改问题,Redis作者进行如下反驳:

反驳一:手动修改时钟问题。不要这么做就好了,否则直接修改Raft日志,那Raft也会无法工作。

 

反驳二:时钟跳跃问题。通过恰当的运维,保证机器时钟不会大幅度跳跃。比如每次通过微小的调整来完成,实际上这是可以做到的。

 

为什么Redis作者优先解释时钟问题?因为在后面的反驳过程中,需要依赖这个基础做进一步解释。

 

二.解释网络延迟、GC问题

Redis作者对网络延迟、进程GC可能导致RedLock失效问题也做了反驳,Martin的GC假设如下:

时间点一:客户端1请求锁定节点A、B、C、D、E

时间点二:客户端1拿到锁后,进入GC,时间比较久

时间点三:所有Redis节点上的锁都过期了

时间点四:客户端2获取到A、B、C、D、E上的锁

时间点五:客户端1结束GC,认为成功获取锁

时间点六:客户端2也认为获取到锁,于是发生冲突

 

Redis作者反驳这个假设其实是有问题的,RedLock是可以保证锁安全的。

 

下面回顾RedLock的具体步骤流程:

步骤一:客户端先获取当前时间戳T1。

步骤二:客户端依次向节点发起加锁请求,且每个请求都会设置超时时间。超时时间是毫秒级的,要远小于锁的有效时间,一般是几十毫秒。如果某一个节点加锁失败,包括网络超时、锁被其它线程持有等情况,那么就立即向下一个Redis节点申请加锁。

步骤三:如果客户端从过半节点加锁成功,则再次获取当前时间戳T2。如果T2 - T1 < 锁的过期时间,则认为客户端加锁成功,否则加锁失败。

步骤四:如果加锁失败,要向全部节点发起释放锁的请求。如果加锁成功,则去操作共享资源。

 

注意:重点在步骤三

加锁成功后为什么要重新获取当前时间戳T2?而且还用T2 - T1的时间,与锁的过期时间做比较?

 

Redis作者强调:

强调一:如果在步骤一到三发生了网络延迟、进程GC等耗时长的异常情况,那么在第三步T2 - T1时是可以检测出来的。如果超出锁设置的过期时间,那这时就认为加锁失败,之后释放所有节点的锁即可。

强调二:如果在步骤三之后发生网络延迟、进程GC等耗时长的异常情况,即客户端确认拿到了锁,去操作共享资源时发生了异常,导致锁失效。那么这不仅仅是RedLock的问题,任何其它锁服务比如zk也都会有类似的问题。

 

例如下面是RedLock在步骤三之后发生NPC的例子:客户端通过RedLock成功获取到锁(通过过半节点加锁成功 + 加锁耗时检查),客户端开始操作共享资源,此时发生网络延迟、进程GC等耗时很长的情况。此时锁过期自动释放,客户端开始操作MySQL(此时的锁可能会被其他线程拿到,出现锁失效的情况)。

 

Redis作者的结论:

结论一:客户端在拿到锁之前,无论经历什么NPC耗时长问题,RedLock都能在第三步检测出来。

结论二:客户端在拿到锁之后,如果发生任何NPC情况,那么RedLock、zk其实也都无能为力。

 

所以Redis作者认为:RedLock在大体一致的时钟基础上,是可以保证正确性的。

 

三.质疑fecing token机制

Redis作者对于Martin提出的fecing token机制,也提出了质疑。

 

质疑一:这个方案必须要求要操作的共享资源服务器有拒绝旧token的能力。例如要操作MySQL,从锁服务拿到一个递增数字的token。然后客户端要带着这个token去改MySQL的某一行,这就需要利用MySQL的事务隔离性来做。

//两个客户端必须利用事务和隔离性达到目的
//注意token的判断条件
UPDATE table T SET val = $new_val WHERE id = $id AND current_token < $token

但如果操作的不是MySQL呢?例如向磁盘上写一个文件或发起一个HTTP请求,那这个方案就无能为力。这对要操作的资源服务器,提出了更高的要求。也就是说,大部分要操作的资源服务器都是没有这种互斥能力的。再者,既然资源服务器都有了互斥能力,那还要分布式锁干什么?所以,Redis作者认为这个方案是站不住脚的。

 

质疑二:即使RedLock没有提供fecing token的能力,但已提供了唯一值。利用这个唯一值,也可以达到与fecing token同样的效果。

 

例如类似CAS的思路:

步骤一:客户端使用RedLock拿到锁

步骤二:在操作共享资源前,先把锁的value标记在要操作的共享资源上

步骤三:客户端处理业务逻辑

步骤四:在修改共享资源时,先判断标记是否与之前一样,一样才修改

 

还是以MySQL为例:

步骤一:客户端使用RedLock拿到锁

步骤二:客户端要修改某一行数据前,先把锁的value更新到这一行中

步骤三:客户端处理业务逻辑

步骤四:客户端修改MySQL这一行数据,把value当做where条件去修改

UPDATE table T SET val = $new_val WHERE id = $id AND current_token = $redlock_value;

可见,这种方案依赖MySQL的事务机制,也达到fecing token的效果。

 

但这里还有个小问题:

两个客户端通过这种方案,先"标记"再"检查+修改"共享资源,那这两个客户端的操作顺序就无法保证。而用Martin提到的fecing token,因为这个token是单调递增的数字,资源服务器可以拒绝小的token请求,保证了操作的顺序性。

 

Redis作者对这问题做了不同的解释:

分布式锁的本质是为了互斥,只要能保证两个客户端在并发时,一个成功,一个失败就好了。分布式锁不需要关心顺序性,而前面Martin的质疑中,一直很关心这个顺序性问题。

 

四.总结Redis作者的结论

结论一:同意Martin提出时钟跳跃对RedLock的影响,但Redis作者认为时钟跳跃是可以避免的,取决于基础设施和运维。

结论二:RedLock在设计时,充分考虑了NPC问题。在RedLock步骤三获取锁之前发生NPC,可以保证锁的正确性。但在步骤三获取锁之后发生NPC,不止是RedLock有问题。其它分布式锁服务同样也有问题,所以不在讨论范畴内。

 

(6)基于zk的分布式锁是否安全

基于zk实现的分布式锁是这样的:

步骤一:客户端1和2都尝试创建临时节点

步骤二:假设客户端1先到达完成创建,那么客户端1加锁成功,客户端2加锁失败

步骤三:客户端1操作共享资源

步骤四:客户端1删除临时节点,释放锁

 

可见zk不像Redis那样,需要考虑锁的过期时间问题。因为zk采用临时节点,客户端拿到锁后只要连接不断,就可以一直持有锁。而且如果客户端异常崩溃,那么临时节点会自动删除,保证锁会被释放。

 

zk没有锁过期的烦恼,还能在异常时自动释放锁,但还是不完美。客户端创建临时节点后,zk是如何保证让这个客户端一直持有锁呢?客户端会与zk维护一个Session,这个Session依赖心跳检测来维持连接。如果zk长时间收不到客户端心跳,就认为Session过期,会删除临时节点。

 

GC问题对zk分布式锁的影响场景如下:

时间点一:客户端1创建临时节点成功,拿到了锁

时间点二:客户端1发生长时间GC

时间点三:客户端1无法给zk发送心跳,于是zk把临时节点进行删除

时间点四:客户端2创建临时节点成功,拿到了锁

时间点五:客户端1结束GC,仍然认为自己持有锁,于是发生了冲突

 

可见,即使使用zk也无法保证进程GC、网络延迟异常场景下的安全性。这就是Redis作者在反驳中提到的:如果客户端已经拿到了锁,但客户端与锁服务器发生失联(例如GC),那不仅RedLock有问题,其它锁服务都有类似的问题,zk也是一样。

 

所以可以得出结论:

一个分布式锁,在极端情况下,不一定是安全的。如果业务数据非常敏感,在使用分布式锁时,一定要注意这个问题,我们不能假设分布式锁100%安全。

 

(7)是否要用RedLock

RedLock只有建立在时钟正确的前提下,才能正常工作。如果可以保证时钟正确这个前提,那么可以使用RedLock。

 

但保证时钟正确,并不是那么简单就能做到的。

第一:从硬件角度来说,时钟发生偏移是时有发生,无法避免。例如CPU温度、机器负载、芯片材料都是有可能导致时钟发生偏移的。

第二:从工作经历来说,就遇到过时钟错误、运维暴力修改时钟的情况。进而影响了系统的正确性,所以人为错误也是很难完全避免。

 

所以对RedLock的个人看法是:尽量不用RedLock,而且它的性能不如单机版Redis,部署成本也高,会优先考虑使用主从 + 哨兵的模式实现分布式锁。

 

(8)如何正确使用分布式锁

Martin提到了fecing token方案,虽然这种方案有很大局限性,但对保证正确性的场景,提供了一个非常好的解决思路。

 

所以可以把这两者结合起来使用:

一.使用分布式锁在服务上层完成互斥目的

虽然极端情况下锁会失效,但可以最大程度把并发请求阻挡在服务最上层,从而减轻操作资源层的压力。

二.但是对于要求数据绝对正确的业务,在资源层一定要做好兜底设计

设计思路可借鉴fecing token方案,两种思路结合,那么对于大多数业务场景,基本可以满足要求。

 

3.Redis和zk的分布式锁实现原理对比

(1)Redis分布式锁的简单实现

(2)Redis官方的RedLock算法

(3)zk分布式锁之排他锁实现原理

(4)zk分布式锁之读写锁实现原理

(5)zk分布式锁的一个简单实现

(6)Redis分布式锁和zk分布式锁的对比

 

问题汇总:

一般实现分布式锁都有哪些方式?使用Redis如何设计分布式锁,使用zk来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?

 

(1)Redis分布式锁的简单实现

Redis分布式锁使用RedLock算法,是Redis官方支持的分布式锁算法,这个分布式锁有3个考量点:

考量点一:互斥,只能有一个客户端获取锁

考量点二:不能产生死锁

考量点三:容错,允许其中的节点出现故障而不影响分布式锁功能

 

一.获取锁

最普通的实现方式就是在Redis里创建一个key。如果创建成功,那么就认为获取锁成功。如果创建失败,即发现已经有该key了,则说明获取锁失败。

 

通常使用如下这样的命令进行创建:

/** 说明一:"my:lock"就是锁的名称,对应于Redis的key */
/** 说明二:Redis中这个名为"my:lock"的key对应的值必须是一个随机值 */
/** 说明三:NX的意思是只有key不存在的时候才会设置成功 */
/** 说明四:"PX 30000"的意思是30秒后锁自动释放 */
SET my:lock 随机值 NX PX 30000

二.释放锁

释放锁就是删除key,但是一般用lua脚本进行删除,并且是判断随机value一样才进行删除。

//删除key的lua脚本
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then" +"return redis.call('del', KEYS[1]);" +
"else" +"return 0;" +
"end;";

为什么必须要用随机值:

因为如果某个客户端获取到了锁,但阻塞了很长时间才执行完。此时可能由于过期时间已到而自动释放了锁,而别的客户端又刚好获取到锁,这个时候如果该客户端直接删除key那么就会有问题。所以才必须先对key设置随机值,然后再按key + 随机值来删除key释放锁。

三.这套Redis获取锁 + 释放锁的方案存在的问题

问题一:单点故障。如果Redis服务是普通的单实例,那么就会有单点故障的风险。

问题二:破坏互斥。如果Redis服务是普通主从架构,Redis进行的是主从异步复制。那么当Redis主节点挂了,key还没同步到从节点时,一旦从节点切换为主节点,其他系统的线程就会拿到锁。

 

(2)Redis官方的RedLock算法

RedLock算法主要是通过过半机制来避免单节点故障导致的锁没同步问题,RedLock算法在获取锁 + 释放锁的方案上基本和前面一致。但实际上这种RedLock算法也有很多问题,不是很完美。

 

RedLock算法获取一把锁的步骤:

步骤一:客户端先获取当前时间戳T1。

步骤二:客户端依次向这5个节点发起加锁请求,且每个请求都会设置超时时间,也是使用"SET my:lock 随机值 NX PX 50"创建锁。超时时间是毫秒级的,要远小于锁的有效时间,而且一般是几十毫秒。如果某一个节点加锁失败,包括网络超时、锁被其它线程持有等各种情况,那么就立即向下一个Redis节点申请加锁。

步骤三:如果客户端从3个以上(过半)节点加锁成功,则再次获取当前时间戳T2。如果T2 - T1 < 锁的过期时间,则认为客户端加锁成功,否则加锁失败。

步骤四:如果加锁失败,要向全部节点发起释放锁的请求。如果加锁成功,则去操作共享资源,也是使用lua按照key + 随机值进行删除。

 

(3)zk分布式锁之排他锁的实现原理

一.获取锁

使用临时顺序节点来表示获取锁的请求,让创建出后缀数字最小的节点的客户端成功拿到锁。

 

步骤一:客户端调用create()方法在"/exclusive_lock"节点下创建临时顺序节点。

步骤二:然后调用getChildren()方法返回"/exclusive_lock"下的所有子节点,接着对这些子节点进行排序。

步骤三:排序后,看看是否有后缀比自己小的节点。如果没有,则当前客户端便成功获取到排他锁。如果有,则调用exist()方法对排在自己前面的那个节点注册Watcher监听。

步骤四:当客户端收到Watcher通知前面的节点不存在,则重复步骤二。

 

二.释放锁

如果获取锁的客户端宕机,那么客户端在zk上对应的临时节点就会被移除。如果获取锁的客户端执行完,会主动将自己创建的临时节点删除。

 

(4)zk分布式锁之读写锁的实现原理

一.获取锁

步骤一:客户端调用create()方法在"/shared_lock"节点下创建临时顺序节点。如果是读请求,那么就创建"/shared_lock/read001"的临时顺序节点。如果是写请求,那么就创建"/shared_lock/write002"的临时顺序节点。

 

步骤二:然后调用getChildren()方法返回"/shared_lock"下的所有子节点,接着对这些子节点进行排序。

 

步骤三:对于读请求:如果排序后发现有比自己序号小的写请求子节点,则需要等待,且需要向比自己序号小的最后一个写请求子节点注册Watcher监听。

 

对于写请求:如果排序后发现自己不是序号最小的请求子节点,则需要等待,并且需要向比自己序号小的最后一个请求子节点注册Watcher监听。

 

注意:这里注册Watcher监听也是调用exist()方法。此外,不满足上述条件则表示成功获取共享锁。

 

步骤四:如果客户端在等待过程中接收到Watcher通知,则重复步骤二。

 

二.释放锁

如果获取锁的客户端宕机,那么zk上的对应的临时顺序节点就会被移除。如果获取锁的客户端执行完,会主动将自己创建的临时顺序节点删除。

 

(5)zk分布式锁的一个简单实现

一.分布式锁的实现步骤

步骤一:每个线程都通过"临时顺序节点 + zk.create()方法 + 添加回调"去创建节点。

 

步骤二:线程执行完创建临时顺序节点后,先通过CountDownLatch.await()方法进行阻塞。然后在创建成功的回调中,通过zk.getChildren()方法获取根目录并继续回调。

 

步骤三:某线程在获取根目录成功后的回调中,会对目录排序。排序后如果发现其创建的节点排第一,那么就执行countDown()方法表示获取锁成功。排序后如果发现其创建的节点不是第一,那么就通过zk.exists()方法监听前一节点。

 

步骤四:获取到锁的线程会通过zk.delete()方法来删除其对应的节点实现释放锁,在等候获取锁的线程掉线时其对应的节点也会被删除。而一旦节点被删除,那些监听根目录的线程就会重新执行zk.getChildren()方法,获取成功后其回调又会进行排序以及通过zk.exists()方法监听前一节点。

 

二.WatchCallBack对分布式锁的具体实现

public class WatchCallBack implements Watcher, AsyncCallback.StringCallback, AsyncCallback.Children2Callback, AsyncCallback.StatCallback {ZooKeeper zk ;String threadName;CountDownLatch countDownLatch = new CountDownLatch(1);String pathName;public String getPathName() {return pathName;}public void setPathName(String pathName) {this.pathName = pathName;}public String getThreadName() {return threadName;}public void setThreadName(String threadName) {this.threadName = threadName;}public ZooKeeper getZk() {return zk;}public void setZk(ZooKeeper zk) {this.zk = zk;}public void tryLock() {try {System.out.println(threadName + " create....");//创建一个临时的有序的节点zk.create("/lock", threadName.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL, this, "abc");countDownLatch.await();} catch (InterruptedException e) {e.printStackTrace();}}//当前线程释放锁, 删除节点public void unLock() {try {zk.delete(pathName, -1);System.out.println(threadName + " over work....");} catch (InterruptedException e) {e.printStackTrace();} catch (KeeperException e) {e.printStackTrace();}}//上面zk.create()方法的回调//创建临时顺序节点后的回调, 10个线程都能同时创建节点//创建完后获取根目录下的子节点, 也就是这10个线程创建的节点列表, 这个不用watch了, 但获取成功后要执行回调//这个回调就是每个线程用来执行节点排序, 看谁是第一就认为谁获得了锁@Overridepublic void processResult(int rc, String path, Object ctx, String name) {if (name != null ) {System.out.println(threadName  + "  create node : " +  name );setPathName(name);//一定能看到自己前边的, 所以这里的watch要是falsezk.getChildren("/", false, this ,"sdf");}}//核心方法: 各个线程获取根目录下的节点时, 上面zk.getChildren("/", false, this ,"sdf")的回调@Overridepublic void processResult(int rc, String path, Object ctx, List<String> children, Stat stat) {//一定能看到自己前边的节点System.out.println(threadName + "look locks...");for (String child : children) {System.out.println(child);}//根目录下的节点排序Collections.sort(children);//获取当前线程创建的节点在根目录中排第几int i = children.indexOf(pathName.substring(1));//是不是第一个, 如果是则说明抢锁成功; 如果不是, 则watch当前线程创建节点的前一个节点是否被删除(删除);if (i == 0) {System.out.println(threadName + " i am first...");try {//这里的作用就是不让第一个线程获得锁释放锁跑得太快, 导致后面的线程还没建立完监听第一个节点就被删了zk.setData("/", threadName.getBytes(), -1);countDownLatch.countDown();} catch (KeeperException e) {e.printStackTrace();} catch (InterruptedException e) {e.printStackTrace();}} else {//9个没有获取到锁的线程都去调用zk.exists, 去监控各自自己前面的节点, 而没有去监听父节点//如果各自前面的节点发生删除事件的时候才回调自己, 并关注被删除的事件(所以会执行process回调)zk.exists("/" + children.get(i-1), this, this, "sdf");}}//上面zk.exists()的监听//监听的节点发生变化的Watcher事件监听@Overridepublic void process(WatchedEvent event) {//如果第一个获得锁的线程释放锁了, 那么其实只有第二个线程会收到回调事件//如果不是第一个哥们某一个挂了, 也能造成他后边的收到这个通知, 从而让他后边那个去watch挂掉这个哥们前边的, 保持顺序switch (event.getType()) {case None:break;case NodeCreated:break;case NodeDeleted:zk.getChildren("/", false, this ,"sdf");break;case NodeDataChanged:break;case NodeChildrenChanged:break;}}@Overridepublic void processResult(int rc, String path, Object ctx, Stat stat) {//TODO}
}

三.分布式锁的测试类

package com.demo.zookeeper.lock;import com.demo.zookeeper.config.ZKUtils;
import org.apache.zookeeper.ZooKeeper;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;public class TestLock {ZooKeeper zk;@Beforepublic void conn() {zk  = ZKUtils.getZK();}@Afterpublic void close() {try {zk.close();} catch (InterruptedException e) {e.printStackTrace();}}@Testpublic void lock() {//10个线程都去抢锁for (int i = 0; i < 10; i++) {new Thread() {@Overridepublic void run() {WatchCallBack watchCallBack = new WatchCallBack();watchCallBack.setZk(zk);String threadName = Thread.currentThread().getName();watchCallBack.setThreadName(threadName);//每一个线程去抢锁watchCallBack.tryLock();//抢到锁之后才能干活System.out.println(threadName + " working...");try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}//干完活释放锁watchCallBack.unLock();}}.start();}while(true) {}}
}

(6)Redis分布式锁和zk分布式锁的对比

一.性能开销方面

对于Redis分布式锁,客户端需要不断去尝试获取锁,比较消耗性能。对于zk分布式锁,当客户端获取不到锁时,只需要注册一个监听器即可。所以zk分布式锁不需要不断主动地尝试获取锁,性能开销小。

 

二.异常释放锁方面

对于Redis分布式锁,如果获取到锁的客户端挂了,那么只能等过期时间后才能释放锁。对于zk分布式锁,获取锁时创建的是临时节点,即便客户端挂了,由于临时节点会被zk删除,所以会自动释放锁。

 

三.实现方面

Redis分布式锁的RedLock算法比较麻烦,需要遍历上锁、计算时间等。而zk的分布式锁语义清晰实现简单,zk的分布式锁比Redis的分布式锁牢靠、而且模型简单易用。

 

四.性能方面

Redis和zk都是基于内存去通过写数据来创建锁的,但是因为zk有过半写机制,所以Redis能够承受更高的QPS。

 

五.支持方面

Redisson可以对分布式锁提供非常好的支持,zk的Curator则没有这么好的支持。

 

4.Redis分布式锁的问题以及使用建议

(1)Redis分布式锁的问题

(2)Redis分布式锁的优点

(3)zk分布式锁

(4)使用建议

(5)一点思考

 

(1)Redis分布式锁的问题

第一种方案:基于Redis单实例 + setnx随机值 + lua删除key

如果出现Redis单点故障,会导致系统全盘崩溃,做不到高可用。除非是那种不太核心的小系统,随便用一下分布式锁,那么可以使用Redis单实例。

 

第二种方案:基于Redis主从架构 + 哨兵 + setnx随机值 + lua删除key

Redis主从+哨兵保证了高可用,Master宕机,Slave会接替。但是存在隐患,即在Master宕机的瞬间:如果刚创建的锁还没异步复制到Slave,那么就会导致重复加锁的问题。虽然主从 + 哨兵保证了高可用,但锁的实现有漏洞,可能会导致系统异常。

 

第三种方案:使用RedLock算法

通过twemproxy、Codis、Redis Cluster可以实现Redis集群分片。面对Redis的多Master集群,此时使用的是RedLock算法,但不推荐,因为实现过程太复杂繁琐、很脆弱。因网络原因,难以实现多节点同时设置分布式锁,锁失效时间都会不一样。不同Linux机器的时间不同步 + 各种无法考虑到的问题,可能导致重复加锁。

 

举个例子:客户端A给5个Redis Master都设置了一个key上了一把锁,失效时间是10s。因为网络等各种情况不同,各个Master对key的过期处理可能不同步。可能会出现:客户端A由于某些原因处理耗时特别长所以还没释放锁,然后过了大概10秒,其中3台机器的key都到期失效了。此时客户端A还没释放锁,而客户端B却发起设置请求,刚好成功加到锁。于是出现两个客户端同时持有锁。

 

RedLock算法存在两个问题:

问题一:实现过程和步骤太复杂,上锁的过程和机制很重很复杂,导致很脆弱,各种意想不到的情况都可能发生。

问题二:网络原因和各服务器的时钟问题导致对key的过期处理并不同步,不够健壮,不一定能完全实现健壮的分布式锁的语义。

 

RedLock算法的问题总结:

第一是太复杂

第二是不健壮可能重复加锁

 

Redis分布式锁总结:

Redis分布式锁实际上没有100%完美的方案,或多或少有点问题。实际生产系统中,有时候用zk分布式锁,有时候也会用Redis分布式锁。

 

(2)Redis分布式锁的优点

Redis分布式锁有一个优点,就是拥有优秀的Redis客户端类库Redisson。Redisson封装了大量基于Redis的复杂操作,比如数据集合(Map、Set、List)的分布式的存储、各种复杂的分布式锁等。甚至基于Redis+Redisson,就可以将Redis作为一个轻量级的NoSQL使用。

 

Redisson对Redis分布式锁的支持非常友好,比如支持可重入锁、读写锁、公平锁、信号量、CountDownLatch等。Redisson支持很多种复杂的锁的语义,提供了各种分布式锁的高级支持,Redisson这个客户端框架本身就有完整的一套Redis分布式锁实现。

 

(3)zk分布式锁

zk分布式锁的优点是锁模型健壮、稳定、简单、可用性高,zk分布式锁的缺点是性能不如Redis,而且部署和运维成本高。

 

Curator客户端主要还是针对zk的一些基础语义进行封装。Curator之于zk,就类似于Jedis之于Redis。Curator也封装了多种不同的锁类型,比如可重入锁、读写锁、公平锁、信号量、CountDownLatch等锁类型。

 

Redis的Jedis可以和Redisson结合起来一起使用,因为Jedis封装了Redis的一些基础语义和操作。

 

(4)使用建议

目前行业里,基本都会有使用Redis或zk做分布式锁。Redis分布式锁没有100%完美和健壮的锁模型,或多或少会导致一些问题。如果业务场景能容忍这些问题,同时需要Redission的复杂的锁类型支持,那么可以使用Redis分布式锁。

 

zk分布式锁有完美健壮的锁模型,但没有太好的开源类库支持复杂锁类型。如果业务场景要求锁的语义健壮稳定,不能出现多客户端同时加到一把锁,且对锁的功能没有特别需求,那么可以使用zk分布式锁。

 

(5)一点思考

设计架构为什么偏好用zk分布式锁,原因两个:

原因一是Redis分布式锁的算法模型有隐患,zk分布式锁的机制更加健壮稳定。

原因二是Redis的本质是分布式缓存,zk的本质是分布式协调服务。

 

此外,如下是一些关于Redis框架的发展看法:

 

Redis现在越来越往队列、分布式锁、发布订阅等功能发展,有点本末倒置。Redis框架应该回归它的本质去发展,它的本质是一个kv缓存。Redis框架如果要发展,应该是纵向发展。比如支持磁盘存储、可以支持磁盘 + 内存的大规模海量数据的kv存储,而且Redis作为开源项目,最好提供一些便于集群管理和运维操作的功能。

 

比如提供便捷的可视化界面实现如下等功能来支持使用方高效管理:一键部署集群、集群上下线节点、集群数据迁移、集群数据备份、不同集群模式的一键转换(单实例模式->主从模式->哨兵模式->集群模式)、一键集群版本滚动升级、子集群模式与业务隔离、热key大value的自动发现与报警、集群资源的监控与报警、多机房集群部署容灾、集群访问量监控与扩容预警等。这些功能虽然Cache Clound有提供,但各大公司都要自己重复造轮子。

 

让人失望的是:Redis Cluster刚出来时还不支持读写分离,其Slave只做高可用自动切换。而且运维极其繁琐,还有RedLock分布式锁算法本身也不健壮。此外还支持队列、支持发布订阅等。Redis天然就不是为了分布式锁这种分布式系统的基础组件来设计的。zk才是最适合做各种分布式系统的基础设施依赖的,而且业界基本各大开源项目,都依赖zk做各种分布式系统的基础设施。

 

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

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

相关文章

理解Rust引用及其生命周期标识(上)

写在前面 作为Rust开发者,你是否还没有完全理解引用及其生命周期?是否处于教程一看就会,但在实际开发过程中不知所措?本文将由浅入深,手把手教你彻底理解Rust引用与生命周期。 关于本文的理解门槛 本文主要面向的是已经基本上了解过Rust这门语言,对引用以及生命周期(及其…

TCP/IP协议栈相关知识

为什么提出TCP/IP参考模型OSI参考模型比较复杂TCP和IP两大协议在网络上广泛使用 三种参考模型如今用的最多的是TCP/IP五层模型,而OSI七层模型更多用于学习。 TCP/IP常见协议要了解协议对应的传输层端口号,因为计算机是通过端口号分辨所使用的是那种协议。 应用层要了解常见协…

Vulkan环境配置 | vscode+msvc 解决方案

Vulkan环境配置 | vscode+msvc 解决方案 前言 如果作为Windows 11侠的你是一个vscode爱好者,凑巧电脑上还安装有Visual Studio,这个时候你突然想配置一个Vulkan开发环境。作为minimalist的你可不希望在电脑上安装任何额外的组件,那么这篇安装指南一定适合你。 准备工作 你需…

mysql索引原理简单说明

本次使用的是mysql5.7.17 首先说下索引中的组合索引,即多个字段组合的索引就叫做组合索引,但是什么时候会生效,什么时候会失效,我不是很清楚 首先建个表造点数据看下情况,CREATE TABLE `bank` (`id` INT(11) PRIMARY KEY AUTO_INCREMENT COMMENT 主键,`bank_end` VARCHAR…

Vulnhub-Troll-1靶机-ftp匿名登录+流量包分析+hydra爆破+ssh登录脚本提权

一、靶机搭建 选择扫描虚拟机选择路径即可二、信息收集 扫ip 靶机ip:192.168.108.144扫开放端口 开放了ftp服务扫版本服务信息信息如下 21/tcp open ftp vsftpd 3.0.2 22/tcp open ssh OpenSSH 6.6.1p1 Ubuntu 2ubuntu2 (Ubuntu Linux; protocol 2.0) 80/tcp open …

八、(了解即可)MyBatis懒加载(或者叫延迟加载)

八、(了解即可)MyBatis懒加载(或者叫延迟加载)@目录八、懒加载(了解即可)8.1 为啥需要懒加载?8.2 懒加载是什么?8.3 开启方式8.4 既然fetchType可以控制懒加载那么我仅仅配置fetchType不配置全局的可以吗?8.5 aggressiveLazyLoading是做什么么的?8.6 注意点8.7 案例验证懒加…

基于惯性加权PSO优化的目标函数最小值求解matlab仿真

1.程序功能描述基于惯性加权PSO优化的目标函数最小值求解matlab仿真。 2.测试软件版本以及运行结果展示MATLAB2022A版本运行 (完整程序运行后无水印) 3.核心程序yfits = []; % 主循环开始 for iter =1: Miteryfit = zeros(Npop,1); % 初始化函数值数组% 更新粒子速度…

题解:at_abc391_e Hierarchical Majority Vote

对于一个长度为 \(3^n\) 的 01 字符串 \(B = B_1B_2\dots B_{3^n}\),定义一种操作获得长度为 \(3^{n-1}\) 的 01 字符串 \(C = C_1C_2\dots C_{3^{n-1}}\):对于 \(i = 1,2,\dots,3^{n-1}\),令 \(C_i\) 为 \(B_{3i}\)、\(B_{3i-1}\)、\(B_{3i-2}\) 中出现次数最多的字符。现给…

解决VScode设置ctrl+,被占用

关闭启用更多系统热键设置可以用OpenArk找找

Xmx_Xms的关系与设置技巧

以下是关于JVM参数 -Xmx(最大堆内存)和 -Xms(初始堆内存)的核心关系解析与设置技巧,通过结构化表格和场景化配置示例帮助你清晰掌握调优方法:一、基础定义与关系对比表参数 作用 默认值(JDK8) 关系说明-Xms 堆内存初始分配大小 物理内存的1/64 程序启动时立即分配的内存-…

vue3创建项目时,报错crypto$2.getRandomValues is not a function

一、问题在新建项目时,使用 npm create vue@latest 创建 vue3项目后,执行命令npm install后,这时候控制台报错TypeError:crypto$2.getRandomValues is not a function二、思路起初以为依赖安装不对,使用命令rm -rf node_modules/删除依赖包,再次执行命令npm install后控制…