分布式锁的设计
- 获得锁, if(get(key)不存在 ) set key, val;
- 删除锁, del key;
获得锁
获得锁之前,要先判断是否已经有锁.
while(1)
val = redis.get(key)
if(val != nil )
wait;
continue;
redis.set(key, val)
break;
但是get和set之间可能遇到别的client set了key,导致多个client 同时获取锁.
如何获取锁的两个过程, get和set的原子性.
使用命令:set key value(uuid) NX PX timeout
NX : key不存在就添加. Key存在就返回nil. Only set the key if it already exist.
XX : key存在才添加, Only set the key if it already exist.
EX / PX : 过期时间的单位, EX表示以秒为单位, PX表示以毫秒为单位.
为什么设置过期时间:
避免客户端崩溃和其他异常导致没有释放锁,锁一直被占用.为什么value定义为uuid的唯一值
避免异常情况下被误删除导致同时两个client持有共享锁.
value的设计
value的设计最好使用uuid,或者时间戳等. 避免锁被误删除.
异常情况的描述
- A客户端拿到对象锁,但是因为一些原因被阻塞导致无法及时的释放锁
- 因为过期时间已经到了,Redis中的锁对象被删除
- B客户端请求锁操作成功
- A客户端这个时候阻塞操作完成,删除key释放锁
- C客户端请求获取所成功
- 这是B、C都拿到锁了,分布式锁失效
为避免这种情况发生,要保证value的设计是唯一的,
只有拿到锁且value符合预期的客户端才能进行删除。
释放锁
release锁有两个步骤:
1. get , 验证value是否满足期望
2. del 锁
这个步骤如何保证原子性?使用redis的WATCH 命令使用乐观锁的方式.
WATCH 命令本身就是一个乐观锁,它可以在 EXEC 命令执行之前,监视一定数量的 key,并在 EXEC 执行时,检查这些 key 是否被修改过,如果是的话,服务器就拒绝执行事务。
redis.watch(lock)
val = redis.get(lock)
if val == 期望
redis.multi
redis.del(key)
redis.exec
悲剧,上边watch的机制仍然不能work. 因为.watch是和multi配合使用. watch实际上是开启了事务之后才生效的. 如何实现?
lua脚本, Redis会把Lua脚本作为一个整体执行,由于Redis是单线程,因此在脚本执行期间,其他脚本或命令是无法插入执行,这个特性符合事务的原子性。
命令原型:
EVAL script numkeys key[key ...] arg [arg...]
可以参考以下的lua脚本:
local key = KEYS[1]
local val = redis.call("GET", key)
if val == ARGV[1]
then
local ret = redis.call("del", key)
return ret
end
直接贴以下命令道redis-cli中执行, 可以加深理解 :
set lock 1234
eval 'local key = KEYS[1] local val = redis.call("GET", key) \
if val == ARGV[1] then local ret = redis.call("del", key) \
return ret end' 1 "lock" "1234"
get lock