16.Redis系列之Redisson分布式锁原理

本文学习Redisson分布式锁的原理以及优缺点

1. Redisson分布式锁原理

lua脚本是原子操作,redis会将整个脚本作为一个整体执行,中间不会被其他命令打断

# RedissonLock.tryLockInnerAsync方法内lua脚本加锁
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
                "if (redis.call('exists', KEYS[1]) == 0) then " +
                        "redis.call('hincrby', 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 " +
                        "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                        "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                        "return nil; " +
                        "end; " +
                        "return redis.call('pttl', KEYS[1]);",
                Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
    }
1.1 加锁原理

1
2

如图与代码所示,sk_10001_stock_lock不存在时,加锁时会创建hash类型键sk_10001_stock_lock,并新增field为客户端id,value设置为1(hincrby key field 1不存在key则会创建它,并且设置field为1),并设置过期时间为60s

1.2 可重入原理

如lua脚本所示,当sk_10001_stock_lock存在客户端id时,会将客户端id的值+1,并且重新设置过期时间,所以保存客户端id就是为了可重入

1.3 锁互斥原理

如lua脚本所示,对于其他的客户端返回锁pttl过期时间,后续流程如代码所示

// RedissonLock.tryLock方法
    public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
        Long ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
        // ttl为空获取到锁
        if (ttl == null) {
            return true;
        }
        // 如果时间超过waitTime则获取锁失败
        time -= System.currentTimeMillis() - current;
        if (time <= 0) {
            acquireFailed(waitTime, unit, threadId);
            return false;
        }
	// 订阅锁释放事件
        CompletableFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
        try {
            subscribeFuture.get(time, TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
        } catch (ExecutionException e) {   
        }

        try {
            // 在最大等待时间内,循环获取锁,直到成功或失败
            while (true) {
                long currentTime = System.currentTimeMillis();
                ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
                // lock acquired
                if (ttl == null) {
                    return true;
                }

                time -= System.currentTimeMillis() - currentTime;
                if (time <= 0) {
                    acquireFailed(waitTime, unit, threadId);
                    return false;
                }

                // 通过信号量semaphore共享锁阻塞等待
                currentTime = System.currentTimeMillis();
                if (ttl >= 0 && ttl < time) {
                   commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } else {
                    commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                }
            }
        } finally {
            // 取消订阅
            unsubscribe(commandExecutor.getNow(subscribeFuture), threadId);
        }
    }

1.4 锁续期原理
// RedissonLock.tryAcquireAsync方法
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        CompletionStage<Long> f = ttlRemainingFuture.thenApply(ttlRemaining -> {
            // 获取到锁
            if (ttlRemaining == null) {
                if (leaseTime > 0) {
                    internalLockLeaseTime = unit.toMillis(leaseTime);
                } else {
                    // 当leaseTime<=0时锁会自动续期
                    scheduleExpirationRenewal(threadId);
                }
            }
            return ttlRemaining;
        });
        return new CompletableFutureWrapper<>(f);
    }
 private void renewExpiration() {
        // Watch Dog机制,每10秒检查是否仍然持有锁,是则续期
        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
	    }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
        
        ee.setTimeout(task);
    }
    // 依次进入方法,最终找到RedissonBaseLock.renewExpirationAsync实现
    protected CompletionStage<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));
    }

通过代码可知,当leasetime<=0时,锁通过Watch Dog机制[其实就是一个后台定时任务线程]每10秒检查是否持有锁,持有则自动续期30s,也就是重新设置了sk_10001_stock_lock中field为客户端id的过期时间为30s

1.5 锁释放原理
public RFuture<Void> unlockAsync(long threadId) {
        // 释放锁
        RFuture<Boolean> future = unlockInnerAsync(threadId);
    
        CompletionStage<Void> f = future.handle((opStatus, e) -> {
            // 取消Watch Dog机制
            cancelExpirationRenewal(threadId);
        });

        return new CompletableFutureWrapper<>(f);
    }
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 " +
                        "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.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
    }

由代码可知,释放锁时将sk_10001_stock_lock中field为客户端id的值依次减1,直到为0后在进行删除,删除后会向redisson_lock__channel通道中发送UNLOCK_MESSAGE消息也就是0L,通知阻塞等待的客户端

2. Redisson优缺点

优点

  • Redisson通过Watch Dog机制解决锁的续期问题
  • 与Zookeeper相比较,Redisson基于Redis性能更高
  • 通过Redisson实现分布式可重入锁,比原生的 SET mylock userId NX PX milliseconds + lua更加简洁,让我们更多关注业务逻辑
  • 在等待申请锁的实现上进行了优化,减少了无效锁申请,提升了资源利用率

缺点

在redisson3.12.5前,无法解决在master节点加锁后,master异步复制给slave时宕机,slave变为了master,其他客户端在新的master上重复加锁问题,需要使用redlock算法获取集群大多数锁时才算获取锁成功额外编码

