Redisson框架实现Redis分布式锁的实现原理

一、前言

先看一段Redisson框架调用

RLock lock = redisson.getLock("myLock");
lock.lock();
//.......业务代码
lock.unlock();

Redisson支持redis单实例、redis哨兵、redis cluster、redis master-slave等各种部署架构

二、Redisson实现redis分布式锁的底层原理

1、lock.tryLock方法之tryAcquire获取锁方法

//带超时时间获取锁,如果在leaseTime内未获取锁,直接返回失败,未超过超时 获取锁失败,那么会阻塞。客户端释放锁后 会继续尝试获取锁
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    long time = unit.toMillis(waitTime);
    long current = System.currentTimeMillis();
    long threadId = Thread.currentThread().getId();
    //获取锁:参数:等待时间,key过期时长(也是锁续约时长),时长单位,获取锁的线程id
    Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
    // lock acquired
    if (ttl == null) {
        return true;
    }
    //获取锁失败执行下面方法....下面分析
}        

1.1、tryAcquireAsync-通过lua脚本获取锁(lua实现锁互斥、可重入)


private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Long> ttlRemainingFuture;
//如果没有设置过 key过期时长
 if (leaseTime != -1) {
     ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
 } else {
 //未设置过key过期时长,默认30s过期
 ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
             TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
 }
 //获取锁方法执行完成后,回调
 ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
     if (e != null) {
         return;
     }
     // lock acquired
     //ttlRemaining 不为空,说明获取锁成功
     if (ttlRemaining == null) {
     //设置了key过期时长,表示设置过超时时间,更新internalLockLeaseTime,并返回
         if (leaseTime != -1) {
             internalLockLeaseTime = unit.toMillis(leaseTime);
         } else {
             //如果没有设置Key过期时长,key默认是30s过期,启动Watch Dog 一个线程 每1/3的internalLockLeaseTime
             //执行一下 任务
             scheduleExpirationRenewal(threadId);
         }
     }
 });
 return ttlRemainingFuture;
}

以redis cluster集群为例,现在某个客户端要加锁,它首先会hash节点选择一台机器。(这里只是选择一台机器,还没有加锁),接着,会发送一段lua脚本到redis上,脚本如下:(用lua脚本的原因:就是为了保证下面指令的原子性)

//判断KEYS[1]是否存在,如果不存在,说明还没有客户端获取锁
"if(redis.call('exists',KEYS[1])==0) then "+ 
      //key不存在,那么用hset命令加锁,field为:客户端id value为:1(客户端获取了一次锁)
     "redis.call('hset',KEYS[1],ARGV[2],1); "+ 
     //设置这个锁的过期时长为 ARGV[1]
     "redis.call('pexpire',KEYS[1],ARGV[1]); "+
     //返回
     "return nil; "+
//if结束     
"end; "+
//如果KEYS[1]已经存在,那么再判断field为ARGV[2]的值是否存在,即是:判断客户端ARGV[2]是否获得了锁
"if(redis.call('hexists',KEYS[1],ARGV[2])==1) then "+
     //如果客户端获取了锁,那么将field的值加1,即是:将对应客户端获取锁的次数加1
     "redis.call('hincrby',KEYS[1],ARGV[2],1); "+
     //设置这个key下field值过期时长
     "redis.call('pexpire',KEYS[1],ARGV[1]); "+
     //返回
     "return nil; "+
//if结束     
"end; "+
//key已经存在,并且获取锁的客户端id不是当前执行方法的客户端id,返回锁key的剩余生存时间
//此时客户端2会进入一个while循环,不停的尝试加锁。
"return redis.call('pttl',KEYS[1]);";

参数:

  • KEYS[1]:代表的是你加锁的那个key
    比如:RLock lock = redisson.getLock("myLock");
  • ARGV[1]:代表的就是锁key的默认生存时间,默认30秒。
  • ARGV[2]:代表的是加锁的客户端的ID,类似于下面这样:
    34634f3f3b-2342-3244-87fd-34234efdsf3423f34f:1

首先用exists myLock命令判断,如果要加的锁key不存在,
那么就用hset myLock 34634f3f3b-2342-3244-87fd-34234efdsf3423f34f:1 1 命令加锁,设置key为myLock,field为客户端id ,值为1 的hash值;接着执行pexpire myLock 30000,设置myLock锁 key的生存时间是30秒。

1.2、锁的互斥

