Redisson分布式锁执行逻辑

文章详细解析了Redisson分布式锁的执行流程,包括tryLock方法的使用,获取和释放锁的逻辑,以及Redisson内部的看门狗(watchdog)机制。指出了手动释放锁失败可能导致的看门狗线程过度活跃,进而引发OOM问题和数据一致性风险。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

又是美好的一天呀~
个人博客地址: huanghong.top

本文预估阅读时长为20分钟左右~

Redisson分布式锁执行逻辑

执行逻辑图

image-20230306162753485

获取锁代码

本地代码

package com.huang.component;

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

/**
 * @Time 2023-02-27 16:25
 * Created by Huang
 * className: RedisComponent
 * Description:
 */
@Slf4j
@Component
public class RedisComponent {
    @Autowired
    private RedissonClient redissonClient;

    private RLock lock;

    @PostConstruct
    public void setup(){
        lock = redissonClient.getLock("distribute-lock");
    }

    public void method(){
        boolean acquireLockFlag = lock.tryLock();
        if(acquireLockFlag){
            log.error("获取锁失败!");
            return ;
        }
        try {
            log.info("获取锁成功!");
            Thread.sleep(3000);
            log.info("执行业务逻辑...");
        } catch (Exception e) {
            log.error("业务逻辑执行异常: {}",e.toString());
        } finally {
            log.info("准备释放锁!");
            lock.unlock();
            log.info("释放锁成功!");
        }
    }
}

org.redisson.RedissonLock#tryAcquireOnceAsync

private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
    // leaseTime 就是租约时间,就是Redis key 的过期时间
    // 无参的tryLock 这里必定为-1 不会进入这个判断
    if (leaseTime != -1) {
        return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    }

    //commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout() 获取到的值
    //private long lockWatchdogTimeout = 30 * 1000; 这个是获取的默认的超时时间30s
    RFuture<Boolean> ttlRemainingFuture = tryLockInnerAsync(waitTime,
                                          commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
										  TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
    //等待获取加锁的结果
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        // 存在异常 直接返回
        if (e != null) {
            return;
        }
        // 加锁成功,下面代码实现锁超时重试,也就是看门狗的逻辑
        // lock acquired
        if (ttlRemaining) {
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}

org.redisson.RedissonLock#tryLockInnerAsync

<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    // 将超时时间保存在RedissonLock的internalLockLeaseTime变量中,用来解决锁超时问题 watchDog机制
    // 真实的获取锁的过程 
	// getName()  distribute-lock
	// Collections.singletonList(getName()) 获取锁的key也就是当前锁的名称,传入lua 脚本中的key对应KEYS[1]
	// internalLockLeaseTime 超时时间 传入lua脚本的参数[1] 对应ARGV[1]
	// getLockName(threadId) 线程唯一标识,对应hash结构的field hash结构的value对应重入次数
    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));
}

对应lua内容

--- KEYS[1] 是锁的key
--- ARGV[1] 是超时时间
--- ARGV[2] 是锁线程名称
local key = KEYS[1] --锁的key
local releaseTime = ARGV[1] -- 锁超时时间
local threadId = ARGV[2]  -- 线程唯一标识
--- 所不存在执行的流程
if (redis.call("exists", key) == 0) then
    -- 不存在获取锁
    redis.call("hset", key, threadId, '1');
    -- 设置有效期
    redis.call("pexpire", key, releaseTime);
    return nil;
end
--- 锁存在执行的流程
if (redis.call("hexists", key, threadId) == 1) then
	-- 锁重入 当前线程对应的value增加一
    redis.call("hincrby",key,threadId,'1');
    -- 重置超时时间
    redis.call("pexpire", key, releaseTime);
    return nil;  --返回结果
end

--- 当key不存在时,返回-2 。 当key存在但没有设置剩余生存时间时,返回-1 。 否则,以毫秒为单位,返回key 的剩余生存时间。
return redis.call('pttl', KEYS[1])

org.redisson.RedissonBaseLock#scheduleExpirationRenewal

protected void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    //判断map中是否存在当前线程对象的实体,如果存在则返回实体,如果不存在则创建新的返回null
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        entry.addThreadId(threadId);
        try {
            // 看门狗实现,创建新的实体需要增加看门狗的逻辑
            renewExpiration();
        } finally {
            if (Thread.currentThread().isInterrupted()) {
                cancelExpirationRenewal(threadId);
            }
        }
    }
}

org.redisson.RedissonBaseLock#renewExpiration

private void renewExpiration() {
    // 从map中获取ExpirationEntry如果为null则直接返回
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    // 如果不为空则创建一个延时任务 task 与Timer返回的TimerTask关联的句柄。
    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);
                }
            });
        }
        //internalLockLeaseTime   就是我们之前获取到的leaseTime不传默认30秒
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
     // 将延时任务放入到ExpirationEntry中  
    ee.setTimeout(task);
}

org.redisson.RedissonBaseLock#renewExpirationAsync

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));
}

问题点

  1. 手动释放锁失败会导致watchdog线程会一直执行,如果大量线程释放锁均失败,会导致OOM
  2. redis主从同步未同步redis-lock,主节点就宕机,从节点升级为主节点后重新接受客户端请求,重新设置redis-lock,造成脏数据

感谢阅读完本篇文章!!!
个人博客地址: huanghong.top

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

喜欢正常冰的冰美式

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

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

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

打赏作者

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

抵扣说明:

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

余额充值