Redis-分布式锁

[复制链接]
发表于 2026-1-7 20:26:43 | 显示全部楼层 |阅读模式
什么是分布式锁?

分布式锁:满足分布式体系或集群模式下多历程可见而且互斥的锁。
分布式锁的焦点头脑就是让各人都利用同一把锁,只要各人利用的是同一把锁,那么我们就能锁住线程,不让线程举行,让步调串行实验,这就是分布式锁的焦点思绪
分布式锁应该满足一些什么样的条件呢?

可见性:多个线程都能看到雷同的效果,注意:这个地方说的可见性并不是并发编程中指的内存可见性,只是说多个历程之间都能感知到变革的意思
互斥:互斥是分布式锁的最根本的条件,使得步调串行实验
高可用:步调不易瓦解,时时候刻都包管较高的可用性
高性能:由于加锁自己就让性能低落,全部对于分布式锁自己须要他就较高的加锁性能和开释锁性能
安全性:安全也是步调中必不可少的一环
常见的分布式锁

Mysql:mysql自己就带有锁机制,但是由于mysql性能自己一样平常,以是接纳分布式锁的情况下,实在利用mysql作为分布式锁比力少见
Redis:redis作为分布式锁黑白常常见的一种利用方式,如今企业级开发中根本都利用redis大概zookeeper作为分布式锁,利用setnx这个方法,如果插入key乐成,则体现得到到了锁,如果有人插入乐成,其他人插入失败则体现无法得到到锁,利用这套逻辑来实现分布式锁
Zookeeper:Zookeeper 是一个开源的分布式和谐服务,用于分布式应用步调的和谐。它提供了一种简朴而高效的方式来实现分布式体系中的数据划一性和和谐。

Redis实现分布式锁的焦点思绪

实现分布式锁时须要实现的两个根本方法:


  • 获取锁:

    • 互斥:确保只能有一个线程获取锁
    • 非壅闭:实验一次,乐成返回true,失败返回false

  • 开释锁:

    • 手动开释
    • 超时开释:获取锁时添加一个超时时间

焦点思绪:
SETNX :SETNX 下令是redis中一个添加数据的下令,SETNX 只有在指定的key不存在时才会设置key的值。如果key已经存在,下令不会举行任何操纵。如果乐成设置key值,返回1,反之返回0
我们利用redis 的setNx 方法,当有多个线程进入时,第一个线程实验获取锁,抢到之后实验使命,然后再开释锁(删除锁),而没有抢到锁的线程则等候肯定时间后再重试即可   


误删情况以及办理方案

阐明:
持有锁的线程在锁的内部出现了壅闭,导致他的锁主动开释(超时开释),这时其他线程,线程2来实验得到锁,就拿到了这把锁,然后线程2在持有锁实验过程中,线程1反应过来,继续实验,而线程1实验过程中,走到了删除锁逻辑,此时就会把本应该属于线程2的锁举行删除,这就是误删别人锁的情况
办理方案:办理方案就是在每个线程开释锁的时间,去判断一下当前这把锁是否属于自己,如果属于自己,则不举行锁的删除,那么怎么举行锁的辨识呢:差别线程设置属于自己的锁的value即可,比如将线程id作为value值,即可到达辨识锁身份的效果
分布式锁的原子性标题以及办理方案

更为极度的误删逻辑阐明:
线程1如今持有锁之后,在实验业务逻辑过程中,他正准备删除锁,而且已经走到了条件判断的过程中,比如他已经拿到了当前这把锁确实是属于他自己的,正准备删除锁,但是此时他的锁到期了,那么此时线程2进来,但是线程1他会接着今后实验,当他卡顿竣事后,他直接就会实验删除锁那行代码,也就是说线程1在即将删除锁时可巧卡顿了,此时线程2可巧进来获取了一把锁,在线程2获取完之后线程1规复了直接把线程2的锁给删掉,这就是删锁时的原子性标题,之以是有这个标题,是由于线程1的拿锁,识锁,删锁,实际上并不是原子性的
办理方案:
利用lua脚本
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis下令,确保多条下令实验时的原子性。Lua是一种编程语言,它的根本语法各人可以参考网站:Lua 教程 | 菜鸟教程
这里重点先容lua脚本中Redis提供的调用函数,语法如下:
  1. redis.call('命令名称', 'key', '其它参数', ...)