如果客户2现在也来尝试加锁:
判断exists myLock已经存在,那么执行第二个If 块,判断myLock锁key的hash数据结构中,是否包含客户端2的ID,但是明显不是的,因为那里包含的是客户端1的ID。
所以,客户端2会执行pttl myLock,获取myLock这个锁key的剩余生存时间。比如还剩10000ms,此时客户端2进入一个while循环,不停的尝试加锁。

1.3、可重入加锁机制

客户端如果已经持有了锁,如果此时 再尝试获取锁 会怎样?比如:

RLock lock = redisson.getLock("myLock");
lock.lock();
//.....
lock.lock();
//.....
lock.unlock();
lock.unlock();

第一个if判断肯定不成立,“exists myLock”会显示锁key已经存在了。
第二个if判断会成立,因为myLock的hash数据结构中包含的那个ID,就是客户端1的那个ID,此时就会执行可重入锁的逻辑incrby myLock 34634f3f3b-2342-3244-87fd-34234efdsf3423f34f:1 1,将field为客户端1的值加1,变成2。就是将获取锁的次数变成了2

2、watch dog自动延期机制

客户端1加锁的锁key默认生存时间30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?

watch dog自动延期机制:只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

private void renewExpiration() {
  ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
  if (ee == null) {
      return;
  }
  Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
      @Override
      public void run(Timeout timeout) throws Exception {
          ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
          if (ent == null) {
              return;
          }
          Long threadId = ent.getFirstThreadId();
          if (threadId == null) {
              return;
          }
          //如果当前线程 还持有锁,那么对它进行延期
          RFuture<Boolean> future = renewExpirationAsync(threadId);
          future.onComplete((res, e) -> {
              if (e != null) {
                  log.error("Can't update lock " + getRawName() + " expiration", e);
                  EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                  return;
              }
              
              if (res) {
                  // reschedule itself
                  renewExpiration();
              } else {
                  cancelExpirationRenewal(null);
              }
          });
      }
      //设置每隔1/3的internalLockLeaseTime时间执行这个task,internalLockLeaseTime默认是30s,于是这里每职下10s执行
  }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
  
  ee.setTimeout(task);
}
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
             //如果持有锁的线程 是当前线程,那么延长锁过期时间
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return 0;",
            Collections.singletonList(getRawName()),
            internalLockLeaseTime, getLockName(threadId));
}

KEYS[1]:Collections.singletonList(getRawName()) 锁的名字
ARGV[2]):当前线程Id
ARGV[1]:锁延期时长,再延期一个internalLockLeaseTime 时长

3、lock.tryLock方法之 获取失败,redisson如何实现阻塞,及其它客户端释放锁后,被阻塞的线程如何收到通知去竞争锁

public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
//获取锁如果成功,那么上面就返回true.如果获取失败,那么继续向下走
//省略部分代码
     //等待剩余时间,小于等于0,说明等待超时,返回false,竞争锁失败   
    time -= System.currentTimeMillis() - current;
    if (time <= 0) {
        acquireFailed(waitTime, unit, threadId);
        return false;
    }
    current = System.currentTimeMillis();
   // 订阅redis 名为`redisson_lock__channel+锁名称`的channel,并且创建RedissonLockEntry
   //这里面并不是所有 获取锁失败的客户端 都会去订阅 ,里面设置了一个信号量,只有先获取信号量成功的线程 才会订阅,获取信号量失败的线程 会被 阻塞。这样做是为了 大量客户端 订阅,同时收到释放锁的信号 产生大量竞争。
   //信号量大小固定值为50
    RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
  // 阻塞等待subscribe的future的结果对象,如果subscribe方法调用超过了time,说明已经超过了客户端设置的最大wait time,则直接返回false,取消订阅,不再继续申请锁了。
    if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
        if (!subscribeFuture.cancel(false)) { //取消订阅
            subscribeFuture.onComplete((res, e) -> {
                if (e == null) {
                    unsubscribe(subscribeFuture, threadId);
                }
            });
        }
        acquireFailed(waitTime, unit, threadId); //表示抢占锁失败
        return false; //返回false
    }
    try {
        //判断是否超时,如果等待超时,返回获的锁失败
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            acquireFailed(waitTime, unit, threadId);
            return false;
        }
        //通过while循环再次尝试竞争锁
        while (true) { 
            long currentTime = System.currentTimeMillis();
            ttl = tryAcquire(waitTime, leaseTime, unit, threadId); //竞争锁,返回锁超时时间
            // lock acquired
            if (ttl == null) { //如果超时时间为null,说明获得锁成功
                return true;
            }
            //判断是否超时,如果超时,表示获取锁失败
            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) {
                acquireFailed(waitTime, unit, threadId);
                return false;
            }

            // 通过信号量(共享锁)阻塞,等待解锁消息.  (减少申请锁调用的频率)
            // 如果剩余时间(ttl)小于wait time ,就在 ttl 时间内,从Entry的信号量获取一个许可(除非被中断或者一直没有可用的许可)。
            // 否则就在wait time 时间范围内等待可以通过信号量
            currentTime = System.currentTimeMillis();
            if (ttl >= 0 && ttl < time) {
                subscribeFuture.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
            } else {
                subscribeFuture.getNow().getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
            }
            // 更新等待时间(最大等待时间-已经消耗的阻塞时间)
            time -= System.currentTimeMillis() - currentTime;
            if (time <= 0) { //获取锁失败
                acquireFailed(waitTime, unit, threadId);
                return false;
            }
        }
    } finally {
        unsubscribe(subscribeFuture, threadId); //取消订阅
    }
