Java分布式锁Redis案例

wen java案例 5

Java分布式锁实战:基于Redis的可靠实现与避坑指南(附完整案例)


目录导读

  1. 为什么需要分布式锁? —— 从超卖问题说起
  2. Redis实现分布式锁的三个核心原则
  3. 方案演进:从SETNX到Redisson的完整代码案例
    • 1 基础版:SETNX + 过期时间(易踩坑)
    • 2 进阶版:Redisson看门狗机制(生产级推荐)
  4. 常见问题深度问答(FAQ)
  5. 性能与可靠性权衡:锁粒度与续期策略
  6. 如何选择适合你的锁方案

为什么需要分布式锁?

在单体应用中,synchronizedLock 即可保证线程安全,但在微服务或集群部署下,多个JVM进程共享同一资源(如数据库库存),本地锁无法跨进程互斥,经典案例:电商秒杀系统,若不对库存扣减操作加锁,高并发下会出现“超卖”,分布式锁的核心需求是:在分布式环境中,确保同一时刻只有一个客户端能执行临界区代码

Java分布式锁Redis案例

Redis实现分布式锁的三个核心原则

  • 互斥性:任意时刻,锁只能被一个客户端持有。
  • 安全性:避免死锁,即锁必须设置过期时间,防止客户端宕机后锁永久占用。
  • 可重入性(可选):同一个线程可重复获取同一把锁,避免自身死锁。

方案演进:从SETNX到Redisson的完整代码案例

1 基础版:SETNX + 过期时间(易踩坑)

错误示范(早期代码):

// 问题:两步操作非原子性,若设置过期时间前宕机,锁无法释放
if (redisTemplate.opsForValue().setIfAbsent("lock", "1")) {
    redisTemplate.expire("lock", 30, TimeUnit.SECONDS);
    // 业务逻辑...
}

正确姿势(原子操作)

// 使用SET key value NX EX 保证原子性
Boolean locked = redisTemplate.opsForValue()
    .setIfAbsent("product:1001", "order-service", 30, TimeUnit.SECONDS);
if (locked) {
    try {
        // 执行业务:扣减库存、创建订单
    } finally {
        // 释放锁时需校验持有者身份,防止误删他人锁
        String owner = redisTemplate.opsForValue().get("product:1001");
        if ("order-service".equals(owner)) {
            redisTemplate.delete("product:1001");
        }
    }
}

⚠️ 隐患:若业务执行超过30秒,锁自动释放,其他线程可进入,造成并发问题,此时需要“看门狗”机制。

2 进阶版:Redisson看门狗机制(生产级推荐)

引入依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.4</version>
</dependency>

核心代码

@Autowired
private RedissonClient redissonClient;
public void deductStock(Long productId) {
    RLock lock = redissonClient.getLock("stock:lock:" + productId);
    try {
        // 尝试加锁,最多等待5秒,锁30秒后自动过期(可配置)
        if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
            // 执行库存扣减逻辑...
            System.out.println("线程:" + Thread.currentThread().getName() + " 成功获取锁并执行业务");
        } else {
            System.out.println("获取锁失败,请稍后重试");
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    } finally {
        // 注意:若锁持有者线程已释放,finally中只会删除自身持有的锁
        if (lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

原理:Redisson内部通过Lua脚本保证加锁与设置过期时间的原子性,并启动“看门狗”定时任务,默认锁的超时时间为30秒,每10秒自动续期一次(若业务未结束),这完美解决了业务长于锁过期时间的痛点。

常见问题深度问答(FAQ)

Q1:为什么不能直接用SETNX+EXPIRE
A:两个命令非原子,中间宕机会导致锁永不释放,引发死锁。

Q2:使用Redisson时,锁自动续期是否会耗尽CPU?
A:不会,看门狗是Netty线程池中的延迟任务,仅在锁存在时每10秒触发一次,业务结束立即取消,开销极低。

Q3:redis主从切换时,锁丢失怎么办?
A:Redis官方建议使用RedLock(多节点独立锁),但RedLock本身有争议,更稳妥的方案是配合ZooKeeper或Etcd实现分布式锁(CP模型),对于多数业务场景,可用Redis主从+哨兵保证高可用。

Q4:锁的粒度如何设计?
A:锁粒度越细,并发越高,库存扣减应按商品ID加锁,而不是将所有商品用同一把锁,但需要注意锁键的碰撞概率与Redis内存占用。

Q5:业务抛异常时,锁是否一定能释放?
A:必须在finally块中释放锁,并检查isHeldByCurrentThread(),避免抛出IllegalMonitorStateException。

性能与可靠性权衡:锁粒度与续期策略

策略 优点 缺点
细粒度锁(按商品ID) 高并发 锁数量多,内存开销大
粗粒度锁(全局) 简单 吞吐率低
固定过期时间 实现简单 不适用于长任务
看门狗续期 自适应 依赖Redisson库

优化建议

  • 业务执行时间稳定时,可设置一个合理的过期时间(如业务最大耗时的5倍),并关闭续期,减少网络交互。
  • 关键业务建议开启看门狗。
  • 考虑用tryLock(waitTime, leaseTime)控制等待与持有时间,避免线程无限等待。

如何选择适合你的锁方案

  • 简单业务(秒杀、优惠券):推荐使用Redisson,代码侵入小,开箱即用。
  • 对一致性要求极高(金融交易):优先选择ZooKeeper或Etcd。
  • 避免自研轮子:在涉及主从切换、重复加锁、可重入等问题上,成熟框架已处理边界情况,自研极易出Bug。

最终建议:在你的Spring Boot项目中集成Redisson,既满足高并发性能,又规避了底层复杂的分布式一致性实现,监控Redis内存与慢查询日志,确保锁键回收及时。


(文章结束)

抱歉,评论功能暂时关闭!