SpringBoot 教程(十四)| SpringBoot 之集成 Redis

一、开篇:Redis 集成核心认知与前置条件

1.1 为什么要集成 Redis?

Redis 是 Java 开发中高频 NoSQL 内存数据库,以 Key-Value 键值对存储为核心,解决三大核心业务场景:

  • 缓存:缓存商品详情、用户信息等高频访问数据,减轻 MySQL 数据库压力;

  • 分布式锁:解决微服务多实例并发冲突(如库存扣减、订单防重提交);

  • 计数器 / 自增序列:生成唯一订单号、统计文章阅读量、接口限流等。

1.2 主流 Redis 客户端选型指南

Spring Boot 生态中三类核心 Redis 客户端,按业务场景精准匹配:

客户端 核心特性 线程安全 序列化默认行为 适用场景
Lettuce Spring Boot 2.x + 默认客户端、基于 Netty 异步非阻塞、原生支持哨兵 / 集群 默认 JDK 序列化(Key 乱码,需自定义 Jackson 序列化) 绝大多数基础场景(单体 / 微服务基础缓存)
Jedis 同步 API 直观易懂、调试简单、需通过连接池管理实例 需连接池管理 默认 JDK 序列化(Key 乱码,需自定义 Jackson 序列化) 小型单体项目、简单同步操作场景
Redisson Redis 官方推荐分布式客户端、封装自动锁续期(看门狗)、支持读写锁 / 红锁 默认 Jackson 序列化(Key 无乱码,特殊场景需补充配置) 微服务分布式场景(秒杀、跨服务并发控制)

1.3 集成流程总览

部署 Redis 服务端 → 选择客户端并添加依赖 → 配置连接信息(含客户端专属配置) → 处理序列化(按需,不同客户端差异直接说明) → 基础操作开发 → 进阶场景实现

其中序列化处理差异:

  • Lettuce/Jedis:默认 JDK 序列化会导致 Key 乱码、Value 二进制不可读,必须自定义 Jackson 序列化(解决乱码 + 支持对象 / 时间类型存储);
  • Redisson:默认已用 Jackson 序列化(Key 清晰、Value 为 JSON),大部分场景无需额外配置,仅特殊场景(如 JDK8 时间类型、跨语言兼容)需补充配置。

1.4 前置条件

已部署 Redis 服务端(本地或云服务器,推荐 Redis 6.x+),确保:

  1. Redis 服务正常运行,IP、端口(默认 6379)、密码正确;
  2. 服务器防火墙开放 6379 端口(云服务器需配置安全组);
  3. 客户端与 Redis 版本兼容(Jedis 3.8.0 兼容 Redis 6.x+,Redisson 3.25.2 兼容 Redis 6.x+)。

二、第一步:添加依赖(按客户端选型)

2.1 选型 1:默认集成 Lettuce(推荐基础场景)

Spring Boot 2.x + 默认内置 Lettuce,无需额外排除依赖,完整 POM 依赖如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.lsqingfeng.springboot</groupId>
    <artifactId>springboot-learning</artifactId>
    <version>1.0.0</version>

    <!-- 依赖管理:统一Spring Boot版本 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.6.2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- 1. Spring Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 2. 数据库相关 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- 3. 工具类 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 4. Redis核心依赖(默认集成Lettuce) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- 连接池依赖:必须添加!否则 lettuce.pool 配置不生效 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
    </dependencies>
</project>

2.2 选型 2:切换集成 Jedis(需排除 Lettuce)

若业务需使用 Jedis 客户端,需先排除默认 Lettuce 依赖,再引入 Jedis 和连接池依赖:

<!-- 1. 排除默认Lettuce,保留Spring Data Redis基础能力 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <exclusions>
        <exclusion>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!-- 2. 引入Jedis客户端(版本3.8.0,兼容Redis 6.x+) -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.8.0</version>
</dependency>

<!-- 3. 连接池依赖:Jedis必须添加,否则 jedis.pool 配置不生效 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

