在Spring项目里配置Redis,表面看就是加一个依赖、写几行连接参数的事,但真到了上线前压测和线上排查阶段,我经常被同事拉去救火,十次里有七次都是因为序列化器没配好、连接池参数拍脑袋、或者缓存注解和TTL没对齐导致的。这篇文章就把我在实际项目里打磨过的一套Redis与Spring整合方案完整拆一遍,从环境准备到RedisTemplate定制,再到缓存、分布式锁、Stream消息队列的配置细节,尽量把“为什么这么配”也讲透。适合刚入门Spring Boot后端的同学,也适合正在排查线上缓存问题的老手,照着抄能少走不少弯路。
1. 项目开局:为什么Spring项目绕不开Redis
1.1 缓存、锁、队列,其实都被Redis包圆了
先说一个很直白的场景:某个用户中心的接口,一天被调用上千万次,每次都要查MySQL里的用户资料,数据库的连接池直接被打满。后来我们在接口前面加了一层Redis缓存,key用userId,value用用户对象的JSON,热点数据的查询耗时从几十毫秒降到了两三毫秒,数据库压力瞬间就下来了。这就是Spring项目里引入Redis最原始、也最常见的诉求:缓存。
但Redis能干的远不止缓存。同一个连接池,我们既可以用它做分布式锁,保证多实例部署下某个定时任务只有一个节点在执行;也可以用它做限流计数器、接口幂等性校验、排行榜、附近的人,甚至是基于List或Stream实现轻量级消息队列。换句话说,Redis在Spring生态里的定位不是一个“可选的缓存组件”,而是一个基础设施。这也是为什么几乎每个Spring Boot项目里都会有spring-boot-starter-data-redis这个依赖。
既然是基础设施,那“配置”就不是随便填个redis://localhost:6379那么简单了。连接参数怎么写、序列化器选哪个、连接池调多大、缓存Key要不要统一加前缀、分布式锁的过期时间怎么估——这些细节直接决定了你再线上扛不扛得住。很多项目初期功能开发得很顺利,一到高并发或集群部署就出各种奇怪问题,追根溯源都是配置阶段埋下的坑。
1.2 三种接入姿势,先搞清楚你处在哪一层
在Spring里用Redis,常见的接入方式有三层,很多人一开始分不清,配置起来就容易混。
第一层是直接用原生客户端,比如Jedis或Lettuce。这种方式最底层,你自己管理连接、自己写命令调用,代码侵入性强,适合非常定制化的场景,但开发效率低。
第二层是Spring Data Redis,也就是spring-boot-starter-data-redis提供的RedisTemplate和StringRedisTemplate。它帮你封装了连接池管理、命令序列化、异常处理等一堆工作,是绝大多数Spring Boot项目的标配。我们后面要讲的“配置”,大部分都是围绕这一层展开的。
第三层是Spring Cache,它把Redis作为CacheManager的底层存储,通过@Cacheable、@CacheEvict等注解,把缓存逻辑从业务代码里抽离出来。这种方式最简单,但灵活性也最差,适合标准的“查缓存→回源→写缓存”场景。
这三层不是互斥关系,一个项目里经常同时用到第二层和第三层。我的习惯是:用Spring Cache处理那些比较规整的热点查询,用RedisTemplate处理分布式锁、幂等校验、数据统计类的精细操作。配置上,两边都要覆盖到,接下来挨个拆。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:先把Redis服务端和Spring Boot项目跑起来
2.1 Redis服务端安装与基础验证
很多新手第一步就卡在“Redis怎么装”。我平时开发机上用的是Windows,生产环境是Linux容器,两边都装过,流程其实差不多。
Windows下最简单的办法是去Redis官方Windows移植版(或tporadowski维护的Windows Release)下载zip包,解压到一个固定目录,比如D:\redis,然后打开命令行切到该目录,执行redis-server.exe就能启动服务。默认端口是6379,启动后会看到一段ASCII logo和端口提示。另开一个终端,运行redis-cli.exe ping,返回PONG就说明服务端正常。
Linux下更简单,Ubuntu/Debian直接apt install redis-server,CentOS用yum install redis,安装完systemctl start redis。这里要注意:有些发行版默认配置里bind 127.0.0.1,只允许本机连接,如果你的Spring Boot应用部署在其他机器上,必须修改配置文件redis.conf里的bind和protected-mode,否则会连接被拒。
验证服务端的命令我常用这3条:
bash复制# 基础连通性测试
redis-cli ping
# 写入一个测试Key并读取
redis-cli set greet "hello spring"
redis-cli get greet
# 查看服务端信息
redis-cli info server
info server里重点关注redis_version字段。Redis 6.x和7.x在部分命令行为上略有差异,但Spring Data Redis的兼容性做得不错,只要不是特别老的5.x版本,基本都够用。还有一个容易忽略的问题:Redis默认有16个库(db0~db15),配置里如果不指定database,默认使用db0。多项目共用同一个Redis实例时,可以用不同的db做物理隔离,但更推荐用不同前缀和独立实例,db隔离只是权宜之计。
2.2 Spring Boot依赖引入与版本匹配细节
Spring Boot项目里引入Redis依赖非常直接,只需要一个starter:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
这个starter会带入Spring Data Redis、Lettuce客户端等核心组件,但不会自动包含连接池依赖。如果你需要配置连接池(高并发场景下强烈建议),还要额外引入:
xml复制<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
这一点经常被遗漏。不引这个依赖,连接池参数就是摆设,Lettuce每次都是新连接,性能差很多。
版本匹配上,我踩过一个比较典型的坑:Spring Boot 2.7项目里用了Spring Data Redis 2.7版本,嵌套的Lettuce版本较老,对Redis 6.0的ACL权限支持不完整,导致用了带用户名密码的账号时认证失败。后来直接升到Spring Boot 3.2,底层Lettuce版本到了6.3,问题才彻底解决。所以如果你用的是Spring Boot 3.x,Java版本一定要是17+,这是硬性要求。如果是老项目卡在Java 8上,就用Spring Boot 2.7.x,不要强行往上试。
引入依赖之后,Spring Boot的自动配置会识别到classpath里的RedisTemplate和LettuceConnectionFactory,自动创建一个RedisTemplate<Object, Object>的Bean。很多新人直接@Autowired这个默认Bean,用不了多久就发现存进去的Key变成了一堆乱码——这就是下一节要详细说的序列化器问题。
2.3 application.yml里的连接参数模板
依赖加好了,接下来就是在配置文件里写连接信息。我一般会把这几个参数都列全,方便调试:
yaml复制spring:
data:
redis:
host: localhost
port: 6379
password: "${REDIS_PASSWORD:}" # 没有密码就留空
database: 0
timeout: 3s
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 2
max-wait: 3s
注意这里有个版本区别:Spring Boot 2.x里配置前缀是spring.redis.*,Spring Boot 3.x改成了spring.data.redis.*。我见过好几个从2.x升级到3.x的项目,只升了依赖忘了改配置前缀,启动时就报“配置项不存在”,应用能起来但连接的是默认的localhost,很隐蔽。
timeout: 3s是连接超时时间,设太短容易在Redis负载高时误报超时,设太长又会让调用端卡死,3秒是我目前在大多数项目里用的平衡点。password不要硬编码在yml里,用环境变量${REDIS_PASSWORD:}注入,既安全又方便不同环境切换。
3. 核心配置:RedisTemplate与序列化器实战
3.1 默认RedisTemplate的坑:JDK序列化乱码
先复现一下最经典的问题。你按默认配置启动项目,然后用redisTemplate.opsForValue().set("user:1", userInfo)存一个对象,再用Redis Desktop Manager或者redis-cli查看,会发现Key不是user:1,而是\xAC\xED\x00\x05t\x00\x07user:1这样的怪东西,Value更是完全不可读。
原因很简单:Spring Data Redis默认的键值序列化器是JdkSerializationRedisSerializer,它用Java默认的序列化机制,把对象转成了一长串字节数组。这个方案有三个致命缺陷:
- 可读性差:你没办法在Redis命令行里直接检查数据到底存的对不对。
- 跨语言不友好:如果其他非Java服务也要读写同一份Redis数据,Java序列化格式完全无法解析。
- 序列化后体积大:对象里每一个字段、类名、包名全都会写进去,占用内存可能是JSON的3到5倍。
所以,自定义RedisTemplate,核心就是把Key和Value的序列化器换掉。我用了很久的组合是:
- Key用
StringRedisSerializer,保证Key是普通的UTF-8字符串,可读、可控。 - Value用
GenericJackson2JsonRedisSerializer,把对象转成JSON。这个序列化器比Jackson2JsonRedisSerializer多存了一个@class字段,反序列化时能自动还原成原来的类型。 - Hash的Key和Value同样分别用String和JSON序列化器。
有些团队为了让Value更干净,会用Jackson2JsonRedisSerializer<Object>,存出来的JSON里没有@class字段,但反序列化时类型信息会丢失,需要手动指定目标类型。这里没有绝对的对错,关键看你更在意可读性还是反序列化的便利性。我偏好Generic版本,省心。
3.2 手写一个生产可用的RedisConfig配置类
下面这个配置类是我项目里一直在用的,结构很清晰,直接复制过去就能用:
java复制@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// Key使用字符串序列化器
StringRedisSerializer keySerializer = new StringRedisSerializer();
template.setKeySerializer(keySerializer);
template.setHashKeySerializer(keySerializer);
// Value使用JSON序列化器
GenericJackson2JsonRedisSerializer valueSerializer =
new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(valueSerializer);
template.setHashValueSerializer(valueSerializer);
template.afterPropertiesSet();
return template;
}
@Bean
public StringRedisTemplate stringRedisTemplate(
RedisConnectionFactory connectionFactory) {
return new StringRedisTemplate(connectionFactory);
}
}
这里有三个细节我特别强调一下:
第一,template.setHashKeySerializer和setHashValueSerializer经常被人漏掉。如果你用opsForHash()存了一份hash结构数据,Key和Value序列化器没设置,会各自走默认的Jdk序列化,照样乱码。
第二,afterPropertiesSet()必须调用。它会根据你设置好的Serializers去初始化template内部的hashValue、value等属性,如果不调用,部分配置可能没生效。
第三,为什么还要额外声明一个StringRedisTemplate?因为很多场景(比如分布式锁、简单计数、接口幂等校验)里Key和Value都是纯字符串,StringRedisTemplate天然就是字符串序列化,用起来更轻量,不需要每次把数字转成字符串再塞进RedisTemplate。两个Template可以共存,按场景选即可。
3.3 Lettuce连接池与参数调优
Lettuce是Spring Boot默认的Redis客户端,它基于Netty,连接是异步非阻塞的,本身是线程安全的。所以这里有个常见误区:Lettuce不一定要配连接池,单连接就能支持高并发。那为什么我们还要配池子?
因为虽然命令处理是异步的,但运维层面仍然需要控制最大连接数,防止某个业务把Redis连接耗尽,影响其他业务。另外,在极端的批量操作场景下,多个物理连接可以分摊TCP连接的内核开销和Netty的EventLoop压力。所以连接池在多数生产项目里是“保险丝”而不是“油门”。
连接池参数怎么定?我给出一个参考模板和调整思路:
yaml复制spring:
data:
redis:
lettuce:
pool:
max-active: 32 # 连接池最大连接数
max-idle: 16 # 最大空闲连接数
min-idle: 8 # 最小空闲连接数,建议高于QPS低谷
max-wait: 3000ms # 获取连接最长等待时间
time-between-eviction-runs: 30s # 空闲连接检测周期
max-active怎么估?一个简单的经验公式:预估QPS x 单次操作平均耗时(秒) / 连接的并发能力。比如某个服务QPS是5000,单次Redis操作平均1ms,理想情况下一个连接就能承载约1000 QPS(因为是异步,所以理论上很能抗),但为了出缓冲,我会把max-active设为预期的物理连接峰值,通常32到64就够用了。翻车案例里常见的是把max-active设成了200甚至更高,结果Redis服务端连接数暴涨,内存和CPU双双报警。
另外,max-wait不要设成-1。-1表示无限等待,一旦连接池满了,业务线程会全部阻塞在获取连接上,最终把整个应用拖死。设成3秒或5秒,超时后直接抛异常,至少能保住业务主流程。
4. 进阶配置:从缓存注解到分布式锁、Stream队列
4.1 Spring Cache注解方式集成Redis
如果只是想把Redis当缓存用,Spring Cache注解方案是最省代码的。首先需要一个CacheManager的配置:
java复制@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.prefixCacheNameWith("cache:")
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
}
关键点:.entryTtl(Duration.ofMinutes(30))是全局默认过期时间,如果你希望不同业务有不同TTL,可以用RedisCacheManagerBuilderCustomizer自定义多个CacheName和对应的TTL。prefixCacheNameWith("cache:")会在所有缓存Key上统一加前缀,这样同一个Redis实例里,缓存数据和业务数据、临时数据就不会互相污染,扫描和排查都方便。
disableCachingNullValues()这里要谨慎。默认情况下Spring Cache允许缓存null值,用于解决缓存穿透问题。如果你把它禁用了,那些“查不到但频繁查询”的key就会每次都穿透到数据库。我的建议是保留缓存null值的能力,但通过额外设置短TTL来控制,比如给空结果单独设置2分钟过期。这个粒度需要在业务代码里实现,Spring Cache的注解方式做不到那么细,只能整体开关。
还有一个坑:缓存序列化器要和你的RedisTemplate一致。很多项目里自己手动操作Redis时用的是JSON序列化,但Spring Cache的CacheManager没有配置,默认还是Jdk序列化,结果同一个key在不同位置读出来的数据结构完全不一样,非常迷惑。上面的配置里显式设置了GenericJackson2JsonRedisSerializer,就是为了和RedisTemplate对齐。
4.2 手写分布式锁时的Redis配置要点
分布式锁是Redis在Spring分布式架构里的另一个高频用途。用Spring Cloud或普通多节点部署时,需要一个全局互斥机制。虽然Redis官方推荐的Redisson框架已经很成熟,但有些人出于依赖最小化、框架偏好等原因,还是会直接用StringRedisTemplate手写锁。
这套实现里,配置层面的要点是:必须使用StringRedisTemplate,不能乱搞序列化器。原因是锁的缓存命令(SET key value NX EX)要求key和value都是字符串,用自定义的Object RedisTemplate容易把value序列化成一堆JSON字节。
标准实现长这样:
java复制public boolean tryLock(String lockKey, String requestId, long expireSeconds) {
// SET key value NX EX —— 原子地设置key,只在key不存在时生效
return stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, Duration.ofSeconds(expireSeconds));
}
public boolean releaseLock(String lockKey, String requestId) {
// 用Lua脚本确保“只有持有者能删除锁”,避免误删别人的锁
String luaScript =
"if redis.call('get', KEYS[1]) == ARGV[1] " +
"then return redis.call('del', KEYS[1]) " +
"else return 0 end";
return Boolean.TRUE.equals(
stringRedisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
Collections.singletonList(lockKey),
requestId));
}
配置层面的教训有几个:
setIfAbsent对应的是SET NX,比先get再set两个命令的方式安全,两个命令之间存在并发窗口。- 锁的过期时间不要拍脑袋填固定值。如果业务逻辑可能执行很久,过期时间设短了锁提前失效,其他线程趁虚而入;设长了锁万一没释放,要等很久。我的做法是:过期时间设为业务预估耗时的5倍,同时在finally里释放锁,兜底保护。
- 释放锁时必须先判断
get(lockKey)是否等于自己的requestId,再del。不判断的话,自己的锁过期后,其他线程已经拿到锁,你用del会把别人的锁删掉,造成严重的互斥失败。
4.3 主从、哨兵与集群模式的配置差异
单机Redis搞定了基础配置,但生产环境单点风险太高。Spring Data Redis支持三种高可用部署模式的配置:主从、哨兵、Cluster。配置方式差异很大,我简单梳理一下。
主从模式在Spring里其实不需要特殊配置,你只需要把客户端指向主节点的地址,从节点的数据复制是Redis服务端自动完成的。但如果主节点宕机,从节点不会自动切换,应用会直接连不上Redis,所以主从模式适合读多写少但不要求高可用的场景。
哨兵模式是主从架构上加了Sentinel进程做自动故障转移。Spring配置里要指向Sentinel,而不是直接指Redis节点:
yaml复制spring:
data:
redis:
sentinel:
master: mymaster
nodes: node1:26379,node2:26379,node3:26379
Lettuce会自动从Sentinel探测当前主节点,并在故障转移后重新路由。这里要注意:哨兵模式一般还需要额外配password,如果Sentinel本身开启了认证,还要在连接信息里带上。
Cluster模式配置更简单:
yaml复制spring:
data:
redis:
cluster:
nodes: node1:6379,node2:6379,node3:6379
Cluster模式下Lettuce会自动发现集群拓扑并处理slot路由和重定向,不需要你手动指定所有节点。但有一个关键限制:Cluster模式不支持多db,spring.data.redis.database必须保持默认的0,否则启动时会报错或者数据写入异常。这个坑我见过不止一次,有人把单机配置迁到Cluster环境忘了删database参数,结果数据分布非常诡异。
4.4 Spring Boot Redis Stream的拉取配置
Redis Stream是Redis 5.0引入的消息队列数据结构,在Spring Boot里用起来很方便。前段时间有个同事问“Spring Boot Redis Stream如何拉取队列消息”,我给他看了两种常见姿势。
姿势一:注解监听@StreamListener
java复制@Component
public class StreamConsumer {
@StreamListener(target = "order-stream", condition = "#message != null")
public void onMessage(org.springframework.data.redis.connection.stream.MapRecord<String, String, String> record) {
String streamKey = record.getStream();
Map<String, String> body = record.getValue();
// 处理业务逻辑
System.out.println("收到订单消息: " + body);
}
}
同时需要启用@EnableRedisRepositories。这种方案适合轻量级场景,配置简单,内部会自动创建消息读取线程。但它的缺点是消费组管理、确认机制都不够透明,出了问题不好定位。
姿势二:手动拉取消息并使用StreamOperations
如果你需要精确控制消费组、ack、pending等行为,建议直接用StreamOperations。先手动创建消费组:
java复制stringRedisTemplate.opsForStream().createGroup("order-stream", "order-group");
然后用一个定时任务或循环去拉取新消息:
java复制// 每次拉取阻塞最多2秒
StreamReadOptions options = StreamReadOptions.empty().count(10).block(Duration.ofSeconds(2));
Consumer consumer = Consumer.from("order-group", "consumer-1");
List<MapRecord<String, Object, Object>> messages = stringRedisTemplate.opsForStream()
.read(consumer, StreamReadOptions.empty().count(10).block(Duration.ofSeconds(2)),
StreamOffset.create("order-stream", ReadOffset.lastConsumed()));
if (messages == null || messages.isEmpty()) {
return;
}
for (MapRecord<String, Object, Object> msg : messages) {
// 业务处理
handleMessage(msg);
// 处理成功后确认,防止消息丢失
stringRedisTemplate.opsForStream().acknowledge("order-stream", "order-group", msg.getId());
}
这里的配置要点是:ReadOffset.lastConsumed()表示只读取当前消费组中尚未被消费的消息。消息处理完成后,必须调用acknowledge确认,否则消息会一直停留在pending队列里,重试机制会把同一条消息反复发给你。如果业务处理失败,则不要ack,让消息留在pending里,方便后续排查和重放。
5. 问题排查与踩坑速查实录
5.1 连接被拒 vs 连接超时,先查这5项
不管配置写得多漂亮,最常遇到的第一类问题就是连不上Redis。错误信息五花八门,但底层原因基本都在下面几类里:
- 服务端没启动或端口不对:
redis-cli ping直接验证,排除法最快的办法。 - 防火墙/Selinux拦截:Linux服务器上检查
firewalld、iptables、云安全组是否放行6379端口。很多人本地能连、服务器上连不上,基本就是这问题。 - bind地址限制:Redis默认只监听127.0.0.1,需要修改
redis.conf里的bind 0.0.0.0或指定内网IP,同时注意protected-mode要设为no,除非你确认只有可信网络能访问。 - 密码错误:Spring配置的
password和Redis实例requirepass不一致。连接超时和认证失败是两回事,认证失败会抛ERR Client sent AUTH, but no password is set。 - Spring Boot版本和配置前缀不匹配:2.x用
spring.redis,3.x用spring.data.redis,写错了应用不会直接报错,但连的永远是本机默认地址。
排查顺序建议是:先手工用redis-cli验证服务端,再检查应用日志里的堆栈信息,最后回头看配置。不要一上来就改代码,八成是环境或者参数的问题。
5.2 缓存数据里出现\xAC\xED乱码
这个很像“主题重现”。用redis-cli查缓存数据时,如果看到\xAC\xED\x00\x05开头,基本就是Jdk序列化器的产物。原因已经分析过了:RedisTemplate默认的序列化器是Jdk,而你又没有重新配置。
解决办法就是本文3.2节里那套自定义配置。这里还想提醒一个细节:如果你已经把乱码数据写进去了,只改配置不会自动清理旧的key。你需要手动删除那些乱码key,或者设置过期时间等它们自然过期。我的习惯是上线前清空一次Redis缓存,或者用redis-cli --scan --pattern '*一堆乱码前缀*'批量删除,避免脏数据影响业务判断。
还需要注意:修改序列化器之后,同一个key在Redis里的组织形式会变,如果旧数据和新数据混在一起,用redisTemplate.opsForValue().get()时可能因为反序列化失败而抛异常。所以,序列化器这类全局配置,尽量在项目早期就定好,后期改动代价高。
5.3 高并发下连接池耗尽与线程阻塞
高并发压测时,如果日志里频繁出现“RedisCommandTimeoutException”或者“Cannot get a connection from pool”,别急着加连接数,先想想是不是连接池参数不合理。
常见的诱因有几种:
- max-active设太小:请求一多,连接不够用,大量线程在
max-wait期间阻塞等待,超过等待时间就异常。 - max-wait设成-1:线程无限阻塞,最终应用线程池打满,整个服务不可用。记住,宁可超时失败也不要无限等。
- 业务代码里没有释放连接:虽然Lettuce本身会归还连接,但如果你用了原生
Jedis或者手动创建了连接对象,用完不close,连接泄漏是纯纯的运营事故。 - Redis本身变慢:慢查询、大Key、内存满了触发淘汰策略,都会导致单次操作耗时上升。即使连接池参数合理,QPS一高,所有请求都积压在Redis上。
排查时先用redis-cli --latency观察实时延迟,再用redis-cli --slowlog get 20查看慢命令。配置层面,主要看max-active是否和服务的并发量匹配,以及max-wait是否设置了合理的超时。一个已经被我验证过多次的务实做法:监控Redis连接数,如果长期保持在max-active的80%以上,优先排查慢查询和业务逻辑,而不是继续加连接数。
5.4 新手高频踩坑速查表
最后整理一份我在指导刚入门同事时常用的速查表,覆盖配置阶段最容易出问题的点:
| 症状 | 根因 | 解决办法 |
|---|---|---|
| 缓存key变成\xAC\xED | RedisTemplate没配序列化器 | 见3.2节自定义配置 |
| Spring Boot 3.x连不上Redis | 配置前缀用了旧版spring.redis |
改成spring.data.redis |
| 连接池参数不生效 | 少了commons-pool2依赖 | 引入org.apache.commons:commons-pool2 |
| 多实例部署时缓存数据错乱 | 缓存key没有业务前缀 | CacheManager里配置prefixCacheNameWith |
| 分布式锁被别人误删 | 释放锁前没校验请求唯一ID | 用Lua脚本校验再del |
| 消息消费后不断重复推送 | 消费成功后没acknowledge |
手动调用ack确认 |
| Cluster模式启动报错 | 还保留了database配置 | 设置database: 0 |
| 应用启动慢,初始化Redis连接超时 | Lettuce懒加载,连接建立缓慢 | 用配置spring.data.redis.timeout加大启动期超时,或手动预热 |
| 缓存了null但TTL很久 | 缓存穿透 | 设置更短TTL,不要完全禁止null缓存 |
这个表我建议你收藏一下,或者直接贴到项目里的confluence文档里。很多问题都是重复出现的,有了速查表,新同事也能快速定位,不用每次都拉着一群人去排查。
我在实际项目里还有一个体会:Redis的Spring配置,本质上是在“好用”和“可控”之间找平衡。全用Spring Cache固然快,但灵活性和可观测性会变差;全用RedisTemplate手写到底,代码冗余又会直线上升。我的建议是,新项目从Spring Cache + StringRedisTemplate组合起步,遇到复杂场景再逐步加深定制。踩过几次坑之后,你自然会形成一套适合自己团队的配置规范,那比复制任何一份模板都更值钱。
