SpringBoot 教程(十四) SpringBoot之集成 Redis
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+),确保:
- Redis 服务正常运行,IP、端口(默认 6379)、密码正确;
- 服务器防火墙开放 6379 端口(云服务器需配置安全组);
- 客户端与 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 序列化,大部分场景无需额外配置,仅以下场景需自定义:
- 需支持 JDK8 时间类型(LocalDateTime/LocalDate);
- 需自定义 JSON 序列化规则(如字段命名策略);
- 跨语言场景(需统一序列化格式)。
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 大要点
- 连接池参数不合理(导致 Redis 连接耗尽)
- 问题:max-active 过大(超过 Redis 服务端 maxclients 配置),或 min-idle 过小(频繁创建连接);
- 解决方案:单机场景 max-active=32~64,集群场景 64~128,min-idle 设为 max-active/2,避免极端值。
- 未设置 Key 过期时间(导致内存泄漏)
- 问题:缓存类 Key 无过期时间,永久占用 Redis 内存,最终导致内存溢出;
- 解决方案:所有缓存 Key 必须通过 expire() 设置过期时间(如 1~24 小时),热点数据可配合缓存更新策略(如 Caffeine 本地缓存)。
- 分布式锁未处理 “锁过期”(业务未完成锁释放)
- 问题:RedisTemplate 实现中,锁过期时间短于业务执行时间,导致其他线程提前获锁;
- 解决方案:RedisTemplate 需自定义 “续命线程”,Redisson 直接用 leaseTime=-1 启用看门狗(默认 30s 续期)。
- Redis 集群未配置主从 + 哨兵(单点故障风险)
- 问题:单节点 Redis 宕机后,整个缓存 / 分布式锁服务不可用;
- 解决方案:生产环境部署 3 主 3 从集群 + 哨兵,确保主节点故障时自动切换从节点。
- 序列化遗漏特殊类型(如 LocalDateTime 反序列化失败)
- 问题:Lettuce/Jedis 未注册 JavaTimeModule,或 Redisson 未自定义序列化器,导致 JDK8 时间类型反序列化报错;
- 解决方案:Lettuce/Jedis 在 RedisConfig 中显式注册 JavaTimeModule,Redisson 按需配置 RedissonConfig。
参考文章
【1】SpringBoot教程(十四) | SpringBoot集成Redis(全网最全)
【2】Redis实现分布式锁方法详细
【3】Redis实现分布式锁
【4】陪你一起学redis(十一)——redis分布式锁
更多推荐
所有评论(0)