2.3 选型 3:集成 Redisson(分布式场景推荐)

Redisson 可与 Lettuce/Jedis 共存,直接引入 Starter 实现自动配置,依赖如下:

<!-- 1. Redisson Starter(自动配置RedissonClient,兼容Spring Boot 2.x/3.x) -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.25.2</version> <!-- 兼容Redis 6.x+,建议使用最新稳定版 -->
</dependency>

<!-- 2. 若需同时使用RedisTemplate,补充以下依赖(仅用Redisson可省略) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

三、第二步:配置 Redis 连接(application.yml)

3.1 Lettuce/Jedis 配置

(一)单机模式

保留数据库、热部署、第三方配置,补充 Redis 核心连接配置:

server:
  port: 19191 # 项目端口

spring:
  # 1. 数据库配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot_learning?serverTimezone=Asia/Shanghai&characterEncoding=utf-8
    username: root
    password: root

  # 2. 热部署配置
  devtools:
    restart:
      enable: true

  # 3. Redis核心配置(Lettuce示例,切换Jedis则将lettuce改为jedis)
  redis:
    host: localhost          # Redis服务地址(本地填127.0.0.1,服务器填公网IP)
    port: 6379               # Redis默认端口
    password: 123456         # Redis密码(无密码则删除此配置)
    database: 0              # 数据库索引(0-15,默认0)
    timeout: 3000ms          # 连接超时时间(单位:ms,避免无限等待)
    lettuce:                 # 客户端专属连接池配置(Jedis用jedis: {})
      pool:
        max-active: 32       # 最大活跃连接数(根据QPS调整,默认8)
        max-idle: 16         # 最大空闲连接数(建议≤max-active)
        min-idle: 8          # 最小空闲连接数(避免频繁创建连接)
        max-wait: -1ms       # 最大等待时间(-1表示无限制,单位:ms)

(二)集群模式

适用于 Redis 集群部署场景(至少 3 主 3 从),配置需包含所有主从节点:

spring:
  redis:
    password: 123456         # 集群统一密码(无密码则删除)
    cluster:
      nodes:                 # 集群节点列表(格式:IP:端口,需包含所有主从节点)
        - 192.168.1.10:7001
        - 192.168.1.10:7002
        - 192.168.1.10:7003
        - 192.168.1.10:7004
        - 192.168.1.10:7005
        - 192.168.1.10:7006
      max-redirects: 3       # 最大重定向次数(默认 3,避免循环跳转)
    # 连接池配置(Lettuce 示例,Jedis 替换为 jedis: {})
    lettuce:
      pool:
        max-active: 64       # 集群场景建议增大连接数(默认 8)
        max-idle: 32
        min-idle: 16
        max-wait: -1ms

3.2 Redisson 配置

Redisson 需单独配置连接信息,支持单机 / 集群模式,且默认已集成 Jackson 序列化(无需额外处理 Key 乱码),(与 Lettuce/Jedis 配置不冲突):

spring:
  redis:
    redisson:
      file: classpath:/redisson-config.yml

再到 src/main/resources下创建一个 redisson-config.yml(里面不能加任何注释)

(一)单机模式

singleServerConfig:
  address: "redis://localhost:6379"
  password: "123456"
  database: 0
  connectionPoolSize: 64
  idleConnectionTimeout: 10000
  connectTimeout: 3000
  timeout: 3000
codec: !<org.redisson.codec.JsonJacksonCodec> {}

(二)集群模式

clusterServersConfig:
  nodeAddresses:
    - redis://192.168.1.10:7001
    - redis://192.168.1.10:7002
  password: "123456"
  scanInterval: 0
codec: !<org.redisson.codec.JsonJacksonCodec> {}

四、第三步:序列化配置(按客户端分别实现)

