聊聊分布式锁
聊聊分布式锁
关于分布式锁
分布式锁一直是分布式系统绕不过的话题,特别是微服务大行其道的今天,系统更依赖分布式锁实现正确的业务功能。因为部署多个相同的业务系统模块,就要求对共享资源互斥操作,不然就造成数据的不一致。如:商品减库存、防止MQ重复业务消息同时执行等。
分布式锁有那些开源实现
常见的有以下几种分布式锁,或者如果搜索”分布式锁”,通常是以下这些实现:Redis-setnx、Redission、Redlock、ZooKeeper、etcd。那我们来看下这些分布式锁的实现方式和优缺点。先说明一点前提,我们在技术选型的时候,往往是要根据具体的业务场景做选择的,不是做纯粹的技术讨论,因为技术是为业务服务的。
Redis setnx
基于Redis锁主要利用Redis的setnx命令实现的,为了防止死锁setnx 和 expire必须一起使用,早期为了解决setnx 和 expire 非原子性通常使用lua脚本解决这个问题。当然Redis在 2.6.12版本以后解决了setnx 和 expire 非原子性的问题了。
在实现业务场景中,为了防止锁误解除、获取锁失败时阻塞等还得写不少的代码.就是说如果你在想生产环境使用,还是得费不少精力解决这些问题,比如重入锁、租期续约、防止锁的误删除等功能,都需要额外的开发。
生产环境为了redis避免单点故障,通过Sentinel或集群实现故障转移,但是Redis的主从是异步通讯的。如果是在主加锁,然后主挂了,从还没有同步加锁信息,那么客户端的业务线程就会出现并行的情况,这个就没有安全性了。
所以我们总结一下就是,基于Redis的setnx只是提供分布式锁的原子性,其它的特性如:安全性、活性等功能,还需要额外实现。如果要在生产环境使用健壮、易用的分布式锁,还需要额外手段保障数据的一致性和安全性。
小米技术部的文章,总结的比较到位,供大家参考。https://xiaomi-info.github.io/2019/12/17/redis-distributed-lock/
Redission
Redisson也是java的redis客户端之一,Redisson提供了分布式锁的API操作,相对setnx更容易用、功能更强大。Redisson加锁、释放锁操作也是通过lua脚本完成的。
Redisson分布式锁的API对开发人员来说,比较友好,相对容易操作。在锁的特性方面,Redisson的锁是支持重入锁和公平锁的。
Redisson支持单实例和哨兵模式,但是哨兵模式的安全性和一致性,我没有测试过,不好说。
Redisson的WatchDog 机制能够很好的解决锁续期的问题,这是一个很大的进步。因为业务端是很难预测业务需要占用锁的时间,为了避免业务还没有执行完就释放分布工锁了,所以占用锁的时间就设置得比较长,从而锁的粒度就比较粗。有了WatchDog就可以改善这个问题了,可以定期去续锁的租期,让锁的粒度更小。
总结一下,使用Redisson作为分布式锁比Redis的setnx命令更容用,功能更强大,这是Redisson的进步之处。缺点还是redis基于异步的主从复制,从而在发生主从切换时,会出现两个线程同时获取到锁的情况。可以看出来,这个是基于Redis实现分布锁的通病。因为本质上Redis还是AP模型,而不是一个CP模型。另外,Redis的服务端的数据复制是异步的,没有一致性算法保障的。如果你的业务场景能够容忍这个缺点,使用Redisson是没有问题的,因为Redis基本是现成的,学习和部署起来也很简单。
有一篇关于Redisson文章,供大家参考。https://github.com/angryz/my-blog/issues/4
Redlock
因为基于Redis实现的分布锁使用的人比较多,而且网上对此方案的缺点讨论的也比较多。Redis的创始人重新设计一套分布式锁的方案, Redlock算法,当然此方案也是基于Redis。Redlock比普通的单实例方法更安全,解决Redis单点故障的问题。Redlock的算法也是在客户端实现的,当然这个方案也要服务端支持的。目前Redisson已经支持Redlock算法,但是没有使用过,不作展开讨论。但是有三篇文章关于Redlock算法是否安全的推荐给大家,看下大佬是怎么分析问题的,非常精彩。
https://redis.io/topics/distlock#correct-implementation-with-a-single-instance
https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
http://antirez.com/news/101
ZooKeeper
ZK是基于ZAB协议来实现数据一致性的,而ZAB在Paxos的基础上设计了一种一致性协议,所以ZK是属于强一致性模型,也就是CP模型。基于Curator客户端开发分布式锁是比较易用的。ZK在实现分布式锁的时候,基于watch模式,获取锁时避免轮询造成CPU的浪费,而Redis是基于轮询获取锁的。由于ZK为了保障数据的强致性,也是付出一些代价的。ZAB 协议对每一个写请求,会在每个节点写一个事务日志,定期的将内存数据保存到磁盘,以保证数据一致性和持久性,方便集群宕机之后的数据可恢复,这些特性对CP模型是必须的,但性能是有影响的。
所以ZK作为分布式协调系统,不适合作为频繁的读写存储系统,能支持并发数也不高。在性能方面,就是redis完胜了。ZK特殊情况下,连接断了以后无法续租。而且在使用ZK必须要规避一些坑,前提条件就是你要先了解,不然在生产环境就够你喝一壶了。
这是阿里巴巴总结的,供大家参考。https://developer.aliyun.com/article/227260
etcd
etcd是一个分布式的,用于存储关键数据、可靠key-value存储的分布式系统。ectd是基于rafe协议开发的,属于强一致性的CP模型,已经应用于k8s等一系统项目。在etcd的V2版本java客户端对分布式锁的封装得不到位,但是V3的java客户端已经改进了很多。其实,你会发现etcd和ZK有很多相同的点,如:都是强一致模型,都是可靠key-value存储、服务器端均有一致性算法支撑等。那我们为什么不选ZK,而是选择etcd呢?
etcd比ZK的性能要好,可以支持更高的并发。etcd可以很方便进行锁的续租,这一点非常重要,因为很多业务是不知道执行所需要的时间的。
也就是说,如果你的业务场景要求你的系统强一致性的,比如商品库存、余额等操作时,就使用强一致性模型-CP模型。在CP模型中可以选择ZK或etcd,因为etcd的并发性能高、锁的粒度更细、V3版本的SDK易用性好,所以选择etcd。如果业务场景不是要求强一致性的,那么用基于Redis方案也是OK的,因为基本上大部分的公司都使用redis作缓存,而且redis的还能支持更高的并发数,团队的学习成本相对低一点。建议单独部署Redis作为分布式锁服务器,避免与作用于缓存的Redis共用。
基于ectd源代码示例
"Talk is cheap. Show me the code." — Linus.
参考代码:https://github.com/afuafu99/example/tree/main/dlock-example
ectd生产部署
生产环境使用集群部署,避免单点故障并实现故障转移。etcd的集群部署三种方式,Static、etcd Discovery、DNS Discovery。如果后期需要对集群进行动态扩容,建议使用etcd Discovery或DNS Discovery模式;如果不需要动态扩容,就直接Static方式部署。
生产环境部署规模与硬件,我觉得官方的文档就已经讲得很详细,这里直接给出连接,可以结合自己规模来计算部署的资源。https://etcd.io/docs/v3.4.0/op-guide/hardware/
需要强调二点,就是生产环境必须使用SSD,而且尽量使用Nvme协议SSD。因为etcd为了实现强一致性,每个请求都将每个请求写入磁盘,还有其它的磁盘操作(如快照等操作),所以磁盘IO对etcd性能影响很大。网络也是要注意的,因为etcd是使用一主多从的模型,所以主从的之间会有大量的心跳和数据复制,占用大量的带宽。所以etcd尽量部署在同一个机房,如果要跨机房部署就必须考虑网络延迟问题。官方文档网络的心跳给了详细建议:https://github.com/etcd-io/etcd/blob/master/Documentation/tuning.md
关于etcd的监控,etcd提供接口获取内部运行的数据,数据为 prometheus 标准格式。通常”etcd_disk_wal_fsync_duration” 磁盘读写延迟很关键,延迟过高会导致集群出现问题。另外还需要跟踪的是,从客户侧监控分布式锁的性能,就是获取锁top90等指标,这个需要在二次开发。
后记
上面的分布式锁内容,属于日常工作中的总结,浅尝辄止,不足之处,欢迎指正。