Redisson实现分布式锁原理及源码分析

为什么要分布式锁

单体应用中,可以通过synchronized等相关锁实现线程间共享数据的独占,但是在分布式环境下,线程锁是不能跨应用的,所以需要通过一个分布式存储组件来实现分布式锁。常用的分布式锁实现组件有Redis和ZooKeeper,由于Redis是AP(可用性)架构的,ZooKeeper是CP(一致性)架构的,根据实际的应用场景,两种实现方案都可以。

大多数场景下,用Redis实现分布式锁就可以了,主要是Redis性能比ZooKeeper更好,实现的分布式锁效率更高。如果需要在强一致性条件下实现分布式锁,那么可以考虑使用ZooKeeper。

此处介绍的是Redis实现分布式锁,一些Redis客户端组件已经为我们封装实现了Redis分布式锁了,比如Redisson。生产环境下,建议使用这些组件实现的分布式锁,以防止自己实现时考虑不周导致线上问题。这些三方组件是通过大量用户验证了的,安全性和性能更高一些。

下面来介绍下分布式锁的实现原理,以及Redisson实现分布式锁的方案和源码。

Redisson实现分布式锁

参考:github.com/redisson/redisson/wiki

原理图

image.png

代码示例

大概逻辑:

// 下单场景
public String order(){
    // 商品编号
    String lockKey = "product:001";
    // 获取锁对象,即RedissonLock
    RLock lock = redisson.getLock(lockKey);
    try {
        // 加分布式锁
        lock.lock();
        // 执行业务逻辑
        System.out.println("抢单逻辑。。。");
    } finally {
        // 释放锁
        lock.unlock();
    }
    return "success";
}

后面的源码分析是根据getLock()lock.lock()unlock(),三个方法来展开的,暂未分析tryLock()的实现。

锁实现关注点

前面的代码是大概的加解锁逻辑,如果要实现分布式锁,可以提前想一想,Redisson的代码需要实现哪些功能呢?

  • 如何原子地获取锁?

可以通过redis的setnx命令实现原子操作,key不存在返回true,key已存在返回false。

  • 客户端服务器宕机,锁如何释放?

即使在finally里有解锁逻辑,但是如果服务器宕机或者应用挂掉时没有走finally逻辑,会导致之前设置的锁无法释放,导致死锁。可以通过expire给指定key设置过期时间,如果客户端服务意外宕机,锁也会在过期后自动释放。

  • 如何保证设置值和设置过期时间的原子性?

redisson中是通过lua脚本封装redis命令setnx和expire实现原子性的,lua脚本的批量命令可以在redis服务端原子地执行,这是redis所支持的特性,关于lua脚本后面会介绍。

  • 如何设置过期时间从而保证业务逻辑刚好执行完释放锁?

如果只是简单地设置一个固定过期时间,可能会出现两种场景问题:第一,如果业务逻辑没有执行完锁过期了,此时别的请求就会加锁,导致之前请求的锁失效,引发并发问题;第二,如果业务逻辑执行完锁没过期,此时就会导致别的请求也无法获取到锁,降低了锁性能。第一个问题,redisson中是通过锁续期来解决的(watch dog)。第二个问题,redisson中是通过redis的发布订阅功能实现的,锁释放后发布一个消息到redis的channel,等待获取锁的请求会订阅到该消息,及时地再次尝试获取锁。

  • 没有竞争到锁的请求是如何处理的?

类似于synchronized的线程锁,竞争失败的请求需要再次获取锁,在redisson中是通过间歇性尝试加锁来实现的。每隔一定的时间再次尝试获取锁,而且该请求会订阅锁的释放消息,一旦锁释放等待中的请求立马尝试加锁,而不用在下一个轮循周期到来时再尝试加锁。这么看来,redisson尝试加锁逻辑是非公平锁的实现。

  • redisson会存在一个请求释放另一个请求的锁吗?

不会。我们可以来分析一下,如果请求A获取锁后执行业务逻辑还没释放锁,请求B也获取到了该锁,请求A比请求B先执行完,此时释放的是请求B的锁。但是此种情况在redisson中是不存在的,因为redisson有锁续命逻辑,请求A获取到锁后别的请求是拿不到锁的。

可以看到,要实现一个分布式锁功能,要考虑的地方还是很多的,不仅要逻辑缜密无逻辑漏洞,还需要考虑到锁的性能。

源码分析