复制代码
比方,我们要实验set name jack,则脚本是如许:
  1. # 执行 set name jack
  2. redis.call('set', 'name', 'jack')
复制代码
操纵redis的拿锁识锁删锁的lua脚本:
  1. -- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
  2. -- 获取锁中的标示,判断是否与当前线程标示一致
  3. if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  4.   -- 一致,则删除锁
  5.   return redis.call('DEL', KEYS[1])
  6. end
  7. -- 不一致,则直接返回
  8. return 0
复制代码
将写好的脚本放到Resource下,然后再掉用即可:
  1. private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
  2.     static {
  3.         UNLOCK_SCRIPT = new DefaultRedisScript<>();
  4.         UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
  5.         UNLOCK_SCRIPT.setResultType(Long.class);
  6.     }
  7. public void unlock() {
  8.     // 调用lua脚本
  9.     stringRedisTemplate.execute(
  10.             UNLOCK_SCRIPT,
  11.             Collections.singletonList(KEY_PREFIX + name),
  12.             ID_PREFIX + Thread.currentThread().getId());
  13. }
  14. 经过以上代码改造后,我们就能够实现 拿锁比锁删锁的原子性动作了~
复制代码
小总结:

基于Redis的分布式锁实现思绪:


  • 利用set nx ex获取锁,并设置逾期时间,生存线程标示
  • 开释锁时先判断线程标示是否与自己划一,划一则删除锁

    • 特性:

      • 利用set nx满足互斥性
      • 利用set ex包管故障时锁依然能开释,克制死锁,进步安全性
      • 利用Redis集群包管高可用和高并发特性
      • 利用lua脚本确保划一性


分布式锁-redission

基于setnx实现的分布式锁存在下面的标题:
重入标题:重入标题是指 得到锁的线程可以再次进入到雷同的锁的代码块中,可重入锁的意义在于防止死锁,比如HashTable如许的代码中,他的方法都是利用synchronized修饰的,如果他在一个方法内,调用另一个方法,那么此时如果是不可重入的,不就死锁了吗?以是可重入锁他的紧张意义是防止死锁,我们的synchronized和Lock锁都是可重入的。
不可重试:是指如今的分布式只能实验一次,我们以为公道的情况是:当线程在得到锁失败后,他应该能再次实验得到锁。
超时开释:我们在加锁时增长了逾期时间,如许的我们可以防止死锁,但是如果卡顿的时间超长,固然我们接纳了lua表达式防止删锁的时间,误删别人的锁,但是究竟没有锁住,有安全隐患
主从划一性: 如果Redis提供了主从集群,当我们向集群写数据时,主机须要异步的将数据同步给从机,而万一在同步已往之前,主机宕机了,就会出现死锁标题。
那么什么是Redission呢

Redisson是一个在Redis的底子上实现的Java驻内存数据网格(In-Memory Data Grid)。它不但提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,此中就包罗了各种分布式锁的实现。
Redission提供了分布式锁的多种多样的功能

Redission依靠:
  1. <dependency>
  2.         <groupId>org.redisson</groupId>
  3.         <artifactId>redisson</artifactId>
  4.         <version>3.13.6</version>
  5. </dependency>
复制代码
 设置Redisson客户端:
  1. @Configuration
  2. public class RedissonConfig {
  3.     @Bean
  4.     public RedissonClient redissonClient(){
  5.         // 配置
  6.         Config config = new Config();
  7.         config.useSingleServer().setAddress("redis://192.168.150.101:6379")
  8.             .setPassword("123321");
  9.         // 创建RedissonClient对象
  10.         return Redisson.create(config);
  11.     }
  12. }
