《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

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

### Redission 可重入分布式锁的实现机制 #### 背景介绍 在分布式环境中,为了防止多个节点同时操作共享资源而导致数据不一致的问题,通常需要引入一种同步控制机制——分布式锁。RedissionRedis 官方推荐的一种高效、易用的分布式锁解决方案[^1]。 #### 可重入锁的概念 可重入锁是指同一个线程可以多次获取同一把锁而不发生死锁的情况。这种特性允许线程在其已经持有的锁内部再次申请该锁,而不会因为重复加锁导致阻塞。Redission 的 `RLock` 接口实现了这一功能,并通过 Redis 的原子性 Lua 脚本保障其一致性[^4]。 --- #### Redission 可重入锁的核心实现原理 ##### 1. 锁的状态管理 Redission 使用 Redis 中的 Hash 数据结构来存储锁的信息。Hash 结构中的字段如下: - **Key**: 表示锁的存在与否。 - **Field (小 Key)**: 存储持有锁的客户端唯一标识符(通常是线程 ID 或会话 ID)。 - **Value**: 记录锁的持有次数(即重入计数器)。每次线程重新进入锁时,此值都会增加;当线程退出锁时,此值减少直到变为零,则释放锁[^3]。 ##### 2. 加锁流程 以下是 Redission 获取可重入锁的主要逻辑: ```java // 尝试获取锁 boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS); ``` - 当某个线程调用 `tryLock()` 方法时,Redission 首先会在 Redis 上创建一个带有过期时间的键值对作为锁。 - 如果当前线程已经是锁的拥有者(即 Field 对应的 Value 已存在),则直接更新重入计数器并延长锁的有效期。 - 否则,尝试设置新的锁记录。如果成功,则初始化重入计数器为 1。 Lua 脚本用于确保整个过程的原子性,避免因网络延迟或其他异常情况引发的竞争条件。 ##### 3. 解锁流程 解锁的过程同样依赖于 Lua 脚本来完成: ```java lock.unlock(); ``` - 线程在调用 `unlock()` 方法时,首先检查自己是否是锁的实际持有者。 - 若确实是持有者,则将对应的重入计数器减一。 - 如果计数器降为零,则删除锁的相关记录,正式释放锁。 需要注意的是,只有真正的锁持有者才能执行解鎖动作,其他线程即使知道锁的名字也无法非法解除他人拥有的锁。 --- #### 技术细节补充 - **锁超时保护** Redission 设定了一个合理的默认超时期限,在超过该期限后未续期的情况下自动释放锁,从而避免死锁的发生。 - **高可用支持** 利用了 Redis Sentinel 或 Cluster 模式下的选举算法,使得即便部分 Redis 实例宕机也不会影响整体服务正常运行[^2]。 - **公平性与性能权衡** Redission 并未严格遵循 FIFO 的排队策略,而是优先考虑效率吞吐量。因此,在极端负载下可能会出现轻微的不公平现象。 --- ### 总结 综上所述,Redission可重入分布式锁基于 Redis 的高性能特性 Lua 脚本的一致性保障,提供了一种简单可靠的方式解决跨进程间的资源共享冲突问题。它的设计既兼顾了灵活性又不失安全性,非常适合现代微服务架构的需求。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值