《redis设计和实战》Redission的可重入、重试、超时释放

🍰 个人主页:不摆烂的小劉
🍞文章有不合理的地方请各位大佬指正。
🍉文章不定期持续更新,如果我的文章对你有帮助➡️ 关注🙏🏻 点赞👍 收藏⭐️

本文主要讨论Redission的基本使用和特点:可重入、重试、超时释放

在分布式系统里,多个服务可能会同时对共享资源进行操作,这样就可能引发数据不一致的问题。Redisson提供了分布式锁的实现,能够保证在分布式环境下同一时间只有一个服务可以访问共享资源。

Redission的使用

  1. 引入依赖
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>
  1. 配置Redisson客户端

@Configuration
public class RedissonConfig {
  @Bean
   public RedissonClient redissonClient(){
       // 配置
       Config config = new Config();
       // 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址
       config.useSingleServer().setAddress("redis://127.0.0.1:6379");
       // 创建RedissonClient对象
       return Redisson.create(config);
   }
}
  1. 实践应用
@Resource
private RedissonClient redissonClient;
@Test
void testRedisson() throws InterruptedException {
    // 获取锁(可重入),指定锁的名称   
    RLock lock = redissonClient.getLock("anyLock");
    // 尝试获取锁 获取锁的最大等待时间(期间会重试),锁自动释放时间,时间单位    
    boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
    // 判断释放获取成功   
    if (isLock) {
        try {
            System.out.println("执行业务");
        } finally {
            //释放锁          
            lock.unlock();
        }
    }
}

Redission可重入

1.什么是可重入?

可重入,同一个线程能够多次获取同一把锁,不会因为之前已经持有这把锁而被阻塞。例如把锁想象成房间的门禁卡,同一个人拿着这张卡,不管进出这个房间多少次,都能顺利通过门禁,而不会被拦住

2.可重入有什么用?

  • 避免死锁:线程在持有锁的过程中,需要再次获取该锁时,如果使用不可重入锁,线程会被阻塞,导致自己等待自己释放锁,从而造成死锁。而可重入锁允许线程在持有锁的情况下再次获取锁
  • 简化代码逻辑:在同一个事务中需要多次操作同一受保护资源,需要相同锁的方法;如果使用不可重入锁,开发者需要在调用嵌套方法时手动处理锁的释放和重新获取,或者锁的颗粒度较大,影响性能

3.可重入锁 vs 不可重入锁

  • 业务流程
    用户点击“立即购买” → 检查库存 → 扣减库存 → 生成订单 → 支付回调。
    这些步骤可能由不同方法调用,但需共享同一把锁(如lock:order:userId)保证原子性。

  • 不可重入锁
    假设方法A(检查库存)获取锁后,调用方法B(扣减库存)。
    若锁不可重入,方法B会因线程A已持锁而被阻塞,导致订单流程中断,甚至触发超时错误

  • 可重入锁
    线程A首次获取锁时,count=1,正常执行方法A。
    进入方法B时,因owner仍是线程A,直接增加count=2,无需重新加锁。
    最终释放锁时,count从2递减到0,锁被删除

3. Redission可重入的实现原理

原理:Hash结构+计数器​​
在这里插入图片描述
Redissionlua脚本位置org.redisson.RedissonLock#tryLockInnerAsync

-- 检查锁的键是否存在
if redis.call('exists', KEYS[1]) == 0 then
    -- 首次获取锁
    -- 哈希表 KEYS[1] 中字段 ARGV[2] 的值增加 1
    redis.call('hincrby', KEYS[1], ARGV[2], 1)
    -- 锁的键KEYS[1]设置过期时间ARGV[1]
    redis.call('pexpire', KEYS[1], ARGV[1])
    return nil
    -- 检查哈希表中指定字段是否存在
elseif redis.call('hexists', KEYS[1], ARGV[2]) == 1 then
     -- 哈希表 KEYS[1] 中字段 ARGV[2] 的值增加 1
    redis.call('hincrby', KEYS[1], ARGV[2], 1)
    redis.call('pexpire', KEYS[1], ARGV[1])
    return nil
end
-- 其他情况返回锁剩余时间
return redis.call('pttl', KEYS[1])

4.代码举例应用

@Resource
    private RedissonClient redissonClient;

    private RLock lock;

    @BeforeEach
    void setUp() {
        lock = redissonClient.getLock("order");
    }

    @Test
    void method1() throws InterruptedException {
        // 尝试获取锁
        boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);
        if (!isLock) {
            log.error("获取锁失败 .... 1");
            return;
        }
        try {
            log.info("获取锁成功 .... 1");
            method2();
            log.info("开始执行业务 ... 1");
        } finally {
            log.warn("准备释放锁 .... 1");
            lock.unlock();
        }
    }
    void method2() {
        // 尝试获取锁
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("获取锁失败 .... 2");
            return;
        }
        try {
            log.info("获取锁成功 .... 2");
            log.info("开始执行业务 ... 2");
        } finally {
            log.warn("准备释放锁 .... 2");
            lock.unlock();
        }
    }

执行结果
在这里插入图片描述

Redission重试

在这里插入图片描述

1.什么是重试?

指定的等待时间内持续尝试获取锁,直到超时或成功获取锁

