聊聊常用的分布式锁

概述

在分布式场景下,通常需要通过锁来解决共享资源问题,当然能通过无锁来避免就最好,毕竟锁是有性能开销的;当前常用的分布式锁主要有redis和zk,下面主要讲解下两者的实现、场景对比及存在的问题等。

常规方案

zookeeper

zk主要是基于创建临时顺序节点和watch监听机制来实现。

临时顺序节点:保证锁的公平,同时保证锁的释放(客户端断开的话会自动删除节点)

watch机制:等待锁的节点会监听前一个节点的删除事件,这样可以避免羊群效应(所谓羊群效应就是一个节点挂掉,所有节点都去监听,然后做出反应,这样会给服务器带来很大的压力,然而结果却是只能有一个节点获取);这点跟AQS的实现是一致的(FIFO等待队列锁唤醒也是监听前一个节点的事件)

整个大致流程:

  1. 一个分布式锁通常用一个 znode 来表示,如果节点不存在,则先创建节点。假设这里用 /test/lock 来表示一个需要创建的分布式锁;
  2. 独占锁的所有客户端,使用锁节点下的子节点来表示;当某个客户端需要占用锁时,就在 /test/lock 节点下面创建一个临时顺序子节点。比如子节点的前缀是 /test/lock/seq-,则第1个占用的锁就是 /test/lock/seq-00000001 ,下一个子节点则是 /test/lock/seq-00000002,以此类推;
  3. 当客户端创建子节点后,需要获取锁节点下所有的子节点进行判断:判断自己创建的节点是否在所有列表中是序号最小的,如果是,则加锁成功;否则监听前一个子节点的变更事件,等待锁的释放;
  4. 一旦队列中后面的节点获取到前一个节点的变更事件后,判断自己的节点序号是否是最小的,如果是,则加锁成功,否则继续监听,直到获取锁;
  5. 锁获得成功后,开始进行业务处理,完成业务处理后,删除掉对应的节点,完成释放锁工作,以便后续节点能捕获到节点事件变更,从而获取锁。

通过zk可以实现锁的公平、锁的可重入以及读写锁(通过对子节点进行读写分组来实现)。

redis

加锁:主要是通过setNX原子互斥性或者lua脚本的原子性来实现;如实现锁的可重入也可以通过lua脚本来实现。另外这里需要设置过期时间,以免程序异常崩溃一直锁住。

释放锁:由于锁是有过期时间的,正常过期时间设置的比业务执行时间多很多,这样确保业务执行完后才释放锁,但是这个时间是很难把握衡量的,所以从逻辑角度来说,在释放锁的时候需要判断当前释放的锁是否是自己加的锁(在加锁的时候加上锁标识,释放的时候进行判断),否则不能释放;这里需要通过lua脚本来保证原子性。

//下面是不可重入的;如果需要可重入则在判断存在且是自己的时候进行计数加,释放的时候进行减数直到最后删除

//加锁
String lua =
                "if redis.call(\"EXISTS\",KEYS[1]) == 0 then\n" +
                        "    redis.call(\"SET\",KEYS[1], ARGV[1])\n" +
                        "    redis.call(\"EXPIRE\",KEYS[1], ARGV[2])\n" +
                        "    return 1\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";



//释放锁
String lua =
                "if redis.call(\"GET\",KEYS[1]) == ARGV[1] then\n" +
                        "    redis.call(\"DEL\",KEYS[1])\n" +
                        "    return 1\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";

当然针对业务执行时间有可能超过过期时间的问题,也可以通过看门狗(定时续锁的过期时间)的方式解决。JAVA的话整体可以参考Redisson。

注意的点:

1、多命令操作时需要保证原子性;例如通过lua脚本
2、如果是集群模式,多个命令操作需要确保是在同一个节点;如果是lua脚本,可以通过统一key前缀来确保所有key散列到同一节点,例如 {common}:buss:lock, 用“{}”来表示
3、在锁释放时需要判断当前锁释放是自己加的

4、针对加锁等待重试时,需要加上随机等待时间,避免羊群效应

对比

指标zookeeperreids
性能zk的加锁和释放锁都是通过操作节点来实现,这样只能在leader节点上操作,同时需要同步到半数以上的follower节点上,这样性能是比较差的redis的主从同步是异步的,同时如果是集群的模式,不同的锁能散裂到不同的节点进行操作,即可以有多个操作节点,所以整体性能比zk好很多
可靠性zk的cp模式保证了一致性,相对比较可靠由上所知,由于主从同步是异步的,当主节点异常的时候,选举新的从节点为主节点,这个时候有可能之前加在原主节点上的锁还没同步到从节点,从而引起锁的丢失,造成同一时间有多个相同的锁,所以可靠性比zk弱
可用性相当AP模式,好些

针对上面提到redis主从异步同步带来的问题,也可以通过redis的红锁来解决(即过半原则),类似NWR(控制一致性)。

问题考虑

在java语言中,由于存在gc,在异常情况下有可能stop the word 的时间比较长,超过了锁的超时时间,这样同时可能产生多个锁的问题。

针对这个问题,当然需要解决gc长时间的问题;另外也可以在业务处理完后结合锁判断(判断锁是否存在或者合法)+ 事务来进行处理。需要注意的是GC有可能发生在任意期间,也许恰巧在判断完后发生;或者又存在网络延迟的问题;这里某些情况只能尽少的减下机率,还是要考虑原子性的问题

其他

  • 通过数据库的排他锁来实现, for update
    • 好处:无需引入第三方组件,架构简单
    • 不足:性能不足
  • 另外如果场景符合的话可以考虑CAS来实现无锁,前提是竞争不激烈,否则回滚、重试的代价比较大;同时需要的话可以加上版本号来解决ABA的问题

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值