4.1 核心问题:为什么要处理序列化?

  • Lettuce/Jedis:默认 JDK 序列化(JdkSerializationRedisSerializer)会导致 Key 乱码(如 user:1 变成 \xAC\xED\x00\x05t\x00\x03user:1)、Value 为二进制不可读,且仅支持实现 Serializable 的对象;
  • Redisson:默认 Jackson 序列化(JacksonJsonRedisSerializer),Key 清晰、Value 为 JSON,但特殊场景(如 JDK8 时间类型、自定义序列化规则)需补充配置。

4.2 场景 1:Lettuce/Jedis 序列化配置(必须自定义)

需通过配置类替换默认序列化器,支持对象、JDK8 时间类型存储,代码如下:

RedisConfig.java(Lettuce/Jedis 通用序列化配置)

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis 序列化配置类:解决 Lettuce/Jedis 的 Key 乱码+对象序列化问题(生产环境必用)
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        // 1. 创建 RedisTemplate 实例
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 2. 配置 Key 序列化器:StringRedisSerializer(避免 Key 乱码)
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);       // 普通 Key 序列化
        redisTemplate.setHashKeySerializer(stringRedisSerializer);   // Hash 结构的 Key 序列化

        // 3. 配置 Value 序列化器:Jackson2JsonRedisSerializer(支持对象存储)
        Jackson2JsonRedisSerializer<Object> jacksonSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();

        // 3.1 解决类型推断问题:替代过时的 enableDefaultTyping(旧方法存在反序列化漏洞)
        objectMapper.activateDefaultTyping(
                LaissezFaireSubTypeValidator.instance,  // 安全的类型校验器
                ObjectMapper.DefaultTyping.NON_FINAL,   // 非 final 类支持类型推断(如 User)
                com.fasterxml.jackson.annotation.JsonTypeInfo.As.PROPERTY  // 类型信息嵌入 JSON(生成 @class 字段)
        );

        // 3.2 配置字段可见性:所有字段(包括 private)均可序列化
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

        // 3.3 解决 JDK8 时间类型(LocalDateTime/LocalDate)序列化问题
        objectMapper.registerModule(new JavaTimeModule());  // 显式注册时间模块(兼容旧版本 Jackson)

        jacksonSerializer.setObjectMapper(objectMapper);
        redisTemplate.setValueSerializer(jacksonSerializer);     // 普通 Value 序列化
        redisTemplate.setHashValueSerializer(jacksonSerializer); // Hash 结构的 Value 序列化

        // 4. 初始化模板配置(必须调用,否则序列化配置不生效)
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}

4.3 场景 2:Redisson 序列化配置(按需补充)

Redisson 默认已实现 Jackson 序列化,大部分场景无需额外配置,仅以下场景需自定义:

  1. 需支持 JDK8 时间类型(LocalDateTime/LocalDate);
  2. 需自定义 JSON 序列化规则(如字段命名策略);
  3. 跨语言场景(需统一序列化格式)。

RedissonConfig.java(Redisson 自定义序列化配置)

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.redisson.codec.Jackson2JsonCodec;
import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Redisson 序列化配置类:仅特殊场景需配置(如支持 LocalDateTime)
 */
@Configuration
public class RedissonConfig {

    @Bean
    public RedissonAutoConfigurationCustomizer redissonAutoConfigurationCustomizer() {
        // 1. 自定义 ObjectMapper,支持 JDK8 时间类型
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.registerModule(new JavaTimeModule());  // 注册时间模块

        // 2. 使用自定义 ObjectMapper 初始化 Jackson 序列化器
        Jackson2JsonCodec jacksonCodec = new Jackson2JsonCodec(objectMapper);

        // 3. 将自定义序列化器注入 Redisson 配置
        return config -> config.setCodec(jacksonCodec);
    }
}

4.4 三种客户端序列化效果对比

