Redisson分布式锁加锁源码分析

一、Redisson的使用方式

        Redisson是分布式架构中常见的分布式锁的一种实现形式,在使用的过程中,一般有两种方式,一中是带过期时间的,另一种是不带过去时间的,以及对应的源码的处理上到底有什么差异呢?我们一起探究一下。

1.1 使用方式一,带过期时间

以下是带过期时间的常见实用方式,虽然源码分析中是在使用方式一的流程中讲到了看门狗机制,但是过期时间填了具体的值,而不是填 -1,例如30秒,这种情况下是不会触发看门狗机制的,参考2.3处的源码

RLock lock = redissonClient.getLock(lockKey);
boolean tryLock = false;
try {
    tryLock = lock.tryLock(10, 30, TimeUnit.SECONDS);
    if (tryLock) {
        //TODO 逻辑处理
    }
} catch() {
    //异常处理
} finally {
    if (tryLock) {
        lock.unlock();
    }
}

1.2 使用方式二,不带过期时间

以下是不带过期时间的使用方式

RLock lock = redissonClient.getLock(lockKey);
boolean tryLock = false;
try {
    tryLock = lock.tryLock(10, TimeUnit.SECONDS);
    if (tryLock) {
        //TODO 逻辑处理
    }
} catch() {
    //异常处理
} finally {
    if (tryLock) {
        lock.unlock();
    }
}


注意:以上是两种使用方式,区别在于方式1加了过期时间,方式2没加过期时间,然而内部的处理逻辑是不一样的。Redisson作为一个分布式锁,其加锁/解锁的逻辑才是我们关注的核心,还有一种使用方式是调用lock()方法,但是这种方式使用相对少,在分析完tryLock的源码后进行分析,
tryLock旨在了解其加锁过程,个人认为并不需要每行代码都弄明白啥意思。掌握了其加锁的核心思想即可。

二、源码解析

2.1 tryLock方法源码

tryLock()源码如下

/**
 * 加锁方法源码
 * @param waitTime 加锁等待时间
 * @param leaseTime 锁的有效时间
 * @param unit 时间单位
 * @return true:加锁成功 false:加锁失败
 * @throws InterruptedException 异常
 */
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
    //将等待时间转成毫秒数
    long time = unit.toMillis(waitTime);
    //系统时间毫秒数
    long current = System.currentTimeMillis();
    //当前线程id
    long threadId = Thread.currentThread().getId();
    //加锁核心逻辑
    Long ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
    //ttl为空,表示加锁成功,返回true
    if (ttl == null) {
        return true;
    } else {
        time -= System.currentTimeMillis() - current;
        if (time <= 0L) {
            this.acquireFailed(waitTime, unit, threadId);
            return false;
        } else {
            current = System.currentTimeMillis();
            RFuture<RedissonLockEntry> subscribeFuture = this.subscribe(threadId);
            if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
                if (!subscribeFuture.cancel(false)) {
                    subscribeFuture.onComplete((res, e) -> {
                        if (e == null) {
                            this.unsubscribe(subscribeFuture, threadId);
                        }

                    });
                }

                this.acquireFailed(waitTime, unit, threadId);
                return false;
            } else {
                boolean var14;
                try {
                    time -= System.currentTimeMillis() - current;
                    if (time > 0L) {
                        boolean var16;
                        do {
                            long currentTime = System.currentTimeMillis();
                            ttl = this.tryAcquire(waitTime, leaseTime, unit, threadId);
                            if (ttl == null) {
                                var16 = true;
                                return var16;
                            }

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

                            currentTime = System.currentTimeMillis();
                            if (ttl >= 0L && ttl < time) {
                                ((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                            } else {
                                ((RedissonLockEntry)subscribeFuture.getNow()).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
                            }

                            time -= System.currentTimeMillis() - currentTime;
                        } while(time > 0L);

                        this.acquireFailed(waitTime, unit, threadId);
                        var16 = false;
                        return var16;
                    }

                    this.acquireFailed(waitTime, unit, threadId);
                    var14 = false;
                } finally {
                    this.unsubscribe(subscribeFuture, threadId);
                }

                return var14;
            }
        }
    }
}

2.2、tryAcquire方法

该方法之后的逻辑是核心方法,可以细读源码逻辑。该方法调用tryAcquireAsync方法,加锁核心逻辑在该方法中