getLock()-获取锁对象

获取锁对象,其实就是获取一个RedissonLock对象(锁的大部分逻辑都是在该对象中实现的),主要是一些初始化工作。

RLock lock = redisson.getLock(lockKey);

RedissonLock继承了RedissonExpirable抽象类,实现了RLock接口,如下图:
截屏2023-08-12 10.26.10.png

RedissonLock构造方法:

// commandExecutorcommandExecutor为通过redis连接配置生成的命令执行器
// name为锁名称
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
    // 对应的RedissonExpirable构造方法
    super(commandExecutor, name);
    this.commandExecutor = commandExecutor;
    // id为通过UUID.randomUUID()生成的uuid
    this.id = commandExecutor.getConnectionManager().getId();
    // internalLockLeaseTime为redis连接配置文件配置的看门狗超时时间,默认30秒
    this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
}

lock()-加锁

开始竞争锁。

lock.lock();
public void lock() {
    try {
        // 获取锁
        lockInterruptibly();
    } catch (InterruptedException e) {
        // 处理中断异常
        Thread.currentThread().interrupt();
    }
}
public void lockInterruptibly() throws InterruptedException {
    lockInterruptibly(-1, null);
}

获取锁:
获取锁成功返回ttl剩余过期时间为null,不再继续执行。获取锁失败返回ttl剩余过期时间不为null,while循环再次获取锁,这里用到了Semaphore实现阻塞ttl时间后轮序和唤醒通知机制。

public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
    long threadId = Thread.currentThread().getId();
    // 通过lua脚本原子地设置锁名称到redis,并设置过期时间(默认30秒)
    // 是一个hash结构,key为锁名称,field为uuid+当前线程id
    // ttl为当前锁剩余过期时间毫秒数
    Long ttl = this.tryAcquire(leaseTime, unit, threadId);
    
    // 请求获取锁成功,ttl返回null,不再执行后面逻辑
    
    if (ttl != null) {
        // ttl不为空,表示当前锁已被占用,没有获取到锁
        // redis的发布订阅,没有抢到锁的请求订阅一个channel,哪里发布的这个channel呢?主请求解锁时发布的
        RFuture<RedissonLockEntry> future = this.subscribe(threadId);
        this.commandExecutor.syncSubscription(future);

        try {
            while(true) {
                // 再次尝试获取锁,刷新ttl
                ttl = this.tryAcquire(leaseTime, unit, threadId);
                if (ttl == null) {
                    // 获取到了锁
                    return;
                }

                if (ttl >= 0L) {
                    // 仍然没有获取到锁,自旋获取锁,非公平
                    // 信号量,阻塞ttl时间后再while,让出cpu,因为信号量的凭证数量为0
                    // 如果获取到锁的请求在ttl之内就自行完毕,难道这里还要等ttl个时间?有唤醒功能
                    this.getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    this.getEntry(threadId).getLatch().acquire();
                }
            }
        } finally {
            // 取消订阅
            this.unsubscribe(future, threadId);
        }
    }
}
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
    // 获取异步结果
    return get(tryAcquireAsync(leaseTime, unit, threadId));
}