对比维度 Lettuce/Jedis(默认 JDK 序列化) Lettuce/Jedis(自定义 Jackson) Redisson(默认 / 自定义 Jackson)
Key 显示 \xAC\xED\x00\x05t\x00\x03user:1 user:1(清晰可读) user:1(清晰可读)
Value 存储 二进制数据(不可直接查看) JSON 字符串(如 {“id”:1,“name”:“张三”}) JSON 字符串(如 {“id”:1,“name”:“张三”})
支持类型 仅实现 Serializable 的对象 常用类型(String、自定义对象、JDK8 时间) 常用类型(String、自定义对象、JDK8 时间)
跨语言兼容性 差(仅 Java 可解析) 好(JSON 格式) 好(JSON 格式)
配置必要性 必须配置(否则乱码) 必须配置(否则乱码) 默认无需配置,特殊场景按需配置

五、第四步:基础操作:RedisTemplate 与 RedissonClient 上手

解释说明:RedisTemplate工具类 是 Spring框架对Jedis和Lettuce客户端的封装,用它可以更方便的在Spring项目里面操作Redis,引入 “spring-boot-starter-data-redis” 依赖,就可以用这个RedisTemplate工具类了。

5.1 实体类准备(示例用)

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;

/**
 * 用户实体类(无需实现 Serializable,Jackson 序列化已兼容)
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Long id;
    private String name;
    private Integer age;
    private String email;
    private LocalDateTime createTime; // JDK8 时间类型(验证序列化兼容性)
}

5.2 统一返回结果类(Result.java)

接口返回统一格式,避免零散返回值,示例中已依赖此类:

import lombok.Data;

/**
 * 统一接口返回结果类
 */
@Data
public class Result<T> {
    // 状态码:200=成功,500=失败
    private Integer code;
    // 提示消息
    private String msg;
    // 返回数据
    private T data;

    // 成功响应(带数据)
    public static <T> Result<T> success(T data) {
        Result<T> result = new Result<>();
        result.setCode(200);
        result.setMsg("操作成功");
        result.setData(data);
        return result;
    }

    // 成功响应(无数据)
    public static <T> Result<T> success() {
        return success(null);
    }

    // 失败响应
    public static <T> Result<T> error(String msg) {
        Result<T> result = new Result<>();
        result.setCode(500);
        result.setMsg(msg);
        result.setData(null);
        return result;
    }
}

5.3 RedisTemplate 基础操作(Lettuce/Jedis 通用)

通过 RedisTemplate<String, Object> 操作 String、Hash、List 等常见数据结构,示例在 Controller 层实现:

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * RedisTemplate 基础操作示例(Lettuce/Jedis 通用)
 */
@RestController
@RequestMapping("/redis/template")
public class RedisTemplateDemoController {

    // 注入自定义配置的 RedisTemplate(泛型 <String, Object>,避免强转)
    private final RedisTemplate<String, Object> redisTemplate;

    // 构造器注入(推荐,避免字段注入的循环依赖风险)
    public RedisTemplateDemoController(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    // ------------------- String 类型示例(缓存单个对象) -------------------
    @GetMapping("/saveUser")
    public Result<User> saveUser() {
        // 1. 创建 User 对象(含 LocalDateTime,验证时间序列化)
        User user = new User(1L, "张三", 25, "zhangsan@test.com", LocalDateTime.now());

        // 2. 存储 String 类型:key=user:{id},value=User 对象,过期时间 1 小时
        String userKey = "user:" + user.getId();
        redisTemplate.opsForValue().set(userKey, user, 3600, TimeUnit.SECONDS);

        // 3. 读取 String 类型:根据 key 获取 User 对象
        User cachedUser = (User) redisTemplate.opsForValue().get(userKey);

        return Result.success(cachedUser);
    }

    // ------------------- Hash 类型示例(缓存对象的多个字段) -------------------
    @GetMapping("/saveUserHash")
    public Result<Map<Object, Object>> saveUserHash() {
        // 1. 创建 Hash 数据:key=userHash:{id},field=属性名,value=属性值
        Long userId = 2L;
        String hashKey = "userHash:" + userId;
        Map<String, Object> userMap = new HashMap<>();
        userMap.put("id", userId);
        userMap.put("name", "李四");
        userMap.put("age", 28);
        userMap.put("createTime", LocalDateTime.now());

        // 2. 存储 Hash 类型:批量添加 field-value
        redisTemplate.opsForHash().putAll(hashKey, userMap);
        // 设置 Hash 过期时间(30 分钟)
        redisTemplate.expire(hashKey, 1800, TimeUnit.SECONDS);

        // 3. 读取 Hash 类型:获取所有 field-value
        Map<Object, Object> cachedUserMap = redisTemplate.opsForHash().entries(hashKey);

        return Result.success(cachedUserMap);
    }
}

5.4 RedissonClient 基础操作(分布式场景)

Redisson 封装了更简洁的 API,且默认支持分布式锁,基础缓存操作示例:

import org.redisson.api.RedissonClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;

/**
 * RedissonClient 基础操作示例(分布式场景推荐)
 */
@RestController
@RequestMapping("/redis/redisson")
public class RedissonDemoController {