private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    return (Long)this.get(this.tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}

2.3、tryAcquireAsync方法

方法源码如下,从源码中可以看出一个非常重要的点,如果你上锁时间填了具体的时间,例如过期时间填了30秒这样,是不会触发看门狗机制的,这点需要特别注意,要出发看门狗机制,又想填过期时间,可以填 -1。

/**
 * 尝试加锁
 * 
 * @param waitTime 等待时间
 * @param leaseTime 过期时间
 * @param unit 时间单位
 * @param threadId 线程id
 * @param <T>
 * @return 泛型类
 */
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    //判断过期时间是否不等于 -1,如果不指定leaseTime就是默认-1
    if (leaseTime != -1L) {
        //加锁,这里加锁结果是直接返回,并不存在看门狗机制
        return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    } else {
        //如果未指定过期时间或者过期时间指定为-1,进入改处理逻辑
        //this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout():设置默认过期时间为30秒(lockWatchdogTimeout的值)
        RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        //这里就是看门狗机制源码,监听线程id
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e == null) {
                if (ttlRemaining == null) {
                    //进行过期时间延长处理
                    this.scheduleExpirationRenewal(threadId);
                }

            }
        });
        return ttlRemainingFuture;
    }
}

2.4、tryLockInnerAsync方法

源码如下

/**
* 加锁底层原理-lua脚本
*
*
*/
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    this.internalLockLeaseTime = unit.toMillis(leaseTime);
    //加锁的核心是通过lua脚本。lua脚本的解释可以自行百度一下
    return this.evalWriteAsync(this.getName(), 
                                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(this.getName()), 
                                this.internalLockLeaseTime, 
                                this.getLockName(threadId));
}

2.5、scheduleExpirationRenewal方法

该方法可以看到Redisson的看门狗机制,也是比较重要

/**
 *看门狗机制
 */
private void scheduleExpirationRenewal(long threadId) {
    RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry();
    RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        entry.addThreadId(threadId);
        //看门狗机制续时间逻辑
        this.renewExpiration();
    }

}

2.6、renewExpiration方法

该方法实现了看门狗机制续约时间源码

/**
 * 看门狗机制续时间的源码
 */
private void renewExpiration() {
    RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
    if (ee != null) {
        Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            public void run(Timeout timeout) throws Exception {
                RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
                if (ent != null) {
                    Long threadId = ent.getFirstThreadId();
                    if (threadId != null) {
                        RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
                        future.onComplete((res, e) -> {
                            if (e != null) {
                                RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
                            } else {
                                if (res) {
                                    RedissonLock.this.renewExpiration();
                                }

                            }
                        });
                    }
                }
            }
        }, 
        //每次续的时间数是默认过期时间 / 3,就是每次续10000毫秒,10秒钟
        this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
        ee.setTimeout(task);
    }
}

三、troLock的方式2的源码

这是不带过期时间的使用方式,推荐使用这种,这种方式是可以触发Redsson看门狗机制的,可以不去过多的考虑时间到了,但是业务还没有执行完的情况

源码如下

public boolean tryLock(long waitTime, TimeUnit unit) throws InterruptedException {
    //实际上也是调用方式1的tryLock方法,区别在于leaseTime传递的是-1,这种加锁方式可以触发看门狗机制,不用担心业务还在执行,却释放了锁的情况
    return this.tryLock(waitTime, -1L, unit);
}
//lock方法源码
public void lock() {
    try {
        //调用lock()方法,等待时间传的是-1
        this.lock(-1L, (TimeUnit)null, false);
    } catch (InterruptedException var2) {
        throw new IllegalStateException();
    }
}

