1. SETNX方案
//加锁
SET key uuid EX 10 NX
//释放锁,先判断是否自己线程,再释放
if (redis.get("lock") == $uuid){
redis.del("lock")
}
好处:
- 原子操作,加锁安全
- 有过期时间,不会造成死锁
- 释放锁时校验线程id,不会释放其他线程的锁
缺点:
- 释放锁不是原子操作,有并发问题
- 锁过期时间难以估计
解决方案:
- 使用lua脚本保证原子操作
- 增加看门狗机制,加锁时,开启守护线程,定时检测锁失效时间,重新续期
2. Redisson方案
RLock lock =redissonClient.getLock("key");
try {
boolean tryLock = lock.tryLock(tryTime, holdTime, TimeUnit.MILLISECONDS);
if(!tryLock){
//加锁失败
log.info("加锁失败");
}
//执行业务逻辑
} catch (Exception e) {
e.printStackTrace();
} finally {
//解锁
if (lock != null && lock.isLocked()&&lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
优点:
- api方便
- 集成看门狗和原子操作,且有公平/非公平锁实现,
缺点:
- 主从集群下可能因为主从复制而丢失分布式锁
在主节点上加锁,加锁成功后,未来得及主从复制到从节点,主节点挂掉,从节点被选为新的主节点,锁就丢失啦
解决方案: 分布式redlock
3. RedLock方案
3.1 前提
- 不需要哨兵和从库,只部署主库
- 部署至少5个redis主库(非集群模式,5个redis示例就行)
3.2 获取锁流程
- 客户端先获取「当前时间戳T1」
- 客户端依次向这 5 个 Redis 实例发起加锁请求(用前面讲到的 SET 命令),且每个请求会设置超时时间(毫秒级,要远小于锁的有效时间),如果某一个实例加锁失败(包括网络超时、锁被其它人持有等各种异常情况),就立即向下一个 Redis 实例申请加锁
- 如果客户端从 >=3 个(大多数)以上 Redis 实例加锁成功,则再次获取「当前时间戳T2」,如果 T2 - T1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败
- 加锁成功,去操作共享资源(例如修改 MySQL 某一行,或发起一个 API 请求)
- 加锁失败,向「全部节点」发起释放锁请求(前面讲到的 Lua 脚本释放锁)
3.3 总结
- 客户端在多个 Redis 实例上申请加锁
- 必须保证大多数节点加锁成功(容错,大部分成功就行)
- 大多数节点加锁的总耗时,要小于锁设置的过期时间(保证加锁有意义)
- 释放锁,要向全部节点发起释放锁请求(部分加锁请求可能是网络原因失败,实际redis已经加锁成功,这部分也需要释放掉)
分布式系统存在三座大山
- N:Network Delay,网络延迟
- P:Process Pause,进程暂停(GC等)
- C:Clock Drift,时钟漂移
3.4 redlock的问题
- P问题导致锁超时问题,如果客户端1获取锁后遇到GC导致长时间未处理结束业务,而分布式锁超时释放了,客户端2获取锁之后,客户端1和2就会并发写数据,形成竞态条件
- C问题导致锁超时问题,如果客户端1获取ABC节点的锁,客户端2获取DE的锁,此时C节点发生时钟漂移,释放锁,随后被客户端2获取,这样两个客户端均认为自己获取到了锁,形成竞态条件
3.5 解决方案
引入fencing Token机制,共享资源服务对这个token进行互斥校验
- 客户端在获取锁时,锁服务可以提供一个「递增」的 token
- 客户端拿着这个 token 去操作共享资源
- 共享资源可以根据 token 拒绝「后来者」的请求
3.6 对fencing token方案的质疑
- 如果NPC问题在获取锁之前发生,则再加锁流程第三步获取T2-T1时间差与锁过期时间对比,便可以解决.
- 如果NPC问题在获取锁之后发生,则无法避免这个问题, 因为现有的分布式锁都无法避免此问题
- 大部分公共资源服务(mysql可以,读写文件/http请求等服务就不行)没有互斥能力,如果有互斥能力,就不需要分布式锁啦
4. Zookeper分布式锁
Zookeeper分布式锁是基于临时节点,即与ZK的连接
4.1 加锁流程
- 客户端 1 和 2 都尝试创建「临时节点」,例如 /lock
- 假设客户端 1 先到达,则加锁成功,客户端 2 加锁失败
- 客户端 1 操作共享资源
- 客户端 1 删除 /lock 节点,释放锁
4.2 特点
- 持锁线程发生异常崩溃后,与ZK连接断开就会释放锁,不需要考虑锁释放时间
- watch 机制,加锁失败,可以 watch 等待锁释放,实现乐观锁
- 性能不如redis
- 部署和运维成本比较高
- 客户端长时间不像ZK发心跳,会释放锁