Spring Boot 整合 Redisson 实现分布式锁:实战指南

Spring Boot 整合 Redisson 实现分布式锁:实战指南

在分布式系统中,分布式锁是解决并发问题的关键组件。Redisson 作为 Redis 官方推荐的 Java 客户端,提供了强大的分布式锁实现,支持自动续期、可重入性、公平锁等高级特性。本文将专注于如何使用 Redisson 框架在 Spring Boot 项目中实现分布式锁,从环境搭建到实战应用,一步到位。

一、Redisson 简介与优势

Redisson 是一个基于 Redis 的 Java 分布式框架,不仅提供了 Redis 客户端功能,更封装了一系列分布式工具类,其中分布式锁是其最常用的功能之一。

Redisson 分布式锁的核心优势

  • 自动续期:内置"看门狗"机制,当业务未执行完时自动延长锁的过期时间,避免锁提前释放
  • 可重入性:支持同一线程多次获取锁,避免死锁
  • 集群支持:提供红锁(RedLock)机制,解决 Redis 集群下的"脑裂"问题
  • 丰富的锁类型:除了普通锁,还支持公平锁、读写锁、联锁等
  • 高性能:基于 Netty 实现,非阻塞 IO,性能优于传统 Jedis 客户端

二、环境准备

2.1 版本兼容说明

Redisson 版本需要与 Redis 版本兼容,推荐组合:

  • Redis 6.x + Redisson 3.16.x 及以上
  • Redis 5.x + Redisson 3.14.x 及以上

本文使用:Spring Boot 2.7.10 + Redisson 3.23.3 + Redis 6.2.6

2.2 引入依赖

pom.xml 中添加 Redisson 依赖(Spring Boot 专用 starter):

xml 复制代码
<!-- Redisson Spring Boot Starter -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.3</version>
</dependency>

<!-- Spring Boot Web(用于测试接口) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Lombok(简化代码) -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

2.3 配置 Redisson

Redisson 支持多种配置方式(YAML/JSON/Java 代码),推荐使用 YAML 配置,在 application.yml 中添加:

yaml 复制代码
spring:
  application:
    name: redisson-lock-demo

# Redisson 配置
redisson:
  # 超时配置
  connect-timeout: 3000  # 连接超时时间(毫秒)
  timeout: 3000          # 命令等待超时时间(毫秒)
  
  # 单节点配置(生产环境常用,简单部署)
  single-server-config:
    address: redis://localhost:6379  # Redis 地址
    password: 123456                 # Redis 密码(无密码可省略)
    database: 0                      # 数据库索引
    connection-pool-size: 16         # 连接池大小
    idle-connection-timeout: 30000   # 空闲连接超时时间(毫秒)
    ping-interval: 0                 # 心跳检测间隔(0表示不检测)

  # 集群配置(多节点高可用,按需开启)
  # cluster-servers-config:
  #   node-addresses:
  #     - redis://192.168.1.101:6379
  #     - redis://192.168.1.102:6379
  #     - redis://192.168.1.103:6379
  #   scan-interval: 2000  # 集群节点扫描间隔(毫秒)

配置说明

  • 单节点配置适用于开发环境或简单生产环境
  • 集群配置适用于高可用场景,需指定所有节点地址
  • connection-pool-size 建议根据并发量调整(默认 64,过小可能导致连接不足)

三、Redisson 分布式锁核心用法

Redisson 提供了 RLock 接口作为分布式锁的核心 API,支持多种锁操作。以下是最常用的锁类型及用法:

3.1 可重入锁(Reentrant Lock)

最常用的锁类型,支持同一线程多次获取锁,避免自己锁死自己。

