分布式锁
介绍
分布式锁是用来干嘛的?主要是用来解决分布式场景中,多应用并发争抢共享资源时,且该资源同一时刻只能一个应用访问,就需要通过加锁来让获得锁的应用执行,获取不到锁的应用不执行或者后续执行。
比如:
分布式应用中的job执行,同一时刻只能一个应用上面的job执行,如果每个应用都执行了该job,可能出现问题(该job是发送邮件的,同一时刻就会发送多封邮件);
Mq中的消费者集群中,广播的消息同一时刻只允许一个应用消费该消息,其他的应用不执行,如果执行了可能出现问题(该消息是用来发送短信的,同一时刻如果多个应用消费了该消息发送了多条相同的短信会出现问题)。
满足条件
为了实现分布式锁,该锁应该具备以下条件
1. 高可用:服务稳定性一定要好,性能要好
2. 不会发生死锁:锁一定要释放,可以正常释放,也要系统异常时释放
3. 互斥性:即同一时刻只能一个应用获得锁,其他应用获取失败
等。。。。。。
方式
实现分布式锁的方式一般有三种:
A.数据库乐观锁
B.基于redis的分布式锁
C.基于zookeeper的分布式锁
数据库
介绍
数据库实现分布式锁比较简单,数据库有两种实现方式;悲观锁和乐观锁
悲观锁
通过数据库的唯一索引实现,即获取锁的时候,大家都往数据表中插入一个数据,谁插入成功谁执行,数据库的唯一索引可以实现相同的记录数据表中只能有一份,达到分布锁的目的,但是执行完后要把这条记录删除,防止死锁发生。由于数据库的记录需要再次请求去删除数据,可能存在程序插入成功执行任务的时候失败,没有去删除数据锁,导致其他应用永远都不能成功插入数据锁,有产生死锁的风险。
乐观锁
数据库实现乐观锁是通过版本号实现的,即表中有个版本号的字段,每次更新操作都使版本号+1,有多个应用查询数据时把版本号也查询出来,更新的时候加上条件版本号=查询时的版本号,如果版本号相同则更新成功,同时版本号+1,如果版本号不一致则不会更新成功。
优点
通过数据库实现分布式锁比较简单。
缺点
分布式应用中大部分性能瓶颈都会出现在数据库层面,在用数据库去实现分布式锁效率低下,而且容易产生死锁。
Redis
介绍
通过中间件redis缓存来实现,即并发场景中多个应用通过向同一个redis(或者redis集群)中发送加锁请求,同一时刻只有一个应用加锁成功,其他应用加锁失败,加锁成功的应用执行后续操作,同时在加锁的时候设置锁的失效时间,失效时间到后,自动释放锁,无需应用再次请求释放锁,防止死锁的发生。
实现方式
通过redis的setNx命令来执行,插入成功则返回1,此时设置过期时间,如果返回0说明加锁失败,已经被其他应用抢先获得锁。注意要用lua脚本保证set和expire两个命令一起执行,lua脚本可以保证原子性,否则容易发生set成功而expire失败的风险,导致死锁发生。
踩坑经历
本人在java代码里面执行lua脚本的时候报不能将string转integer的错,脚本如下:
"return redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) "
有两个参数,argv[1]:key名称,argv[2]:超时时间,后来才发现原因是我用的默认的序列化方式(jdk),该序列化会导致往redis set的内容前面会加上乱码字符串,导致argv[2]变成了字符串,进而导致失败,解决办法是更改redis的序列话方式,改成jackson序列化。
优点
redis实现起来方式简单;性能较好
缺点
缓存易丢失;如果没有主动释放锁程序异常了,只能等到失效时间自动释放锁,容易导致其他应用无法及时执行
Zookeeper
介绍
zookeeper有两种加锁方式来实现分布式锁,一种是排他锁,一种是共享锁。
排他锁
比如并发情况下应用都往/exclusive节点下创建/lock的临时节点,zookeeper会保证同一时刻只有一个客户端创建lock节点成功,成功的即代表加锁成功,而失败的在/exclusive节点下创建一个watcher监听,用来监听lock节点的变更情况。那什么时候释放锁呢?有两种情况会触发
获得锁的客户端宕机,那么该临时节点就会被删除(客户端和服务端是长连接);
客户端正常业务结束后,会主动删除创建的临时节点。
一旦/lock节点释放了,zookeeper会通知监听/exclusive节点下的其他客户端,重新获得下一个锁,可以保证客户端应用顺序执行。
共享锁
并发情况下,众多客户端都往/shared节点下床架/lock临时顺序节点,比如客户端1创建了001_lock_r的临时读节点,客户端2创建了002_lock_w的临时写节点,客户端3创建了003_lock_r的临时读节点,zookeeper能够按照时间保证节点的有序性,创建完临时节点后,所有的客户端都对/shared注册子节点的变更watcher监听,同时确定自己的节点序号在子节点中的顺序。
对于读请求,如果没有比自己节点还小的写请求,则说明获取锁成功,可以正常读取;
对于写请求,如果没有比自己小的请求,说明获取锁成功,可以正常写;
其他情况客户端都需要等待。
读写请求后都会删除创建的临时节点,并触发变更操作通知下一轮的请求。
优点
可靠性高,实现复杂度低。
缺点
理解起来比缓存和数据库的要难,性能没有redis实现的好
参考链接