南乐网站建设公司,淄博五厘网络技术有限公司,深圳市住建设局网站,重庆就业网本文主要简述一下基于set命令的Redis分布式锁的原理。
一#xff0c;a线程持有的锁不要被b线程同时持有→setnx
抢锁的时候#xff0c;最核心的就是#xff0c;a线程持有的锁不要被b线程同时持有#xff0c;放在基于set命令的redis分布式锁中来看#xff0c;就是“如果锁…本文主要简述一下基于set命令的Redis分布式锁的原理。
一a线程持有的锁不要被b线程同时持有→setnx
抢锁的时候最核心的就是a线程持有的锁不要被b线程同时持有放在基于set命令的redis分布式锁中来看就是“如果锁key存在我就不能抢锁key不存在我才能抢”
用原子的redis命令来说就是
setnx key valuekey可以设置为你业务中要抢的锁的名字和其他key区分开。
一开始redis提供了set、setex设置key value的同时设置过期时间、setnx如果不存在key就设置key value。
二如果锁被别人释放了怎么办→ThreadLocal和UUID
一个比较常见的担心是如果a线程加的锁被b线程释放了怎么办毕竟只是一个set命令用del就可以删除也就是解锁那怎么办呢。
在前面我们只设置了key的名字对value没有做要求。其实我们可以给value加一个唯一ID让别的线程识别到这个锁是否是自己持有。
线程id是否可以呢我们直接用线程的id作为value让线程看一下是否是自己的id简单直接。答案是不可以线程id在机器中是唯一的但是这是分布式锁多个机器之间线程id不唯一。
我们倒也不用直接跳到分布式id这么远其实uuid足矣。uuid中包括了机器的IEEE识别号如果有网卡从网卡的mac地址获得优点是全球唯一且简单、代码方便uuid不用于自增主键的原因是它不区域递增以及可能造成网卡的mac地址泄露曾用于寻找梅丽莎病毒制作者。
此时我们就需要用到ThreadLocal让每个线程私有uuid。就这样我们加入了ThreadLocal和UUID解决了锁被其他线程释放的问题。
三锁无人释放问题→加过期时间
1只靠setnx命令死锁无法释放
那么紧接着问题来了 如果加锁的线程挂掉了导致锁无法正常释放怎么办要知道redis的数据如果没有特意设置过期时间默认的是永不过期。所以我们要设置一个过期时间。
redis中设置过期时间的命令为expire命令我们当然可以使用expire来给上面的setnx命令来加过期时间如下
setnx lock XXX
expire lock 10 //给lock键设置过期时间为10秒2setnxexpire命令不够原子
第二个问题来了setnxexpire命令不够原子即如果有线程只执行了加锁命令setnx在第二个命令expire执行之前就挂掉了又怎么办呢因为这两个是分开执行的意味着是存在这种可能的。因此我们就要想方法来将其一次执行。
1set key value ex second nx
正如一开始所述redis在一开始只提供了set、setnx、setex几个命令却没有其他参数因此要执行必须分开执行。
在 Redis 2.6.12 之后Redis 扩展了 SET 命令的参数使我们可以直接在一条命令中完成所有操作即如果不存在再设置Key valuenx同时设置过期时间ex。
注set命令中ex参数设置秒px参数设置毫秒
2Lua脚本
redis整合了Lua语言我们可以基于redis的eval命令通过Lua脚本来完成原子性操作
eval命令如下
eval 脚本内容 Ke个数 key列表 参数列表如果觉得每次都要上传Lua脚本占带宽还可以在Redis启动时将Lua脚本上传由redis将脚本缓存客户端拿到sha1在使用lua脚本时只需要上传sha1值即可[1]。即通过script load命令上传脚本得到sha1再基于evalsha命令通过sha1参数执行指定的lua脚本。
evalsha命令使用示例如下
script load ${cat lua_demo.lua}
evalsha 脚本sha1值 key个数 key列表 参数列表四如何评定过期时间→看门狗线程续期
现在好了一个基本的redis分布式锁的方案已经有了那么还有什么问题呢。仔细想想就会发现问题出在了业务执行时间上。我们应该如何衡量业务执行时间并由此设置过期时间呢
一个项目里有的业务执行时间长有的执行时间短。就算是同一个业务你通过链路追踪或者监控知道了业务执行大多数是多久下面分为两种情况
情况1我们假设就按照大多数业务能完成的时间来设置过期时间这肯定是不行的因为有的业务没完成就释放了锁极容易出现问题。
情况2我们按照一个比较大大到应该不可能出现问题的过期时间总行了吧也不行第一“应该不可能”谁知道以后有没有可能有的业务就是时间长到超过了过期时间。第二为了少数业务执行时间长的情况而给所有业务包括大多数只需要较短持有锁时间的业务都施加很长的过期时间很明显项目并发能力会严重下降。
怎么办呢长了也不行短了也不行。这时候我们可以加入看门狗方案。什么意思呢就是说我们可以给线程设置一个不算长对于大多数线程都比较合理的时间哪怕短点也无所谓。我们专门设置一个守护线程thread.setDaemon(true)用于监控线程的看门狗线程看门狗线程会在过期时间快到了且锁还没有被释放时就去用expire命令给锁续期。
同时配合一个延迟队列DelayQueue本质是个堆Heap在加锁时将装有线程id和续期时间注意是续期时间续期时间要比key的过期时间早一点的entity扔进去延迟队列根据续期时间排序这样堆中排在最上面的永远是续期时间最近的entity堆的特性。
看门狗线程的任务内容就是while循环对延迟队列用take方法阻塞的去拿最近一个需要判断是否要续期的entity有的话就进行续期这样我们就可以用一个线程完成对整个项目的所有锁的续期这样的分布式锁耗时短的业务用一次过期时间就可以完成耗时长的业务也可以有看门狗线程来实现续期过期时间。
五总结→建议不要自己开发用现成的如Redisson
上面实现的分布式锁解决了如下几个问题
a线程持有的锁不要被b线程同时持有→setnx如果锁被别人释放了怎么办→ThreadLocal和UUID只依靠setnx持有锁的线程挂掉导致无人去释放锁→给锁添加过期时间expire命令设置过期时间setnx和expire不够原子→set命令和参数ex、nx或者lua脚本配置命令eval、evalsha过期时间不好评定→加入看门狗守护线程配合延迟队列看门狗线程用死循环使用take方法阻塞的等待将最近要续期的任务将其续期。
实际使用上我个人的看法是轮子不需要重复开发我们只需要用Redisson实现的分布式锁就具有看门狗线程[2]。简简单单两三行代码就有更完善的分布式锁使用。除非一些特殊的原因不然不要重复造轮子以及如非必要勿增实体奥卡姆剃刀原则。 RLock testLock redissonClient.getLock(test_lock);boolean res testLock.tryLock(10, TimeUnit.SECONDS);参考文章 [1]用jedis执行lua脚本 [2]redisson中的看门狗机制总结