尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Spring Boot整合Redis:缓存、序列化与分布式锁实战
做后端开发Redis基本是绕不开的组件。我第一次被它圈粉是因为有个内部系统每天高峰期被同一个查询接口打成筛子数据库CPU飙到99%加了Redis缓存之后QPS翻了几倍还不止数据库负载直接掉到20%。后来我在好几个Spring Boot项目里都接入了Redis从简单缓存到分布式锁都试过也踩了不少坑。这篇文章就从一个实际开发者的角度把Redis能干什么、为什么要用它、在Spring Boot里怎么落地以及我实际工作中踩过的那些问题一次说明白。刚学Spring Boot想加缓存的新手可以看已经用了Redis但遇到乱码、穿透、连接池问题的朋友也可以直接跳到对应章节找方案。1. 先搞清楚Redis到底解决了什么问题1.1 为什么数据库不够用了才轮到Redis出场先看一个很常见的场景。一个商品详情页接口背后查数据库数据库里可能关联了好几张表用户每次打开页面都要走一遍这个查询。数据量大或者用户多的时候数据库压力直线上升。数据库是磁盘存储查询需要对索引、对磁盘IO哪怕走了索引单次查询也要几百微秒。如果这个接口每秒被访问几千次磁盘IO和CPU会先扛不住数据库一旦慢下来整个服务都会跟着连锁反应。这时候Redis出现的意义就是把数据放到内存里。内存的读写速度和磁盘不是一个量级Redis官方给出的性能数据是单实例每秒可以处理十万级别甚至更高数量的请求单次读写延迟通常在亚毫秒级别。这还不算最关键的真正让它适合做缓存的原因在于Redis采用单线程模型加事件驱动配合IO多路复用机制处理请求避免了传统并发编程里的锁竞争和线程切换开销。所谓IO多路复用简单理解就是一条线程同时盯着大量连接谁有数据来了就处理谁没有数据就继续等待不会为了一个请求专门开一个线程。打个比方磁盘上的数据库像一个大型仓库查一个零件要去仓库里翻箱倒柜Redis像一张放在桌边的白板常用的信息直接写在白板上一眼就能看到。查询流程就从“去仓库翻找”变成了“先看白板”。这也是为什么绝大多数缓存场景Redis都是第一选择。注意Redis快是快但不是万能的。它把所有数据放内存里内存有限而且宕机后数据会丢失虽然有持久化机制但不能当数据库主业用。所以Redis适合存那些“丢了可以重建、重新拉取代价低”的数据比如热点查询结果、验证码、排行榜而不是存绝对不能丢的核心业务数据。1.2 缓存之外Redis还能干这些事Redis能做的远不止缓存。它本身是一个键值存储系统但底层支持多种数据结构每种结构对应一类典型场景。我日常用得最多的是这么几种String最基础的结构做缓存、计数器、分布式ID、限流都和它有关。Hash适合存对象比如用户信息更新某个字段只操作一个字段不用整个对象序列化重写。List可以做简单的消息队列也适合维护最新N条数据。Set天然去重适合收藏列表、标签集合这类场景。ZSet有序集合排行榜、热度排序按分数排序效率极高。举个例子统计一篇文章的阅读量每次都去数据库UPDATE计数器压力会很大。用Redis的INCR命令直接在内存里加1然后定期把计数同步回数据库。再比如排行榜数据库里按分数排序数据量大时查询压力不小用ZSet的ZREVRANGE命令直接取出Top N代码简单性能也好。这些场景说到底都是把高频操作从数据库搬到内存再在后台做数据回写思路是一致的。另一个常见的用途是分布式锁。多台服务器同时执行某个任务比如秒杀扣减库存需要保证同一时刻只有一台机器在处理。用Redis的SET命令加上NX和EX参数可以做到原子性地“锁不存在就设置成功、同时带上过期时间”这个后面会重点讲也是面试高频点。还有会话共享多个服务节点共享同一份登录态直接把Session存Redis整个集群都能验证同一个会话这是解决分布式会话问题的常见套路。2. 项目里引入Redis前的准备工作2.1 依赖引入和版本选择Spring Boot接入Redis官方提供了现成的starterMaven项目里加一个依赖就好。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency如果是Spring Boot 3.x依赖名称不变只是底层自动配置的默认客户端有所差异。这里说明一下Spring Data Redis底下有两个主流客户端Jedis和Lettuce。早年很多项目用Jedis后来Spring Boot默认切成了Lettuce。Lettuce基于Netty实现本质上是一个多路复用的连接模型一条连接可以并发处理多个请求天然比Jedis的连接池更省连接资源。大多数情况下直接用默认的Lettuce就行没必要手动引入Jedis。版本选择上我建议很直接不要手动指定这两个客户端的版本让Spring Boot的依赖管理统一帮你管。非要自定义版本时优先选择和你Spring Boot版本匹配的版本否则容易出现不兼容的报错。Spring Boot 2.x对应的spring-data-redis一般在2.7系列Spring Boot 3.x对应的是3.x系列混用时代码编译没问题运行起来却可能出现序列化或者连接协议上的诡异问题。启动类不需要额外写什么注解依赖加好、配置写好后Spring Boot的自动配置会帮你创建RedisConnectionFactory、RedisTemplate这些Bean。你要做的就是注入使用或者按需覆盖。2.2 配置文件里的那些参数含义接入Redis最基础的是在配置文件里写连接信息。我一般写成这样spring: data: redis: host: 127.0.0.1 port: 6379 password: database: 0 timeout: 3s lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 max-wait: 2000ms如果你用的是Spring Boot 2.x注意配置前缀是spring.redisSpring Boot 3.x改成了spring.data.redis。这个改版坑了不少人升级版本后配置没跟着迁移结果应用启动没问题一执行Redis操作就报连接不上。这类问题排查起来非常误事所以建议先检查配置前缀。这些参数具体什么意思我拆开讲。host和port是Redis服务所在位置开发环境一般是127.0.0.1和默认6379端口。password是认证密码没配认证就留空。database是Redis的逻辑分区默认有0到15共16个库可以用它把不同业务数据隔离开比如0号库放业务缓存1号库放验证码避免不同业务key相互干扰。不过现在更推荐的做法是用项目名或模块名做key前缀来区分因为单机Redis在集群模式下多数据库支持有限按前缀区分更适合将来拆集群。timeout是连接超时时间Lettuce模式下建议显式配置不然默认超时可能比较长Redis服务挂了时接口会在等待连接上卡很久。lettuce.pool里的max-active是最大活跃连接数决定同一时刻有多少个并发操作可以拿到连接max-idle是最大空闲连接数控制连接池里最多保留多少空闲连接min-idle是最小空闲连接数适当设置可以避免流量突增时现建连接max-wait是拿连接时的最大等待时间。注意max-wait设成-1是有风险的。如果连接池被占满请求会一直阻塞在这里接口超时时间再长也没用。生产环境我通常建议设置明确的等待时间比如2000ms等到就直接抛异常返回一个降级结果好过整个线程池被拖死。3. Spring Boot里操作Redis的几种姿势3.1 RedisTemplate和StringRedisTemplate怎么选Spring Data Redis提供两个核心操作类。StringRedisTemplate是RedisTemplate的子类区别在于它的key和value默认都使用String序列化器。换句话说存进去什么字符串取出什么字符串不会有额外的序列化包装。RedisTemplate则更通用默认情况下key和value都使用JdkSerializationRedisSerializer会把对象序列化成二进制再存进Redis。那该怎么选我的经验是如果你的业务是纯String场景比如存验证码、存JSON字符串、存计数直接用StringRedisTemplate简单直接Redis客户端里看到的key也是可读的方便排查。如果你要把Java对象直接缓存比如把一个用户对象直接放进Redis那就用RedisTemplate配合合适的序列化器让它自动完成对象和JSON之间的转换。但这里有个特别容易踩的坑两者混用。你用一个Template存数据用另一个Template取数据如果序列化器不一致取出来的数据就是乱的最常见的就是key变成一串二进制乱码比如开头出现\xac\xed\x00。这基本是每个用过Redis的人都会遇到一次的问题后面专门讲。3.2 序列化方案怎么选序列化器决定了你写进Redis的东西以什么形态存在。默认的JdkSerializationRedisSerializer有一个问题序列化后的数据带Java类型描述信息体积很大而且其他语言读不懂在Redis管理工具里看就是一串可读性极差的字节。生产环境如果用这个排查数据会非常痛苦。我个人的配置习惯是key用StringRedisSerializer保证key是人类可读的value用GenericJackson2JsonRedisSerializer把对象序列化成JSON字符串可读性和通用性都更好也跨语言。Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); StringRedisSerializer stringSerializer new StringRedisSerializer(); GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }有人会问为什么不直接把所有对象都用JSON存因为GenericJackson2JsonRedisSerializer会在JSON里带上class字段用来在反序列化时还原真实类型。这个设计有好处也有坏处好处是存任意对象都能恢复坏处是JSON里多了一段类型信息体积略大而且如果你后端改了对象包名或类名老数据可能反序列化失败。如果确定只缓存某几个固定类型也可以直接用Jackson2JsonRedisSerializer并指定目标类体积更小、更可控。还有一个小提示如果项目里有大量List、Map等集合类型要缓存需要确认序列化器能正确处理泛型信息。GenericJackson2JsonRedisSerializer在这方面的兼容性相对好一些用起来省心。3.3 一个完整的业务缓存代码示例讲完配置看一段实际业务代码。最常见的场景是查询用户信息并缓存我用Cache-Aside模式来写这也是最主流的缓存模式即“旁路缓存”先查缓存缓存没有再查数据库查完数据库写入缓存并设置过期时间。Service public class UserService { private static final String USER_CACHE_KEY user:info:; private final StringRedisTemplate stringRedisTemplate; private final ObjectMapper objectMapper; public CacheUserVO getUserById(Long userId) { String cacheKey USER_CACHE_KEY userId; String cachedJson stringRedisTemplate.opsForValue().get(cacheKey); if (StringUtils.hasText(cachedJson)) { return objectMapper.readValue(cachedJson, CacheUserVO.class); } // 模拟查数据库 CacheUserVO user queryUserFromDb(userId); if (user ! null) { stringRedisTemplate.opsForValue().set(cacheKey, objectMapper.writeValueAsString(user), 30, TimeUnit.MINUTES); } return user; } }这里有几个细节值得注意。第一key的设计用了业务前缀加ID比如user:info:10001冒号是Redis社区约定俗成的分隔风格一眼就能看出这个key属于哪个模块。第二缓存过期时间设成30分钟这是提前确定好的业务语义。第三缓存值是JSON字符串所以这里用StringRedisTemplate就够了不涉及对象序列化器简单可靠。更新数据的场景我遵循的原则是“先更新数据库再删除缓存”。比如用户修改了昵称数据库更新成功之后把缓存里的旧值删掉。下一次查询时缓存没有数据会从数据库重新加载新值缓存自然就对了。为什么不先更新缓存因为在一个并发环境里更新缓存和更新数据库很难做到原子一致删除缓存反而是最简单安全的方式——最多就是缓存暂时没命中多查一次数据库数据是不会错的。4. 踩坑记录序列化、穿透和连接池4.1 缓存里的二进制乱码到底怎么回事我相信每个用Spring Data Redis的同学都见过这种场景用管理工具一看key是一串\xac\xed\x00\x05t\x00value也是一堆乱码。原因很简单默认RedisTemplate用的是JDK序列化把key里的String对象用ObjectOutputStream方式写进RedisRedis里存的不是原始字符串而是Java序列化后的字节流。这个现象不是出错了而是序列化方式决定的。解决方案就是我前面配置里写的显式指定RedisTemplate的序列化器key和hashKey用StringRedisSerializervalue用JSON序列化器。一旦你显式配置之后所有通过这个RedisTemplate写入的数据就都是可读的字符串了。这里要特别提醒一件事如果你项目里已经有一批JDK序列化的旧数据改了序列化器之后新旧数据格式不兼容旧数据在读的时候会反序列化报错。所以投产项目改序列化器要规划好清理逻辑先删掉旧数据或者加版本号区分别在改动后才发现线上旧缓存全部读不了。另外一个容易被忽略的问题是如果不同环境用了不同的序列化配置同样一个key在不同环境看到的形态可能不一样。建议把序列化器的配置写进公共代码或统一配置类统一管理不要在业务代码里临时new一个RedisTemplate。4.2 缓存穿透、击穿、雪崩的应对这三个词是Redis缓存的核心问题也是实际项目中绕不开的坎。缓存穿透请求查询一个不存在的key缓存和数据库都没有每次请求都直接打到数据库。比如一个恶意请求不断访问不存在的用户ID数据库被无效查询轰炸。如果请求量很大数据库接口就会被这些查不出结果的请求拖垮。应对方式有两种比较实用。第一种是空值缓存查询数据库发现查不到数据时也写一个缓存value是null或一个特殊标记TTL设短一些比如30秒到60秒这样后续请求短时间内会命中缓存不会继续穿透到数据库。第二种是布隆过滤器在缓存前面加一层判断把可能存在的ID维护在布隆过滤器里过滤器判断不存在就说明数据一定不存在直接返回不再查库。布隆过滤器的缺点是存在误判率因为是概率型数据结构所以通常用在用户量特别大的场景大多数中小项目用空值缓存就够了。缓存击穿某个热点key突然过期同时大量请求并发访问这个key缓存未命中所有请求全部打到数据库。解决思路就是给这个热点key加互斥锁只有第一个请求去查库和回填缓存其他请求等待或者降级返回。还有一种思路是干脆不给热点key设过期时间逻辑上由后台定时刷新让这个key理论上永远不过期从而避免击穿。缓存雪崩大量key在同一时间集中过期或者缓存服务直接宕机导致一波汹涌的查询全部打到数据库。解决办法包括给TTL增加一个随机值让过期时间分散开比如基础过期时间30分钟加上0到10分钟的随机数搭建Redis集群提升可用性接口层面做降级和熔断当缓存不可用时直接返回兜底数据而不是把所有压力转嫁给数据库。4.3 连接池、超时和大Key问题连接池配置不当是生产环境比较容易翻车的地方。常见症状是应用运行一段时间后日志里出现连接获取失败的异常或者大量超时。原因一般是max-active太小或者某些慢操作长时间占着连接不释放。排查思路是这样先看Redis客户端连接数监控如果连接数持续打满max-active说明并发量超过了连接池上限可以适当调大max-active如果单次操作本身很慢那调大连接池只是治标不治本要去优化操作本身。超时配置也是老生常谈。lettuce.pool里的max-wait如果设成-1系统高峰期所有线程都在等连接最终表现为接口大面积超时表面看是Redis问题实际是等待资源导致的线程堆积。我习惯把max-wait设成2000ms宁愿快速失败返回错误也不让线程无限等下去。大Key问题是指某个key的value特别大比如一个Hash里存了几十万个字段或者一个String存了几MB数据。读这种大Key的耗时会明显偏高还可能导致Redis主从复制时同步慢阻塞其他命令。我遇到过一个案例有个List在生产环境积累了几十万条数据每次取其中一段数据都要遍历大内存空间对端实例的CPU被顶得很高。解决方法是拆分按业务维度或者时间维度拆成多个小key比如日志按天拆key而不是全堆在一个key里。另外生产环境一定要禁用KEYS命令它需要遍历整个键空间Redis单线程执行时会阻塞服务正确做法是使用SCAN命令分批遍历不影响正常请求。5. 进阶玩法分布式锁和缓存注解5.1 用Redis实现一个简单的分布式锁多实例部署时传统的JDK锁在单台机器内有效跨机器就没办法互斥了。比如秒杀场景两台服务器同时处理同一个订单的扣库存如果不加锁库存可能扣超。分布式锁可以让多台机器之间实现互斥Redis基于单线程的特性天然适合做这件事。最简单的实现是这条命令SET lock_key unique_value NX PX 30000。含义是只有当lock_key不存在时才设置成功NX同时设置过期时间PX过期时间为30000毫秒30秒。获取锁时每个线程生成一个唯一值释放锁时先比较value是否是自己持有再删除避免删到别人的锁。public boolean tryLock(String lockKey, String requestId, long expireMillis) { return stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, Duration.ofMillis(expireMillis)); } public void unlock(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; stringRedisTemplate.execute(new DefaultRedisScript(script, Long.class), Collections.singletonList(lockKey), requestId); }这里有个关键点释放锁不能用简单的GET之后判断再DEL因为这两个操作不是原子的。比如线程A的锁到了过期时间自动释放线程B拿到锁然后A这时才执行DEL就把B的锁删了。解决办法就是用Lua脚本把“判断是否是自己持有”和“删除”合并成一个原子操作单线程的Redis执行Lua脚本时不会插入其他命令这样就安全了。为什么释放锁要放在Lua脚本里这是面试官特别喜欢问的点。不过说句实话真实的分布式锁实现没有这么简单还要考虑锁续期、主从切换导致的锁丢失等极端问题。如果项目对锁的可靠性要求很高直接上Redisson框架它内置了看门狗机制自动续期。打个比喻锁在业务还没执行完之前看门狗会每隔一段时间自动给锁续命防止业务没跑完锁先过期了。自己写分布式锁适合学习或者简单场景生产环境业务复杂的还是用成熟方案更省心。5.2 Cacheable让缓存代码更简洁如果不想在每个业务方法里手动写查缓存、写缓存、删缓存的代码Spring的缓存抽象可以帮你把这部分模板代码去掉。核心是Cacheable、CachePut、CacheEvict这几个注解。Cacheable(value userCache, key #userId) public CacheUserVO getById(Long userId) { return queryUserFromDb(userId); } CacheEvict(value userCache, key #userId) public void updateUser(Long userId, String nickname) { updateUserInDb(userId, nickname); }Cacheable表示执行方法前先查缓存命中缓存就直接返回结果不执行方法体没命中则执行方法并把返回值写入缓存。CacheEvict用于删除缓存通常在更新操作之后调用。两者组合起来正好对应前面说的“先更新数据库再删除缓存”的模式。不过用注解缓存的坑也不小。第一默认的key生成策略比较简单多参数时要用SpEL表达式精确指定否则缓存粒度可能不符合预期。第二方法一定要通过Spring代理调用也就是从外部类调用才行同一个类内部方法直接调用this.method()代理不生效缓存逻辑直接被跳过这是最常见的注解失效原因。第三缓存的对象必须能被序列化如果你缓存的对象没有合适的序列化实现存进去再取出来就会出问题。所以注解缓存适合缓存语义明确、对象结构稳定的业务简单粗暴手动用StringRedisTemplate的方式则适合灵活多变的场景两者不冲突。最后分享一点我个人的体会。Redis真的不难上手难的是把它用在合适的地方并且把边界条件想清楚。每个key的TTL要设计、序列化方式要统一、连接池参数要配合压测来调、大Key和热点key要有预案。我在实际项目里见过太多因为Redis配置随意导致线上故障的例子多数时候不是Redis本身的问题而是使用姿势的问题。希望这篇能让你少走一些弯路。如果你项目里已经遇到了奇怪的缓存问题也可以顺着这个思路去排查先看key是怎么存的再看缓存和数据库的更新顺序最后看连接池和监控指标大部分问题都能在这个过程中找到答案。
RELATED

相关推荐

企业级智能体难在工程:OpenClaw部署与治理全解析

企业级智能体难在工程:OpenClaw部署与治理全解析

1. 先破除一个误区:企业级智能体之难,难在工程而非模型很多团队拿到 OpenClaw 的第一反应,是急着调模型、叠 Prompt、压测对话效果。我见过不少项目,Demo 演示时全屋惊叹,结果一到内网部署、连上真实业务系统&#xff…

📅 2026/10/17 18:00:12
Windows平台XDMA驱动开发与PCIe高速传输工程实践

Windows平台XDMA驱动开发与PCIe高速传输工程实践

1. 项目概述1.1 为什么突然聊起 XDMA做 FPGA 数据采集和高性能传输的同仁,最近一定绕不开 XDMA 这个词。尤其是当你手里的 PCIe 项目从 Linux 环境迁到 Windows 平台,或者需要在 Windows 上做视频采集、高速 ADC 数据回放、NVMe 协议桥接这类应用时&…

📅 2026/10/17 18:00:12
Kimi K2 + Claude Code 接入 3 个神级开源 MCP,相当丝滑

Kimi K2 + Claude Code 接入 3 个神级开源 MCP,相当丝滑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/17 18:00:12
MORE NEWS

更多资讯

📰

Kafka接入AI实战:从Lag排查到Function Calling自动化运维

1. 为什么要把Kafka接进AI:这个标题背后要解决的真实问题先聊点实在的。做Kafka运维和研发的朋友,应该都有过这种经历:半夜被ConsumerGroup的Lag告警叫起来,打开电脑连上跳板机,一条一条查kafka-consumer-groups.sh --…

📰

harness 工程化实践:用 AGENTS.md 与 Makefile 把 TaoToken 接入 CI 流水线

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

大模型推理-基于prefill和decode阶段特性,PD分离,势在必行!TaoToken统一Key下的PD分离部署大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

2026年5月开源模型Token服务实测榜:TaoToken统一Key下DeepSeek/GLM/Kimi三层架构性能拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Codex 下载与本地部署实战:从安装到运行全流程指南(TaoToken 统一 Key 接入版)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

QML自制数字软键盘:向TextInput/TextField键入数字并实现左移右移删除

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