Redis实现分布式锁

更多精彩内容,请关注微信公众号:后端技术小屋golang

1、redis分布式锁的简易实现

用redis实现分布式锁是一个老生常谈的问题了。由于redis单条命令执行的原子性和高性能,当多个客户端执行setnx(相同key)时,最多只有一个得到成功。所以在对可用性要求不是特别高的场景下,redis分布式锁方案不失为一个性价比高的实现。redis

  1. 多个客户端执行setnx lockid random px lock-duration
    其中lockid与被锁住资源惟一对应。random为随机值,用于客户端断定自身是否为该锁的owner。lock-duration为锁保持时长,由业务操做耗时决定。
  2. 对于客户端来讲,执行命令后,若是redis返回1,则表示抢到锁;不然没抢到
  3. 对于抢到锁的客户端,完成业务操做以后,需主动删除该锁。

2、redis分布式锁的注意事项

1. 锁必需要设定一个过时时间

若是不设置过时时间,考虑以下时序:算法

  • 客户端A抢锁成功
  • 客户端A的进程异常退出,没来得及主动释放锁
  • 其余客户端试图抢锁(毫无疑问是失败的)

如上所示,客户端A抢到锁了,可是因为某些异常致使进程尚未来得及释放锁就退出了。这样其余客户端setnx的返回永远是0,即永远也抢不到锁。后端

相反,若是设置过时时间,即便客户端A没有主动释放锁,到了过时时间以后redis也会自动释放。微信

  • 客户端A抢锁成功
  • 客户端A的进程异常退出,没来得及主动释放锁
  • 其余客户端抢锁失败
  • 锁自动过时
  • 其余客户端抢锁成功

2. 获取锁的命令不能分为两步执行

若是实现为,dom

setnx lockid
expire lockid lock-duration

除非使用lua script, 不然redis没法支持上述两个命令的原子性,当第一个命令执行完成后,抢到锁的客户端A异常退出了,那么其余客户端将永远抢到锁。分布式

注:redis在2.6.12版本后已经支持setnx命令的TTL参数,这个问题不复存在源码分析

3. 锁的值必须设置为随机值

假设锁的值为固定值,考虑以下状况性能

  • 客户端A抢到锁,执行业务操做
  • 客户端A因为某些缘由阻塞,超过了锁有效时间,致使锁自动被释放
  • 客户端B抢锁成功,执行操做
  • 客户端A从阻塞中恢复,主动释放锁,执行del lockid
  • 客户端B建立的锁被客户端A删除。此时客户端C抢锁成功,客户端B与C的业务操做产生竞态。

若是锁的值是随机值,而且每次成功加锁时,都记录该随机值的话,而且释放锁时,判断锁的值是否等于记录值,等于则del, 不等于则跳过。lua

4. 释放锁时,需使用lua script封装保证原子性

若是不使用lua封装释放锁的逻辑,考虑时序:

  • 客户端A抢到锁,执行业务操做
  • 客户端A完成业务操做,主动释放锁:首先get lockid,发现记录值和锁当前值相等,断定该锁为本身所加。
  • 客户端A因为某些缘由阻塞(好比GC),超过锁有效时间,锁被redis自动释放
  • 客户端B成功抢锁
  • 客户端A从阻塞中恢复,执行下一步del lockid,客户端B加的锁被A释放
  • 客户端C抢锁成功,B与C产生竞态

而redis执行lua script的原子性能避免上述问题。

5. 多个redis节点保证高可用

若是只在一个redis节点上抢锁,若是该节点宕机,将致使全部的客户端都抢不到锁,没法保证服务的高可用。

3、redsync实现一览

redlock是一种基于redis的分布式锁算法。而redsync是redlock算法的golang实现,其暴露了三个API:加锁(Lock),解锁(Unlock),续锁(Extend)

1. Lock

  • 首先随机生成一个value
  • 针对全部redis链接,执行set lockid value NX PX lock-duration
  • 若是超过半数链接上的请求都正常返回,且now < start + (1 - factor) * expire,意味着抢锁成功
  • 不然先清理key, 而后重试,重试时间间隔可由用户自定义。

2. Unlock

针对全部redis实例,执行lua脚本。这里会判断key对应的value和Mutex在Lock时使用的value值是否一致,只有一致了执行del命令。此举是为了保证每一个客户端不会释放别的客户端建立的锁。

if redis.call("GET", KEYS[1]) == ARGV[1] then
        return redis.call("DEL", KEYS[1])
    else
        return 0
    end

若是有超过半数实例上的请求返回,则意味着释放锁成功。不然断定失败。

3. Extend

Extend操做是为了保证当客户端业务处理时长超过expire时间时,客户端可主动延长锁的过时时间,而无需二次抢锁。针对全部redis链接,执行lua脚本,从新设置过时时间

if redis.call("GET", KEYS[1]) == ARGV[1] then
        return redis.call("pexpire", KEYS[1], ARGV[2])
    else
        return 0
    end

半数以上返回成功,则意味着Extend成功

推荐阅读

更多精彩内容,请扫码关注微信公众号:后端技术小屋。若是以为文章对你有帮助的话,请多多分享、转发、在看。
二维码

相关文章
相关标签/搜索