    // 注入 Redisson 自动配置的 RedissonClient
    private final RedissonClient redissonClient;

    public RedissonDemoController(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    // ------------------- String 类型示例 -------------------
    @GetMapping("/saveUser")
    public Result<User> saveUser() {
        User user = new User(3L, "王五", 30, "wangwu@test.com", LocalDateTime.now());
        String userKey = "user:" + user.getId();

        // Redisson 操作 String:直接获取 Bucket(桶)对象,支持泛型
        redissonClient.getBucket(userKey)
                .set(user, 3600, TimeUnit.SECONDS); // 存储并设置过期时间

        // 读取 String:直接获取泛型对象,无需强转
        User cachedUser = redissonClient.getBucket(userKey).get();

        return Result.success(cachedUser);
    }
}

六、第五步:进阶场景:分布式锁实现

6.1 方案 1:基于 RedisTemplate 实现分布式锁

6.1.1 分布式锁工具类(RedisTemplate 版)

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;

/**
 * RedisTemplate 分布式锁工具类(基础场景通用)
 */
@Component
public class RedisTemplateLockUtil {

    private final RedisTemplate<String, Object> redisTemplate;

    public RedisTemplateLockUtil(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 加锁:原子操作,避免并发问题
     * @param lockKey 锁的 Key(如 "stock:lock:1001")
     * @param lockValue 锁的 Value(用 UUID 区分线程,避免误删)
     * @param expireTime 锁过期时间(秒),防止死锁
     * @return true=加锁成功,false=加锁失败
     */
    public boolean lock(String lockKey, String lockValue, long expireTime) {
        Boolean isLocked = redisTemplate.opsForValue()
                .setIfAbsent(lockKey, lockValue, expireTime, TimeUnit.SECONDS);
        // 避免 Boolean 空指针(自动拆箱风险)
        return Boolean.TRUE.equals(isLocked);
    }

    /**
     * 解锁:验证 Value 一致性,避免误删其他线程的锁
     * @param lockKey 锁的 Key
     * @param lockValue 锁的 Value(需与加锁时的 Value 一致)
     */
    public void unlock(String lockKey, String lockValue) {
        String storedValue = (String) redisTemplate.opsForValue().get(lockKey);
        // 只有当存储的 Value 与当前线程的 Value 一致时,才删除锁
        if (lockValue != null && lockValue.equals(storedValue)) {
            redisTemplate.delete(lockKey);
        }
    }
}

6.1.2 业务示例:库存扣减(RedisTemplate 锁)

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;

/**
 * 库存扣减业务示例(基于 RedisTemplate 分布式锁)
 */
@RestController
@RequestMapping("/business/stock")
public class StockDeductWithTemplateLockController {

    private final RedisTemplateLockUtil redisTemplateLockUtil;
    private final RedisTemplate<String, Object> redisTemplate;

    public StockDeductWithTemplateLockController(RedisTemplateLockUtil redisTemplateLockUtil, 
                                                RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplateLockUtil = redisTemplateLockUtil;
        this.redisTemplate = redisTemplate;
    }

