什么是分布式锁
当多个系统对同一数据进行修改时,并且要求这个修改是原子性的,那么就要应用到分布式锁。
分布式锁的应用场景
秒杀时解决库存超卖问题
分布式锁的特点
i)互斥性
任意时刻,只有一个客户端能够持有锁
ii)不会发生死锁
即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端加锁成功
iii)容错性
只要大部人的redis节点正常运行,客户端就可以加锁和解锁
iiii)解锁
加锁和解锁必须为同一个客户端,客户端不能解锁他人的锁
常用redis命令
setnx: key不存在时,为key设置指定值
expire: 设置key的过期时间,单位:秒
getset:设置指定key的值,并返回 key 的旧值
del:用于删除已经存在的键
分布式锁的实践
- 错误示例
if(redis->setnx(key, value)){
redis->expire(key, value)
}
实现思路: 在当前锁没有被占用的情况下,加锁成功后,给锁设置一个过期时间。
但是由于setnx/expire不具有原子性,如果在执行expire前该进程崩溃,则会导致锁永远存在,后续进程在获取锁时发现锁已存在而无法枷锁。
- 正确示例
redis->set(key, value, ['nx', 'px'=>expire])
参数说明:
nx: set if not exist , 即当key不存在时,进行set操作,若key已经存在,则不做任何操作
px:设置key的过期时间,单位:ms
- 解锁实现思路
最常见的一种错误解锁方式是直接通过del来进行的
redis->del(key)
这种错误原因是不具有拥有者标识[谁加锁,谁解锁],任何客户端都可以随时解锁
所以在用setnx加锁时,应该给value设置一个唯一的id,或者用uuid这种随机数。当解锁的时候,先获取value,判断是否为当前进程加的锁,再去删除
$uuid = xxxx;
redis->set('test', $uuid, ['nx', 'px'=>expire])
try{
}finally{
if($uuid === reids->get(key)){
redis->del(key)
}
}
注:但是get和del并非原子操作,所以还是会存在进程安全问题
正确解锁步骤,使用lua脚本,通过redis的eval来运行脚本。新建一个unlock.lua
if redis.call('get', KEYS[1])== ARGV[1]
then
return redis.call('del', KEYS[1]);
else
return 0;
end
//运行脚本
redis->eval(unlock.lua, [key, value], 1);
使用脚本的好处
redis 在2.6推出了脚本功能,允许开发人员使用lua语言编写脚本传到redis中执行。
i)原子操作:redis 会将整个脚本作为一个整体执行,中间不会被其他命令插入
ii) 复用:客户端发送的脚本永久储存在redis中,意味着其他客户端可以复用该脚本
参考资料:
redis setnx原子性_面试被问Redis锁??的缺点,被打击的扎心了
nx set 怎么实现的原子性_面试官:谈谈分布式锁的实现