redis分布式锁

背景

分布式应用进行逻辑处理时经常会遇到并发问题。

比如一个操作要修改用户的状态,修改状态需要先读出用户的状态,在内存里进行修 改,改完了再存回去。如果这样的操作同时进行了,就会出现并发问题,因为读取和保存状 态这两个操作不是原子的。(Wiki 解释:所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何context switch线程切换。)这个时候就要使用到分布式锁来限制程序的并发执行。


分布式锁

分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占 时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。

占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑。先来先占, 用完了,再调用 del 指令释放茅坑。

> setnx lock:balance true
  do something...
> del lock:balance

但是有个问题,如果逻辑执行到中间出现异常了,可能会导致del指令没有被调用,这样 就会陷入死锁,锁永远得不到释放。

于是我们在拿到锁之后,再给锁加上一个过期时间,比如 10s,这样即使中间出现异常也可以保证10秒之后锁会自动释放。

> setnx lock:balance true OK
> expire lock:balance 10 
   do something critical ... 
> del lock:balance

但是以上逻辑还有问题。如果在setnx和expire之间服务器进程突然挂掉了,可能是因 为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。

这种问题的根源就在于 setnx 和 expire 是两条指令而不是原子指令。如果这两条指令可以一起执行就不会出现问题。也许你会想到用 Redis 事务来解决。但是这里不行,因为 expire 是依赖于 setnx 的执行结果的,如果 setnx 没抢到锁,expire 是不应该执行的。事务里没有 if- else 分支逻辑,事务的特点是一口气执行,要么全部执行要么一个都不执行。

使用set命令解决分布式锁

Redis 2.8 版本中作者加入了 set 指令的扩展参数,使得 setnx 和 expire 指令可以一起执行,彻底解决了分布式锁。

> set lock:codehole true ex 5 nx OK 
  ... do something critical ... 
> del lock:codehole

超时问题

Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候锁过期了,第二个线程重新持有了这把锁,但是紧接着第一个线程执行完了业务逻辑,就把锁给释放了,第三个线程就会在第二个线程逻辑执行完之间拿到了锁。

为了避免这个问题,Redis 分布式锁不要用于较长时间的任务。如果真的偶尔出现了,数据出现的小波错乱可能需要人工介入解决。

有一个更加安全的方案是为 set 指令的 value 参数设置为一个随机数,释放锁时先匹配 随机数是否一致,然后再删除 key。但是匹配 value 和删除 key 不是一个原子操作,Redis 也没有提供类似于 delifequals 这样的指令,这就需要使用 Lua 脚本来处理了,因为 Lua 脚本可以保证连续多个指令的原子性执行。

# delifequals
if redis.call("get",KEYS[1]) == ARGV[1] then
	return redis.call("del",KEYS[1])
else
	return 0
end

可重入性

可重入性是指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程的多次加锁,那么这个锁就是可重入的。Redis 分布式锁如果要支持可重入,需要对客户端的 set 方法进行包装,使用线程的 Threadlocal 变量存储当前持有锁的计数。


集群环境问题

redis分布式锁的使用非常简单,一条指令就可以完成加锁操作。不过在集群环境下,这种方式是有缺陷的,它不是绝对安全的。

比如在 Sentinel 集群中,主节点挂掉时,从节点会取而代之,客户端上却并没有明显感 知。原先第一个客户端在主节点中申请成功了一把锁,但是这把锁还没有来得及同步到从节 点,主节点突然挂掉了。然后从节点变成了主节点,这个新的节点内部没有这个锁,所以当 另一个客户端过来请求加锁时,立即就批准了。这样就会导致系统中同样一把锁被两个客户 端同时持有,不安全性由此产生。


不过这种不安全也仅仅是在主从发生 failover 的情况下才会产生,而且持续时间极短, 业务系统多数情况下可以容忍。

Redlock 算法

为了解决这个问题,Antirez 发明了 Redlock 算法,它的流程比较复杂,不过已经有了

很多开源的 library 做了良好的封装,用户可以拿来即用。

为了使用 Redlock,需要提供多个 Redis 实例,这些实例之前相互独立没有主从关系。 同很多分布式算法一样,redlock 也使用「大多数机制」。

加锁时,它会向过半节点发送 set(key, value, nx=True, ex=xxx) 指令,只要过半节点 set 成功,那就认为加锁成功。释放锁时,需要向所有节点发送 del 指令。不过 Redlock 算法还需要考虑出错重试、时钟漂移等很多细节问题,同时因为 Redlock 需要向多个节点进行读写,意味着相比单实例 Redis 性能会下降一些。


注:该文来源于读钱文品老师著的《Redis 深度历险:核心原理和应用实践》

展开阅读全文

页面更新:2024-03-07

标签:死锁   分布式   节点   线程   原子   指令   加锁   客户端   逻辑   操作

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top