文章目录
- 什么是分布式锁?
- 如何用Redis实现分布式锁?
- 分布式锁的改进
- 锁过期处理
- 集群环境下Redis宕机问题
- RedLock的引入
- RedLock的实现步骤
- RedLock带来的弊端
什么是分布式锁?
我们在学多线程的时候遇到过ReetrantLock
,这种锁主要应用于多线程
竞争资源。如果是多进程
竞争资源的话就需要引入分布式锁
的概念了。比如在微服务架构中,多个用户进程
修改同一条数据且只能有一个用户修改成功,就需要用到分布式锁
,今天我们就来讲讲如何用redis
来实现分布式锁。
如何用Redis实现分布式锁?
使用redis
中的String
类型。
redis
中有set NX(set if not exists)
命令可以实现当key
不存在时才插入,如果key
已经存在,则不做任何操作。
- 如果
key
不存在,那么插入成功,表示获取锁成功。 - 如果
key
存在,那么插入失败,表示获取锁失败。
获取锁:
假设客户端A
已经使用setnx
命令实现了获取锁的操作(获取锁成功会返回1)
setnx try_lock 1
那么此时客户端B
再次使用setnx
命令时会失败(获取锁失败会返回0)
释放锁:
只有当客户端A
使用del
命令释放锁,客户端B
才能获取锁成功
del try_lock
以上只是简单的获取锁和释放锁的操作,还存在很多弊端:
- 设想一种情况,如果客户端A在获取锁之后宕机了,那么
try_lock
也就永远不会被删除,所以就会一直存在redis
中,也就是说其他客户端永远拿不到锁,无法执行业务,这个bug
太大了! - 还有一个缺点就是,任何客户端都可以释放这把锁,所以我们需要为这把锁设置一个唯一标识,只有设置这把锁的用户才有权利释放这把锁。
setnx key unique_value
//释放锁 比较unique_value是否相等,避免误释放
if redis.get("key") == unique_value then
return redis.del("key")
可以看到释放锁的步骤具体由GET和DEL操作实现,因为这是两个操作,所以就涉及到原子性问题了,具体问题如下:
- 客户端A执行get命令判断锁是自己的
- 客户端B重新加锁(假设客户端A加的锁刚好过期的时候)
- 客户端A执行del命令释放锁(那么客户端A释放的就是客户端B加的锁)
为了解决上述问题,可以使用Lua脚本(因为Redis处理每个请求是单线程执行的,在执行一个Lua脚本时其它请求必须等待,直到这个Lua脚本处理完成
):
KEYS[1]
即为try_lock
,ARVG[1]
是客户端的唯一表示
if redis.call("get",KEYS[1]) == ARVG[1] then
return redis.call("del",KEYS[1])
else
return 0
end
分布式锁的改进
基于以上try_lock
永远无法被删除的情况,可以引入过期时间。这样一来,如果某个客户端在获取锁之后宕机了,因为有过期时间的存在,所以key
在一定时间之后也会失效,其他客户端就可以继续获取锁进行操作了。
比如:
设置一个serverLock
,并且10s
之后过期
set serverLock 1 nx ex 10
就可以解决上述问题了,但是又会发生新的问题:
如果一个业务执行的时间过长,导致在获取锁之后的10s
内无法完成,所以redis
就会在业务执行完之前释放锁,释放锁之后,后进来的第二个进程就可以重新加锁,并且也可以执行业务操作,假设第一个进程在第15s
执行完业务,由于在第10s
的时候锁已经过期(第二个进程在第11s
进行可以加锁),那么第一个进程执行完业务之后所释放的就是第二个进程加的锁,并且在第11s
到第15s
之间第一个进程和第二个进程在同时执行业务,就会导致数据冲突。
锁过期处理
基于以上锁过期问题,我们可能会想到延长锁的过期时间。比如本来过期时间是10s
,我们改为100s
。但是这样的话,回到最初的问题,如果客户端在获取锁之后宕机,100s
之后这个锁才会过期,也就意味着整整100s
内其余客户端是无法获取锁的!对于高并发的应用来说,100s
的时间是相当长的!
既然延长锁的过期时间行不通,那怎么办?
我们可以在执行业务的过程中延长锁的过期时间,其实本质上
还是延长过期时间,只不过这种做法效率比较高。
如果在业务执行过程中,锁快要过期了,我们可以适当延长锁的过期时间(延长的时间短一些,延长的次数多一些)这样就不会发生在长时间内其余客户端无法获取锁的情况了。
具体实现就是为锁设置一个守护线程,定时去检测锁的过期时间,如果锁快过期了,就为锁延长过期时间。
集群环境下Redis宕机问题
我们都知道为了保证高可用,redis
一般都是基于集群来分布的,并且redis
主从节点之间的数据传输是异步的。假设redis
主节点获取锁之后宕机,数据还没有来得及同步到从节点,由于数据不一致问题,执行切主操作之后新的主节点依然可以获取锁,所以会产生数据冲突。
RedLock的引入
基于集群环境下分布式锁产生的问题,可以引入RedLock
(红锁)。不过要想使用RedLock
需要一个特定的环境,即至少有五个节点,这五个节点都是主节点且孤立存在。
RedLock
的基本思想就是对所有的
主节点进行加锁操作,如果能对半数以上
的节点加锁,那么就认为客户端加锁成功,否则的话加锁失败。这样一来,如果集群环境下某个主节点获取锁之后宕机了,那么此时其余主节点也依然拥有锁,所以客户端依然可以进行后续的操作,锁住的数据也不会丢失。
RedLock的实现步骤
- 客户端先获取当前时间
T1
- 客户端向
N
个主节点开始加锁,加锁时需要使用set nx
命令并且为锁设置过期时间并且需要带上客户端的唯一标识。还需要另外给加锁操作设置一个超时时间,这个超时时间要远小于锁的过期时间。 - 客户端一旦对
半数以上
的节点加锁成功,那么客户端会再次获取一个当前时间T2
,然后计算T2-T1
的值,如果这个值小于锁的过期时间,那么就认为加锁成功。 - 如果加锁成功,可以继续后面的业务操作;如果加锁失败,需要释放掉所有的锁
可见,RedLock
加锁需要满足两个条件:
- 能对半数以上的节点加锁成功
T2-T1
的值小于锁的过期时间
RedLock带来的弊端
- 需要额外部署
redis
主节点,花费很高,并且运维的成本也比较高。 - 通过
RedLock
的加锁操作可以看出RedLock
锁比较重,操作起来很麻烦。 - 主从切换的情况很少见,所以为了极少情况的发生选用
RedLock
性价比不高。