最近帮一个做上门烹饪预约服务系统的朋友排查线上缓存问题,Redis 明明连得上,业务接口也正常,但有一次活动大促时突然一批用户订单状态错乱。我在 Redis Desktop Manager 里翻了一遍缓存,看到一排 \xAC\xED\x00\x05t... 开头的 key,当时就知道问题出在哪了:Spring Boot 整合 Redis,大多数人只关心能不能 set/get,没关心数据到底是怎么存进去的。
Spring Boot 整合 Redis 的教程满大街都是,但大部分都只写到“加依赖、改配置、注入 RedisTemplate”这一步。等你真的把项目推到集群环境,序列化乱码、缓存穿透、连接池空转、Stream 消息堆积、分布式锁失效,随便一个坑都能让你加班到深夜。我今天就把这套整合流程里真正决定成败的环节完整串一遍,从服务端安装、依赖版本、序列化配置、数据类型使用,到 Stream 队列拉取和分布式锁选型,每一步都会给出实际踩坑之后沉淀下来的做法。
1. 版本选择与Redis服务端准备:先让连接能通
1.1 依赖引入和版本匹配:照着老教程写代码容易翻车
Spring Boot 整合 Redis 的时候,常规第一步是引入 spring-boot-starter-data-redis。这个起步依赖会把 Spring Data Redis 和 Lettuce 连接池一起带进来,非常简单:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
但如果你在一个老项目里手动指定过版本,或者引入了一堆第三方组件,版本冲突就会变成第一个坑。最典型的一个组合是 springfox 3.0.0 + Spring Boot 2.6+,很多项目集成 Redis 的同时还要接 Swagger 文档,结果项目启动时出现路径匹配异常,或者报一些和 Redis 一点关系都没有的类找不到错误。
Spring Boot 2.6 之后,Spring MVC 的路径匹配策略从 AntPathMatcher 换成了 PathPatternParser,而 springfox 3.0.0 还停留在老的方式上,所以启动时容易 NPE。这类问题排查起来非常迷惑,因为它表面上和 Redis 无关,实际上是你把所有中间件堆在一起时才会暴露。
我的建议是:
- 新项目直接用 Spring Boot 2.7.x 或 3.x,API 文档用
springdoc-openapi,彻底绕开 springfox 兼容性问题; - 老项目如果必须用 springfox,就把 Spring Boot 锁在 2.5.x,别为了升级而升级;
- 一旦引入 Redis,先把
spring-boot-starter-data-redis的版本和 Spring Boot 父 POM 对齐,不要单独指定版本,否则容易出现低层 API 不兼容。
1.2 Windows 本地开发环境:下载 Redis 后这样启动最省心
做本地开发时,很多人会问 Redis 到底怎么下。如果你在 Windows 上工作,最简单的方式是直接下载 Redis 的 Windows 压缩包,解压后会有 redis-server.exe 和 redis-cli.exe,还有一份默认配置文件。
你只需要做三件事:
bash复制# 先启动服务端,默认端口 6379
redis-server.exe
# 再开一个命令行窗口,测试连通性
redis-cli ping
如果返回 PONG,说明服务端正常。要注意的是,官方 Redis 其实不提供 Windows 版本,Windows 下的包基本是社区维护的移植版。如果公司内部允许用 Docker,我更推荐走 Docker 方案,少踩很多跨平台兼容性的坑。
1.3 用 Docker 搭建 Redis 主从:本地模拟生产环境
从热搜词里能看到“docker 安装 redis 主从”是被搜爆的话题。原因很简单,生产环境你几乎不可能用单机 Redis,一旦主节点宕机,从节点顶上,这就是最基础的高可用架构。用 Docker 搭主从非常快:
bash复制# 创建主节点
docker run -d --name redis-master -p 6379:6379 redis:7.2 redis-server --appendonly yes
# 创建从节点,从节点不需要暴露到宿主机,容器网络互通即可
docker run -d --name redis-slave --link redis-master redis:7.2 redis-server --replicaof redis-master 6379
进去验证一下主从是否同步:
bash复制docker exec -it redis-slave redis-cli info replication
看到 role:slave 和 master_link_status:up,就说明从节点成功连上主节点了。这里有一个关键点:从节点默认是只读的,不要在业务代码里往从节点写数据。后面在 Spring Boot 侧要配置读写分离,或者干脆用 Redis Sentinel / Redis Cluster 来管理节点切换,而不是傻傻地手动改 IP。
1.4 Spring Boot 连接配置:环境不同配置写法有差异
Spring Boot 2.x 里 Redis 的配置项前缀是 spring.redis,Spring Boot 3.x 改成了 spring.data.redis。如果你跟着网上的教程抄,很容易因为版本差异导致配置没生效。
以 Spring Boot 2.7 为例:
yaml复制spring:
redis:
host: 127.0.0.1
port: 6379
password: yourpassword
database: 0
timeout: 3000ms
如果是 Spring Boot 3.x:
yaml复制spring:
data:
redis:
host: 127.0.0.1
port: 6379
password: yourpassword
database: 0
timeout: 3000ms
改完配置之后,跑通一个最简单的验证接口,确认 RedisTemplate 能正常写入和读取。如果这一步能过,说明服务端、客户端、网络层面都没有问题,后面才进入真正的“高手区间”。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 序列化器配置:可视化工具里不再是乱码的关键
2.1 默认 JDK 序列化器到底可怕在哪
Spring Boot 的 RedisAutoConfiguration 默认会创建一个 RedisTemplate<Object, Object>,这个模板默认用的序列化器是 JdkSerializationRedisSerializer。Java 默认序列化会把对象序列化成二进制字节,再存到 Redis 里。你如果用 Redis Desktop Manager 或者 Another Redis Desktop Manager 连接上去,看到的缓存值就是一堆 \xAC\xED 开头的乱码。
乱码只是表象,真正的隐患在于:
- Java 默认序列化后的数据可读性极差,排查问题时你根本不知道某个 key 里存的是什么;
- 如果业务对象没有实现
Serializable,调用redisTemplate.set()时会直接抛异常; - 序列化后的体积比原始数据大了很多,浪费 Redis 内存;
- 跨语言调用时,其他语言压根没法解析 Java 序列化的数据。
所以在整合 Redis 后的第一个正式配置,就是替换序列化器。很多项目“能跑但不敢动”,就是因为一开始没换序列化器,后面缓存全变成历史包袱,动数据等于动雷区。
2.2 一套适合绝大多数项目的序列化配置
常规推荐方案是:key 用 StringRedisSerializer,value 用 GenericJackson2JsonRedisSerializer。这样 key 可读,value 是 JSON 格式,跨语言调用也方便。
java复制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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
StringRedisSerializer stringSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
这里有两个细节要特别注意:
GenericJackson2JsonRedisSerializer序列化时会在 JSON 里额外保存@class字段,记录对象的全限定类名,反序列化时才能准确还原类型;- 如果业务对象没有无参构造函数,或者内部有复杂泛型,反序列化时可能报 “Invalid type definition” 或者 “Unrecognized field” 错误。
问题通常集中在两个地方:一是你存的 DTO 没有无参构造,二是字段名不匹配。加一个无参构造函数、补齐 getter/setter 就能解决大多数情况。如果实在不想让 @class 字段占空间,也可以自定义 ObjectMapper 甚至直接用 Fastjson2 的序列化器,但那样需要更细心地处理类型丢失问题,不适合刚上手时直接用。
2.3 用可视化工具验证配置是否正确
配置完之后,用 Redis Desktop Manager 连接 Redis,然后再执行一次写入操作,你会看到 key 已经是可读的字符串,value 是 JSON 格式,再也不用对着一堆 \xAC\xED 猜数据了。
这里顺便给一个排查思路:
- 如果
key可读但value是乱码,说明 key 序列化器已经换成字符串,但 value 序列化器没有生效; - 如果
key本身也是乱码,说明整个 template 的序列化器没换成功,多半是你注入的是自动配置生成的RedisTemplate,而不是你自定义的 Bean; - 如果连接工具连不上 Redis,检查密码、端口,以及 Redis 配置文件里的
bind是不是默认绑定了127.0.0.1。容器环境下,很多人忘了改protected-mode或者把端口映射出来,导致 RedisDesktopManager 一直连接超时。
3. Redis数据类型的落地实操与Stream队列消息拉取
3.1 String、Hash、List、Set、ZSet:多数项目只需要这五张牌
Redis 数据类型是面试题常客,但在 Spring Boot 里很多人只会用 opsForValue().set() 和 get()。实际开发中,合理选择数据类型比多写几行代码重要得多。
| 数据类型 | 典型场景 | RedisTemplate 操作入口 | 注意事项 |
|---|---|---|---|
| String | 验证码、Token、单个订单状态 | opsForValue() |
设置过期时间用 set(key, value, timeout, TimeUnit) |
| Hash | 用户资料、商品详情等对象数据 | opsForHash() |
针对单个 field 操作,不能单独设置过期时间 |
| List | 简单消息队列、活动排队 | opsForList() |
leftPush / rightPop,数据会积压 |
| Set | 去重、点赞、标签、共同好友 | opsForSet() |
支持交并补运算,适合做关系计算 |
| ZSet | 排行榜、热门商品、延时任务排序 | opsForZSet() |
分数相同时再按字典序排,注意 score 精度 |
代码示范:
java复制// 验证码:带过期时间
stringRedisTemplate.opsForValue().set("verify:code:13800138000", "123456", 5, TimeUnit.MINUTES);
// 用户信息:Hash 按字段存取
HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
hash.put("user:1001", "name", "张三");
hash.put("user:1001", "level", "VIP");
// 排行榜:给订单量加分
redisTemplate.opsForZSet().incrementScore("ranking:chef", "chef:1001", 1);
我见过很多新手把 Hash 当成 String 来用,把一个 JSON 字符串塞进 Redis,然后遇到需要改其中一个字段时就必须整串取出、反序列化、改完再写回去。这个操作在并发情况下很容易丢失更新。正确做法是,如果对象有多个独立字段需要频繁更新,优先考虑 Hash。
3.2 Redis Stream:在 Spring Boot 中拉取队列消息的两种方式
很多人只知道 Redis 能做缓存和分布式锁,忘了 Redis 5.0 之后还有个专门做消息队列的 Stream 类型。网上经常有人搜“Spring Boot redis stream 如何拉取队列消息”,说明大家开始意识到,业务场景里不一定非要引入 Kafka 或者 RabbitMQ,轻量场景下 Redis Stream 足够了。
举一个真实场景:上门烹饪预约系统里,用户在 App 上完成支付之后,后台需要通知厨师、更新订单状态、给用户发消息。如果把通知逻辑直接写在支付接口里,一旦其中一个下游服务超时,整个支付流程就被拖慢。正确做法是:支付成功后把一条“订单已支付”的消息写入 Redis Stream,后台服务作为消费者拉取消息,再异步去通知其他系统。
Spring Boot 里常见有两种消费模式。
第一种是手动拉取,适合需要自己控制消费节奏的场景:
java复制// 先创建消费者组,如果 Stream 不存在则自动创建
stringRedisTemplate.opsForStream().createGroup("queue:order:paid", "group-order");
// 循环拉取消息
while (running) {
List<MapRecord<String, Object, Object>> records = stringRedisTemplate.opsForStream().read(
Consumer.from("group-order", "consumer-1"),
StreamReadOptions.empty().count(10).block(Duration.ofSeconds(3)),
StreamOffset.create("queue:order:paid", ReadOffset.lastConsumed())
);
for (MapRecord<String, Object, Object> record : records) {
try {
Map<Object, Object> value = record.getValue();
// 处理订单支付事件
handlePaidEvent(value);
// 手动确认,避免消息重复消费
stringRedisTemplate.opsForStream().acknowledge("queue:order:paid", "group-order", record.getId());
} catch (Exception e) {
// 记录失败日志,配合延时重试机制处理
}
}
}
第二种是使用 Spring Data Redis 提供的 StreamMessageListenerContainer,实现类似消息监听的效果:
java复制StreamMessageListenerContainer<String, MapRecord<String, Object, Object>> container =
StreamMessageListenerContainer.create(
redisConnectionFactory,
StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()
.pollTimeout(Duration.ofMillis(100))
.build()
);
container.receive(
Consumer.from("group-order", "consumer-1"),
StreamOffset.create("queue:order:paid", ReadOffset.lastConsumed()),
message -> {
Map<Object, Object> value = message.getValue();
handlePaidEvent(value);
stringRedisTemplate.opsForStream().acknowledge("queue:order:paid", "group-order", message.getId());
}
);
container.start();
两种方式核心逻辑一致,最大的区别在于手动拉取可以掌控阻塞时间和拉取频率,而监听容器把多线程、轮询这些细节封装好了,代码更简洁。
用 Stream 时要注意:
- 消费者组必须先创建,否则
Consumer.from会报错; ReadOffset.lastConsumed()适合从“当前消费者组已读取的位置”继续消费,如果消息被接收但是没确认,下次还能捞回来;- 消息处理必须幂等,同一个订单事件可能被重试多次,处理逻辑里要做唯一 ID 判断;
- Stream 的消息默认不会自动删除,消费完需要手动
acknowledge,否则 PEL 里的待确认消息越积越多,内存也会出现问题。
3.3 数据类型选型逻辑:别一上来就上 RabbitMQ
很多初学者有一个误区:项目里只要涉及异步任务,第一反应就是上 MQ。但如果只是几台服务间的内部通知、削峰场景,没有必要引入一套重量级消息中间件。Redis Stream 支持消费者组、ACK 确认、消息回溯,对内部系统级通知完全够用。
选型时我一般这样判断:
- 任务量小、允许丢失一点消息:直接 List 队列;
- 需要可靠消费、按组消费、消息回溯:Redis Stream;
- 需要跨部门跨系统对接、每天千万级消息量:再考虑独立 MQ。
Redis Stream 不是银弹,但对预约服务、订单通知这类业务来说,它已经能覆盖绝大多数场景,而且运维成本极低。Redis 本来就在你的技术栈里,凭什么还要再养一套 Kafka?
4. 连接池、主从与部署:换到生产环境前要改的配置
4.1 Lettuce 连接池默认关闭,这是很多人不知道的坑
Spring Boot 默认使用 Lettuce 作为 Redis 客户端。问题在于,Lettuce 连接池默认是关闭的,需要额外引入 commons-pool2 依赖并且手动开启。
某些低并发项目可能没发现这个问题,但只要并发量一上来,每次操作 Redis 都新建连接,连接成本会高得离谱,表现就是接口 RT 突然飙高、Redis 服务端连接数暴涨。
引入依赖:
xml复制<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
再开启连接池配置:
yaml复制spring:
redis:
host: 127.0.0.1
port: 6379
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 2
max-wait: 200ms
连接池参数到底开多大?没有固定值,要根据业务压测结果调整,但有一个大致原则:max-active 不用开得特别大,因为每个 Redis 连接都是轻量级的,真正影响吞吐的是网络带宽和 Redis 本身处理能力。建议从 16 开始,然后压测看响应时间曲线。
4.2 主从环境下的连接配置:别把从节点地址写死
如果我们在 Docker 里搭了主从环境,Spring Boot 侧要连接主节点,一般配置主节点地址就够了,因为从节点是备份,读写分离需要额外封装。但如果你用的是 Redis Sentinel 或者 Cluster,配置就不同了。
Redis Sentinel 模式:
yaml复制spring:
redis:
sentinel:
master: mymaster
nodes: 127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381
Redis Cluster 模式:
yaml复制spring:
redis:
cluster:
nodes: 127.0.0.1:6379,127.0.0.1:6380,127.0.0.1:6381
在没有哨兵和集群的情况下,如果你只有主从两个 Redis,想实现读写分离,需要自己定义多个 LettuceConnectionFactory 分别指向主和从。我不太建议在没有哨兵的情况下手工做读写分离,主节点挂了之后从节点不会自动提升,读从节点会出现读不到最新数据的问题。
4.3 打包部署:从 jar 到外部 Tomcat 的注意事项
热词里有一串“spring boot tomcat 部署”、“spring boot 项目开发环境命令行运行项目”、“spring boot 中间件部署”,这些都说明大家在实战部署时容易卡住。
开发环境下,直接在项目根目录运行:
bash复制mvn spring-boot:run
或者先打包再跑:
bash复制mvn clean package
java -jar target/demo-0.0.1-SNAPSHOT.jar
如果项目要部署到外部 Tomcat,则需要把打包方式改为 war,并继承 SpringBootServletInitializer:
java复制@SpringBootApplication
public class Application extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(Application.class);
}
}
同时修改 pom.xml:
xml复制<packaging>war</packaging>
然后在 pom.xml 里把内嵌 Tomcat 标记为 provided,防止和外部 Tomcat 冲突。
无论 jar 还是 war,Redis 地址都建议从环境变量里读取,而不是硬编码在配置文件里:
yaml复制spring:
redis:
host: ${REDIS_HOST:127.0.0.1}
port: ${REDIS_PORT:6379}
这样部署到测试、预发、生产环境时,不需要改代码,只需要在容器环境或 Tomcat 启动脚本里配置环境变量。我第一次布置外部 Tomcat 时就是忘了这点,改配置之后加了备注:“下次不要扔给运维一个手工改信息的包”。
5. Redis分布式锁:从setnx命令到Redisson组件的实战选型
5.1 setnx + expire 的经典陷阱
只要聊 Redis,绕不开分布式锁,这也是面试题中稳定出现的一道题。大多数面试者都知道用 SETNX,但如果你直接把 setnx 和 expire 写成两条命令,就会埋下大坑:如果 setnx 执行成功后进程崩溃,还没执行 expire,锁就永远不会过期,其他线程永远拿不到锁。
正确的写法是使用单条原子命令:
java复制Boolean locked = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, Duration.ofSeconds(30));
setIfAbsent 内部使用 SET key value NX EX 原子操作,把“加锁”和“设置过期时间”合成一步,从根上解决了死锁问题。
释放锁的时候也不能简单 delete,要判断 value 是不是自己的标记,防止把别人后来获取的锁误删:
java复制String token = stringRedisTemplate.opsForValue().get(lockKey);
if (requestId.equals(token)) {
stringRedisTemplate.delete(lockKey);
}
这一步能防住绝大多数“自写分布式锁”的并发问题,但如果业务方法执行时间超过锁的过期时间,锁已经自动释放,其他线程仍然可能拿到锁,于是两个线程同时进入临界区。
5.2 上 Redisson:把锁续期这种麻烦事丢给组件
自己实现锁续期非常繁琐,很容易写出一堆 bug。现实中我更推荐直接用 Redisson,它是 Redis 官方推荐的 Java 客户端,提供了封装好的 RLock。
引入依赖:
xml复制<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.24.3</version>
</dependency>
配置 Redisson 客户端:
java复制@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword("yourpassword")
.setConnectionPoolSize(16);
return Redisson.create(config);
}
}
业务中使用:
java复制RLock lock = redissonClient.getLock("order:pay:1001");
boolean locked = false;
try {
locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("系统繁忙,请稍后再试");
}
// 处理业务逻辑
} finally {
if (locked && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
Redisson 默认有个 Watch Dog 机制,会在锁快要过期时自动续期,只要客户端还持有锁,锁就不会在业务执行中途消失。这让“业务超时导致锁提前释放”的问题被透明化解决,比手写续期靠谱得多。
5.3 结合预约业务场景的选型复盘
我为什么会把锁粒度单独拿出来说?因为在实际项目里,很多人喜欢一把大锁锁全表。比如支付回调接口,锁 key 直接写成 order:lock,结果所有订单共用一个锁,并发能力直接被拉跨。
在婚庆服务预约平台、上门烹饪预约服务系统这类场景里,锁粒度一定要落到业务对象上,比如:
java复制RLock lock = redissonClient.getLock("booking:order:" + orderId);
不同订单之间互不干扰,同一个订单相关的关键操作才会竞争锁。锁的使用范围也要明确,不要在锁里执行 Redis 之外的重型 IO 操作,否则锁持有的时间太长,其他线程就会长时间阻塞。
至于“Redis 分布式锁在集群环境下是否绝对安全”,这个问题在面试里经常被追问。主从架构下,如果主节点已经加锁但还没同步到从节点时,主节点恰好宕机,从节点晋升为主节点之后锁就丢了,原锁对应的事务可能和新的锁节点上的事务并发执行。要彻底解决这个问题,需要引入 RedLock,但 RedLock 本身也有争议。我的经验是:单纯的项目业务场景下,用 Redisson 的单节点锁就够了;如果对数据一致性要求极高,不要只依赖分布式锁,而是用数据库乐观锁、消息幂等等手段做兜底。
6. 集成自检与常见场景排障:把坑提前排掉
6.1 集成完成后的一份自检清单
Spring Boot 整合 Redis 完成后,建议照着下面这张表检查一遍,尤其是第一次接手的模块:
| 检查项 | 检查方式 | 出现问题时的表现 |
|---|---|---|
| 服务端连通性 | redis-cli ping |
PONG 正常,超时说明网络或防火墙 |
| 密码认证 | 配置密码后连接时是否报 NOAUTH | 客户端连接失败 |
| 序列化配置 | Redis Desktop Manager 查看 key 和 value | 出现 \xAC\xED 或者不可读 JSON |
| 连接池配置 | 压测时观察 Redis 连接数 | 连接数过高或瞬时连接风暴 |
| TTL 设置 | keys * 查看带过期时间的 key |
key 无限增长 |
| Stream 消费者组 | XINFO GROUPS queue:order:paid |
消息堆积、lag 越来越大 |
| 锁释放 | 业务结束后 exists 锁key |
锁 key 残留,长期不释放 |
6.2 缓存三大劫:穿透、击穿、雪崩
这一节虽然是老生常谈,但每次帮别人排查线上问题,最后都能落到这三个问题上。
- 缓存穿透:请求一个不存在的 key,每次都会打到数据库。解决思路是缓存空值,或者用布隆过滤器先把明显不存在的 key 过滤掉。
- 缓存击穿:热点 key 失效的瞬间,大量请求同时打到数据库。解决思路是用分布式锁控制只有一个线程查库。
- 缓存雪崩:大量 key 在同一时间段失效,数据库瞬间被打爆。解决思路是给过期时间加随机值,不要让 key 成片地一起过期。
代码层面对应做法:
java复制// 设置过期时间时加随机值
int randomL = 300 + new Random().nextInt(180);
stringRedisTemplate.opsForValue().set(cacheKey, data, Duration.ofSeconds(randomL));
用锁做缓存重建时,可以配合前面写的 setIfAbsent 方式:
java复制Boolean locked = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(30));
if (Boolean.TRUE.equals(locked)) {
try {
Object dbData = queryFromDb();
stringRedisTemplate.opsForValue().set(cacheKey, dbData, Duration.ofSeconds(300));
} finally {
stringRedisTemplate.delete(lockKey);
}
}
6.3 排查时我习惯用的一条命令链
最后分享一个我自己的排查习惯。遇到 Redis 相关问题时,我不会直接看业务日志,而是先在命令行用 redis-cli 快速定位:
bash复制# 检查键是否存在、剩余过期时间
redis-cli keys "*order*"
redis-cli ttl booking:order:1001
# 检查是否出现慢查询
redis-cli slowlog get 10
# 检查内存使用
redis-cli info memory
# 查看当前连接数
redis-cli info clients
这些命令能帮你快速判断是数据没写进去、数据过期、慢查询拖累 Redis,还是连接数爆炸。很多问题不是 Spring Boot 代码的问题,而是 Redis 服务端运维侧的问题,先把这一层确认清楚,再去查代码会高效得多。
我自己的经验是,Spring Boot 整合 Redis 本身并不复杂,复杂的是你把它放进真实业务场景之后,如何选择合适的数据结构、如何处理序列化、怎么保证缓存与数据库的一致性、怎么让消息不会积压、怎么让分布式锁在集群环境下不失效。你把这些底层逻辑理清楚了,再来写代码,那就是水到渠成的事,根本不需要背那么多所谓的“最佳实践”。