//        return get(tryLockAsync(waitTime, leaseTime, unit));
}

4、锁的释放机制

执行lock.unlock(),就可以释放分布式锁,实际上就是每次都对myLock数据结构中的那个加锁次数减1。如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:
del myLock命令,从redis里删除这个key。
其它的客户端就可以尝试完成加锁

释放锁的流程:

如果lock键不存在,通过publish指令发送一个消息表示锁已经可用。

如果锁不是被当前线程锁定,则返回nil

由于支持可重入,在解锁时将重入次数需要减1

如果计算后的重入次数>0,则重新设置过期时间

如果计算后的重入次数<=0,则发消息说锁已经可用

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
     return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
             "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                     "return nil;" +
                     "end; " +
                     //对持有锁 线程数 减一
                     "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                     "if (counter > 0) then " +
                     //大于0,那么对锁续约,时长internalLockLeaseTime
                     "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                     "return 0; " +
                     "else " +
                     //如果为0,删除这个key
                     "redis.call('del', KEYS[1]); " +
                     //发送一条消息对channel,这样订阅了 这个channel的其它客户端会收到消息,
                     //感知到锁已经被释放,去竞争锁
                     "redis.call('publish', KEYS[2], ARGV[1]); " +
                     "return 1; " +
                     "end; " +
                     "return nil;",
             Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
 }

5、Redisson实现Redis分布式锁的缺点

上面方案最大的问题就是:如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的master slave实例。
但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。
接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。
此时就会导致多个客户端对一个分布式锁完成了加锁。
这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。
所以这个就是redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。

6、总结

根据上面解析,那么可总结出一张redisson获取锁的流程图如下:
在这里插入图片描述

参考:
https://mp.weixin.qq.com/s?__biz=MzU0OTk3ODQ3Ng==&mid=2247483893&idx=1&sn=32e7051116ab60e41f72e6c6e29876d9&chksm=fba6e9f6ccd160e0c9fa2ce4ea1051891482a95b1483a63d89d71b15b33afcdc1f2bec17c03c&scene=21#wechat_redirect

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redisson是基于Redis实现的Java驻留内存数据网格的开源框架,提供了丰富的分布式锁实现方式。下面介绍基于Redisson的最优实现。 1. 初始化Redisson客户端 ```java Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); RedissonClient redisson = Redisson.create(config); ``` 2. 获取分布式锁 ```java RLock lock = redisson.getLock("myLock"); lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); } ``` 3. 设置加锁超时时间和释放锁时限制 ```java RLock lock = redisson.getLock("myLock"); boolean locked = lock.tryLock(10, 60, TimeUnit.SECONDS); try { if (locked) { // 业务逻辑 } else { // 获取锁失败 } } finally { if (locked) { lock.unlock(); } } ``` 4. 实现可重入锁 ```java RLock lock = redisson.getLock("myLock"); lock.lock(); try { lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); } } finally { lock.unlock(); } ``` 5. 实现公平锁 ```java RLock lock = redisson.getFairLock("myFairLock"); lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); } ``` 6. 实现读写锁 ```java RReadWriteLock rwlock = redisson.getReadWriteLock("myReadWriteLock"); rwlock.readLock().lock(); try { // 读操作业务逻辑 } finally { rwlock.readLock().unlock(); } rwlock.writeLock().lock(); try { // 写操作业务逻辑 } finally { rwlock.writeLock().unlock(); } ``` 以上就是基于Redisson实现Redis分布式锁的最优实现Redisson提供了丰富的分布式锁实现,可以根据业务需求选择合适的锁类型。同时,Redisson还提供了许多其他功能,如分布式对象、分布式限流等,可以方便地实现分布式应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值