//lock的加锁源码,核心在于有注释的几行代码,与tryLock的区别在于没有等待时间的概念,
//可以触发看门狗机制,默认过期时间和时间续费逻辑一样
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    //当前线程id
    long threadId = Thread.currentThread().getId();
    //先尝试加锁
    Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
    //表示加锁失败
    if (ttl != null) {
        RFuture<RedissonLockEntry> future = this.subscribe(threadId);
        if (interruptibly) {
            this.commandExecutor.syncSubscriptionInterrupted(future);
        } else {
            this.commandExecutor.syncSubscription(future);
        }

        try {
            //while(true)处理,也可以成为自旋锁
            while(true) {
                //加锁逻辑
                ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
                表示加锁成功
                if (ttl == null) {
                    //加锁成功,结束方法
                    return;
                }

                if (ttl >= 0L) {
                    try {
                        ((RedissonLockEntry)future.getNow()).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException var13) {
                        if (interruptibly) {
                            throw var13;
                        }

                        ((RedissonLockEntry)future.getNow()).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else if (interruptibly) {
                    ((RedissonLockEntry)future.getNow()).getLatch().acquire();
                } else {
                    ((RedissonLockEntry)future.getNow()).getLatch().acquireUninterruptibly();
                }
            }
        } finally {
            this.unsubscribe(future, threadId);
        }
    }
}

以上就是我对分布式锁Redisson的加锁过程源码解读,如有错误,欢迎指正

### Redisson分布式锁加锁性能表现分析 Redisson 是一种基于 Redis 的 Java 工具包,提供了多种分布式对象和数据结构,其中包括分布式锁Redisson分布式锁(`RedissonLock`)是一种可重入锁,支持公平锁、非公平锁以及多线程并发控制。 #### 1. 加锁机制与性能优化 Redisson分布式锁加锁时会尝试通过 Redis 的 `SETNX` 命令实现原子操作[^1]。如果锁被其他线程占用,则当前线程不会立即进入自旋状态,而是通过监听锁的释放通知来等待锁的释放。这种机制避免了频繁的轮询操作,从而显著提升了性能。具体流程如下: - 当一个线程尝试获取锁时,如果锁已被其他线程持有,该线程会订阅 Redis 的发布/订阅(Pub/Sub)通道。 - 在锁释放时,持有锁的线程会通过 Redis 的 Pub/Sub 功能向所有订阅者发送解锁通知。 - 接收到通知后,等待中的线程重新尝试获取锁。 这种方式不仅减少了 CPU 的空转时间,还降低了 Redis 的负载压力。 #### 2. 锁的超时与续期 为了防止死锁问题,Redisson分布式锁引入了锁自动过期机制。在加锁时,可以通过设置锁的有效时间(TTL)来确保锁不会永久占用。此外,Redisson 提供了锁续期功能,允许线程在持有锁期间定期延长锁的有效时间[^3]。这种设计保证了即使在极端情况下(如线程挂起或网络延迟),锁也不会因超时而被误释放。 #### 3. 并发性能评测 Redisson分布式锁在高并发场景下的性能表现主要取决于以下因素: - **Redis 实例的性能**:Redis 是单线程模型,其处理能力受限于底层硬件资源(如 CPU 和网络带宽)。因此,在高并发场景下,Redis 的性能瓶颈可能会成为 Redisson 分布式锁的主要限制因素。 - **网络延迟**:由于 Redisson 的锁操作依赖于 Redis 的远程调用,网络延迟会对加锁和解锁的性能产生直接影响。建议将 Redis 主从节点部署在同一局域网内以减少延迟[^3]。 - **锁竞争程度**:当多个线程同时竞争同一把锁时,Redisson加锁性能会受到一定影响。然而,由于 Redisson 使用了 Pub/Sub 机制而非自旋等待,其性能下降幅度相对较小。 #### 4. 测试结果参考 根据实际测试数据,Redisson分布式锁在以下条件下表现出良好的性能: - 在低竞争场景下(即少量线程竞争锁),加锁和解锁的平均耗时通常小于 1 毫秒。 - 在高竞争场景下(即大量线程竞争锁),加锁和解锁的平均耗时可能上升至 5-10 毫秒,但仍然优于传统的自旋锁实现[^1]。 #### 5. 性能优化建议 为了进一步提升 Redisson 分布式锁的性能,可以采取以下措施: - 配置 Redis 主从复制架构,确保主节点专注于处理写请求,而从节点负责读请求。 - 禁用 Redis 的持久化功能(如 RDB 和 AOF),以减少对主节点性能的影响。 - 合理设置锁的有效时间和续期间隔,避免因锁超时导致的异常行为。 ```java // 示例代码:使用 Redisson 实现分布式锁 RLock lock = redissonClient.getLock("myLock"); try { // 尝试加锁,最多等待 10 秒,锁有效时间为 30 秒 boolean isLocked = lock.tryLock(10, 30, TimeUnit.SECONDS); if (isLocked) { // 执行业务逻辑 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { // 释放锁 lock.unlock(); } ``` ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值