2.重试有什么用?

  • 重试可以增加锁获取的机会,提高业务操作的成功率;如果一个用户请求因为暂时无法获取锁而立即返回失败,影响用户体验
  • 当锁被释放时,等待的线程会被通知,减少了无效的CPU消耗,得益于Redis的发布-订阅功能和Netty的非阻塞IO模型 [Netty具体细节后续再讨论]

3.Redission重试的实现原理

  1. 订阅锁释放通知频道
RFuture<RedissonLockEntry> subscribeFuture = subscribe(lockName);
  1. 等待通知或超时
// 简化版等待逻辑
RedissonLockEntry entry = subscribeFuture.getNow();
RLatch latch = entry.getLatch();  // 分布式信号量

// 尝试获取信号量,最多等待剩余时间
boolean acquired = latch.tryAcquire(remainingTime, TimeUnit.MILLISECONDS);
if (acquired) {
    // 收到锁释放通知,再次尝试获取锁
    retryLock();
} else {
    // 超时,返回获取失败
    return false;
}
  1. 拿到锁执行业务

  2. 释放锁
    释放锁会广播释放锁的通知

-- KEYS[1]: 锁名称
-- KEYS[2]: 锁释放通知频道
-- ARGV[1]: 释放消息(固定值0)
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;

Redission超时释放、看门狗

1.什么是超时释放、看门狗?

超时释放:锁被获取时,Redisson会为锁设置一个默认超时时间(如30秒)

看门狗:自动续期机制,用于解决长耗时业务的锁超时问题

2.超时释放VS看门狗

机制锁的行为举例
超时释放锁在固定时间后自动释放(如30秒 )
最大等待时间4s,锁自动释放的时间5s
redissonClient.getLock(“order”).tryLock(4L,5L ,TimeUnit.SECONDS);
看门狗未指定leaseTime或leaseTime=-1
业务执行时间可能超过超时时间 锁的有效期被不断续期,直到业务完成或线程异常
最大等待时间1s,
redissonClient.getLock(“order”).tryLock(1L, TimeUnit.SECONDS);

3. Redission重试的实现原理

超时释放是为Hash设置了固定的过期时间

看门狗,异步定时任务续期锁的过期时间
看门狗原理:

参数含义示例值
KEYS[1]锁的Redis键“lock:123”
ARGV[1]新过期时间(毫秒)5000
ARGV[2]锁的唯一标识(UUID)“unique-id-123”
--检查锁是否存在
if (redis.call('exists', KEYS[1]) == 0) then 
	-- 在哈希表 KEYS[1] 中创建字段 ARGV[2]值初始化为1
	redis.call('hincrby', KEYS[1], ARGV[2], 1); 
	-- 设置键的过期时间为 ARGV[1] 毫秒
	redis.call('pexpire', KEYS[1], ARGV[1]); 
	return nil; 
end; 
-- 哈希表中是否存在字段 ARGV[2]
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
	-- 递增字段 ARGV[2] 的值
	redis.call('hincrby', KEYS[1], ARGV[2], 1); 
	-- 重置键的过期时间为 ARGV[1] 毫秒
	redis.call('pexpire', KEYS[1], ARGV[1]); 
	return nil; 
end; 
	-- 返回锁的剩余时间(毫秒)
	return redis.call('pttl', KEYS[1]);

Redission注意事项

1.释放锁的顺序与计数器一致性

Redisson 通过计数器(Hash结构中的字段值)实现可重入,每次获取锁时计数器递增,释放时递减。
必须成对释放锁:确保每个 lock()/tryLock()对应一个 unlock(),且在finally块中释放,避免因异常导致计数器未归零,引发锁提前释放或死锁

RLock lock = redissonClient.getLock("lock:order");
try {
    lock.lock(); // 可重入获取锁(计数器+1)
    // 嵌套调用其他需同一锁的方法
    nestedMethod();
} finally {
	// 可重入锁的计数器与线程绑定,需通过 isHeldByCurrentThread() 校验,防止跨线程释放锁
    if (lock.isHeldByCurrentThread()) {
      // 释放锁(计数器-1,若为0则删除锁)
  	  lock.unlock();
	}
}

2. 同步与异步重试

同步重试(lock()):线程会阻塞直到获取锁
异步重试(tryLockAsync()):结合 RFuture 实现非阻塞调用,需处理 whenComplete 回调中的异常,避免内存泄漏。

lock.tryLockAsync(5, 10, TimeUnit.SECONDS)
    .whenComplete((acquired, e) -> {
        if (acquired) {
            // 异步执行业务逻辑
        } else {
            log.error("锁获取失败:", e);
        }
    });

3.避免 “假死锁” 陷阱

若线程持有锁但进入死循环,看门狗会持续续期,导致其他线程无法获取锁(非真正死锁,但表现类似)
避免使用看门狗,除非特殊业务场景,必须设置锁持有时间

参考:
[1]https://redisson.org
[2]https://springdoc.cn/spring-boot-redisson/
[3]redisson使用—redisson官方文档+注释
[4]https://www.cnblogs.com/jackson0714/p/redisson.html

🍉文章不定期持续更新,如果我的文章对你有帮助➡️ 关注🙏🏻 点赞👍 收藏⭐️

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值