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 分布式锁,能有效解决分布式系统中的超卖、重复提交、资源竞争等问题,为高并发业务保驾护航。

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

相关推荐
码事漫谈2 小时前
C++编程陷阱:悬空引用检测方法与防范指南
后端
码事漫谈2 小时前
缓存友好的数据结构设计:提升性能的关键技巧
后端
sheji34163 小时前
【开题答辩全过程】以 springboot高校社团管理系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
Terio_my3 小时前
Spring Boot 集成 Redis 缓存解决方案
spring boot·redis·缓存
相与还4 小时前
IDEA+SpringBoot实现远程DEBUG到本机
java·spring boot·intellij-idea
聆风吟º4 小时前
远程录制新体验:Bililive-go与cpolar的无缝协作
开发语言·后端·golang
野犬寒鸦4 小时前
从零起步学习Redis || 第四章:Cache Aside Pattern(旁路缓存模式)以及优化策略
java·数据库·redis·后端·spring·缓存
Terio_my4 小时前
Spring Boot 缓存技术详解
spring boot·后端·缓存
caibixyy5 小时前
Spring Boot 集成 Kafka 详解
spring boot·kafka