Java锁
- synchronized
- Lock
单机锁,不适合分布式场景
分布式场景下的锁
锁的种类
- MySQL
- Zookeeper
- Reids
MySQL
自带悲观锁for update关键字;
- 优点:理解起来简单,不需要维护额外的第三方中间件(比如Redis,Zk)
- 缺点:需要自己考虑锁超时,加事务等等,性能局限于数据库
Zookeeper
Zookeeper提供一个多层级的节点命名空间(节点称为znode),每个节点都用一个以斜杠(/)分隔的路径表示,而且每个节点都有父节点(根节点除外),类似于文件系统。
有序节点
客户端申请向ZK加一个key为“my_lock”锁直接在“my_lock”这个锁节点下,创建一个顺序节点,这个顺序节点有zk内部自行维护的一个节点序号。比如客户端A申请加锁,会创建一个“xxx-0001”的节点,客户端B申请加锁创建一个“xxx-002”的节点,创建节点成功后,客户端会判断序号最小的节点,如果是则加锁成功。
临时节点
考虑这么个场景:假如客户端A当前创建的子节点为序号最小的节点,获得锁之后客户端所在机器宕机了,客户端没有主动删除子节点;如果创建的是永久的节点,那么这个锁永远不会释放,导致死锁;由于创建的是临时节点,客户端宕机后,过了一定时间zookeeper没有收到客户端的心跳包判断会话失效,将临时节点删除从而释放锁。
事件监听
如果获得锁的客户端释放锁时将其他客户端全部唤醒,当客户端数量较多时容易阻塞其他操作。所有客户端都被唤醒,这种情况被称之为“羊群效应”。
最好的情况应该只唤醒新的最小节点对应的客户端,做法就是通过事件监听,每一个客户端监听他所创建节点的前一个节点。
Redis
提供了SETNX(set if not exists)这样具有互斥性的指令
Redis锁
使用
Java自带的关键字synchronized和jdk中的Lock类只适合单个应用,不适合分布式场景,通过Redis可以设置分布式锁,spring集成Redis后的RedisTemplate提供了API
1 | @Component |
引发的问题
锁未释放
当服务A获取的Redis锁后因为某些意外情况挂了,这个时候服务A还没来得及删除Redis锁,这样的话其他服务就都无法获取锁,这个时候时候就要给锁设置上超时时间
1 | /** |
但是设置了超时时间后又引出了另一个问题:当执行的业务逻辑过于复杂或者其他原因,执行时间超出了锁的时间,导致其他线程提前获取了锁。这种情况下就要设置合理的超时时间但是又不能过长,这个时候就需要每隔一段时间延长加锁时间。
1 | /** |
锁释放的安全性
Redis的加锁实际上是通过判断set一个key成功来判断的,释放锁则是将这个key删除。
当使用synchronized或者Lock持有锁后,释放锁的动作也只能有持有锁的线程来执行,但是现在这种情况我们只要调用RedisLock#releaseLock方法就可以删除Redis的key从而释放锁。为了保证释放锁的安全我们要记录下加锁的线程,只有加锁的线程才能手动释放锁。
1 | /** |
可重入
synchronized或者ReentrantLock都是可重入锁,即当前线程可以重复持有锁,同时加锁多次就需要解锁多次。AQS中有个state变量作为计数器,这里也需要一个计数器来记录加锁次数
1 | /** |
阻塞
synchronized竞争锁是阻塞性的,ReentrantLock也实现了阻塞性的lock方法
1 | /** |
Redisson锁
Redisson锁其实就是解决了上面所说的问题。源码大概流程(补充了一些自己的注释):
加锁&可重入
1 | /** |
异步续时
1 | private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) { |
阻塞
1 | /** |
RedLock (红锁)
Redisson锁的问题
在Redis主从的情况下,我们加锁是在加主库上,然后主库异步复制到从库。
但是如果发生了主从切换可能就存在锁丢失的风险:
- 客户端加锁成功
- 主库发生异常宕机,key未同步到从库
- 从库被升级为新的主库,key丢失(锁丢失)
为此,Redis的作者提出的RedLock的解决方案
RedLock的前提
Redlock 的方案基于 2 个前提:
- 不再需要部署从库和哨兵实例,只部署主库
- 但主库要部署多个,官方推荐至少 5 个实例
RedLock的流程
- 客户端先获取「当前时间戳T1」
- 客户端依次向这 5 个 Redis 实例发起加锁请求,且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁
- 如果客户端从 >=3 个(半数以上) Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败
- 加锁成功,去操作共享资源
- 加锁失败,向「全部节点」发起释放锁请求
1 | @Component |