JAVA-实战8 Redis实战项目—雷神点评(5)Redisson

旅立ちの日だよ

Redisson

基于SETNX实现的分布式锁存在如下问题:

不可重入:同一个线程无法多次获取同一把锁
不可重试:获取锁只尝试一次就返回false,没有重试机制
超时释放:锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患
主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,如果从并同步主中的锁数据,则会出现锁实现

为了解决这些问题,需要引入分布式锁工具——Redisson

Redisson是一个在Redis的基础上实现的java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的ava常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

可重入锁(Reentrant Lock)
公平锁(Fair Lock)
联锁(MultiLock)
红锁(RedLock)
读锁(ReadWriteLock)
信号量(Semaphore)
可过期性信号量(PermitExpirableSemaphore)
闭锁(CountDownLatch)

Redisson入门案例

引入Redisson依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.22.0</version>
</dependency>

编写Redisson配置类

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.100.128:6379").setPassword("1234");
        return Redisson.create(config);
    }
}

修改优惠券秒杀方法如下:

@Slf4j
@Service
public class VoucherOrderServiceImpl implements VoucherOrderService {
    @Autowired
    private VoucherSecondKillMapper voucherSecondKillMapper;
    @Autowired
    private VoucherOrderMapper voucherOrderMapper;
    @Autowired
    private RedisIdWorker redisIdWorker;
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    // 注入RedissonClient对象
    @Autowired
    private RedissonClient redissonClient;

    @Override
    @Transactional
    public ResultData VoucherSecondKill(Long VoucherId) {
        VoucherSecondKillData Voucher = voucherSecondKillMapper.selectById(VoucherId);

        if(Voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return ResultData.error("The SecondKill dose not start!");
        }

        if(Voucher.getEndTime().isBefore(LocalDateTime.now())) {
            return ResultData.error("The SecondKill has ended!");
        }

        if(Voucher.getStock()<1) {
            return ResultData.error("The Stock is not enough!");
        }
        long UserId = Long.parseLong(CurrentHolder.getCurrent().getId());
        // 使用RedissonClient优化锁
        RLock lock = redissonClient.getLock(REDISSON_KEY_PREFIX+UserId);
        boolean IsLock = lock.tryLock();
        if(!IsLock) {
            return ResultData.error("Error:Repeat Order!");
        }
        try {
            VoucherOrderService proxy = (VoucherOrderServiceImpl) AopContext.currentProxy();
            return proxy.CreateNewVoucherOrder(VoucherId);
        } finally {
            lock.unlock();
        }

    }

    @Override
    @Transactional
    public ResultData CreateNewVoucherOrder(Long VoucherId) {
        Long UserId = Long.valueOf(CurrentHolder.getCurrent().getId());
        QueryWrapper<VoucherOrderData> orderlwp = new QueryWrapper<VoucherOrderData>();
        orderlwp.eq("voucher_id",VoucherId).eq("user_id",UserId).select("count(*) as Number");
        List<Map<String,Object>> Result = voucherOrderMapper.selectMaps(orderlwp);
        Long Count = (Long) Result.get(0).get("Number");
        if(Count>0) {
            return ResultData.error("You have already purchased this voucher!");
        }

        LambdaUpdateWrapper<VoucherSecondKillData> lwp = new LambdaUpdateWrapper<VoucherSecondKillData>();
        lwp.eq(VoucherSecondKillData::getVoucherId,VoucherId);
        voucherSecondKillMapper.UpdateSecondKillVoucher(lwp,1L);

        VoucherOrderData NewVoucherOrder = new VoucherOrderData();

        Long VoucherOrderId = redisIdWorker.NewId("Order");
        NewVoucherOrder.setId(VoucherOrderId);

        NewVoucherOrder.setUserId(UserId);

        NewVoucherOrder.setVoucherId(VoucherId);
        voucherOrderMapper.insert(NewVoucherOrder);

        return ResultData.success(NewVoucherOrder);
    }
}

封装常量工具类:

public class RedisConstants {
    public static final String REDISSON_KEY_PREFIX = "lock:order:";
}

Redisson的可重入锁

重入锁

‌锁重入‌(Reentrant Lock),又称可重入锁,是指同一个线程在已经持有某个锁的情况下,可以再次成功获取该锁而不会发生死锁或阻塞的现象。

在分布式系统(如使用 Redisson)或单机多线程环境中,锁重入是一个非常重要的特性,它极大地降低了编程的复杂性。

Redisson可重入锁原理

Redisson可重入锁的关键在于在简单的加锁去锁操作上添加了计数操作

Redisson可重入锁的示意图如下:
image

Redisson可重入锁实现

Redisson可重入锁实现的核心在于使用了Redis的Hash数据结构‌和Lua脚本‌来保证原子性。

Hash结构

与简单的 SETNX 命令不同,Redisson的可重入锁在Redis中存储的数据结构是一个 ‌Hash‌。

Key‌:锁的名称。
Field‌:唯一标识持有锁的客户端和线程,格式为UUID:ThreadId
Value‌:锁的重入次数(计数器)。

这种结构允许Redisson区分是哪个线程持有的锁,并记录该线程重复加锁的次数

Lua脚本

Redisson的所有加锁操作都是通过执行Lua脚本来完成的,这确保了判断锁状态、设置锁值和过期时间等一系列操作的原子性


准备测试程序如下:

@Slf4j
@SpringBootTest
class LeiShenCommentApplicationTests {

    @Resource
    private RedissonClient redissonClient;

    private RLock lock;

    @BeforeEach
    void SetUp() {
        lock = redissonClient.getLock("test");
    }

