Redis实现分布式锁

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

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

  1. 多个客户端执行setnx lockid random px lock-duration
    其中lockid与被锁住资源唯一对应。random为随机值,用于客户端判定自身是否为该锁的owner。lock-duration为锁保持时长,由业务操作耗时决定。

  2. 对于客户端来说,执行命令后,如果redis返回1,则表示抢到锁;否则没抢到

  3. 对于抢到锁的客户端,完成业务操作之后,需主动删除该锁。

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

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

如果不设置过期时间,考虑如下时序:

  • 客户端A抢锁成功

  • 客户端A的进程异常退出,没来得及主动释放锁

  • 其他客户端试图抢锁(毫无疑问是失败的)

如上所示,客户端A抢到锁了,但是由于某些异常导致进程还没有来得及释放锁就退出了。这样其他客户端setnx的返回永远是0,即永远也抢不到锁。

相反,如果设置过期时间,即使客户端A没有主动释放锁,到了过期时间之后redis也会自动释放。

  • 客户端A抢锁成功

  • 客户端A的进程异常退出,没来得及主动释放锁

  • 其他客户端抢锁失败

  • 锁自动过期

  • 其他客户端抢锁成功

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

如果实现为,

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, 不等于则跳过。

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节点上抢锁,如果该节点宕机,将导致所有的客户端都抢不到锁,无法保证服务的高可用。

三、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成功

(0)

相关推荐

  • 七种方案!探讨Redis分布式锁的正确使用姿势

    前言 日常开发中,秒杀下单.抢红包等等业务场景,都需要用到分布式锁.而Redis非常适合作为分布式锁使用.本文将分七个方案展开,跟大家探讨Redis分布式锁的正确使用方式.如果有不正确的地方,欢迎大家 ...

  • Redis:从应用到底层,一文帮你搞定

    总感觉哪里不对,但是又说不上来 1.基本类型及底层实现 1.1.String 用途: 适用于简单key-value存储.setnx key value实现分布式锁.计数器(原子性).分布式全局唯一ID ...

  • 应该没人比我更细了吧:带你深入剖析Redis分布式锁!

    什么是分布式锁 说到Redis,我们第一想到的功能就是可以缓存数据,除此之外,Redis因为单进程.性能高的特点,它还经常被用于做分布式锁. 锁我们都知道,在程序中的作用就是同步工具,保证共享资源在同 ...

  • 继续项目实战,集成Redis分布式锁(大神勿进)

    本文是我们小项目的第三篇文了,本次我们来把分布式锁应用到我们的项目中,使用Redis实现的分布式锁功能,这一切都是为我们往后的工作做铺垫,希望大家能get到分布式锁这项新技能. 第一篇:Spring ...

  • 基于Redis实现分布式锁

    我们知道分布式锁的特性是排他.避免死锁.高可用.分布式锁的实现可以通过数据库的乐观锁(通过版本号)或者悲观锁(通过for update).Redis的setnx()命令.Zookeeper(在某个持久 ...

  • Redis分布式锁升级版RedLock及SpringBoot实现

    分布式锁概览 在多线程的环境下,为了保证一个代码块在同一时间只能由一个线程访问,Java中我们一般可以使用synchronized语法和ReetrantLock去保证,这实际上是本地锁的方式.但是现在 ...

  • Redis分布式锁的正确实现方式

    前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...

  • Redis分布式锁抽丝剥茧

    之前码甲哥写了两篇有关线程安全的文章: ·你管这叫线程安全?·.NET八股文:线程同步技术解读 分布式锁是'线程同步'的延续 最近首度应用'分布式锁',现在想想,分布式锁不是孤立的技能点,这其实就是跨 ...

  • 从入门到精通-Redis,图文并茂、分布式锁、主从复制、哨兵机制、Cluster集群、缓存击穿、缓存雪崩、持久化方案、缓存淘汰策略 附案例源码

    导读 篇幅较长,干货十足,阅读需要花点时间,全部手打出来的字,难免出现错别字,敬请谅解.珍惜原创,转载请注明出处,谢谢~! 学习之前,先附上一张知识脑图,百度上找哒~~~ NoSql介绍与Redis介 ...

  • Redis实现分布式文件夹锁

    缘起 最近做一个项目,类似某度云盘,另外附加定制功能,本人负责云盘相关功能实现,这个项目跟云盘不同的是,以项目为分配权限的单位,同一个项目及子目录所有有权限的用户可以同时操作所有文件,这样就很容易出现 ...