【Redis基础和应用】(六)分布式锁

分布式锁

1. 基本实现

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

分布式锁的实现主要依赖setnx(set if not exists)这个指令实现,只允许一个客户端占坑。先来占坑,用完了,再调用del指令释放坑。

相关指令

# 为了方便理解, 这里的Key就是“lock:codehole"
> setnx lock:codehole true
OK
# do something,执行业务逻辑
> del lock:codehole
(integer) 1

但是这里有一个问题,如果逻辑执行中出现异常,可能会导致del lock:codehole这个指令没有被调用,那么就会陷入死锁,锁永远无法释放。

为了解决这个问题,需要在拿到锁以后,在给锁加上一个过期时间,比如5s,这样即使执行过程中出现了问题,锁过了过期时间也会自动释放

> setnx lock:codehole true
OK
> expire lock:codehole 5
# do something
> del lock:codehole
(integer) 1

但是以上逻辑还有问题,如果在setnx和 设置expire指令之间,服务器突然发生异常,就可能导致expire得不到执行,也会造成死锁。

这种问题的根源就在于setnxexpire不是原子指令。但是这里不能使用redis的事务来解决,因为expire是依赖setnx的执行结果的,如果setnx没有抢到锁,expire是不应该执行的。(如果没有锁,怎么设置锁的过期时间呢?)事务里面没有if-else分支,要么全部执行,要么一个都不执行。

为了解决这个难题,Redis开源社区出现了很多分布式锁的工具包。这意味着不仅仅要学会使用Jedis,同时还需要引入分布式锁的library,随后,Redis2.8版本中,作者加入了set指令的扩展参数,使得setnx可以和expire一起执行,解决了分布式锁的乱象。

> set lock:codehole true ex 5 nx # set lock:codehole = true expire is 5s if key is not sxists
OK
# do something
> del lock:codehole

其中,set <key> <value> ex <time> nx就是分布式锁的奥义所在。

2. 超时问题

Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的业务逻辑执行时间太长,长到超过锁的过期时间,那么就会出现问题。

因为在执行业务逻辑的时候,锁过期了,那么就会有其他线程重新获取到这把锁,导致临界区同时有两个线程在执行,那么就会导致执行结果出现错误。

因此,Redis分布式锁不适合较长时间的任务,如果真的出现了错误,造成较小的数据错乱就可能需要人工介入。

tag = random.nextint()
if redis.set(key, tag, nx=True, ex=5):
    do_something()
    redis.delifequals(key, tag) # 假象的del if equals

有一个稍微安全的方案是,将set的指令的value参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再(删除Key)释放锁,这是为了确保当前线程占有的锁不会被其他线程释放,除非这个锁是因为过期被服务器释放的。

但是匹配随机数和删除key不是一个原子操作,Redis没有提供类似于delifequals的指令,这时就需要Lua脚本来处理了,因为Lua脚本可以保证连续多个指令的原子性执行。

3. 可重入性

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

在此,作者不推荐使用可重入锁,因为它加重了客户端的复杂度,在编写业务方法时注意在逻辑结构上进行调整,避免使用可重入锁。

4. 集群环境下的分布式锁

上述的分布式锁的实现是使用一条指令就可以完成,不过在集群环境下,这种方式是有缺陷的,它不是绝对安全的。

比如:在Sentinel集群环境中,当主节点挂掉时,从节点会取而代之,但是客户端上并没有明显感知。比如,原先第一个客户端在主节点上申请成功了一把锁,但是这把锁还没来得及同步到从节点,主节点突然挂掉了,然后从节点变成了主节点,这个新的主节点没有这把锁,所以当另一个客户端就可以立即获取锁,导致了一定的不安全性。

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

Redlock算法

为了解决这个问题,Antirez发明了Redlock算法,已经有很多开源的library做了良好的封装,用户可以直接使用redlock-py

import redlock

addrs = [{
    "host":"localhost",
    "port": 6379,
    "db":0
},{
    "host":"localhost",
    "port": 6479,
    "db":0
},{
    "host":"localhost",
    "port": 6579,
    "db":0
}]
dlm = redlock.Redlock(addrs)
success = dlm.lock("user-lck-leesure", 5000)
if success:
    print("lock success")
    dlm.unlock("user-lck-leesure")
else:
    print("lock failed")

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

加锁时,他会向过半结点发送set(key,value, nx=True, ex=xxx)指令,只要过半的结点set成功,就认为加锁成功。

释放锁时,需要向所有结点发送del指令,不过Redlock算法还需要考虑出错重试,时钟漂移等很多细节问题,同时,因为Redlock需要向多个结点进行读写,也就是相比单个实例Redis的性能会下降一些。

Redlock使用场景

如果你很在乎高可用性,希望即时挂了一台Redis后也完全不受影响,就应该考虑Redlock,不过代价也是有的,需要更多的Redis实列,性能也下降了。代码上还需要引入额外的library,运维上也需要特殊对待,这些都是需要考虑的成本,使用前请再三斟酌。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

企鹅宝儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值