分布锁详解和Redis分布锁原理详解
一、分布式锁
分布式锁概念:
分布式锁其实可以理解为:控制分布式系统有序的去对共享资源进行操作,通过互斥来保持一致性。 举个不太恰当的例子:假设共享的资源就是一个房子,里面有各种书,分布式系统就是要进屋看书的人,分布式锁就是保证这个房子只有一个门并且一次只有一个人可以进,而且门只有一把钥匙。
实现步骤
- 加锁
A获得了钥匙,进入房子里,看书。 - 解锁
A离开房间,并且还了钥匙。 - 锁超时
为了避免死锁,我们可以在一定单位时间内,将A清除房间,并且把钥匙放回原处。
分布式锁引擎
分布式锁的实现有很多,比如基于数据库、memcached、Redis、系统文件、etcd、zookeeper等。它们的核心的理念跟上面的过程大致相同。
二、Redis分布式锁
set命令、setnx命令和lua脚本:
- setnx
用法:SETNX KEY_NAME VALUE
意义:指定的 key 不存在时,为 key 设置指定的值
原理:redis内部使用两条命令实现该功能,set + expire,无法保证原子性。
在2013年,Redis就发布了2.6.12版本,并且官网(set命令页),也早早就说明了“SETNX, SETEX, PSETEX可能在未来的版本中,会弃用并永久删除”。 - set
用法:SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
意义:设置给定 key 的值
原理:redis版本在2.6.12之前,set是不支持nx参数的,如果想要完成一个锁,那么需要两条命令。
2.6.12之后支持nx,保证可以一个进程实现setnx功能。 - lua
- 功能:
set能保证单个key设置,如果需要设置多个key。可以使用lua.
保证原子性的原因说的通俗一点:
就算你在lua里写出花,执行也是一个命令(eval/evalsha)去执行的,一条命令没执行完,其他客户端是看不到的。 - 参考:
我之前的文章:Redis 脚本
官方文档: https://redis.io/commands/eval
redis分布式锁原理
- 多个客户端抢锁
- 抢到锁的客户端执行业务
- 抢到锁的客户端业务执行完,释放锁
- redis分布式锁伪代码
String uuid = xxxx;
// 伪代码,具体实现看项目中用的连接工具
// 有的提供的方法名为set 有的叫setIfAbsent
set Test uuid NX PX 3000
try{
// biz handle....
} finally {
// unlock
if(uuid.equals(redisTool.get('Test')){
redisTool.del('Test');
}
}
- redis分布式锁简单原理图
redis分布式锁 超时处理问题
- 为什么要有超时机制呢?
进程A不讲道理啊,锁没等释放呢,万一崩了,直接原地把锁带走了,导致系统中谁也拿不到锁。
防止客户端异常退出,无法释放锁,所有通过超时机制去释放锁。 - 获取锁的客户端没有执行完业务,锁被超时机制释放了?
如果进程A又不讲道理,操作锁内资源超过笔者设置的超时时间,那么就会导致B进程拿到锁,等进程A回来了,回手就是把其他进程的锁删了。
找不到锁其实还算好的,万一此刻有个进程C过来加锁成功,那么进程B就把进程C的锁释放了。
以此类推,进程C可能释放进程D的锁,进程D…(禁止套娃),具体什么后果就不得而知了。
redis分布式锁 超时处理解决方案
方案一:维护守护进程
- 增加守护进程,进程未执行完业务不让锁超时,给当前进程的锁续加时间。
方案二:释放锁的时候验证
- 加锁的时候加上客户端标识
在用setnx(这里的setnx并不是redis命令,理解加锁操作)的时候,key虽然是主要作用,但是value也不能闲着,可以设置一个唯一的客户端ID,或者用UUID这种随机数。 - 解锁的时候释放标识
当解锁的时候,先获取value判断是否是当前进程加的锁,再去删除。 - 伪代码
String uuid = xxxx;
// 伪代码,具体实现看项目中用的连接工具
// 有的提供的方法名为set 有的叫setIfAbsent
set Test uuid NX PX 3000
try{
// biz handle....
} finally {
// unlock
if(uuid.equals(redisTool.get('Test')){
redisTool.del('Test');
}
}
- redis分布式锁简单超时处理原理图
- redis分布式锁简单超时处理原理图,步骤解释
- 进程A加锁。
- 进程A加锁成功。
- 进程B加锁, 进程B加锁失败,进入自旋状态,等待再次加锁。
- 进程A执行业务,业务执行时间很长,大于加锁时间。
- 进程B请求加锁。
- 进程B加锁成功。
- 进程B执行业务。
- 此时进程A业务执行完成,释放锁。发现自己的锁没了,存在进程B的锁,但是不是进程A加的锁,进程A没权限删除进程B的锁。
- 进程B业务执行完成,释放锁
- 进程B释放锁完成。
三、分布式锁五中特性
互斥性:
当一个线程/进程加锁成功后,其他线程/进程无法加锁,具有排他性。
锁失效机制:
加锁成功后,应用服务器宕机导致锁未能释放,服务恢复后一直获取不到锁。应设置超时时间,防止出现类似死锁情况。
阻塞锁(可选):
当前资源已被加锁,其他线程/进程来加锁是否阻塞等待,还是立即返回
可重入性(可选)
当前锁的持有者是否能再次进入。
公平性(可选):
加锁的顺序和请求是顺序一致,还是随机抢锁。
四、Redis分布式锁实现可重入性
Redis分布式锁基本上已经实现了分布式锁的特性。下面是关于可重入性的实现。
加锁计数
- 进程A第一次加锁成功,计数为1.
- 允许进程A第二次加锁,锁计数为2.
- 同理,同一个进程多次加锁,锁计数累计递增。
解锁计数
- 如果进程A不异常对出的情况,加锁和解锁是成对出现的。
- 进程A释放一次锁,计数减1。
- 当进程A的锁计数减到0,释放锁,允许其他进程加锁。
以上redis分布式锁,是基于单机的redis来实现。多机reids需要考虑每一台机器的加锁情况。