赞
踩
目录
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它是一个基于Redis实现的高级分布式锁客户端。同时Redisson是Redis官网指定的RedlLock使用Java语言的实现。
网络上沿着Redisson的官方文档的介绍已经很多,公平锁、读写锁、信号量、闭锁的用法和源码解析。我这里就不做赘述。本篇博文主要讲使用联锁MultiLock和红锁RedLock的必要场景,以及可能出现的问题
因为Redis是CAP中AP架构设计,主打的就是牺牲出现可能性比较小的一致性,达到高可用,高性能的目的。RedLock算法的出现就是为了解决redis实现的分布式锁的不能保证强数据一致性的问题,即便如此,RedLock也只是尽量将数据不一致的可能性降到最低,并不能完全解决这个问题。下面就重点探讨RedLock也可能出现的问题。
因为RedissonRedLock是继承自RedissonMultiLock的,只是重写了failedLocksLimit方法,即允许锁获取失败个数,获取锁的等待时间等方法。总体来说还是paxos过半机制那一套。所以先说RedissonMultiLock吧。
所谓联锁,其实就是将多个RLock形成一个锁组合,遍历组合内各个key,分别去获取锁,但是它允许获取失败的锁(Key)为0。具体代码如下:
- protected int failedLocksLimit() {
- return 0;
- }
这种锁适合资源严格互斥的需要分布式锁加持的方法,也就是适合业务中,可能会需要更新数据的每个key,被严格保护,即使是其他的方法中,需要使其中的一个Key,此时也只能互斥的不能获取资源。在我的上一篇博文中举例:场景(1):一个订单中有多种商品,提交订单的时候,每种商品的库存需要被扣除。这种场景就需要用MultiLock,而不是RedLock。但是如果redis是单机模式,主从模式部署,服务器宕机怎么办呢?没有备份实例,或者备份实例不能自动替换,没有灾备方案。
那么就用哨兵或者集群模式部署可以解决吗?这两种模式都具备故障转移的功能,但是在转移的过程中,会出现脑裂的问题。也就是多个key中某一个key映射到的某台实例上的数据在故障转译中丢失了一部分,正好这个key的缓存数据在节点故障恢复后丢失了,对应的锁也不存在了。
RedLock就是为此设计的,即使多个Key中少部分key因为故障,数据丢失,失去了锁,但组合锁总体仍然具备分布式锁功能。具体就是重写RedissonMultiLock的两个方法。
- public RedissonRedLock(RLock... locks) {
- super(locks);
- }
-
- @Override
- protected int failedLocksLimit() {
- return locks.size() - minLocksAmount(locks);
- }
-
- protected int minLocksAmount(final List<RLock> locks) {
- return locks.size()/2 + 1;
- }
从源码可以看到,相对于RedissonMultiLock,它的改动并不大,仍然是根据CRC16 hash映射到不同的节点,所以RedLock的key,如果完全选取业务相关的互斥资源的key,在获取分布式锁的时候,需要获取redis主节点服务实例的个数,可能是偶数,也可能是不大于3的奇数。这样不能实现过半算法。
假如主节点Master有5个,如果key选取的不合适,5个key都映射到同一个主节点上,这个主节点出现故障转移,那不就跟单机模式一样吗?但是如果完全选取与业务不相干的key,每次请求的使用的key都一样,岂不是不相干不互斥的请求(举例:前面一个订单需要商品A,紧接着接受到一个需要商品B的订单提交请求),也要争用同一个锁吗?而每次key随机选取不一样,可能需要互斥争用锁的请求,变成不互斥的业务请求了,这就失去了分布式锁的本来目的,本末倒置。
很多人认为,要实现RedLock锁,至少需要5台redis Master实例,因为官方推荐?官方这里只是举例,并不是推荐至少需要5个主节点实例。我想应该是翻译的问题。
看源码,和官方说明,也就是至少是3个节点而已。
那RedLock的选用场景是什么样的呢?或者说RedLock的lockKey设置,有做补偿方案吗?
首先能选取RedLock的方案就代表可以容忍部分数据不一致,比如
场景(2):线程1,请求参数中有A、B、C三个key,而线程2请求参数只有C(或者 C、E、F),假如线程1中的C刚好因为故障转移失效了,线程2获取锁成功,同时修改C相关的资源。(或者线程2只有C获取失败,整体获得分布式锁)这不就改变了保证数据一致性,要达到互斥的初衷了吗?
由此可见,RedLock的局限性也非常大,可能只适合分布式部署,参数固定请求方法吧。旨在减小故障转移带来的同时获取到锁的可能性罢了。这也是我之前的项目,没有选取RedLock,而是选用MultiLock的原因。
如果非要做一个补偿方案不可,那就是目前的Redisson源码要重写,不论lockKey有多少个,所有的key需要在所有的master节点上创建内存数据,但是redis主节点存储数据是只能分片,不能冗余。所以需要加前后缀,相同前后缀规则就会得到相同互斥key。比如key test在A、B、C节点上的数据就是testA、testB、testC,哪怕只有一个key,即可实现过半机制,从而在算法功能和业务功能上都可以将RedLock适用场景拓展开。也算是修补算法实现的不足。(算法本身没问题,算法实现的不太好)
我再此主要列举了业务上可能出现冲突的问题,至于RedLock因为环境因素的可能导致的问题,网络上好多人总结过了,我就不赘述,但还是引用一下:
问题1: 宕机重启之后,2个客户端拿到同一把锁。
假设5个节点是A, B, C, D, E,客户端1在A, B, C上面拿到锁,D, E没有拿到锁,客户端1拿锁成功。 此时,C挂了重启,C上面锁的数据丢失(假设机器断电,数据还没来得及刷盘;或者C上面的主节点挂了,从节点未同步)。客户端2去取锁,从C, D, E 3个节点拿到锁,A, B没有拿到(还被客户端1持有),客户端2也超过多数派,也会拿到锁。
解决方案- 延迟重启;但是由于时钟跳变的因素,导致延迟重启时效(无法解决该问题);
问题2:脑裂问题:就是多个客户端同时竞争同一把锁,最后全部失败。
比如有节点1、2、3、4、5,A、B、C同时竞争锁,A获得1、2,B获得3、4,C获得5,最后ABC都没有成功获得锁,没有获得半数以上的锁。官方的建议是尽量同时并发的向所有节点发送获取锁命令。客户端取得大部分Redis实例锁所花费的时间越短,脑裂出现的概率就会越低。 需要强调,当客户端从大多数Redis实例获取锁失败时,应该尽快地释放(部分)已经成功取到的锁,方便别的客户端去获取锁,假如释放锁失败了,就只能等待锁超时释放了(获取锁失败尽快释放,redisson源码已经实现了)
问题3:效率低,主节点越多,获取锁的时间越长;
问题4: 时钟跳跃
刚上面讨论的方案严格依赖时钟,而5台机器上面的时钟是可能有误差的。
时钟跳跃的意思就是:实际时间只过了1s钟(假设),但系统里面2次时间之差可能是1分钟,也就是系统之间发生了跳跃。发生这种情况,可能是运维人员认为修改了系统时间。
时钟跳跃会产生2个后果:
(1)延迟重启机制失效。时钟跳跃可能导致机器挂了立马重启,从而出现上面的问题。
(2)时钟跳跃导致客户端拿到锁之后立马失效。endTime - beginTime 差值太大。这虽然不影响正确性,但影响拿锁的效率。
那么时钟回拨呢?endTime - beginTime会成为负值,不影响算法的正确性。
问题3: 客户端大延迟(比如full GC),2个客户端拿到同一把锁。
理论上,一切有超时强制释放机制的锁,都可能产生这个问题。服务端把锁强制释放了,但是客户端的代码并没有执行完,卡在了某个地方(比如full GC,或者其它原因导致进程暂停),这把锁被分配给了另外一个客户端。
针对这个问题,Redis又提出了watch dog机制。大致意思就是,锁快要到期之前,发现客户端业务逻辑还没执行完,就给锁续期,避免锁被强制释放,分配给另外一个客户端。但是,锁续期本身是个网络操作,也没办法保证续期一定成功!
从这个案例中,可以得到2个重要启示:
(1)在分布式系统中,严格依赖每台机器本机时钟的算法,都可能有风险。
(2)一切具有“超时强制释放机制”的锁,都可能导致客户端还在持有锁的情况下,锁被强制释放。
不管是MultiLock还是RedLock,先抛开业务功能上的缺陷问题(适用场景局限的问题),上面的问题加起来总结成一个就是:业务代码还没有执行完,锁数据直接丢失了,看门狗WatchDog续期都没法续期。
那么我说下我自己的方案:业务计算完毕后,将要通过redisClient修改数据的时候,不要用RedisTemplate这种。而是将所有需要修改的调用,全部打包放到redis事务来执行。在提交事务之前,再次获取一下所有的key是否存在,如果有一个不存在(RedLocK有超过半数的不存在)就手动回滚所有的操作,如果伴有数据库操作,用数据库事务回滚。
Redis事务是通过MULTI、EXEC、WATCH和UNWATCH等命令实现的。在Redisson客户端中,RTransaction对象会在调用commit()方法时生成一个MULTI命令,并将所有操作参数保存在该命令cache中。当事务执行完毕并调用commit()方法时,Redisson会向Redis服务器发送EXEC命令以执行该事务中的所有操作。如果其中任何一个操作失败,Redisson会立即回滚整个事务(加了WATCH命令)。redis事务执行失败就是报错,抛出异常给RedissonTransaction。
- // 定义事务选项
- TransactionOptions options = TransactionOptions.defaults()
- .timeout(1) // 设置事务超时时间,单位为秒,默认为60秒
- .retryAttempts(2) // 设置事务重试次数,默认为3次
- .retryInterval(100); // 设置事务重试间隔时间,单位为毫秒,默认为1000毫秒
-
- // 创建RTransaction对象,开始事务
- RTransaction transaction = redisson.createTransaction(options);
-
- try {
- // 在事务中执行一系列操作
- transaction.getBucket("key1").set("value1");
- transaction.getMap("map1").put("field1", "value2");
- transaction.getSet("set1").add("value3");
-
- // 提交事务
- transaction.commit();
- } catch (Exception e) {
- // 回滚事务
- transaction.rollback();
- } finally {
- // 关闭Redisson连接
- redisson.shutdown();
- }
redis的事务的作用在此体现出来了,相比Lua脚本,执行失败会回滚前面所有操作。
业务代码怎么实现回滚呢?这就更简单了,只需要clear掉cache里面的所有指令,并且释放还存在的key锁就行。
更多资源分享,请关注我的公众号:搜索或扫码 砥砺code
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。