[TOC]
1. 高效分布式锁
互斥
在分布式高并发的条件下,我们最需要保证,同一时刻只能有一个线程获得锁,这是最基本的一点。
防止死锁
在分布式高并发的条件下,比如有个线程获得锁的同时,还没有来得及去释放锁,就因为系统故障或者其它原因使它无法执行释放锁的命令,导致其它线程都无法获得锁,造成死锁。
所以分布式非常有必要设置锁的有效时间,确保系统出现故障后,在一定时间内能够主动去释放锁,避免造成死锁的情况。
性能
对于访问量大的共享资源,需要考虑减少锁等待的时间,避免导致大量线程阻塞。
所以在锁的设计时,需要考虑两点。
- 锁的颗粒度要尽量小。比如你要通过锁来减库存,那这个锁的名称你可以设置成是商品的ID,而不是任取名称。这样这个锁只对当前商品有效,锁的颗粒度小。
- 锁的范围尽量要小。比如只要锁2行代码就可以解决问题的,那就不要去锁10行代码了。
重入
我们知道ReentrantLock是可重入锁,那它的特点就是:同一个线程可以重复拿到同一个资源的锁。重入锁非常有利于资源的高效利用。关于这点之后会做演示。
2. 单机锁
使用setnx()方法获得锁
用eval执行lua脚本删除锁
用lua脚本可以做到原子操作
详细使用: https://www.cnblogs.com/linjiqin/p/8003838.html redisson也支持单机部署,而且使用更简单
3. redisson锁
3.1 原理
Redisson是一个基于java编程框架netty进行扩展了的redis。 Redisson是架设在Redis基础上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了Redis键值数据库提供的一系列优势,基于Java实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作其底层是实现Lock接口实现的。
加锁和解锁都是通过lua脚本来执行,
3.1.1 可重入加锁机制
对同一个锁,可以lock多次,对应的也需要unlock多次.
Redisson可以实现可重入加锁机制的原因,我觉得跟两点有关:
- Redis存储锁的数据类型是 Hash类型
- Hash数据类型的key值包含了当前线程信息。
具体值: 这里数据类型是Hash类型,Hash类型相当于我们java的 <keyName,<field,value» 类型,
这里keyName是指 ‘redisson’,
field值它的组成是:uuid + 当前线程的ID,
value是重入次数
uuid是客户端实例化时就创建好了,它是客户端的标识
重入过程:
Redisson实现分布式锁(1)—原理 - 雨点的名字 - 博客园 (cnblogs.com)
3.1.2 (watchDog)看门狗
在持有锁的时间内,业务没有执行完,怎么办?(应该继续拥有锁,知道业务执行完成),所以需要一个线程去监听.
原理: 额外启动一个线程,每隔10s检测业务是否完成,未完成则续期
使用: 不设置锁的失效时间,看门狗则自动生效
只有获得锁时没有指定失效时间,看门狗才会生效,默认失效时间为30s,
不过要注意,如果业务(比如数据库)出现死锁,导致看门狗一直续期,整个程序就会死锁,这种情况要好生处理,不要让业务出现死锁
3.1.3 加锁过程
先竞争锁,成功后设置看门狗,
尝试获得锁的场景下,如何解决锁竞争?
使用非公平锁(默认场景): 使用发布订阅的方式竞争, 订阅后异步等待(有tryLock嘛),在死循环中尝试加锁(默认等7.5s),成功或失败都取消订阅
使用公平锁 : 在redis中维护一个list做队列, 用队列来排队拿锁 , 整个过程使用lua脚本维护
Redisson 分布式锁实现之源码篇 → 为什么推荐用 Redisson 客户端 - 云+社区 - 腾讯云 (tencent.com)
【分布式锁】02-使用Redisson实现公平锁原理_meser88的博客-CSDN博客
3.1.4 释放锁过程
重入锁的释放
锁释放后发布消息
以上两步用lua脚本完成
取消看门狗
Redisson 分布式锁实现之源码篇 → 为什么推荐用 Redisson 客户端 - 云+社区 - 腾讯云 (tencent.com)
公平锁的方式和非公平锁差不多,只是不用发布释放锁的信息,释放后,队列也不删除,一把锁一个队列
Redisson 分布式锁源码 07:公平锁释放 - 知乎 (zhihu.com)
3.2 使用
# 引入包
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.4.3</version>
</dependency>
# 初始化bean
@Bean
RedissonClient getRedissonClient() {
Config config = new Config();
SingleServerConfig serverConfig = config.useSingleServer().setAddress("redis://"+host+":"+port).setConnectTimeout(10000)
.setTimeout(3000).setIdleConnectionTimeout(10000)
.setRetryInterval(1500).setRetryAttempts(3)
.setConnectionPoolSize(64).setConnectionMinimumIdleSize(10);
if (password != null && !"".equals(password)) {
serverConfig.setPassword(password);
}
return Redisson.create(config);
}
@Autowired
private RedissonClient redissonClient;
# 获得锁
lock = redissonClient.getLock(""lockName"")
lockFlag = lock.tryLock(0, 5, TimeUnit.SECONDS);
if(lockFlag){
// 一大堆代码
}
# 解锁
lock.unlock();
3.3 工具类
4. 缺陷
由于节点之间是采用异步通信的方式。如果刚刚在 Master 节点上加了锁,但是数据还没被同步到 Salve。这时 Master 节点挂了,它上面的锁就没了,等新的 Master 出来后(主从模式的手动切换或者哨兵模式的一次 failover 的过程),就可以再次获取同样的锁,出现一把锁被拿到了两次的场景。
加锁的时候会找到一个主节点进行加锁
其解决方案:
redisson有redLock方案,红锁是多把锁合成一把锁,获得锁时会向全部节点发送lua脚本申请锁,只有获得(n/2+1)个节点的锁时才真正获得锁,(获得锁需要时间,而锁也有过期时间,所以最终时间是过期时间减去获得锁花费的时间)
https://segmentfault.com/a/1190000016976564?utm_source=tag-newest
5. redLock
核心概念就是获得锁需要节点数过半才能获得锁
假设 有锁的时候死了一台,剩下节点上的锁没有释放, 别人要来拿锁的时候,就凑不齐半数了,
但是如果死节点获得了锁,死了后马上就启动,也不行,还是会出现分布式锁的问题,所以需要延迟重启,就是等redis上的key都过期了再启动
需要延迟启动场景:
假设我们一共有 A、B、C 这三个节点。
1.客户端 1 在 A,B 上加锁成功。C 上加锁失败。
2.这时节点 B 崩溃重启了,但是由于持久化策略导致客户端 1 在 B 上的锁没有持久化下来。 客户端 2 发起申请同一把锁的操作,在 B,C 上加锁成功。
3.这个时候就又出现同一把锁,同时被客户端 1 和客户端 2 所持有了。
【原创】(求锤得锤的故事)Redis锁从面试连环炮聊到神仙打架。 - why技术 - 博客园 (cnblogs.com)
7.1 加锁过程
假设有5个完全独立的redis主服务器
获取当前时间戳
client尝试按照顺序使用相同的key,value获取所有redis服务的锁,在获取锁的过程中的获取时间比锁过期时间短很多,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取下一个redis实例。
比如:TTL为5s,设置获取锁最多用1s,所以如果一秒内无法获取锁,就放弃获取这个锁,从而尝试获取下个锁
client通过获取所有能获取的锁后的时间减去第一步的时间,这个时间差要小于TTL时间并且至少有3个redis实例成功获取锁(过半原则),才算真正的获取锁成功
如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移);
如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例;因为可能已经获取了小于3个锁,必须释放,否则影响其他client获取锁
7.2 释放锁过程
7.3 缺陷
总的来说因为redLock强依赖时间,而在分布式架构中,时间不是完全可靠的,所以导致了一系列问题
7.3.1 获得锁期间发生阻塞
作者 Martin 给出这张图,首先我们上一讲说过,RedLock中,为了防止死锁,锁是具有过期时间的。这个过期时间被 Martin 抓住了小辫子。
- 如果 Client 1 在持有锁的时候,发生了一次很长时间的 FGC 超过了锁的过期时间。锁就被释放了。
- 这个时候 Client 2 又获得了一把锁,提交数据。
- 这个时候 Client 1 从 FGC 中苏醒过来了,又一次提交数据。
这还了得,数据就发生了错误。RedLock 只是保证了锁的高可用性,并没有保证锁的正确性。
这个时候也许你会说,如果 Client 1 在提交任务之前去查询一下锁的持有者是不自己就能解决这个问题? 答案是否定的,FGC 会发生在任何时候,如果 FGC 发生在查询之后,一样会有如上讨论的问题。
那换一个没有 GC 的编程语言? 答案还是否定的, FGC 只是造成系统停顿的原因之一,IO或者网络的堵塞或波动都可能造成系统停顿。
7.3.2 发生时间漂移等时间不一致问题
如果某个 Redis Master的系统时间发生了错误,造成了它持有的锁提前过期被释放。
- Client 1 从 A、B、C、D、E五个节点中,获取了 A、B、C三个节点获取到锁,我们认为他持有了锁
- 这个时候,由于 B 的系统时间比别的系统走得快,B就会先于其他两个节点优先释放锁。
- Clinet 2 可以从 B、D、E三个节点获取到锁。在整个分布式系统就造成 两个 Client 同时持有锁了。
没有完美的方案,就看使用者怎么权衡
看zookeeper方式有没有更好的方案?
Redis RedLock 完美的分布式锁么? - 云+社区 - 腾讯云 (tencent.com)
6. 其他分布式锁
4. Spring Integration
是spring系列的,是一种便捷的事件驱动消息框架用来在系统之间做消息传递的,
分为 Message -> MessageChannel -> Message Endpoint, 中间还有 Channel Interceptor
在分布式锁领域实现方式有
- Gemfire
- JDBC
- Redis
- Zookeeper
这里以redis为例 功能和redisson差不多,使用起来也差不多,但貌似不支持看门狗机制
5. Spring Integration-redis 和 redisson 的区别
内容 | redisson | Integration-redis |
---|---|---|
可重入机制 | 直接在redis中写次数 | 通过reentrantLock类实现重入机制 |
获得锁机制 | 所有线程直接去redis竞争 | 先在应用内部竞争,成功者再去redis竞争 |
6. zookeeper分布式锁
zk是非可重入锁,借助Apache curator框架,可以实现重入锁功能
此方式是通过客户端在zk上创建临时顺序节点来加锁(能创建成功就会表示加锁成功), 释放锁时会通知下一个加锁的客户端进行加锁, 所以它是公平锁, 也没有过期时间和开门狗的机制
在性能上 redis会优于zk, 因为zk是强一致的, redis是最终一致性
【redis】redis和zookeeper分布式锁的区别(优点、缺点)_zk分布式锁与redis分布式锁优缺点-CSDN博客