实现分布式锁的方式有Redis
分布式锁、MySQL
分布式锁、ZooKeeper
分布式锁,Redis
分布式锁的本质就是在Redis
中“占坑”。
实现一个分布式锁需要满足哪些条件呢?
- 互斥性:在任何时刻都只能有一个客户端获得锁;
- 健壮性:即使某个客户端在获得锁期间故障而没有释放锁,也要保住后续其他客户端可以获得锁;
- 高可用:只要大部分节点正常,客户端就可以获得锁和释放锁;
- 唯一性:加锁和解锁必须是同一个客户端;
如何实现Redis分布式锁?
如何加锁?
1. setnx + del
setnx key value # 如果key不存在,则set;如果key存在,则set失败
del key # 删除key
setnx
是set if not exist
的缩写,通过set key
获得锁,使用完后通过del
命令释放锁。
如果某个客户端获得锁期间故障了del命令没有被调用怎么办?
2. setnx + expire
setnx key value # 如果key不存在,则set;如果key存在,则set失败
expire key 5 # 设置key过期时间
del key # 删除key
使用setnx
命令set key
之后给key设置个过期时间,这样即使客户端在获得锁期间异常也可以保证锁会在过期时间到来时自动释放锁。
但是
setnx + expire
是两个操作,不是原子性的,如果在setnx
和expire
期间客户端异常了,还是会导致锁不会被释放,如何解决呢?
expire
是依赖于setnx
的执行结果,如果setnx
没抢到锁,expire
是不应该执行的。事务里没有if-else
分支逻辑,要么全部执行要么一个都不执行,所以此处不能使用Redis
事务解决。
3. set扩展参数
set key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL] #setkey并设置过期时间
del key # 删除key
EX seconds
:设置key
的过期时间为seconds
秒;PX milliseconds
:设置key
的过期时间为milliseconds
毫秒;NX
:只有key
不存在才会进行set
操作;XX
:只有key
已存在才会对key
进行操作;
对set
命令加上扩展参数,使得setnx + expire
操作能一起执行。如果客户端在获得锁期间故障,那么也将会在超时时间到来时释放锁。
4. Redisson
setnx + lua
加锁只作用在一个Redis
节点上。
在分布式情况下,客户端A在主节点set key
成功获得锁,主节点还未将锁同步到从节点,主节点就故障了,此时会选举一个从节点升级为新的主节点,但是这个新的主节点并没有存在客户端A获取成功的这个锁,此时客户端B请求获得锁成功,导致系统中同时有两个客户端持有锁,如何解决呢?
RedLock算法
加锁时,它会向集群中其他主节点发送set(key, value, nx=True, ex=xxx)
命令,只要超过n/2 + 1
个节点set
成功,那就认为加锁成功。释放锁时,需要向所有节点发送del
指令。
Redisson
提供了对RedLock
算法的封装,使用Lua
脚本实现原子性的加锁和释放锁操作。
如何释放锁?
1. del
客户端加锁是set
一个key
,那释放锁直接使用del
命令将该key
删除即可。
假设线程A在获得锁操作期间,锁超时释放了,此时线程B获得了这个锁,当线程A操作完成时进行了一个
del
操作将锁释放了,线程B操作完成去释放锁时发现锁不存在或者也释放了别的线程的锁,怎么办呢?
2. get + del
在set key
的时候,将value
设置为一个唯一的客户端标识或UUID
,客户端在释放锁时,先get key
的value
校验加锁线程是否为自己,如果是,则释放锁。
但是
get + del
操作并不是原子性的,还是有线程安全问题,怎么解决呢?
3. Lua脚本
使用Lua
脚本通过eval/evalsha
命令执行get + del
操作,Lua
脚本保证原子性操作。
eval `if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) 1`
KEYS[1]
:操作的key
;ARGV[1]
:参数;