    @Test
    void method1() {
        boolean IsLock = lock.tryLock();
        if(!IsLock) {
            log.error("Get Lock Error! 1");
            return;
        }
        try {
            log.info("Get Lock Succeed! 1");
            method2();
            log.info("Start method! 1");
        }finally {
            log.info("UnLock! 1");
            lock.unlock();
        }
    }

    @Test
    void method2() {
        boolean IsLock = lock.tryLock();
        if(!IsLock) {
            log.error("Get Lock Error! 2");
            return;
        }
        try {
            log.info("Get Lock Succeed! 2");
            log.info("Start method! 2");
        }finally {
            log.info("UnLock! 2");
            lock.unlock();
        }
    }
}

执行测试:

首先同一线程下执行一个方法
image
image

模拟重入同一个方法
image
image

Redisson可重入锁的锁重试和WatchDog机制

Redisson可重入锁添加了锁重试和WatchDog两大机制,用于解决分布式环境下锁竞争、业务超时以及节点故障等问题

锁重试机制

在分布式系统中,多个服务实例同时请求同一把锁时,如果直接返回失败,会导致用户体验差或业务中断;如果无限阻塞等待,又会造成资源浪费。Redisson 通过tryLock方法实现了智能的“等待+重试”机制,即锁重试机制。

核心APItryLock如下:

boolean isLocked = lock.tryLock(WaitTime, LeaseTime, TimeUnit);

WaitTime‌: 最大等待时间。如果在指定时间内未获取到锁,则返回false。
LeaseTime‌: 锁持有时间(自动释放时间)如果未指定(即使用无参 lock()),则启用看门狗机制;如果指定了具体数值,则锁会在该时间后强制释放,且‌不启动‌看门狗。

工作原理:

首次尝试‌:客户端执行Lua脚本尝试加锁。
订阅通知‌:如果加锁失败,客户端会订阅Redis的PubSub频道(通常以锁名为通道名),监听锁释放的消息。
信号量等待‌:客户端利用信号量(Semaphore)或定时任务进行等待。

如果在等待期间接收到“锁已释放”的通知,客户端会立即再次尝试获取锁。
如果未收到通知,客户端会根据剩余等待时间进行轮询或休眠,避免频繁请求消耗CPU。

超时退出‌:如果超过WaitTime仍未获取到锁,则取消订阅并返回 false。

WatchDog机制

分布式锁必须设置过期时间,以防止持有锁的节点宕机导致死锁。但如果业务逻辑执行时间超过了锁的过期时间,锁会被自动释放,导致其他线程进入临界区,引发并发安全问题。‌看门狗机制就是为了解决“业务没做完,锁先丢了”的问题

触发条件:

仅在使用无参lock()或未指定LeaseTimetryLock()时生效。‌
如果手动指定LeaseTime(如lock(10, TimeUnit.SECONDS)),看门狗‌不会‌启动,锁将在指定时间后强制释放。

工作原理:

初始加锁‌:调用lock()时,Redisson 默认设置锁的过期时间为30秒‌(可通过 Config.lockWatchdogTimeout 配置)。
启动后台任务‌:加锁成功后,Redisson 启动一个后台定时任务(Watchdog)。
定期续期‌

Watchdog 每隔 10秒‌(默认过期时间的1/3)检查一次。
如果当前线程仍然持有该锁,Watchdog会向Redis发送PEXPIRE命令,将锁的过期时间重新重置为30秒。

停止续期‌

当调用unlock()释放锁后,Watchdog任务被取消。
如果客户端节点宕机,Watchdog进程随之消失,不再发送续期命令,锁将在30秒后自动过期释放,避免死锁。

时间线示例:

假设默认过期时间为 30 秒,续期间隔为 10 秒:

T=0s‌: 线程 A 加锁成功,TTL=30s,启动 Watchdog。
T=10s‌: Watchdog 检测到锁仍被持有,执行续期,TTL 重置为 30s。
T=20s‌: Watchdog 再次续期,TTL 重置为 30s。
T=25s‌: 线程 A 业务执行完毕,调用 unlock()。
T=25s+‌: 锁被删除,Watchdog 任务取消。

若业务执行了 40 秒:

T=30s‌: 如果没有 Watchdog,锁已过期。但因为有 Watchdog 在 T=10s 和 T=20s 的续期,此时锁依然有效。
T=30s‌: Watchdog 第三次续期,TTL 重置为 30s。
T=40s‌: 业务结束,解锁。

总体流程示意图如下(其中TTL取决于是否设置LeaseTime,未设置则默认30s,设置则取决于LeaseTime的值):
image

Redisson的MultiLock

‌Redisson的MultiLock‌是一种分布式锁机制,它允许将多个独立的RLock对象组合成一个统一的锁实例,实现对多个资源的‌原子性加锁与解锁‌操作。

工作原理:

加锁过程(lock()

按顺序尝试获取每一个RLock
如果某个锁获取失败,MultiLock会自动释放‌已获取的所有锁‌,并进入等待状态
只有当所有锁都成功持有时,MultiLock才算真正加锁成功
支持设置超时时间(tryLock),在指定时间内未能全部获取则返回失败。

解锁过程(unlock()

调用unlock()时,会‌依次释放所有子锁‌。
仅允许‌锁的持有线程‌进行解锁,否则会抛出IllegalMonitorStateException异常。

简易示例如下:

RLock lock1 = redisson1.getLock("lock1");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");

// 组合成 MultiLock
RLock multiLock = redisson.getMultiLock(lock1, lock2, lock3);

// 加锁(可设置超时)
multiLock.lock(10, TimeUnit.SECONDS);

try {
    // 执行跨资源的临界区操作
} finally {
    // 自动释放所有子锁
    multiLock.unlock();
}

可以看出,MultiLock的缺陷在于运维成本高、实现复杂

posted @ 2026-05-06 01:18  tcswuzb  阅读(44)  评论(0)    收藏  举报