复制代码
怎样利用Redission的分布式锁:
  1. @Resource
  2. private RedissionClient redissonClient;
  3. @Test
  4. void testRedisson() throws Exception{
  5.     //获取锁(可重入),指定锁的名称
  6.     RLock lock = redissonClient.getLock("anyLock");
  7.     //尝试获取锁,参数分别是:获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位
  8.     boolean isLock = lock.tryLock(1,10,TimeUnit.SECONDS);
  9.     //判断获取锁成功
  10.     if(isLock){
  11.         try{
  12.             System.out.println("执行业务");         
  13.         }finally{
  14.             //释放锁
  15.             lock.unlock();
  16.         }
  17.         
  18.     }
  19.    
  20. }
复制代码
分布式锁-redission可重入锁原理

在Lock锁中,他是借助于底层的一个voaltile的一个state变量来记载重入的状态的,比如当前没有人持有这把锁,那么state=0,如果有人持有这把锁,那么state=1,如果持有这把锁的人再次持有这把锁,那么state就会+1 ,如果是对于synchronized而言,他在c语言代码中会有一个count,原理和state雷同,也是重入一次就加一,开释一次就-1 ,直到镌汰成0 时,体现当前这把锁没有被人持有。
在redission中,我们的也支持支持可重入锁
在分布式锁中,他接纳hash结构用来存储锁,此中大key体现体现这把锁是否存在,用小key体现当前这把锁被哪个线程持有,以是接下来我们一起分析一下当前的这个lua表达式
这个地方一共有3个参数
KEYS[1] : 锁名称
ARGV[1]: 锁失效时间
ARGV[2]: id + ":" + threadId; 锁的小key
exists: 判断数据是否存在 name:是lock是否存在,如果==0,就体现当前这把锁不存在
redis.call('hset', KEYS[1], ARGV[2], 1);此时他就开始往redis里边去写数据 ,写成一个hash结构
Lock{
id + ":" + threadId : 1
}
如果当前这把锁存在,则第一个条件不满足,再判断
redis.call('hexists', KEYS[1], ARGV[2]) == 1
此时须要通过大key+小key判断当前这把锁是否是属于自己的,如果是自己的,则举行
redis.call('hincrby', KEYS[1], ARGV[2], 1)
将当前这个锁的value举行+1 ,redis.call('pexpire', KEYS[1], ARGV[1]); 然后再对其设置逾期时间,如果以上两个条件都不满足,则体现当前这把锁抢锁失败,末了返回pttl,即为当前这把锁的失效时间
如果小伙帮们看了前边的源码, 你会发现他会去判断当前这个方法的返回值是否为null,如果是null,则对应则前两个if对应的条件,退出抢锁逻辑,如果返回的不是null,即走了第三个分支,在源码处会举行while(true)的自旋抢锁。
  1. "if (redis.call('exists', KEYS[1]) == 0) then " +
  2.                   "redis.call('hset', KEYS[1], ARGV[2], 1); " +
  3.                   "redis.call('pexpire', KEYS[1], ARGV[1]); " +
  4.                   "return nil; " +
  5.               "end; " +
  6.               "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
  7.                   "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
  8.                   "redis.call('pexpire', KEYS[1], ARGV[1]); " +
  9.                   "return nil; " +
  10.               "end; " +
  11.               "return redis.call('pttl', KEYS[1]);"
复制代码


分布式锁-redission锁重试和WatchDog机制

阐明:由于课程中已经阐明白有关tryLock的源码剖析以及其看门狗原理,以是笔者在这里给各人分析lock()方法的源码剖析,渴望各人在学习过程中,可以大概把握更多的知识
抢锁过程中,得到当火线程,通过tryAcquire举行抢锁,该抢锁逻辑和之前逻辑雷同
1、先判断当前这把锁是否存在,如果不存在,插入一把锁,返回null
2、判断当前这把锁是否是属于当火线程,如果是,则返回null
以是如果返回是null,则代表着当前这哥们已经抢锁完毕,大概可重入完毕,但是如果以上两个条件都不满足,则进入到第三个条件,返回的是锁的失效时间
  1. long threadId = Thread.currentThread().getId();
  2. Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
  3. // lock acquired
  4. if (ttl == null) {
  5.     return;
  6. }
