• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Redis分布式锁-附实现原理和优化过程Redis的常用命令

武飞扬头像
Stephen·You
帮助1

目录

问题现象:

拓展:什么是分布式锁?

拓展:分布式锁的主流实现方式?

问题分析:

1、setnx命令 expire命令 del命令

2、set key value EX seconds/PX milliseconds NX命令 del命令

3、set key value EX seconds/PX milliseconds NX命令 lua脚本

解决方法:

4、Redisson框架

5、Redisson框架 RedLock红锁

 拓展:Redis的常用命令:


问题现象:

        最近在学习分布式锁的相关知识,学到了用Redis做分布式锁的思路和相关技术,所以结合了自己的理解,写了这篇文章,特此记录一下,希望对大家有用,问题如题:

        Redis分布式锁-附实现原理和优化过程(Redis的常用命令)!


拓展:什么是分布式锁?

        一种用于控制和解决分布式系统中分布式事务并发问题的锁!


拓展:分布式锁的主流实现方式?

        【tips:该拓展节点的内容大家不感兴趣的只需看和本文主题相关的第3点(redis分布式锁)即可。感兴趣的话可以了解一下,以后我也会补充相关知识的文章,也随时欢迎大家留言和我探讨或者分享自己的看法,谢谢!!!

        目前,有3种主流的分布式锁实现方式:

  1. 数据库锁:数据库基本上都对读写操作具有相应的锁规则,用于保证数据库事务并发问题。
  2. zookeeper分布式锁:在zookeeper中维护了一个临时顺序节点树,该节点树中的节点顺序具有锁的排他特性。当线程去获取加锁资源前,先去访问临时顺序根节点,从根节点按顺序生成子节点并记录序号派发给线程。若该序号是子节点中最小的,那就加锁成功,操作完业务逻辑后,就删除临时顺序节点树中线程序号对应的节点;否则的话就一直等待,并监听前一个序号节点,直到前一个序号节点被删除时,就能加锁了。
  3. redis分布式锁:redis中有一种插入key的规则(setnx)是:先判断一个key是否存在,如果不存在,则可以插入数据,并返回1,如果存在则无法插入数据,并返回0。这种规则具有锁的排他特性。当线程去获取加锁资源前,先去setnx一个key,如果返回1,则加锁成功操作完业务逻辑,就删除key;否则就一直等待直到返回1时,就能加锁了。

问题分析:

        OK,相信大家已经了解了redis为什么可以用来做分布式锁了,下面就说说基于这个思路出发,的实验过程:

        【tips:在开始下面的内容之前,先提示一下大家:为了尽量不影响文章的观感,所以我在文章末尾提供了一些我自己记录的Redis的常用命令,当然也有setnx命令expire命令的用法,有需要的朋友请自提,对了解下面的内容有很大的帮助,所以我个人强烈建议提前看一下,当然如果你是redis大佬,请当我没说。】

        首先Redis分布式锁,最简单的实现方式无非就是:

1、setnx命令 expire命令 del命令

        先用setnx命令设置一个key,由于redis库中没有该key,所以返回1(成功)。因此设置成功,也表示加锁成功。后面的线程再去加锁(setnx同一个key)就会返回0(因为该key已经存在)。

        然后用expire命令给key加过期时间,否则一旦redis服务宕机了,那就是这个key就相当于永久不过期,即死锁了。

        此时就可以在程序中开始执行分布式事务来完成业务逻辑了。

        最后就是用del命令删除我们设置的key,即释放锁资源,这样才能被其他线程获取到该锁。

总结:看上去似乎不错,但显然存在一些不对劲的问题呢?别急,一个个来!

缺点1.死锁:死锁问题并未完全解决,因为setnx和expire不是一个原子性(整体性)操作,也就是说有可能在setnx命令之后,expire命令之前,redis就宕机了,这样的话依然会导致死锁问题。

2、set key value EX seconds/PX milliseconds NX命令 del命令

        为了彻底解决死锁问题,就需要解决设置key设置过期时间这两个操作的原子性。redis提供了一种合并句式就可以实现这个想法。

【tips:再次提示一下,这个合并句式的用法在文末的Redis的常用命令中有提到,感兴趣的朋友请自提。】

        OK,至此我们通过使用这个合并句式取代上面的两步操作,就可以解决原子性问题,也就是解决了死锁问题?

总结:那么还有什么其他问题呢?当然有咯,下一个缺点走起!

