聊一聊分布式锁

此文同步发表在知乎账号:聊一聊分布式锁

分布式锁想必已经很常见了,我主要从整体上讲一讲以及一些具体的思路细节和常见的问题总结

特点

  • 互斥:锁必须是互斥的,即不能两个线程同时拿到锁。redis中我们可以lua脚本保证原子性,因为redis本身就是单线程的
  • 防止死锁:即不能出现某个锁迟迟不能释放
  • 可重入:即一个线程对资源加锁后可以对同一个资源再次加锁

实现方式

数据库
  • 原理
    • 乐观锁:加version
    • 悲观锁:select for update
  • 缺点:发生竞争时会涉及到事务回滚,性能较低、浪费资源
  • 优点:无需引入额外的分布式锁组件,维护成本低
zookeeper

满足CAP中的CP

  • 原理
    • 客户端 1 和 2 都尝试创建「临时节点」,例如 /lock
    • 假设客户端 1 先到达,则加锁成功,客户端 2 加锁失败
    • 客户端 1 操作共享资源
    • 客户端 1 删除 /lock 节点,释放锁
  • 优点
    • 不需要考虑锁的过期时间,客户端连接中断则锁被释放
    • watch 机制,加锁失败,可以 watch 等待锁释放,实现乐观锁
  • 缺点
    • 性能不如redis
    • 部署和运维成本高
    • 客户端与 Zookeeper 的长时间失联,锁被释放问题
Redis

Redis本身也有悲观锁和乐观锁的实现,悲观锁获取不到锁则阻塞,乐观锁获取不到则直接返回,一般我们实际更多的使用乐观锁。Redis实现分布式锁应该是目前使用最广泛的,下面也结合分布式锁的特点,讲一讲redis如何实现分布式锁(实质上是Redission的实现方式)

Redission

加锁流程
keys: ["lockkey"];
args:["leaseTime","randUUid+threadId"] //threadId前面需要加上randUUid:避免不同机器threadId重复
if(redis.call('exists',keys[1]) ==0) then  //如果key不存在,即没有加锁
  redis.call('hset',keys[1],args[2],1); //记录当前线程加锁次数为1,实现锁的可重入
  redis.call('pexpire',keys,args[1]); //记录锁过期时间
  return 
if(redis.call('hexists',keys[1],args[2]) == 1) then //如果当前key存在,且加锁线程是当前线程
  redis.call('hincryby',keys[1],args[2],1); //加锁次数+1
  redis.call('pexpire',keys[1],args[1]); //锁续期
return redis.call('pttl',keys[1]); //返回锁剩余时间
解锁流程
keys:["lockkey","channelName"]
args:["unlockMessage","leaseTime","randUUid+threadId"]
if(redis.call('exists',keys[1]) == 0) then //如果key不存在,说明早已经解锁了
 redis.call('publish',keys[2],args[1]) //通知等待获取锁的线程,锁被释放了,可以尝试获取锁
  return
if(redis.call('hexists',keys[1],args[3]) == 0) then //说明解锁的线程和加锁的线程不一致,无法解锁
  return nil;
local counter = redis.call('hincrby',keys[1],args[3],-1) //解锁,count--,考虑重入情况
if(counter > 0) then //说明锁被重入了,还需要被加锁的线程持有
 redis.call('pexpire',keys[1],args[2]) //锁续期
 return 0;
else
 redis.call('del',keys[1]); //解锁,删除key
 reids.call('publish',keys[2],args[1]); //通知等待获取锁的线程,锁被释放了,可以尝试获取锁
  return 1;
end
return nil;
看门狗watch dog
  • 目的:解决我们加锁时,不好设置锁过期时间从而导致锁失效问题
  • 原理:watch dog只有在业务方加锁时没有显示设置锁过期时间才会生效,要使watchLog机制生效,我们在加锁时不要设置锁过期时间。当我们使用Redission加锁没有设置过期时间时,Redission会初始默认设置过期时间为30然后watch dog 在当前节点存活时每10s给分布式锁的key续期30s,机制启动且代码中没有释放锁操作时,watch dog 会不断的给锁续期;如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally 中
  • watchlog的延时时间 可以由 lockWatchdogTimeout指定默认延时时间,但是不要设置太小
  • watchdog 会每 lockWatchdogTimeout/3时间,来实现异步延时,最终还是通过lua脚本来进行延时

常见问题

事务中使用分布式锁会有问题吗,怎么解决?

我们一般都会使用spring的声明式注解,但是注意一定不要在事务内部释放锁,可以有两种方案

  • 一是在外层加锁和解锁,锁的逻辑包裹事务,建议使用这种方式
  • 二是如果在事务内加锁了,那么可以通过TransactionSynchronizationManager注册事务监听器,事务提交后再释放锁
redis分布式锁会存在锁失效问题吗,怎么解决?

redis锁失效主要会存在故障转移时,比如在master加锁了,master挂了,数据还没有同步到slave,slave升级成master,之前的加锁信息丢失了,导致可以重新加锁,可以采用Redlock实现

  • RedLock
    • 多节点redis的分布式锁,只要大部分redis节点存活(半数以上),就可以正常提供服务
      前提:不再需要部署从库和哨兵实例,只部署主库;但主库要部署多个,官方推荐至少 5 个实例,注意不是集群
    • Redission对RedLock进行了实现

如果不采用RedLock,那么我们的系统需要做好兜底逻辑,比如利用数据库的唯一键或者乐观锁,当出现了锁失效,两个请求同时拿到了锁做了数据更新操作,利用数据库的机制保障数据的准确性

实际如何使用分布式锁的?分布式锁的过期时间是怎么设置的?

有两种方式,不过无论哪种方式,务必保证在finally中释放锁

  • 评估接口耗时,然后加锁时设置一个较大的过期时间,平均rt的几倍,然后finally释放锁
  • 加锁时不设置过期时间,redission默认初始过期时间是30s,然后启动看门狗,每隔10s续期过期时间30s,也是需要finally中释放,一般这种用的比较多
Redis分布式锁会死锁吗?比如服务器重启或者机器突然断电

理论上不会,如果加锁时设置了过期时间则会到期释放,如果没有设置使用Redission时会初始设置,同时通过看门狗自动续期。比如正常服务器重启会等待请求执行完成,如果出现异常比如断电,那么看门狗续期也会中断,最终锁都会有一个失效时间。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zlp1992

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值