复制代码
接下来会有一个条件分支,由于lock方法有重载方法,一个是带参数,一个是不带参数,如果不带参数传入的值是-1,如果传入参数,则leaseTime是他自己,以是如果传入了参数,此时leaseTime != -1 则会进去抢锁,抢锁的逻辑就是之前说的那三个逻辑
如果是没有传入时间,则此时也会举行抢锁, 而且抢锁时间是默认看门狗时间
  1. commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout()
  2. ttlRemainingFuture.onComplete((ttlRemaining, e)
复制代码
这句话相当于对以上抢锁举行了监听,也就是说当上边抢锁完毕后,此方法会被调用,详细调用的逻辑就是去背景开启一个线程,举行续约逻辑,也就是看门狗线程
  1. RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
  2.                                         commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
  3.                                         TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
  4. ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
  5.     if (e != null) {
  6.         return;
  7.     }
  8.     // lock acquired
  9.     if (ttlRemaining == null) {
  10.         scheduleExpirationRenewal(threadId);
  11.     }
  12. });
  13. return ttlRemainingFuture;
复制代码
此逻辑就是续约逻辑,注意看commandExecutor.getConnectionManager().newTimeout() 此方法
Method( new TimerTask() {},参数2 ,参数3 )
指的是:通过参数2,参数3 去形貌什么时间去做参数1的变乱,如今的情况是:10s之后去做参数一的变乱
由于锁的失效时间是30s,当10s之后,此时这个timeTask 就触发了,他就去举行续约,把当前这把锁续约成30s,如果操纵乐成,那么此时就会递归调用自己,再重新设置一个timeTask(),于是再过10s后又再设置一个timerTask,完成不绝的续约
那么各人可以想一想,假设我们的线程出现了宕机他还会续约吗?固然不会,由于没有人再去调用renewExpiration这个方法,以是比实时间之后自然就开释了。
  1. private void renewExpiration() {
  2.     ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
  3.     if (ee == null) {
  4.         return;
  5.     }
  6.    
  7.     Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
  8.         @Override
  9.         public void run(Timeout timeout) throws Exception {
  10.             ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
  11.             if (ent == null) {
  12.                 return;
  13.             }
  14.             Long threadId = ent.getFirstThreadId();
  15.             if (threadId == null) {
  16.                 return;
  17.             }
  18.             
  19.             RFuture<Boolean> future = renewExpirationAsync(threadId);
  20.             future.onComplete((res, e) -> {
  21.                 if (e != null) {
  22.                     log.error("Can't update lock " + getName() + " expiration", e);
  23.                     return;
  24.                 }
  25.                
  26.                 if (res) {
  27.                     // reschedule itself
  28.                     renewExpiration();
  29.                 }
  30.             });
  31.         }
  32.     }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
  33.    
  34.     ee.setTimeout(task);
  35. }
复制代码
分布式锁-redission锁的MutiLock原理

为了进步redis的可用性,我们会搭建集群大概主从,如今以主从为例
此时我们去写下令,写在主机上, 主时机将数据同步给从机,但是假设在主机还没有来得及把数据写入到从机去的时间,此时主机宕机,哨兵会发现主机宕机,而且推选一个slave变成master,而此时新的master中实际上并没有锁信息,此时锁信息就已经丢掉了。

为了办理这个标题,redission提出来了MutiLock锁,利用这把锁咱们就倒霉用主从了,每个节点的职位都是一样的, 这把锁加锁的逻辑须要写入到每一个主丛节点上,只有全部的服务器都写入乐成,此时才是加锁乐成,假设如今某个节点挂了,那么他去得到锁的时间,只要有一个节点拿不到,都不能算是加锁乐成,就包管了加锁的可靠性。  


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表