java 复制代码
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 RedissonLockService {

    // 注入 Redisson 客户端
    @Resource
    private RedissonClient redissonClient;

    // 锁的 key(按业务场景定义,如商品ID、订单ID)
    private static final String LOCK_KEY = "product:lock:%s";  // %s 用于替换业务ID

    /**
     * 获取锁(阻塞式)
     * @param businessId 业务ID(如商品ID)
     * @return 锁对象
     */
    public RLock getLock(String businessId) {
        String lockKey = String.format(LOCK_KEY, businessId);
        return redissonClient.getLock(lockKey);
    }

    /**
     * 加锁并执行业务(自动续期)
     * @param businessId 业务ID
     * @param task 业务任务
     */
    public void executeWithLock(String businessId, Runnable task) {
        RLock lock = getLock(businessId);
        try {
            // 加锁(30秒过期,Redisson 会自动续期)
            lock.lock(30, TimeUnit.SECONDS);
            // 执行业务
            task.run();
        } finally {
            // 释放锁(仅当前线程持有锁时才释放)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 尝试加锁(非阻塞式)
     * @param businessId 业务ID
     * @param waitTime 最多等待时间
     * @param leaseTime 锁持有时间
     * @param task 业务任务
     * @return 是否加锁成功并执行
     */
    public boolean tryExecuteWithLock(String businessId, long waitTime, long leaseTime, 
                                     TimeUnit unit, Runnable task) {
        RLock lock = getLock(businessId);
        try {
            // 尝试加锁:最多等 waitTime,持有 leaseTime 后自动释放
            boolean locked = lock.tryLock(waitTime, leaseTime, unit);
            if (locked) {
                task.run();
                return true;
            }
            return false;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

3.2 公平锁(Fair Lock)

公平锁保证线程获取锁的顺序与请求顺序一致,避免"饥饿"现象(某些线程长期获取不到锁)。适用于对顺序性要求高的场景。

java 复制代码
/**
 * 获取公平锁
 */
public RLock getFairLock(String businessId) {
    String lockKey = String.format(LOCK_KEY, businessId);
    return redissonClient.getFairLock(lockKey);
}

// 使用方式与可重入锁一致
public void executeWithFairLock(String businessId, Runnable task) {
    RLock fairLock = getFairLock(businessId);
    try {
        fairLock.lock(30, TimeUnit.SECONDS);
        task.run();
    } finally {
        if (fairLock.isHeldByCurrentThread()) {
            fairLock.unlock();
        }
    }
}

3.3 读写锁(ReadWrite Lock)

适用于"多读少写"场景,允许多个读操作并发执行,但写操作需要独占锁,提高读操作的并发效率。

java 复制代码
import org.redisson.api.RReadWriteLock;

/**
 * 读写锁示例:查询用读锁,更新用写锁
 */
public class ReadWriteLockService {

    @Resource
    private RedissonClient redissonClient;

    private static final String RW_LOCK_KEY = "product:rwlock:%s";

    /**
     * 读操作(共享锁,可并发)
     */
    public void readData(String businessId, Runnable readTask) {
        RReadWriteLock rwLock = redissonClient.getReadWriteLock(String.format(RW_LOCK_KEY, businessId));
        RLock readLock = rwLock.readLock();
        try {
            readLock.lock(30, TimeUnit.SECONDS);
            readTask.run();
        } finally {
            if (readLock.isHeldByCurrentThread()) {
                readLock.unlock();
            }
        }
    }

    /**
     * 写操作(独占锁,互斥)
     */
    public void writeData(String businessId, Runnable writeTask) {
        RReadWriteLock rwLock = redissonClient.getReadWriteLock(String.format(RW_LOCK_KEY, businessId));
        RLock writeLock = rwLock.writeLock();
        try {
            writeLock.lock(30, TimeUnit.SECONDS);
            writeTask.run();
        } finally {
            if (writeLock.isHeldByCurrentThread()) {
                writeLock.unlock();
            }
        }
    }
}

四、实战场景:分布式库存扣减

以电商库存扣减为例,演示 Redisson 锁如何解决超卖问题。

4.1 库存服务实现

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class StockService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;  // Spring 提供的 Redis 操作工具
    @Resource
    private RedissonLockService redissonLockService;  // 上文实现的锁服务

    // 库存 Redis Key(格式:stock:{商品ID})
    private static final String STOCK_KEY = "stock:%s";

    /**
     * 初始化库存
     */
    public void initStock(String productId, int initialStock) {
        stringRedisTemplate.opsForValue().set(String.format(STOCK_KEY, productId), String.valueOf(initialStock));
        log.info("初始化库存成功,商品ID:{},初始库存:{}", productId, initialStock);
    }

    /**
     * 扣减库存(无锁版本,高并发下会超卖)
     */
    public boolean deductStockWithoutLock(String productId) {
        String stockKey = String.format(STOCK_KEY, productId);
        Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get(stockKey));
        if (stock == null || stock <= 0) {
            log.error("库存不足,商品ID:{}", productId);
            return false;
        }
        // 高并发下此处会超卖(非原子操作)
        stringRedisTemplate.opsForValue().set(stockKey, String.valueOf(stock - 1));
        log.info("扣减库存成功,商品ID:{},剩余库存:{}", productId, stock - 1);
        return true;
    }

    /**
     * 扣减库存(Redisson 锁版本,避免超卖)
     */
    public boolean deductStockWithLock(String productId) {
        // 使用 Redisson 锁执行扣减逻辑
        final boolean[] result = {false};
        redissonLockService.executeWithLock(productId, () -> {
            String stockKey = String.format(STOCK_KEY, productId);
            Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get(stockKey));
            if (stock != null && stock > 0) {
                stringRedisTemplate.opsForValue().set(stockKey, String.valueOf(stock - 1));
                log.info("扣减库存成功,商品ID:{},剩余库存:{}", productId, stock - 1);
                result[0] = true;
            } else {
                log.error("库存不足,商品ID:{}", productId);
                result[0] = false;
            }
        });
        return result[0];
    }

    /**
     * 查询库存
     */
    public Integer getStock(String productId) {
        String stock = stringRedisTemplate.opsForValue().get(String.format(STOCK_KEY, productId));
        return stock == null ? 0 : Integer.parseInt(stock);
    }
}

4.2 接口暴露(用于测试)

java 复制代码
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

@RestController
@RequestMapping("/stock")
public class StockController {

    @Resource
    private StockService stockService;

    /**
     * 初始化库存
     */
    @PostMapping("/init")
    public String initStock(@RequestParam String productId, @RequestParam int initialStock) {
        stockService.initStock(productId, initialStock);
        return "初始化成功,商品ID:" + productId + ",库存:" + initialStock;
    }

    /**
     * 无锁扣减库存(测试超卖用)
     */
    @PostMapping("/deduct/no-lock")
    public String deductWithoutLock(@RequestParam String productId) {
        boolean success = stockService.deductStockWithoutLock(productId);
        return success ? "扣减成功" : "库存不足";
    }

    /**
     * Redisson 锁扣减库存
     */
    @PostMapping("/deduct/redisson-lock")
    public String deductWithLock(@RequestParam String productId) {
        boolean success = stockService.deductStockWithLock(productId);
        return success ? "扣减成功" : "库存不足";
    }

    /**
     * 查询库存
     */
    @GetMapping("/query")
    public String queryStock(@RequestParam String productId) {
        return "商品ID:" + productId + ",当前库存:" + stockService.getStock(productId);
    }
}

4.3 高并发测试(JMeter)

  1. 测试步骤

    • 初始化库存:POST http://localhost:8080/stock/init?productId=1001&initialStock=100
    • 用 JMeter 创建 1000 个线程并发请求扣减接口
    • 分别测试无锁版本和 Redisson 锁版本,观察最终库存
  2. 测试结果

    • 无锁版本:最终库存可能为负数(如 -23),出现超卖
    • Redisson 锁版本:最终库存为 0,无超卖,并发安全

五、Redisson 锁高级特性解析

5.1 自动续期(看门狗机制)

当调用 lock(30, TimeUnit.SECONDS) 时,Redisson 会启动一个后台线程(看门狗),每隔 10 秒(30/3)检查锁是否仍被当前线程持有:

  • 若持有,则延长锁的过期时间至 30 秒
  • 若业务执行完成(调用 unlock()),则停止续期
  • 若线程意外终止,看门狗也会终止,锁会在 30 秒后自动释放

优势:无需预估业务执行时间,避免锁提前释放导致的并发问题。

5.2 红锁(RedLock)机制

针对 Redis 主从集群的"脑裂"问题(主节点宕机,从节点未同步锁信息),Redisson 实现了红锁算法:

  1. 同时向多个独立的 Redis 节点(通常 5 个)请求加锁
  2. 只有超过半数(≥3)节点加锁成功,才认为总锁加锁成功
  3. 释放锁时,向所有节点发送释放请求

使用方式

java 复制代码
import org.redisson.api.RedissonClient;
import org.redisson.client.RedisClient;

import java.util.Arrays;
import java.util.List;

public class RedLockService {

    @Resource
    private RedissonClient redissonClient;

    public void executeWithRedLock(String businessId, Runnable task) {
        // 多个独立 Redis 节点的锁
        List<String> nodeAddresses = Arrays.asList(
            "redis://192.168.1.101:6379",
            "redis://192.168.1.102:6379",
            "redis://192.168.1.103:6379",
            "redis://192.168.1.104:6379",
            "redis://192.168.1.105:6379"
        );
        
        // 创建红锁
        RLock[] locks = nodeAddresses.stream()
            .map(address -> {
                // 连接每个节点并获取锁
                RedisClient client = RedisClient.create(address);
                return client.getLock("product:lock:" + businessId);
            })
            .toArray(RLock[]::new);
        
        RLock redLock = redissonClient.getRedLock(locks);
        
        try {
            // 红锁加锁(等待100ms,持有30秒)
            boolean locked = redLock.tryLock(100, 30000, TimeUnit.MILLISECONDS);
            if (locked) {
                task.run();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (redLock.isHeldByCurrentThread()) {
                redLock.unlock();
            }
        }
    }
}

六、最佳实践与注意事项

  1. 锁的粒度要细

    • 错误示例:用一个全局锁 stock:lock 控制所有商品的库存
    • 正确示例:按商品ID加锁 stock:lock:1001,不同商品的锁互不影响,提高并发效率
  2. 避免长时间持有锁

    • 锁内逻辑应尽量简短,避免 IO 密集型操作(如数据库查询、远程调用)
    • 若必须执行耗时操作,可考虑异步处理,缩短锁持有时间
  3. 释放锁的正确姿势

    • 必须在 finally 中释放锁,确保业务异常时也能释放
    • 释放前判断 lock.isHeldByCurrentThread(),避免释放其他线程的锁
  4. 集群环境推荐红锁

    • 单节点 Redis 存在单点故障风险,生产环境建议用 Redis 集群 + 红锁机制
  5. 监控与告警

    • 监控锁的获取成功率、等待时间、续期次数
    • 当锁等待时间过长时触发告警,可能预示并发过高或业务逻辑耗时过长

七、总结

Redisson 凭借强大的功能和易用性,成为 Spring Boot 项目实现分布式锁的首选框架。本文从环境搭建、核心用法到实战场景,详细介绍了 Redisson 分布式锁的使用,重点包括:

  • 可重入锁解决基本并发问题
  • 公平锁保证请求顺序
  • 读写锁优化"多读少写"场景
  • 红锁机制保证集群环境下的可靠性

掌握 Redisson 分布式锁,能有效解决分布式系统中的超卖、重复提交、资源竞争等问题,为高并发业务保驾护航。

如果本文对你有帮助,欢迎点赞收藏,如有疑问或补充,欢迎在评论区交流!

相关推荐
星辰徐哥7 小时前
Spring Boot 微服务架构设计与实现
spring boot·后端·微服务
星辰徐哥7 小时前
Spring Boot 数据导入导出与报表生成
spring boot·后端·ui
明夜之约7 小时前
Spring Boot 自动装配源码
java·spring boot·后端
Leaton Lee7 小时前
Spring Boot分层架构详解:从Controller到Service再到Mapper的完整流程
java·spring boot·后端·架构
Micro麦可乐7 小时前
Spring Boot 实战:从零设计一个短链系统(含完整代码与数据库设计)
数据库·spring boot·后端·哈希算法·雪花算法·短链系统
Jinkxs7 小时前
Resilience4j- 与 Spring Boot 快速集成:自动配置与基础注解使用
java·spring boot·后端
毕设源码_郑学姐7 小时前
计算机毕业设计springboot网络相册设计与实现 基于Spring Boot框架的在线相册管理系统开发与应用 Spring Boot驱动的网络影集设计与实践
spring boot·后端·课程设计
辣机小司7 小时前
【踩坑记录:Spring Boot 配置文件读取值不一致?警惕 YAML 的“八进制陷阱”与 SnakeYAML 版本之谜】
java·spring boot·后端·yaml·踩坑记录
一条小锦吕*7 小时前
基于Spring Boot + 数据可视化 + 协同过滤算法的推荐系统设计与实现(源码+论文+部署全讲解)
spring boot·算法·信息可视化
Jinkxs7 小时前
Prometheus - 监控微服务:Spring Boot 应用指标暴露与监控
spring boot·微服务·prometheus