Spring Boot3 用Redisson实现分布式锁:同行们,这些坑别再踩了!

内容分享5小时前发布
0 0 0

Spring Boot3 用Redisson实现分布式锁:同行们,这些坑别再踩了!

作为互联网软件开发同行,你是不是也遇到过这样的情况:在 Spring Boot3 微服务项目里,多实例部署后,库存扣减出现超卖、订单重复生成,排查半天发现是并发场景下的资源竞争问题?明明在单服务里用了本地锁,到了分布式环境下却完全失效 —— 这实则就是没做好分布式锁导致的。今天咱们就聚焦一个核心问题:Spring Boot3 到底该怎么用 Redisson 实现可靠的分布式锁? 从问题根源到实操步骤,咱们一步步说清楚。

为啥分布式环境下,本地锁不管用了?

在单服务部署时,我们用 synchronized 或者 ReentrantLock 就能解决并发问题,由于所有请求都在同一个 JVM 进程里,锁能控制所有线程的资源访问。但到了微服务架构,一个服务部署 3 台、5 台实例是常事,列如用户下单请求,可能被负载均衡分发到实例 A、实例 B、实例 C 上 —— 这时候本地锁就像 “家门钥匙只锁了自己家,却管不了邻居家的门”,实例 A 的锁挡不住实例 B 的线程访问一样资源,最终必然出现数据错乱。

那为啥选 Redisson 做分布式锁?市面上常见的分布式锁方案里,Redis 原生锁需要自己处理过期时间、死锁预防,代码繁琐还容易出 bug;ZooKeeper 锁性能又稍弱,不适合高并发场景。而 Redisson 作为 Redis 的 Java 客户端,不仅封装了分布式锁的底层逻辑(列如自动续期、可重入、公平锁支持),还能和 Spring Boot3 无缝集成,咱们开发时不用重复造轮子,效率和可靠性都能兼顾。

实操步骤:Spring Boot3 集成 Redisson 分布式锁,4 步就能搞定

第一步:引入依赖,少走版本兼容的坑

Spring Boot3 对应的 Redisson 版本有讲究,提议用 3.20.x 及以上版本,避免和 Spring Boot3 的依赖冲突。在 pom.xml 里添加这两个依赖:

<!-- Redisson核心依赖 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.21.3</version>
</dependency>
<!-- Spring Boot3 Web依赖(如果项目已有可忽略) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

这里提醒一句:如果你的项目用的是 Gradle,依赖配置可以对应调整,核心是保证 Redisson starter 版本和 Spring Boot3 匹配,实在不确定的话,去 Redisson 官网查最新的兼容说明。

第二步:配置 Redis 连接,3 行配置搞定基础信息

在 application.yml(或 application.properties)里添加 Redis 的连接信息,Redisson 会自动读取这些配置创建客户端:

spring:
  redis:
    host: 127.0.0.1  # 你的Redis地址
    port: 6379       # Redis端口
    password: 123456 # Redis密码(没有的话可省略)
    database: 0      # 使用的Redis数据库索引

如果是 Redis 集群或者哨兵模式,配置稍复杂一点,但 Redisson 也支持,列如集群配置只需加 “cluster.nodes” 字段,具体可以参考 Redisson 的官方文档,这里咱们先以单机 Redis 为例,聚焦锁的核心用法。

第三步:核心代码实现,两种锁场景要分清

Redisson 支持多种锁类型,咱们开发中最常用的是可重入锁(ReentrantLock)和公平锁(FairLock),先看最基础的可重入锁实现:

第一创建一个 Service 层接口,列如 InventoryService,模拟库存扣减场景:

public interface InventoryService {
    // 扣减库存方法,参数:商品ID、扣减数量
    boolean deductInventory(Long productId, Integer quantity);
}

然后写实现类,注入 RedissonClient,用 lock () 和 unlock () 控制锁的获取与释放:

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Service
public class InventoryServiceImpl implements InventoryService {

    // 注入Redisson客户端(自动配置,无需手动创建)
    @Resource
    private RedissonClient redissonClient;

    // 模拟库存存储,实际项目中应该用数据库
    private static final HashMap<Long, Integer> INVENTORY_MAP = new HashMap<>();

    // 初始化库存,列如商品ID=1001的库存有100件
    static {
        INVENTORY_MAP.put(1001L, 100);
    }