    /**
     * 库存扣减接口
     * @param productId 商品 ID
     * @return 扣减结果
     */
    @GetMapping("/deduct/{productId}")
    public Result<String> deductStock(@PathVariable Long productId) {
        // 1. 定义锁 Key(按商品 ID 区分,避免全局锁导致性能问题)
        String lockKey = "stock:lock:" + productId;
        // 2. 定义锁 Value(UUID 区分线程,防止误删其他线程的锁)
        String lockValue = UUID.randomUUID().toString();
        // 3. 锁过期时间(10 秒,需大于业务执行时间)
        long expireTime = 10;

        try {
            // 4. 尝试加锁
            boolean isLocked = redisTemplateLockUtil.lock(lockKey, lockValue, expireTime);
            if (!isLocked) {
                return Result.error("当前请求过多,请稍后重试");
            }

            // 5. 核心业务:库存扣减(模拟从 Redis 读取库存并扣减)
            String stockKey = "stock:" + productId;
            // 初始化库存(首次调用时设置,实际项目从 DB 加载)
            redisTemplate.opsForValue().setIfAbsent(stockKey, 100);
            // 读取当前库存
            Integer currentStock = (Integer) redisTemplate.opsForValue().get(stockKey);

            // 6. 库存校验
            if (currentStock == null || currentStock <= 0) {
                return Result.error("商品 ID:" + productId + ",库存不足");
            }

            // 7. 扣减库存
            redisTemplate.opsForValue().set(stockKey, currentStock - 1);
            return Result.success("商品 ID:" + productId + ",库存扣减成功,剩余库存:" + (currentStock - 1));

        } finally {
            // 8. 解锁(必须在 finally 中,确保无论业务成功/失败都释放锁)
            redisTemplateLockUtil.unlock(lockKey, lockValue);
        }
    }
}

6.2 方案 2:基于 Redisson 实现分布式锁

6.2.1 分布式锁工具类(Redisson 版,可选)

Redisson 已封装完善的锁 API,工具类可按需封装(简化重复调用):

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;

/**
 * Redisson 分布式锁工具类(生产环境推荐)
 */
@Component
public class RedissonLockUtil {

    private final RedissonClient redissonClient;