尝试加锁:
加锁成功走续命逻辑,加锁失败会还行上级方法中的while循环获取锁逻辑。

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, final long threadId) {
    if (leaseTime != -1) {
		// lock()不会进来
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
	// leaseTime获取的是redis配置中的时间,默认30秒
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    ttlRemainingFuture.addListener(new FutureListener<Long>() {
        @Override
        public void operationComplete(Future<Long> future) throws Exception {
            if (!future.isSuccess()) {
                return;
            }
        	// tryLockInnerAsync()方法异步执行成功后
            // 得到当前锁过期剩余时间
            Long ttlRemaining = future.getNow();
            // lock acquired
            if (ttlRemaining == null) {
                // 加锁成功,走这里
                // 超时刷新,锁续命逻辑,延时任务xxx/3,主线程锁存在,重置为30秒
                scheduleExpirationRenewal(threadId);
            }
        }
    });
    return ttlRemainingFuture;
}

加锁的lua脚本分析:

<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    // 时间单位转换
    internalLockLeaseTime = unit.toMillis(leaseTime);

    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
              "if (redis.call('exists', KEYS[1]) == 0) then " +
              // 锁名为key在redis中不存在,加锁并设置过期时间,注意此处的数据结构是hash
              // key为锁名,filed为uuid+threadId,value为1,过期时间为传入的时间
              "redis.call('hset', KEYS[1], ARGV[2], 1); " +
              "redis.call('pexpire', KEYS[1], ARGV[1]); " +
              "return nil; " +
              "end; " +
              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
              // 锁对应的filed存在,可重入锁,对应的filed加1,重置过期时间
              // 暂不分析这种情况
              "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
              "redis.call('pexpire', KEYS[1], ARGV[1]); " +
              "return nil; " +
              "end; " +
          	  // 锁名为key在redis中已存在,但是不是自己的锁,返回当前锁过期剩余时间,单位毫秒
              "return redis.call('pttl', KEYS[1]);",
              Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

续命逻辑:
后台异步任务。加锁成功后,每隔30/3=10秒重置当前锁的过期时间。

private void scheduleExpirationRenewal(final long threadId) {
    // tryLock的实现,暂不分析
    if (expirationRenewalMap.containsKey(getEntryName())) {
        return;
    }

    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            // 锁续命
            RFuture<Boolean> future = commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
                    "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                        // 当前锁存在,将锁过期时间重置为30秒
                     	"redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return 1; " +
                    "end; " +
                    "return 0;",
                      Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
            
            future.addListener(new FutureListener<Boolean>() {
                @Override
                public void operationComplete(Future<Boolean> future) throws Exception {
                    expirationRenewalMap.remove(getEntryName());
                    if (!future.isSuccess()) {
                        log.error("Can't update lock " + getName() + " expiration", future.cause());
                        return;
                    }
                    
                    if (future.getNow()) {
                        // reschedule itself
                        // 递归实现续命逻辑
                        scheduleExpirationRenewal(threadId);
                    }
                }
            });
        }
        // 默认每30/3=10秒间歇性执行
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

    if (expirationRenewalMap.putIfAbsent(getEntryName(), task) != null) {
        task.cancel();
    }
}

unlock()-解锁

主动解锁。

lock.unlock();
public void unlock() {
    // 解锁
    Boolean opStatus = (Boolean)this.get(this.unlockInnerAsync(Thread.currentThread().getId()));
    if (opStatus == null) {
        // 释放当前锁失败
        throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: " + this.id + " thread-id: " + Thread.currentThread().getId());
    } else {
        if (opStatus) {
            // 成功解锁,取消锁续命定时任务
            this.cancelExpirationRenewal();
        }

    }
}
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('exists', KEYS[1]) == 0) then " +
              	// 锁不存在,发布消息,等待的请求订阅了该channel
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; " +
            "end;" +
            "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
              	// 锁已被释放
                "return nil;" +
            "end; " +
          	// 之前这里设置的是1
            "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
            "if (counter > 0) then " +
              	// 重入锁解锁
                "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                "return 0; " +
            "else " +
              	// 释放锁,发布消息
                "redis.call('del', KEYS[1]); " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; "+
            "end; " +
            "return nil;",
            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.unlockMessage, internalLockLeaseTime, getLockName(threadId));

}

LUA脚本

redis2.6之后支持的可以在redis服务端执行的命令。
redis管道不支持多命令原子操作,redis也有事务,但是官方更建议使用lua实现事务。

redis中使用lua脚本格式:

eval script numkeys key [key ...] arg [arg ...]

命令解释:

script:具体的lua命令。
numkeys:参数用于指定键名参数的个数。
key [key …]:从 EVAL 的第三个参数开始算起,表示在脚本中所用到的那些 Redis 键(key),这些键名参数可以在 Lua 中通过全局变量 KEYS 数组,用 1 为基址的形式访问( KEYS[1] ,KEYS[2] ,以此类推)。对应redis命令中的key。
arg [arg …]:附加参数,可以在 Lua 中通过全局变量 ARGV 数组访问,访问的形式和 KEYS 变量类似( ARGV[1] 、ARGV[2] ,诸如此类)。

lua示例:

> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

其中:return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}表示这是一段有返回值的lua脚本。数字2指明了键名参数的数量,key1key2是键名参数,分别使用KEYS[1]KEYS[2]访问,firstsecond是附加参数,通过ARGV[1]ARGV[2]访问。

在lua脚本中执行redis命令:

redis.call();
redis.pcall();

示例:

> eval "return redis.call('set','foo','bar')" 0
OK
> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 foo bar
OK
> get foo
bar
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Firechou

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值