Spring Boot 和 Redis 这对组合,基本是 Java 后端开发绕不开的标配。缓存、分布式锁、排行榜、消息队列,Redis 几乎成了业务系统里最趁手的“瑞士军刀”。这篇文章我打算把整合步骤完完整整捋一遍,从 Redis 本地安装开始,到 Spring Boot 里配置 RedisTemplate、缓存注解、分布式锁、Redis Stream,再到常见的连接工具选型和问题排查,一条线走到底。无论你是刚接触 Redis 的新人,还是准备在项目里引入 Redis 但被各种“序列化报错”折磨过的开发,这篇文章应该都能给你省下不少时间。
我最早接触 Redis 是在一个电商项目里,当时从 Windows 上装 Redis 就折腾了一下午,后来又因为没配置序列化器导致 key 全部变成二进制乱码,真正把 RedisTemplate 用顺手之后才发现,绝大多数问题都不是 Redis 本身的问题,而是整合细节没做到位。这篇就按我的实际操作顺序来写,尽量把每个“为什么”也讲清楚,这样你照做之后还能举一反三。
1. 整合前的准备工作:先让 Redis 跑起来
1.1 本地安装:Windows 跟 Linux 的两条路
很多人一上来就卡在第一步:Redis 官方其实不提供 Windows 安装包。搜索热词里“redis下载”“windows安装redis”“redis安装教程”出现频率很高,说明这一关确实劝退不少人。你如果在生产环境用 Windows 服务器,这种场景本身少见,本地开发的话有两条比较省心的路。
第一条路是 Windows 上装 WSL2(Windows Subsystem for Linux),在 Ubuntu 里直接 sudo apt install redis-server,装完就一个 redis-server 命令的事。这种方式最接近生产环境,我推荐优先用。第二条路是直接用 Docker Desktop,跑一个 Redis 容器,命令特别简单:
bash复制docker run -d -p 6379:6379 --name redis redis:7.2 redis-server --appendonly yes
这里我把持久化也开上了,--appendonly yes 对应的就是 AOF 持久化,开发环境建议开着,省得 Redis 一重启数据全没了,排查问题的时候还得琢磨“我明明 set 了,怎么又不见了”。
Linux 环境就更直接了,CentOS 或 Ubuntu 上用包管理器安装,或者去 Redis 官网下载源码包编译安装。编译安装的话注意把 make test 跑一遍,能排除一部分环境兼容问题。另外官方最近已经发布了 8.x 版本,但生产环境通常还在用 7.x,这篇内容用 7.x 没问题,整合步骤在 8.x 上依然适用。
装完之后别急着走,先验证一下服务是否正常:
bash复制redis-cli ping
如果返回 PONG,说明服务是通的。这一步虽然简单,但能帮你把“Redis 本身的问题”和“Spring Boot 整合的问题”在第一时间切分开,排查效率会高很多。
1.2 引入依赖和项目配置
服务启动之后,回到 Spring Boot 工程里。你只需要引入一个起步依赖:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Spring Boot 2.x 和 3.x 默认使用 Lettuce 作为 Redis 客户端,不需要额外引入 Jedis。如果你要使用连接池,还需要引入 commons-pool2:
xml复制<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
这一步很容易漏,没加 commons-pool2 但又在配置里写了连接池参数,启动时会因为找不到连接池类而报错。这个坑我踩过一次,后来就习惯了“引入 starter 的同时顺手把 pool 依赖也带上”。
接下来是配置文件。这里要特别提醒一个版本差异:Spring Boot 2.x 的配置前缀是 spring.redis,到了 Spring Boot 3.x 就改成了 spring.data.redis。网上很多老教程用的还是 spring.redis,你如果照抄到 3.x 项目里会发现启动时配置不生效,连接还是默认的 localhost:6379。正确的 3.x 写法是这样的:
yaml复制spring:
data:
redis:
host: 127.0.0.1
port: 6379
password: 你的密码(没有就删掉这行)
database: 0
timeout: 3s
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 0
max-wait: 3s
max-active 是连接池最大连接数,max-idle 是最大空闲连接数,max-wait 是拿连接的最大等待时间。这些参数不是越大越好,连接数开太多反而会占用 Redis 服务端的文件描述符,一般业务系统 max-active 在 16~32 之间就够用了,你需要压测时再调。
1.3 序列化策略:这一步千万别省
很多新手第一次用 RedisTemplate 写入数据,然后用可视化工具一看,发现 key 变成了一长串类似 \xac\xed\x00\x05t\x00... 的乱码,value 也没法直接阅读。原因是 Spring Data Redis 默认使用 JdkSerializationRedisSerializer,把对象序列化成二进制的 JDK 格式。这对程序来说没问题,但对排查问题、跨语言调用、可视化操作都是灾难。
所以整合 Redis 的第一件事,就是自定义 RedisTemplate 的序列化器。我的经验是:key 用 StringRedisSerializer,保证 key 可读;value 用 GenericJackson2JsonRedisSerializer,把对象序列化成 JSON 字符串。这样写入 Redis 的数据非常直观,用 redis-cli 也能直接查看到内容。
序列化器也会影响缓存注解的存储格式,后面讲缓存的时候还会再提到,这个决策是贯穿整个项目的,所以一开始就定好,后面能省掉一大堆反序列化的坑。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心配置:把 RedisTemplate 调成顺手的样子
2.1 手动创建 RedisTemplate 并配置序列化
Spring Boot 的自动配置会往容器里放一个 RedisTemplate,但它用的是默认序列化器,不满足我们的需求。所以一般需要自己在配置类里重写一个:
java复制@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
StringRedisSerializer stringSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
// key 全部使用字符串序列化
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
// value 使用 JSON 序列化
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
这里我用的是 GenericJackson2JsonRedisSerializer,它会在序列化时把对象的类全名写入 JSON 里(比如 @class 字段),反序列化时就能自动还原成原类型。代价是序列化结果里多了一些类信息,稍微占点空间,但换来的通用性和便利性非常值。
一个常见问题是 LocalDateTime 等 Java 8 时间类型的反序列化。GenericJackson2JsonRedisSerializer 内部如果没有注册 JavaTimeModule,会出现类似 InvalidDefinitionException: Java 8 date/time type not supported 的报错。解决方案是引入 jackson-datatype-jsr310 依赖,并在 ObjectMapper 里注册 JavaTimeModule,或者直接封装一个带 ObjectMapper 的序列化器。这部分比较细节,建议你直接写一个自定义的 RedisConfig,把 ObjectMapper 统一管理好。
2.2 RedisTemplate 和 StringRedisTemplate 怎么选
Spring Boot 里还有预设好的 StringRedisTemplate,它相当于 key 和 value 都是 StringRedisSerializer 的 RedisTemplate。什么时候用 RedisTemplate,什么时候用 StringRedisTemplate,一开始容易搞混。
我的判断标准很简单:如果你的 value 也全是字符串,直接用 StringRedisTemplate,比如存验证码、存 JSON 字符串本身;如果你要存对象、要利用 RedisTemplate 的对象序列化能力,用自定的 RedisTemplate。但要注意,两者操作同一个 key 时,序列化器不同,可能导致读不到数据。比如 StringRedisTemplate 写入的 key 是纯字符串,RedisTemplate(配置不当的情况下)写入的可能是带前缀的二进制 key,两个 template 操作同一个 key 就会出现“明明 set 了却查不到”的诡异现象。所以项目里建议固定用一个 RedisTemplate,不去混用。
如果你要操作的确实是 String,但又想复用对象序列化的能力,也可以直接在 StringRedisTemplate 里手动设置 valueSerializer 为 JSON 序列化器,效果是一样的。
2.3 连接池与连接参数详解
Lettuce 基于 Netty,本身是一个多线程复用的连接模型。它跟 Jedis 的最大区别是:Jedis 实例不是线程安全的,需要从连接池里拿实例;而 Lettuce 实例是线程安全的,多个线程可以共享同一个连接,但它也支持连接池模式。
如果并发量比较高,建议开启连接池,避免所有请求共享同一条连接造成竞争。配置里的 min-idle 是连接池最小空闲连接数,我习惯设置为 1~2,这样即使流量低谷期也保持着一条预热连接,避免冷启动时创建连接带来的延迟。timeout 参数控制的是读写超时时间,在 Spring Boot 3.x 的配置语法里是 spring.data.redis.timeout,单位可以写成 3s 或 3000ms,建议设置短一点,避免 Redis 故障时业务线程被阻塞过久。
还有一个容易被忽略的参数是 spring.data.redis.lettuce.pool.max-wait,它控制从连接池获取连接的最大等待时间。如果连接池被占满,获取连接的线程会等待,超过这个时间就抛异常。调这个参数时要想清楚:宁可快速失败让请求感知到 Redis 不可用,也不要让所有请求都堵在等待连接上,把整个服务拖垮。生产环境这个值建议控制在 3~5 秒以内。
3. 业务落地:缓存注解、分布式锁和消息队列
3.1 缓存注解 @Cacheable 整合
Redis 最常见的用途就是缓存。Spring Boot 里不用手动写 get/set,直接用 @Cacheable、@CachePut、@CacheEvict 这几个注解就能接入缓存。
启用方式很简单,在主类或任意配置类上加 @EnableCaching 注解。然后定义一个 RedisCacheManager:
java复制@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.transactionAware()
.build();
}
entryTtl(Duration.ofMinutes(30)) 设置默认过期时间,这是很关键的一步。默认的 RedisCacheManager 不会给缓存设置过期时间,意味着数据会一直存在,如果业务逻辑没做好更新机制,时间一长就会出现脏数据。我建议不管什么场景,缓存一定要有过期时间,宁可过期后短暂重新加载,也别让脏数据一直留着。
transactionAware() 表示缓存操作参与 Spring 事务。比如在一个事务里先更新数据库,再删除缓存,如果事务回滚了,缓存删除也会跟着回滚,避免缓存和数据库的不一致。这个特性在写操作多、一致性要求高的场景里很有用。
注意 @Cacheable 默认不缓存 null 值,如果方法返回 null,会抛异常。要么在方法里对空值做处理,要么把 disableCachingNullValues() 改成 getAllowCachingNullValues()。但缓存 null 值有一定风险,它可能掩盖了“数据不存在”和“数据查询失败”的区别。更稳妥的方式是返回 Optional 包装类,或者用空对象模式。
3.2 Redis 分布式锁的正确打开方式
分布式锁是 Redis 在分布式系统里的另一个高频应用。我在面试和实际项目里都经常被问到,所以这块值得认真写。
最基础的实现是用 setIfAbsent,对应 Redis 的 SET key value NX PX 命令:
java复制public boolean tryLock(String lockKey, String requestId, long expireTime) {
return redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, Duration.ofMillis(expireTime));
}
public boolean releaseLock(String lockKey, String requestId) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
return redisTemplate.execute(redisScript, Collections.singletonList(lockKey), requestId) > 0;
}
为什么释放锁要用 Lua 脚本?因为“判断锁是不是自己的”和“删除锁”是两个操作,如果分开执行,中间可能被其他线程插入。比如线程 A 的锁还没删除就过期了,线程 B 拿到了新锁,此时 A 再去删除,就会把 B 的锁误删掉。用 Lua 脚本可以把“获取值、比对、删除”打包成一个原子操作,这样就安全了。requestId 一般用 UUID 或当前线程的唯一标识,用来区分锁的持有者。
这里还有个关键点:锁的过期时间怎么设?如果业务执行时间超过了锁的过期时间,锁提前释放,其他线程就会进入临界区。这需要“续期机制”,就像 Redisson 里的 watchdog,它会在锁快过期时自动延长锁的生存时间。如果你不想引入 Redisson,可以自己写一个后台线程定时续期,但要注意在 finally 里把续期线程也关掉,不然锁释放了线程还在空转。
分布式锁的使用场景也要分辨清楚。只是防止重复提交、缓存击穿这种毫秒级短任务,自研锁完全够用;如果涉及跨服务、需要严格互斥而且持有时间不确定的任务,建议用 Redisson,它的看门狗续期和公平锁机制更完善,少踩不少坑。
3.3 用 Redis Stream 拉取队列消息
Redis Stream 是 Redis 5.0 引入的消息队列数据结构,很多系统拿它作为轻量级消息中间件。在 Spring Boot 里,可以用 Spring Data Redis 的 StreamMessageListenerContainer 来监听和拉取消息。
我第一次实际用 Stream 的时候遇到一个困惑:怎么“拉取队列消息”而不是“订阅消息”。如果用 XREAD 命令,它就是客户端主动拉取;用 XREADGROUP 可以配合消费者组,让多个实例共同消费一个队列,每条消息只会被组里的一个消费者消费,不会重复处理——这是用 Stream 做工作队列的核心方式。
Spring Boot 里的容器监听方式大致是这样的(简化版):
java复制StreamMessageListenerContainer<String, ObjectRecord<String, String>> container =
StreamMessageListenerContainer.create(
connectionFactory,
StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()
.pollTimeout(Duration.ofSeconds(1))
.targetType(String.class)
.build()
);
container.receive(
Consumer.from("my-group", "consumer-1"),
StreamOffset.create("my-stream", ReadOffset.lastConsumed()),
message -> {
System.out.println("收到消息: " + message.getValue());
}
);
container.start();
需要注意几个前提:Stream 队列和消费者组需要提前创建。默认情况下,如果 my-stream 不存在,容器会直接报错。你可以通过在启动时执行 XGROUP CREATE my-stream my-group 0-0 MKSTREAM 来完成初始化,其中 0-0 表示从头开始消费,MKSTREAM 表示如果 Stream 不存在就创建。这也是热词搜索里“spring boot redis stream 如何拉取队列消息”问得最多的一个地方,很多人配好容器后一直等不到消息,就是因为消费者组没有创建成功。
还有一个经验是:消费到消息之后,一定要手动确认 XACK。如果没有 ACK,消息会一直留在 Pending 列表里,消费者重启后会反复消费到同一条消息。Spring Data Redis 的消息回调里自带 Acknowledge 对象,处理完业务逻辑后调用 acknowledge() 即可。这跟 RabbitMQ 的手动确认模型是一个思路,Redis Stream 最容易被忽略的就是这个 ACK 环节。
4. 数据类型与工具链使用心得
4.1 五大常用数据类型在 Spring Boot 里的操作
Redis 之所以强大,很大程度是因为它不只有 KV,还有多种专门的数据结构。Spring Data Redis 为每种类型都封装了 opsForXxx 操作类,用起来很直接。
| 数据类型 | 操作入口 | 典型场景 |
|---|---|---|
| String | ValueOperations | 缓存、计数器、验证码 |
| Hash | HashOperations | 对象存取、购物车、用户属性 |
| List | ListOperations | 简单的消息队列、最新列表 |
| Set | SetOperations | 去重、共同好友、随机抽奖 |
| ZSet | ZSetOperations | 排行榜、按分数排序的数据 |
举个例子,String 类型里的 increment 方法对应 Redis 的 INCR 命令,是原子自增,非常适合做发号器、计数器、库存扣减。我之前做过一个活动系统,就用 increment 做限流计数,直接规避了并发写数据库的问题。Hash 类型可以单独更新某个字段,适合存用户资料这类对象,修改一个字段不用整个序列化和覆写。ZSet 做排行榜就更经典了,zAdd 添加成员和分数,zReverseRangeWithScores 取排名,天然支持按分数范围和排名查询。
很多面试题会问 Redis 各种数据类型的底层结构,比如 String 底层是 SDS,ZSet 底层是跳表+哈希表。但实际开发中更重要的是选型:什么时候用 Hash 而不是 String,什么时候用 Stream 而不是 List。我的经验是,ZSet 是常被忽视的一类数据结构,很多看起来复杂的业务,比如“最近热门文章”“在线用户按活跃度排序”,都能用 ZSet 几行代码搞定。
Set 和 Hash 在业务里也很有用。Set 的两个集合可以做交集、并集、差集运算,比如“共同关注”就是 sInterstore 一下的事。Hash 的字段级更新能力能避免并发覆盖整个对象,这在做购物车类的功能里比较关键。
4.2 连接工具选型:Redis Desktop Manager 和它的替代品
本地开发的时候,我习惯用一个可视化客户端直接看 Redis 里的数据,排查“这个 key 到底有没有写进去”“过期时间还剩多少”这类问题比 redis-cli 舒服。
Redis Desktop Manager(RDM)是老牌工具,但后来从免费变成了商业软件,老版本虽然还能用,但对新版 Redis 支持得不是很好。如果还要免费开源的工具,我比较推荐 Another Redis Desktop Manager(AMD),它跨平台、支持 SSH 隧道、可视化查看 Stream 消息,日常开发完全够用。如果你想用官方出品的,Redis Insight 也是免费的,功能更全面,只是稍微重一点。
可视化工具的连接配置项很简单:地址、端口、密码,跟 application.yml 里保持一致。连接失败时,优先检查三件事:Redis 是否启动了(redis-cli ping)、防火墙是否有拦截、protected-mode 是否允许远程访问。
另外我习惯在命令行环境下也保持熟练使用 redis-cli 的能力,毕竟服务器上不可能总有图形界面。redis-cli 的几个高频命令值得记牢:keys *(只建议在测试环境用,生产环境别用)、type key、ttl key、memory usage key,排查数据问题时这几个命令就是最趁手的工具。
5. 常见问题排查与避坑实录
5.1 高频故障速查表
我在多个项目里整合 Redis,翻来覆去遇到的问题其实就那几类。整理成一张表,方便你直接对照排查。
| 异常现象 | 根本原因 | 解决办法 |
|---|---|---|
| 启动报 Unable to connect to Redis | Redis 未启动、端口未开放、密码错误 | redis-cli ping 验证,检查配置文件和防火墙 |
| key 变成二进制乱码 | 序列化器使用默认 JDK 序列化 | 配置 String 序列化的 key,JSON 序列化的 value |
Can't get Jedis connection |
连接池耗尽或配置错误 | 检查连接池参数,确认 commons-pool2 依赖是否引入 |
Java 8 date/time type not supported |
缺少 jsr310 模块 | 引入 jackson-datatype-jsr310,注册 JavaTimeModule |
| 缓存注解不生效 | 没有加 @EnableCaching |
在配置类上加启用注解 |
| Stream 容器收不到消息 | 消费者组不存在或 Offset 配置不对 | 提前创建消费者组,并检查 StreamOffset 配置 |
| Redis 重启后数据全没了 | 没开启 RDB 或 AOF 持久化 | 配置 appendonly yes 或调整 RDB 策略 |
连接超时的问题,重点说一个坑。之前有个项目,Redis 服务在单独一台机器上,应用偶尔出现连接超时。排查才发现是 Redis 的 timeout 参数设了 0(默认不主动断开空闲连接),但是应用端的 Lettuce 空闲超时时间设得比服务端短,连接被应用判死但服务端还开着,下次复用时就异常。处理方式是把 Lettuce 的空闲验证打开,或者在配置里设置合理的 timeout 值,让不可用连接及时释放。
5.2 一些我个人的实操经验
最后分享几个我实际干活时养成的习惯。
第一个是给 key 设计规范。我习惯用“业务名:实体名:id”这样的格式,例如 order:user:1001。这样不管在 redis-cli 还是可视化工具里,都能一眼看出这个 key 是干什么的。避免裸用 user1001 这种没有命名空间的 key,不同业务之间混在一起,后期根本没法维护。
第二个习惯是给所有缓存设置 TTL。Redis 当缓存用时,没有过期时间就是一个隐患。宁可多设几个不同的 TTL 策略,比如热点数据 10 分钟、会话数据 30 分钟、配置数据 1 小时,也不要一直放着不删。我踩过最大的坑就是缓存数据永不过期,结构变更后旧数据还在,导致线上数据对不上,排查了很久才发现是缓存问题。
第三个习惯是善用 Lua 脚本。像限流、锁、原子计数这类操作,尽量用 Redis 的 Lua 脚本实现为一个原子操作,避免多次网络往返和并发竞态。Spring Data Redis 的 DefaultRedisScript 可以直接传参数和 keys,用起来并不复杂,但能解决非常多的并发问题。
第四个习惯是不要在生产环境用 keys * 命令。数据量大的时候它会阻塞 Redis 服务。如果确实需要遍历 key,可以用 scan 命令逐步迭代,或者挂一个专门的工具在低峰期处理。安全起见,生产环境甚至可以禁用 keys 命令,减少误操作风险。
第五个习惯是区分测试和生产的连接配置。本地调试时我经常把日志级别调到 DEBUG,用 RedisTemplate 打印出来的命令能直接看到实际发给 Redis 的指令,排查序列化问题特别有效。生产环境当然不开 DEBUG,但调试阶段这个技巧能帮你快速定位问题出在客户端还是服务端。
整合 Spring Boot 和 Redis 并不难,把序列化、连接配置、过期时间这几个基础问题想清楚,后面再用缓存、分布式锁、Stream 就会顺很多。我个人的体会是,Redis 用得好不好,很大程度取决于对数据结构和原子操作的理解,这部分可以在实际项目里慢慢积累。希望这篇文章能帮你少踩一些我当年踩过的坑。