缺点2.锁过期误删:当我搞定业务逻辑,就需要去del锁了,假如在我del命令之前,锁就过期了(你无法确定需要给锁加多长的过期时间。业务处理、网络波动、cpu性能、磁盘io等都是不稳定因素),然后这个锁又被其他线程获取到了,接着你执行了del命令,就会导致:误删了其他线程设置的锁!!!

        有人说这个问题可以通过给锁设置合适的过期时间来解决,这个确实是对的,但这个时间不太好把握就是了,总是有种隐患的感觉,因为如果往大了想,假如这个业务实际完成时间确实要很久呢,对吧......那么有没有绝对的解决方法呢?

        当然是有的,那就是:先判断这个锁是否被当前线程所拥有,再去考虑是否执行del命令即可!例如我将这个key的value设置成一个随机数(只要保证和其他线程设的value不同即可),那当我去del它的时候,先去get这个key拿到value再去对比一下是不是我原来set进去的value即可的值,该key是否为当前线程所拥有了,具体的java代码实现逻辑如下:

  1.  
    //判断锁是否为当前线程所持有
  2.  
    if (random.equals(jedis.get(key))) {//random:之前setnx key时的value值
  3.  
    jedis.del(key); //释放锁:同一个线程设置的key,才可删
  4.  
    }

        至此这个锁过期误删的问题,就有了绝对的解决方法了,但是呢,还是有个曾经踩过的坑需要预防的,是什么呢?

        那就是原子性问题!原子性问题都是很极端的问题,虽然概率极小,但就是有隐患,作为逻辑严谨的理科生,我们自然不能置之不理。和上文提到的是同样的问题:

        假如我if判断了当前线程确实持有key,然后在执行del操作之前,刚好这个锁就过期了,然后被其他线程获取了,那这个时候del它,依旧无法解决锁过期误删的问题。

3、set key value EX seconds/PX milliseconds NX命令 lua脚本

        由于redis中不存在能实现上面两个操作的合并句式,因此为了解决这种原子性问题,就需要强大的lua脚本登场了。

        lua脚本的作用就是,允许用户编写redis命令集合,然后执行该脚本时,里面的命令就会保证原子性(整体性),要不都成功,要不都失败!!!

        看到这里,估计会有小伙伴发现:这不就是事务吗?

        是的,你完全可以把lua脚本的作用理解为实现了redis事务,虽然它的作用远不止如此。

        OK,那么现在的代码就需要修改成如下:

  1.  
    String luaScripts = "if redis.call('get',KEYS[1]) == ARGV[1] then redis.call('del',KEYS[1]) return 1 else return 0 end";   
  2.  
    Object result = jedis.eval(luaScripts, Collections.singletonList(key), Collections.singletonList(random));
  3.  
     
  4.  
    //判断是否成功
  5.  
    return result.equals(1L);

        至此就彻底解决了锁过期误删的问题。

总结:别说了,肯定还有下一个问题。

缺点:3.业务逻辑未执行完:显然如果锁时间设置的不合理,会导致业务还未执行完就提前过期了,这虽然不是技术问题了,但却是很头疼的实际问题,因为业务不执行完,相当于白干了,不发工资了,浪费资源了,显然这又是一个因为锁过期引起的问题。


解决方法:

        OK,了解我或者看过我文章的人,应该都知道,这个part就是文章问题的答案了。那么本期的问题答案呢,有两个解决方案,分别适用于两种情况,废话不多说,继续:

4、Redisson框架

        OK,为了彻底解决一直以来困扰大家很久的锁过期时间引起的问题,今天的重头戏来了:
        Redisson框架中有一个WatchDog(看门狗)的功能机制,它的作用其实很简单;就是在加锁之后,默认是每隔10s就会去判断一次“当前线程是否持有key(锁)”(据说)。
        如果还持有key,那就延长key的过期时间(延长多久呢?据说是可以设定)。

        就这么简单,我感觉我自己都可以在加锁线程里面,写个守护线程去实时监控线程对锁的持有权,再相应的延长过期时间了!说笑而已,不好意思,这个WatchDog应该还有一些我不知道的底层逻辑在避免一些我没注意到坑,不过我最近没空去使用Redisson框架,你们看我写这部分的文案用词(都用上据说这个词了,我之前写文章从不用这个词的啊)就知道了。

        好了,实践出真知是我一直都在坚持的原则,所以后面有空我一定会去实践一下这个Redisson框架,领教一下它的厉害。现在只能请各位小伙伴自行实践,所以就不提供代码了。

        当然了,既然都形成公认的技术框架了,那上文提到的什么原子性问题、死锁、锁过期误删问题,肯定是都已经解决了,这个我还是有信心下结论的。

        至此,上文提到的所有问题,通过Redisson框架就都可以彻底解决了!