RLock rLock = redissonClient.getLock(PRODUCT_STOCK_KEY + "_lock");
RLock rLock = redissonClient1.getLock(PRODUCT_STOCK_KEY + "_lock");
RLock rLock = redissonClient2.getLock(PRODUCT_STOCK_KEY + "_lock");
RedissonRedLock redissonRedLock = new RedissonRedLock(rLock, rLock1, rLock2);
redissonRedLock.tryLock(30, 60, TimeUnit.SECONDS);

https://github.com/redisson/redisson/blob/master/CHANGELOG.md
16-Apr-2020 - 3.12.5 released
Improvement - increased RLock reliability during failover. RedLock was deprecated

但是对于redisson3.12.5及之后就不存在该问题会等到master异步复制给slave完成后才会进行加锁

 /**
     * Returns Lock instance by name.
     * <p>
     * Implements a <b>non-fair</b> locking so doesn't guarantees an acquire order by threads.
     * <p>
     * To increase reliability during failover, all operations wait for propagation to all Redis slaves.
     *
     * @param name - name of object
     * @return Lock object
     */
    RLock getLock(String name);

欢迎关注公众号算法小生

### Redis 分布式锁Redisson实现原理 #### 1. **Redis 分布式锁** Redis 分布式锁的核心思想是通过 `SETNX` 和 `EXPIRE` 来实现加锁操作。在早期版本(Redis 2.6.12之前),由于 `SETNX` 不支持设置过期时间,因此需要分两步完成:先调用 `SETNX` 创建键值对表示锁定状态,再调用 `EXPIRE` 设置过期时间以防止死锁[^3]。 然而这种两步操作无法保证原子性,可能会因网络延迟或其他异常导致锁未成功创建却设置了过期时间。为此,Redis 2.6.12引入了新的命令 `SET key value NX PX milliseconds`,其中 `NX` 表示只有当键不存在时才执行设置操作,`PX` 则用于指定毫秒级的过期时间。这种方式能够在一个命令内完成加锁并设定超时,从而有效解决了上述问题。 #### 2. **Redisson 实现分布式锁** Redisson 是基于 Redis 开发的一个 Java 客户端库,它不仅实现了更高级别的抽象接口,还提供了多种类型的分布式对象和服务功能。对于分布式锁而言,Redisson 提供了一种更加健壮可靠的解决方案——`RLock` 接口及其子类实例化方式。 - **加锁逻辑** Redisson 使用 Lua 脚本来确保整个加锁过程具有原子性。该脚本会检查目标资源是否已被占用;如果未被占用,则尝试获取锁并将当前线程 ID 记录下来作为持有者的唯一标识符[^1]。 - **续命机制** 当某个客户端成功获得锁之后,Redisson 会在后台启动一个定时器任务定期向服务器发送续约请求延长锁的有效期限,直到显式解锁为止。此设计可以避免因长时间运行的任务而导致锁提前失效的情况发生[^2]。 - **自旋重试策略** 如果初次未能取得所需资源,则按照预定义间隔不断重复尝试直至达到最大等待时限或最终放弃争夺控制权。 - **公平性和可靠性保障措施** 在某些特殊情况下(比如网络分区), 可能会出现部分节点认为自己已经拿到了全局唯一的锁,但实际上其他地方也有竞争者存在的情形下, redisson 还特别考虑到了这一点并通过内部复杂的协调算法尽可能减少冲突概率[^4]. #### 性能对比分析 | 特性 | Redis 原生分布锁 | Redisson | |-------------------------|------------------------------------------|----------------------------------| | 加锁效率 | 较高 | 略低 | | 锁安全性 | 存在网络抖动等问题 | 更安全可靠 | | 功能扩展能力 | 单纯提供基础加解鎖功能 | 支持更多特性如自动续租、可重入等 | | 易用程度 | 需要开发者手动处理很多细节 | API 封装良好易于集成 | 从表中可以看出虽然原生态方法简单高效但在实际应用过程中往往面临诸多挑战;而借助第三方工具包则可以在一定程度上弥补这些不足之处. ```java // 示例代码展示如何利用Redisson进行分布式锁管理 import org.redisson.api.RLock; import org.redisson.api.RedissonClient; public class DistributedLockExample { private final RedissonClient redissonClient; public void acquireAndReleaseLock(String lockName) throws InterruptedException{ RLock lock = redissonClient.getLock(lockName); try { boolean isLocked = lock.tryLock(10, TimeUnit.SECONDS); // 尝试获取锁最长等待时间为10秒 if(isLocked){ System.out.println(Thread.currentThread().getName()+" acquired the lock."); Thread.sleep(5000L); // Simulate some work }else{ System.err.println("Failed to get lock after waiting..."); } } finally { if(lock.isHeldByCurrentThread()){ lock.unlock(); System.out.println(Thread.currentThread().getName()+ " released the lock."); } } } } ``` ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

算法小生Đ

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

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

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

打赏作者

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

抵扣说明:

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

余额充值