    public RedissonLockUtil(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    /**
     * 获取重入锁并尝试加锁
     * @param lockKey 锁的 Key
     * @param waitTime 最大等待时间(秒):获取不到锁时最多等多久
     * @param leaseTime 锁持有时间(秒):-1 表示启用看门狗自动续期
     * @return RLock 锁对象(需手动解锁)
     * @throws InterruptedException 线程中断异常
     */
    public RLock tryLock(String lockKey, long waitTime, long leaseTime) throws InterruptedException {
        RLock lock = redissonClient.getLock(lockKey);
        // 尝试加锁:waitTime 内获取不到则返回 null
        boolean isLocked = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
        return isLocked ? lock : null;
    }

    /**
     * 解锁:验证当前线程是否持有锁,避免误删
     * @param lock 锁对象
     */
    public void unlock(RLock lock) {
        if (lock != null && lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

6.2.2 业务示例:库存扣减(Redisson 锁)

import org.redisson.api.RLock;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 库存扣减业务示例(基于 Redisson 分布式锁)
 */
@RestController
@RequestMapping("/business/stock")
public class StockDeductWithRedissonLockController {

    private final RedissonLockUtil redissonLockUtil;
    private final RedissonClient redissonClient;

    public StockDeductWithRedissonLockController(RedissonLockUtil redissonLockUtil, 
                                                RedissonClient redissonClient) {
        this.redissonLockUtil = redissonLockUtil;
        this.redissonClient = redissonClient;
    }

    /**
     * 库存扣减接口(Redisson 锁,支持自动续期)
     * @param productId 商品 ID
     * @return 扣减结果
     */
    @GetMapping("/deduct/redisson/{productId}")
    public Result<String> deductStock(@PathVariable Long productId) {
        // 1. 定义锁 Key(按商品 ID 区分)
        String lockKey = "stock:lock:" + productId;
        RLock lock = null;

        try {
            // 2. 尝试加锁:waitTime=5 秒(最多等 5 秒),leaseTime=-1(启用看门狗自动续期)
            lock = redissonLockUtil.tryLock(lockKey, 5, -1);
            if (lock == null) {
                return Result.error("当前请求过多,请稍后重试");
            }

            // 3. 核心业务:库存扣减(Redisson 操作 Redis 数据)
            String stockKey = "stock:" + productId;
            // 初始化库存(首次调用时设置)
            redissonClient.getBucket(stockKey).setIfAbsent(100);
            // 读取当前库存
            Integer currentStock = (Integer) redissonClient.getBucket(stockKey).get();

            // 4. 库存校验
            if (currentStock == null || currentStock <= 0) {
                return Result.error("商品 ID:" + productId + ",库存不足");
            }

            // 5. 扣减库存
            redissonClient.getBucket(stockKey).set(currentStock - 1);
            return Result.success("商品 ID:" + productId + ",库存扣减成功,剩余库存:" + (currentStock - 1));

        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return Result.error("请求被中断,请重试");
        } finally {
            // 6. 解锁(工具类已验证线程持有权,避免误删)
            redissonLockUtil.unlock(lock);
        }
    }
}

七、总结:选型指南与生产避坑

7.1 客户端选型终极建议

业务场景 推荐客户端 核心理由
单体项目 / 基础缓存 Lettuce Spring Boot 默认集成,异步非阻塞,无需额外配置,性能优于 Jedis
小型同步操作项目 Jedis API 直观,调试简单,适合新手入门(需手动管理连接池)
微服务 / 分布式锁 / 秒杀 Redisson 官方推荐,封装看门狗续期、红锁等高级特性,避免手动处理分布式锁细节

7.2 生产环境避坑 5 大要点

  1. 连接池参数不合理(导致 Redis 连接耗尽)
  • 问题:max-active 过大(超过 Redis 服务端 maxclients 配置),或 min-idle 过小(频繁创建连接);
  • 解决方案:单机场景 max-active=32~64,集群场景 64~128,min-idle 设为 max-active/2,避免极端值。
  1. 未设置 Key 过期时间(导致内存泄漏)
  • 问题:缓存类 Key 无过期时间,永久占用 Redis 内存,最终导致内存溢出;
  • 解决方案:所有缓存 Key 必须通过 expire() 设置过期时间(如 1~24 小时),热点数据可配合缓存更新策略(如 Caffeine 本地缓存)。
  1. 分布式锁未处理 “锁过期”(业务未完成锁释放)
  • 问题:RedisTemplate 实现中,锁过期时间短于业务执行时间,导致其他线程提前获锁;
  • 解决方案:RedisTemplate 需自定义 “续命线程”,Redisson 直接用 leaseTime=-1 启用看门狗(默认 30s 续期)。
  1. Redis 集群未配置主从 + 哨兵(单点故障风险)
  • 问题:单节点 Redis 宕机后,整个缓存 / 分布式锁服务不可用;
  • 解决方案:生产环境部署 3 主 3 从集群 + 哨兵,确保主节点故障时自动切换从节点。
  1. 序列化遗漏特殊类型(如 LocalDateTime 反序列化失败)
  • 问题:Lettuce/Jedis 未注册 JavaTimeModule,或 Redisson 未自定义序列化器,导致 JDK8 时间类型反序列化报错;
  • 解决方案:Lettuce/Jedis 在 RedisConfig 中显式注册 JavaTimeModule,Redisson 按需配置 RedissonConfig。

参考文章
【1】SpringBoot教程(十四) | SpringBoot集成Redis(全网最全)
【2】Redis实现分布式锁方法详细
【3】Redis实现分布式锁
【4】陪你一起学redis(十一)——redis分布式锁

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