总结:那么是否已经完美了呢?你这么问那答案肯定就不是了。

缺点:4.锁重复:这是最后一个问题了,当然这个问题是要看情况的。其实在redis单机模式下,Redisson框架就已经很完美了,但是呢我们今天的主题还记得么?是分布式锁!而在大型分布式项目中Redis肯定是要搭建集群的,由于是集群,那就可能会出现这种情况:假如我在master节点上面加了锁,结果master结果又宕机了,更可悲的是这个加锁的数据还没来得及同步到其他slave节点上去,当主从切换之后,slave成为了新的master节点,这是又有其他线程去加同一个锁,而由于新的master上面并没有之前的加锁信息,数据库查询该key,所以又加锁成功了。因此就会导致:当前线程之前在master上加锁成功了;主从切换之后,其他线程在新的master上也加锁成功的锁重复问题了。

5、Redisson框架 RedLock红锁

        为了解决锁重复问题,就需要用到RedLock红锁了!

        RedLock是一种联合锁,其原理一大堆文字,我就不细说了,感兴趣的伙伴可以自行百度,我这里说一下结合我个人理解和简化后的原理:

        首先我们先搭建一个redis集群(多个master节点 多个slave):

学新通

        1、当线程需要加锁时,就会按顺序向集群中的所有master节点发起加锁请求。

        2、每个master节点都会通过加锁,来表态自己是否同意(相当于投票,有超时弃权机制)。

        3、最后如果票数>1/2的master总数(如图则是票数要大于2.5,即至少要3台master同意),那就加锁成功,否则就加锁失败,加锁失败,则所有master都要执行解锁操作。

        过程应该很简单吧,当然这种做法自然是很消耗资源的,毕竟力量越大,责任越大。

        期待它未来的持续优化吧!!!

总结:那么现在是否已经真正的完美了呢?

缺点:其实我个人认为已经是非常完美了,硬要说的话那就是资源消耗和技术成本方面的了。


 拓展:Redis的常用命令:

set key value:

        设置key的值为value,key存在则覆盖,无视类型,无返回值。

setex key seconds value:

        设置key的值为value,key存在则覆盖,无视类型,并以秒为单位设置过期时间,成功则返回OK,失败返回error错误。

psetex key milliseconds value:

        设置key的值为value,key存在则覆盖,无视类型,并以毫秒为单位设置过期时间,成功则返回OK,失败返回error错误。

setnx(set if not exist的缩写)key value:

        key存在则不操作,直接返回0;

        key不存在则设置key的值为value,并返回1。

        由于setnx这种独特的排他性,使其满足了作为锁的特点。

ttl key:

        如果key不存在,返回-2;

        如果key存在且永久/没有过期时间,返回-1;

        如果key存在且有过期时间,返回以秒为单位的过期时间。

expire key seconds:

        给key设置过期时间。

persist key:

        清除key的过期时间,使其永久。

set key value EX seconds/PX milliseconds NX/XX(默认XX)

        给key设置value值,同时设置过期时间。

        EX seconds/PX milliseconds:用于指定过期时间的单位。

        NX/XX:NX表示key不存在则返回OK,存在则返回nil,XX与NX相反,所以NX模式就相当于setnx expire 命令的结合。

示例:

        设置key为abc,值为”123,过期时间60秒:

        set abc “123 EX 60

        设置key为abc,值为”123,过期时间60秒:

        set abc “123 PX 60000

        如果key不存在,则设置key为abc,值为”123,过期时间60秒:

        set abc “123 EX 60 NX

        如果key存在,则设置key为abc,值为”123,过期时间60秒:

        set abc “123 EX 60 XX

get key:

        如果可以存在,且为String类型,返回key的值。

        如果key存在,但不是Stiring类型,返回error错误。

        如果key不存在,返回nil。

getset key value:

        设置key的值为value,key存在则覆盖,返回key的旧值。

        如果key不存在,则返回null。

        如果key的值不是String类型则返回error错误。

unlink key...:

        异步删除一个或多个key(空格分隔多个key)。不会阻塞redis服务器(redis默认单线程),先在命名空间(数据库)中把key删掉就返回(此时通过get获取key值为null),后台再去释放空间(真正从内存上删除,所以这期间就是内存泄漏)。

del key...:

        同步删除一个或多个key。这会导致阻塞,所以不建议使用del命令删除大key(如集合、高级类型的key值可能占用空间会达到几个GB),大部分场景都会使用unlink代替del,除非是要尽快删除掉一个占用空间正在极速增大的key。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgagjgh
系列文章
更多 icon
同类精品
更多 icon
继续加载