
1. 先想明白Spring Boot Redis 到底解决了什么问题做后端这几年Redis 几乎是“标配外挂”但要真上手 Spring Boot Redis很多新手第一反应是“到处复制粘贴代码”结果要么序列化一堆乱码要么缓存过期时间不对甚至在测试环境连不上 Redis 直接崩掉。这篇内容我不会只给你一堆配置而是把从环境准备、依赖引入、序列化方案、缓存治理到分布式锁这一条线完整拆开结合我在项目里实际踩过的坑给你一套可以直接抄作业的整合教程。先说清楚Spring Boot 整合 Redis本质就三件事——连接 Redis 服务器、选对数据结构和序列化方式、把 Redis 用对场景缓存、分布式锁、计数器、会话共享等。你不需要一次性把 Redis 所有功能都塞进项目里但连接方式和核心 API 一定是绕不开的。适合谁看刚学 Spring Boot 的小白或者是写了两三年业务但一直没系统整理过 Redis 整合方案的开发。前者能照着步骤跑通后者能在“序列化”“缓存失效策略”“分布式锁细节”这几个点上得到一些启发。社区上 Redis 的案例一搜一大把但多数只讲“怎么跑通”很少讲“为什么这么做、踩过了什么坑”。这篇补上。2. 准备阶段无论什么系统先把 Redis 跑起来2.1 Redis 安装Windows、macOS、Linux 和 DockerRedis 官方其实不直接支持 Windows但现在开发机上用 Windows 的人特别多所以社区出了 Microsoft archive 维护过的 Windows 版本以及更新一点的 tporadowski 版本。Windows 下最简单的方式是下载 Redis-x64-x.x.x.zip解压后直接运行 redis-server.exe不需要编译也不需要额外依赖Redis 默认端口 6379。这个方式我用过做本地开发和测试完全没有问题。不过要注意Windows 版 Redis 的版本可能落后官方有些命令支持不全生产环境千万别用 Windows 版。macOS 下最省事的安装方式是用 Homebrewbrew install redis brew services start redis这条命令会把 Redis 注册成后台服务每次开机自启日常开发特别省心。不想用 Homebrew 的话也可以redis-server直接前台启动看到那个 ASCII art 的 Redis logo 就说明成功了。Linux 上如果是 CentOS 或 Ubuntu最稳妥的是源码编译安装因为系统自带源里的版本有时候非常老老版本缺少一些新特性。编译安装三步走wget https://download.redis.io/releases/redis-7.0.14.tar.gz tar xzf redis-7.0.14.tar.gz cd redis-7.0.14 make编译完成之后src/redis-server就是可执行文件。源码安装的好处是可以自己指定安装路径、配置文件但代价是需要编译环境gcc、make 等。如果你不想折腾编译Ubuntu 上apt install redis也够用。Docker 方式是目前团队协作里最推荐的方式因为一条命令就能给所有开发统一环境docker run -d --name redis \ -p 6379:6379 \ redis:7.0 \ redis-server --appendonly yes这里我特意加了--appendonly yes也就是开启 AOF 持久化。开发环境无所谓但如果你在本地做数据操作测试加了这个能防止容器一删数据全没。很多新手在这里会踩一个坑docker 里用官方 image 默认只用 RDB 持久化容器删除后数据没了就开始怀疑是不是代码写错了。其实跟代码没关系是持久化配置问题。2.2 可视化工具一定要有一款顺手的 Redis 客户端连接 Redis 的命令行工具是redis-cli做验证最好使但日常查看 key、检查过期时间、看内存占用时命令行效率太低。市面上常用的可视化客户端有几个Redis InsightRedis 官方出的支持 key 的树形浏览、JSON 查看、Slow Log 分析界面干净功能现代。Another Redis Desktop Manager (AnotherRedisDesktopManager)开源、更新频繁功能上我觉得比原来的 Redis Desktop Manager 更顺手连接 windows 版 Redis 也不会有怪问题。Redis Desktop Manager (RDM)老牌工具但新版开始收费社区版停留在旧版本功能够用但界面偏旧。我的建议是直接用 Another Redis Desktop Manager 或 Redis Insight。它们支持 Redis 6 的 ACL 账号密码机制Windows 和 macOS 都用得很稳。在项目里真正要关注的不是你用什么客户端而是key 到底长什么样、value 到底是什么编码。这部分和后面的序列化强相关下面细讲。2.3 创建一个 Spring Boot 工程我用的是 Spring Boot 2.6.x原因后面说。创建工程的方式有 IDEA、Spring Initializr、或者直接命令行。无论哪种方式核心依赖只需要一个 starterdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency这个 starter 默认会引入Lettuce作为底层 Redis 客户端库它是异步驱动的比老的 Jedis 更适合高并发场景。接着在application.yml里写连接信息spring: redis: host: localhost port: 6379 password: database: 0 timeout: 3000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0这里的database: 0是 Redis 内置的分库逻辑Redis 一共有 0~15 号库。开发环境随意但生产环境强烈建议不同业务用不同 database或者干脆一套环境一套 Redis避免 key 互相污染。lettuce.pool配置的是连接池注意只有引入了spring-boot-starter-data-redis时连接池默认不生效需要额外加commons-pool2依赖dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependency如果你不加这个依赖连接池参数会被忽略每次请求都创建新连接高并发下性能就会有影响。3. 核心细节RedisTemplate 序列化方案与常用 API3.1 为什么 RedisTemplate 默认序列化会出现乱码Spring Boot 注入两个现成的操作类StringRedisTemplate和RedisTemplate。前者 key 和 value 都是字符串用途有限但安全后者是对象操作问题都出在它的默认序列化上。默认情况下RedisTemplate使用 JDK 序列化JdkSerializationRedisSerializer写入 Redis 的数据会带一串类似\xAC\xED\x00\x05t\x00的二进制头在客户端里看就是乱码。更麻烦的是这个序列化机制要求对象必须实现Serializable一旦你换了序列化方式旧数据全部无法读取只能清库。所以项目里真实做法是自定义一个 RedisConfig替换默认序列化器。最经典的组合是 key 用StringRedisSerializervalue 用Jackson2JsonRedisSerializer或者也可以直接统一用GenericJackson2JsonRedisSerializer。前者灵活后者会自动带上类型信息反序列化方便。我的方案是这样Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); // key 用字符串序列化 StringRedisSerializer stringSerializer new StringRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); // value 用 JSON 序列化 GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }为什么 key 一定用字符串因为在 Redis 里 key 本身就是字符串如果你用 JDK 序列化key 会变成一堆二进制前缀你不仅没法在客户端里直观看到 key也没法用redis-cli按前缀批量删除。value 用 JSON 的好处是人眼可读、跨语言通用排查问题的时候特别重要。3.2 使用 RedisTemplate 操作五大常用数据结构Redis 有五种基本数据类型String、Hash、List、Set、ZSet。Spring Boot 里对应的方法分别封装在ValueOperations、HashOperations、ListOperations、SetOperations、ZSetOperations里。举个例子简单缓存一个用户信息Service public class UserCacheService { private final RedisTemplateString, Object redisTemplate; public UserCacheService(RedisTemplateString, Object redisTemplate) { this.redisTemplate redisTemplate; } public void cacheUser(User user) { String key user:info: user.getId(); redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES); } public User getUserById(Long id) { String key user:info: id; Object value redisTemplate.opsForValue().get(key); if (value instanceof User) { return (User) value; } return null; } }这里要注意一个细节用GenericJackson2JsonRedisSerializer反序列化后拿到的对象实际是LinkedHashMap而不是User所以直接强转会报错。解决方案有两种一是在ObjectMapper里配置activateDefaultTyping让 JSON 里带上class类型信息二是拿到数据后手动用ObjectMapper转成目标对象。第一种方式简单但安全性稍差只适合内部系统第二种更可控。我在项目里用的是第二种会单独写一个 JsonUtil避免代码里到处转型。Hash 操作在一些业务里特别有用比如记录购物车商品数量这种场景一个 key 对应多个 field天然适合 HashredisTemplate.opsForHash().put(cart:1001, sku:2001, 2); Integer quantity (Integer) redisTemplate.opsForHash().get(cart:1001, sku:2001);需要注意 Hash 的 value 如果是Integer反序列化回来也可能变成Double或LinkedHashMap取决于你序列化器的配置。别慌这属于正常现象写个类型转换工具或者统一用 String 存储再按需解析就行。3.3 配置缓存管理器从 Cacheable 到过期时间自定义Spring Boot 里整合 Redis 的一个重头戏是声明式缓存。使用Cacheable装饰方法框架会自动帮你查询 Redis 里有没有 key没有就执行方法并写入结果。但直接用默认配置会有几个坑默认缓存使用 JDK 序列化默认过期时间永久默认 key 规则不够友好。解决方式是配置一个RedisCacheManagerConfiguration public class CacheConfig { 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) .build(); } }.disableCachingNullValues()这里有个讲究默认情况下如果方法返回 nullSpring 也会尝试缓存 null这在缓存穿透场景下有风险所以直接禁用空值入缓存。要是你确实需要缓存空值来防止穿透就得配合过期时间来用。然后就可以像这样用Service public class ProductService { Cacheable(value product, key #id) public Product getProductById(Long id) { // 从数据库查询 return productMapper.selectById(id); } }这里生成的 Redis key 结构是product::123::是默认的分隔符可以通过配置改成别的。用声明式缓存最大的坑是你无法在代码里精确控制过期时间所有同类缓存都走同一个RedisCacheConfiguration。要解决这个可以用RedisCacheManagerBuilderCustomizer对不同缓存单独设置。园区里有过这种需求商品缓存 10 分钟订单缓存 1 小时如果不做定制只能全部 30 分钟那就很别扭所以建议还是稍微研究一下定制方式。4. 整合中的关键场景缓存治理、分布式锁与集群部署4.1 缓存穿透、击穿、雪崩与治理思路缓存穿透、击穿、雪崩是面试题更是真实生产事故。我身边不止一个人遇到过这种情况流量一大数据库直接被拖垮。简单理解这三个概念穿透请求查询了一个不存在的 keyRedis 里没有数据库里也没有所有请求直接砸到数据库。击穿某个热点 key 在过期瞬间大量请求正好涌进来瞬间全部打到数据库。雪崩大量 key 同时过期或者 Redis 集群整体不可用流量层层打到数据库引发连锁崩溃。Spring Boot Redis 场景下的治理手段各有侧重。穿透的标准解法是布隆过滤器加空值缓存。在往里写 Redis 之前先判断 key 是否存在再决定放行还是拦截如果连数据库都没有就把空值短时间缓存起来防止同一批恶意请求往复穿透。布隆过滤器判断一个 key 不存在是绝对准确的但存在时可能误判这个特性用来做拦截刚刚好。击穿的标准解法是互斥锁也就是分布式锁保证只有一个请求能去数据库重建缓存其他请求等待缓存刷新后直接命中。雪崩则可以通过把过期时间分散比如base random()这样同一时刻过期的 key 数量不会太高。另外 Redis 主从加哨兵、Redis Cluster 高可用也是抗雪崩的底层保障。4.2 手写一个 Redis 分布式锁要注意哪些细节Spring Boot 场景下可以用 Redisson 实现锁但为了理解原理我建议先手写一个简易版本再迁移到 Redisson。一个合格的 Redis 分布式锁必须满足三点互斥、不死锁、可重入。最简单的方式是使用SET key value NX EX 30命令一次性同时设置 key 和过期时间。注意如果分开执行SETNX和EXPIRE进程中途挂了就会变成不死锁所以必须合为一条命令。Spring Data Redis 里对应的是Boolean locked redisTemplate.opsForValue().setIfAbsent(lock:order: orderId, token, 30, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // 业务逻辑 } finally { // 释放锁 redisTemplate.delete(lock:order: orderId); } }这里有个潜在问题如果业务执行时间超过 30 秒锁自动过期了第二个线程拿到锁之后第一个线程执行完才去删除锁就会误删第二个线程的锁。解决方式是 value 存一个唯一 token删除时用 Lua 脚本判断 token 是不是自己的是才删if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这个脚本用DefaultRedisScriptLong执行。我现在项目里已经直接用 Redisson 的RLock了因为 Redisson 底层自带看门狗自动续期省去了手工续期的麻烦。但希望你理解上面的原理因为面试和排查问题的时候光会用 Redisson 是不够的。4.3 集群部署时Spring Boot 应该怎么连生产环境一般不会单机部署 Redis至少是主从。主从的作用是数据备份和读写分离写走 Master读走 Slave。Spring Boot 接入主从的方式很简单使用spring.redis.下没有主从的通用写法一般是通过RedisSentinelConfiguration或RedisClusterConfiguration来配置。主从复制方式可以通过 docker 模拟docker run -d --name redis-master -p 6379:6379 redis docker run -d --name redis-slave -p 6380:6379 redis redis-server --slaveof 172.17.0.2 6379Spring Boot 加上这个依赖配置后会自动发现主节点并写入读请求默认还是走主节点。如果要读写分离需要重写RedisTemplate里的setReadFrom让 Lettuce 客户端把读请求路由到从节点。Redis Cluster 则要求至少三主三从Spring Boot 的配置方式变成了spring: redis: cluster: nodes: - 192.168.1.10:6379 - 192.168.1.11:6379 - 192.168.1.12:6379这里踩过最隐蔽的一个坑Cluster 模式下使用RedisTemplate操作多个 key 时要特别小心因为不同 key 的 hash slot 可能分布在不同的节点上如果你的业务逻辑对多个 key 操作需要原子性那就得用 hash tag 把 key 强制放到同一个 slot 里否则用pipeline写多个 key 时会报CROSSSLOT错误。这个问题不熟的人很容易踩业务一复杂就开始莫名其妙。4.4 监控 Redis 运行状态Spring Boot Admin 和 Redis 日志很多团队直到 Redis 挂掉才意识到没有监控。推荐两个层面的监控应用层面集成 Spring Boot Admin可监控 Spring Boot 应用的健康状态、内存、HTTP 接口等但注意它的健康检查默认只判断 Redis 是否可连接连上了就算 UP不能体现运行质量。Redis 层面启用 Redis 的 Slow Log查看超过一定毫秒数的命令判断是否存在慢查询。在redis-cli里执行CONFIG SET slowlog-log-slower-than 10000就能把超过 10ms 的命令记录下来再用SLOWLOG GET查看。Redis 日志也要开启因为像Command timed out这样的 Lettuce 超时错误往往能从日志里看到当时 Redis 服务器的负载。Spring Boot 侧也可以暴露 Redis 缓存相关的指标management: endpoints: web: exposure: include: health,metrics加了依赖spring-boot-starter-actuator通过/actuator/metrics/cache.gets等接口能看到缓存命中情况。生产现场排查问题时命中率比连接状态有用得多。5. 常见问题与排查技巧照着这个表避坑就行5.1 Windows 与本地开发里最容易遇到的问题问题一Windows 下 Redis 连接被拒最常见的原因是 redis-server.exe 没启动或者启动后防火墙拦截了 6379 端口。先用telnet 127.0.0.1 6379测试端口通不通。通了但连不上再看 Spring Boot 配置里有没有把 host 写成了localhost有时候 Windows 上localhost解析成 IPv6 的::1而 Redis 只监听了 IPv4就会出现诡异连不上的情况。这种可以直接用127.0.0.1替代 host 解决。问题二Lettuce 的 Command timed out 异常就是热搜词里那个错误io.lettuce.core.RedisCommandTimeoutException: Command timed out这个错误在生产环境特别常见多数原因是客户端连接池不够、Redis 执行大 key 命令阻塞了、或者网络延迟非常高。排查思路看 Redis 端INFO commandstats确认有没有慢命令。看客户端连接数如果接近 max-active说明连接池不够。看是否用到KEYS命令这个命令会在单线程 Redis 上阻塞所有操作导致大量超时严重的话 Redis 直接假死。KEYS在 Spring Boot 里对应redisTemplate.keys(pattern)生产环境千万别用。要用 SCAN 取代它逐批遍历所有的 key。这个错误看多了就发现超时只是表面现象底子多半还是用错了命令或者连接数不合理。问题三序列化后反序列化失败如果出现ClassCastException或者 JSON 解析异常基本就是两个原因value 用了 JDK 序列化后存入但配置改成了 JSON或者GenericJackson2JsonRedisSerializer的反序列化结果和你预期的 class 不一致。这一块没什么捷径只能把真实验证的测试类写在代码里保证每次启动跑通几条基本的读写测试别等上线了再发现。5.2 缓存一致性数据库和 Redis 到底谁先更新这是所有缓存方案都躲不开的经典问题。我见过最朴素的代码是这样先更新数据库再删除 Redis 缓存。这个方案很经典而且实际效果在大多数业务下都够用。但如果有并发更新同一个 key 的需求会出现短暂的不一致线程 A 更新数据库线程 B 在 A 删除缓存之前读到旧缓存然后又写回了新数据库值。解决方案是用分布式锁串行化这个更新过程或者使用 binlog 订阅的方式异步刷新缓存。从经验角度我建议大多数中小项目先做“先更新数据库再删除缓存”。因为它逻辑简单、易排查不引入太多中间件。只有当出现复杂的多级缓存一致性需求时再考虑上消息队列。别一听“缓存与数据库一致性”就造火箭。5.3 关于 Spring Boot 版本的一点提醒Spring Boot 2.4 之后Spring 不再推荐使用spring.factories自动装配而在 Spring Boot 3.x 中javax.*改为jakarta.*Redis 相关的 starter API 也有一些调整比如构造器和自动配置类的变化。现在的代码基本都是基于 Spring Boot 2.6.x 写的因为这个版本还能兼容老项目的javax接口网上资料最多踩坑最少。如果你用的是 Spring Boot 3.x整合 Redis 的核心思路没变但注意两点一是确保 JDK 版本是 17 以上二是某些第三方库比如老版本的 Redisson可能不兼容 Spring Boot 3需要升级到适配版本。别一上来就跟着教程复制粘贴先确认版本兼容性。6. 实战小结从连接到一个完整的功能链路6.1 一个包含缓存、锁、监控的完整示例假设要做一个商品详情查询接口要求热点数据缓存 10 分钟缓存失效时只允许一个请求查库其他请求等待到超时为止。第一步商品服务Service public class ProductDetailService { Autowired private RedisTemplateString, Object redisTemplate; Autowired private RedissonClient redissonClient; public Product getProductDetail(Long id) { String cacheKey product:detail: id; Object cached redisTemplate.opsForValue().get(cacheKey); if (cached ! null) { return JsonUtil.toBean(cached, Product.class); } String lockKey lock:product:detail: id; RLock lock redissonClient.getLock(lockKey); boolean locked false; try { locked lock.tryLock(3, 10, TimeUnit.SECONDS); if (!locked) { // 拿到不锁直接查一次数据库 return getFromDb(id); } // 再查一次缓存避免排队线程重复查库 cached redisTemplate.opsForValue().get(cacheKey); if (cached ! null) { return JsonUtil.toBean(cached, Product.class); } Product product getFromDb(id); redisTemplate.opsForValue().set(cacheKey, product, 10, TimeUnit.MINUTES); return product; } finally { if (locked) { lock.unlock(); } } } private Product getFromDb(Long id) { // 模拟数据库查询 return new Product(id, Redis 实战); } }这个代码看起来简单但把关键问题都处理了缓存优先、分布式锁防击穿、拿到锁后二次检查防止排气队重复查库、异常情况下锁必然释放。第二步监控这个接口的缓存命中。建议用 Spring Boot Actuator 暴露redisTemplate.opsForValue().get执行前后的状态或者在服务里主动加一个简单的计数器用 AtomicLong 记录命中数与请求数通过/actuator/health和自定义端点暴露出去。这段简单代码在生产救过命。第三步别忘了删除缓存的操作。后台如果修改了商品信息需要显式地把对应 key 删掉否则用户会一直看到旧数据。标准做法redisTemplate.delete(product:detail: productId);不要想着用CacheEvict注解扩展一下就行实际上了 RedisTemplate 直接 delete 的耦合度最低尤其是在复杂条件更新的时候。6.2 整个过程我个人的体会整合 Redis 不断踩坑后我最大的心得是序列化方案一次想清楚比什么都重要。项目一开始如果默认 JDK 序列化等到线上积累几百万 key 再改 JSON麻烦远比你想象的大。第二个心得是Redis 把明明简单的 key-value 玩出花来但稳定性才是王道。连接池大小、超时时间、大 key 拆分这些基础事每个都值得在项目初期就写清楚规范。再分享一个经验本地开发一定要区分环境配置。开发环境不要使用 Redis 0 号库和生成环境共用最好启动一个明码的本地 Redis 实例所有操作都在一个可控的环境里做调试起来才不心累。好这篇就到这里建议你先从最简单的 RedisTemplate 存取开始跑通再对照上面的代码逐步加上缓存、锁、监控。真遇到问题的时候再回来翻翻这些坑。祝顺利。