    @Override
    public boolean deductInventory(Long productId, Integer quantity) {
        // 1.定义锁的key:用商品ID区分不同锁,避免锁冲突
        String lockKey = "inventory:lock:" + productId;
        // 2.获取可重入锁
        RLock lock = redissonClient.getLock(lockKey);

        try {
            // 3.获取锁:最多等待5秒,获取到锁后10秒自动释放(防止死锁)
            boolean isLocked = lock.tryLock(5, 10, TimeUnit.SECONDS);
            if (!isLocked) {
                // 没获取到锁,返回失败(列如提示“当前请求过多,请稍后再试”)
                System.out.println("获取锁失败,商品ID:" + productId);
                return false;
            }

            // 4.获取到锁,执行库存扣减逻辑
            Integer currentInventory = INVENTORY_MAP.get(productId);
            if (currentInventory == null || currentInventory < quantity) {
                System.out.println("库存不足,商品ID:" + productId + ",当前库存:" + currentInventory);
                return false;
            }
            // 扣减库存
            INVENTORY_MAP.put(productId, currentInventory - quantity);
            System.out.println("库存扣减成功,商品ID:" + productId + ",剩余库存:" + (currentInventory - quantity));
            return true;

        } catch (InterruptedException e) {
            // 处理中断异常
            Thread.currentThread().interrupt();
            System.out.println("扣减库存时发生中断,商品ID:" + productId);
            return false;
        } finally {
            // 5.释放锁:只有当前线程持有锁时才释放,避免误释放别人的锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

这里有两个关键细节要注意:

一是锁的 key 必须 “唯一”,列如用 “业务类型 + 商品 ID” 作为 key,避免不同业务、不同商品共用一把锁,导致 “锁粒度太大” 的问题;

二是 tryLock () 的三个参数:等待时间(5 秒)、自动释放时间(10 秒)、时间单位,这三个参数能有效预防死锁 —— 即使服务突然宕机,10 秒后锁也会自动释放,不会卡住资源。

如果你的业务需要 “公平锁”(即先请求的线程先获取锁,避免线程饥饿),只需把获取锁的代码改成:

// 获取公平锁
RLock fairLock = redissonClient.getFairLock(lockKey);
// 后续的tryLock()和释放锁逻辑和可重入锁一致

公平锁适合对 “顺序性” 要求高的场景,列如秒杀活动中的排队逻辑,但要注意:公平锁的性能比可重入锁稍低,非必要场景优先用可重入锁。

第四步:测试验证,模拟高并发场景

为了验证分布式锁的效果,咱们写一个 Controller 层接口,用 Postman 或者 JMeter 模拟多请求并发:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

@RestController
@RequestMapping("/inventory")
public class InventoryController {

    @Resource
    private InventoryService inventoryService;

    // 扣减库存接口,参数:商品ID、扣减数量
    @GetMapping("/deduct")
    public String deduct(@RequestParam Long productId, @RequestParam Integer quantity) {
        boolean result = inventoryService.deductInventory(productId, quantity);
        return result ? "库存扣减成功" : "库存扣减失败(库存不足或请求过多)";
    }
}

启动 Spring Boot3 项目后,用 JMeter 创建 100 个线程,同时调用
http://localhost:8080/inventory/deduct?productId=1001&quantity=1—— 如果没有分布式锁,100 次请求后库存可能会变成负数(超卖);而加上 Redisson 锁后,最终库存会准确变成 0,不会出现超卖问题,这就说明锁生效了。

三、避坑指南:这 3 个细节不注意,锁等于白加

不要忘记释放锁,必须放在 finally 里:如果业务逻辑执行中抛出异常,没释放的锁会一直占用,直到自动过期,这期间会影响其他请求。所以必定要把 unlock () 放在 finally 块里,确保无论是否有异常,锁都会被释放。

避免 “锁超时” 导致业务没执行完:如果你的业务逻辑执行时间超过了 tryLock () 设置的 “自动释放时间”(列如 10 秒),锁会提前释放,导致并发问题。这时候可以用 Redisson 的 “自动续期” 功能 —— 不用手动设置自动释放时间,锁会在业务执行期间自动续期,业务结束后再释放,代码只需把 tryLock () 改成:

// 只设置等待时间,不设置自动释放时间,Redisson会自动续期
boolean isLocked = lock.tryLock(5, TimeUnit.SECONDS);

锁的粒度不能太粗也不能太细:列如给 “所有商品的库存” 用一把锁(key=“inventory:lock”),会导致所有商品的扣减请求都排队,性能太差;但如果给 “每个库存变更操作” 都用不同的锁,又会导致锁太多,管理复杂。提议按 “业务 + 唯一标识”(如商品 ID、用户 ID)设计锁 key,平衡性能和可靠性。

总结:Redisson 分布式锁,记住这 3 个核心点

  1. 集成简单:Spring Boot3+Redisson starter,依赖 + 配置两步搞定,不用手动管理 Redis 连接和锁的底层逻辑;
  2. 用法灵活:支持可重入锁、公平锁、读写锁等多种锁类型,满足不同业务场景;
  3. 可靠性高:自动续期、死锁预防、线程安全释放,解决了 Redis 原生锁的诸多痛点。

最后想跟同行们说:分布式锁看似简单,但实际项目中 “超卖”“重复下单” 这些问题,许多时候都是锁的细节没处理好导致的。如果你在 Spring Boot3 集成 Redisson 的过程中,遇到了版本兼容、锁失效、性能瓶颈等问题,欢迎在评论区留言分享 —— 咱们一起讨论解决方案,少踩坑、多提效!

© 版权声明

相关文章

暂无评论

none
暂无评论...