SpringBoot整合Redis实战:序列化、缓存穿透与分布式锁
发布时间:2026/9/26 22:52:50 锦皓数字建站

1. 先把环境弄明白Redis装不好后面全是坑这个月好几个朋友问我SpringBoot连Redis的事有的是连不上有的是存进去取出来乱码还有一些是存个对象直接报序列化错误。问题五花八门但归根结底大部分人在动手之前根本没用过Redis也不知道Redis装起来到底是个什么东西。这里先给个整体概念。Redis是内存数据库跑起来之后默认监听6379端口。SpringBoot要做的事情就是通过spring-boot-starter-data-redis这个依赖让你的Java程序能连上它、发命令、收数据。SpringBoot本身没有任何魔法它只是把Jedis或者Lettuce这两个都是Java的Redis客户端包了一层让你少写一堆样板代码。这个内容能帮你解决什么环境搭建、SpringBoot配置、序列化问题、分布式锁、多环境部署还有我在实际项目里踩过的那些配置文件和工作排查的坑。这两个客户端的区别值得说一下。SpringBoot 2.x之后默认用的Lettuce它底层是Netty多路复用高并发下连接利用率高。Jedis是阻塞IO并发高的时候得靠连接池撑着。网上很多教程还在教怎么配Jedis连接池其实SpringBoot 2.x以后用Lettuce的话默认共享连接就是多路复用的你配一个很大的池子意义不大除非你有特殊场景。接下来先搞定Redis本体。这个前置步骤完不成后面SpringBoot层面全是白搭。1.1 Linux环境安装最顺畅的姿势看官方文档我个人最推荐的方式是源码编译安装。别怕编译这两个字其实就是三行命令的事。先到Redis官网下载稳定版一般次版本号是偶数的那个就是稳定版比如6.2.x、7.0.x。奇数版本号比如6.3、7.1是开发版生产环境别碰。wget https://download.redis.io/releases/redis-7.0.11.tar.gz tar xzf redis-7.0.11.tar.gz cd redis-7.0.11 make make install编译完make install会把可执行文件放到/usr/local/bin目录下包括redis-server和redis-cli。然后你起服务的时候不要直接裸跑redis-server那会在前台一直挂着终端一关就没了。标准做法是改配置文件然后后台启动cp redis.conf /etc/redis.conf sed -i s/^daemonize no/daemonize yes/ /etc/redis.conf redis-server /etc/redis.conf这里daemonize yes是让Redis在后台运行。用这种方式启动日志默认会打到stdout但后台模式下就没有stdout了所以生产环境还必须把logfile的路径配上比如logfile /var/log/redis.log不然出了故障你连排查的依据都找不到。1.2 用Docker的人主从配置能省一半事要是你平时就是Docker环境开发装Redis其实更省事尤其想做主从复制或者集群的话。一条命令跑单机docker run -d --name redis \ -p 6379:6379 \ -v /myredis/data:/data \ -v /myredis/conf/redis.conf:/etc/redis/redis.conf \ redis:7.0 \ redis-server /etc/redis/redis.conf注意挂载配置文件的写法。不挂载配置启动就是默认配置持久化策略、密码、最大内存全都没有裸奔状态。另一个细节Docker里的Redis容器默认是以root跑的如果配置文件里开了protected-mode你在宿主机上用客户端连会被当成外部访问给弹回来。实际项目里还是要配密码和bind策略别图省事。1.3 Windows用户的实际操作路径我一直觉得Windows下的原生Redis官方包就停留在一个老版本上目前Windows版Redis停在了5.0.x公司电脑没法装WSL或者不想用Docker的时候有两个选择去Redis官方GitHub仓库的Windows分支找归档zip包双击运行redis-server.exe就能用。用choco install redis-64或者直接去下载Redis绿色解压版运行方式都一样。Windows下用redis-cli做连通性测试redis-cli -h 127.0.0.1 -p 6379 ping这个命令如果能回复PONG说明你的Redis服务已经站起来了可以进入下一步。1.4 redis-cli和可视化客户端调试阶段的命根子官方自带redis-cli是做故障排查最快的方式之一。比如你想直接看SpringBoot塞进去的key到底长什么样redis-cli KEYS * GET yourKeyName TTL yourKeyName我见过太多人在SpringBoot里一脸懵RedisDesktopManager那个现在都叫Another Redis Desktop Manager了或者Redis Insight这类可视化工具一打开整个数据结构一目了然。尤其是查key过期时间、看哪些key占内存可视化工具比敲命令直观太多。2. SpringBoot集成Redis配置项和后端代码一次走通2.1 依赖引入和版本隐性问题新建SpringBoot项目时选依赖直接勾Spring Data Redis就行。如果用的Maven手动加这一段dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency需要在pom里额外引入common-pool2吗以前很多人会加因为在旧版本里如果不在类路径上放一个Apache Commons Pool2你无法使用连接池。但是SpringBoot 2.x之后内置了Lettuce这个依赖不再必须。除非你主动配置了commons-pool2相关的连接池参数否则不需要加。加上了也不会出错但没必要。因为RedisTemplate需要Jackson序列化的话那样还得再引入jackson的依赖这个后面序列化章节会细说。要注意的隐性问题SpringBoot的starter版本要跟你的SpringBoot主版本对应这个一般不用管SpringBoot BOM会管好。真正容易出问题的是你自己引了jedis版本跟SpringBoot管理版本冲突导致运行时NoSuchMethodError这个是我的亲身经历代码里加了jedis坐标但没写version后来被SpringBoot BOM指到了老版本方法找不到直接炸了。2.2 YAML配置单机、密码、多库全参数SpringBoot里Redis的自动配置只需要在application.yml里写spring.redis开头的配置这里给一份我常用的完整参数spring: data: redis: host: 127.0.0.1 port: 6379 password: yourpassword database: 0 timeout: 3s lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2 max-wait: 2s注意SpringBoot版本差异SpringBoot 2.x用spring.redisSpringBoot 3.x用spring.data.redis。这个问题是在项目升级的时候最容易被忽略的迁移到3.x之后原本的配置静默失效然后你在那边排查半天为什么连了别人的Redis。参数解释一下参数作用建议值hostRedis服务地址局域网用内网IP别用localhostport6379默认按需修改password连接密码生产必配database默认16个库用哪个单机环境0-15建议各业务分开timeout连接超时别配太长3秒足够pool.max-active最大活跃连接数16-32之间我就满意了pool.max-wait拿到连接的最大等待毫秒数2秒上限别让线程死等关于timeout参数Redis官方凉了就是客户端跑个ping对方没回就等。你把它设成分钟级等客户端把连接都给耗死系统就直接卡死了听我的3s到头了。2.3 缓存注解的开关EnableCaching别漏很多刚用Redis做缓存的兄弟在Service方法加Cacheable然后发现根本没生效。原因多半是忘了在主类加EnableCaching注解。SpringBootApplication EnableCaching public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }这个注解是Spring的Cache抽象总开关不加它Spring容器里根本不会注册RedisCacheManager所有的缓存注解都会形同虚设。别问我是怎么知道的我一开始也是漏了它排查了两小时日志干干净净最后发现是总开关没开。2.4 StringRedisTemplate和RedisTemplate到底用哪个SpringBoot默认配好了两个BeanRedisTemplateObject, Object和StringRedisTemplate。很多人上来就直接Autowired RedisTemplate觉得模板名字大就完事了。但实际上RedisTemplate默认用的是JdkSerializationRedisSerializer存进去的字符串会带一堆乱码类型前缀数据看着就是\xAC\xED\x00\x05t\x00\x05hello这种非常难看而且跨语言客户端比如Python去读根本解析不了。而StringRedisTemplate所有key和value都按String处理读写干净、直观。我的建议能用StringRedisTemplate就用尤其做缓存、存JSON、计数器这种场景。如果非要用RedisTemplate存对象那你必须自定义序列化器这个下一节展开讲。Service public class UserService { Autowired private StringRedisTemplate stringRedisTemplate; public void cacheUser(String userId, String json) { stringRedisTemplate.opsForValue().set(user: userId, json, 30, TimeUnit.MINUTES); } }比如热点用户信息你在Service层已经把对象转成了JSON字符串这种情况下存String就够了根本不需要序列化器介入。3. 序列化问题深挖乱码、ClassCastException和可读性3.1 同样存一个字符串进Redis的差别有多大直接上一段对比。用RedisTemplate和StringRedisTemplate各写一个字符串stringRedisTemplate.opsForValue().set(hello, world); redisTemplate.opsForValue().set(hello, world);然后用可视化客户端一看StringRedisTemplate写入的key是hellovalue是world非常干净。RedisTemplate写入的key是\xAC\xED\x00\x05t\x00\x05hellovalue是\xAC\xED\x00\x05t\x00\x05world。这个\xAC\xED开头的东西就是Java序列化写入的Magic NumberSTREAM_MAGIC。Redis只是个存储方它不知道你存的是Java序列化字节还是字符串。你拿Redis命令行或者其它语言客户端去读看到的自然是一堆乱码。如果两边都用SpringBoot程序自己读倒还能读回来因为反序列化用的也是同一套Java序列化规则。但读出来的东西是Object用的时候要强转而且如果你实体类字段变了、版本号变了反序列化直接报错缓存直接废掉。3.2 自定义RedisTemplate序列化配置的正确打开方式更通用做法是自定义一个RedisTemplatekey用StringRedisSerializervalue用Jackson2JsonRedisSerializer这样存的value就是可读的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; } }这里我推荐用GenericJackson2JsonRedisSerializer它跟Jackson2JsonRedisSerializer的区别在于前者会在JSON里带上class类型信息这样反序列化的时候才能还原成对应的Java对象。后者需要你自己指定目标类型不指定类型它反序列化出来就是个LinkedHashMap。但是注意带类型信息也意味着写入的内容里每条数据体积会大一些。数据量小的无所谓海量数据、追求极致内存效率的场景就要考虑这个额外开销了。3.3 常见报错LocalDateTime序列化失败用Jackson序列化框架处理实体类如果实体里有LocalDateTime、LocalDate这种Java 8时间类型直接序列化会报错因为Jackson默认不支持这些类型。解决办法是注册JavaTimeModule并且对日期格式做统一处理。最省事的方式是全局配置ObjectMapperConfiguration public class JacksonConfig { Bean public ObjectMapper objectMapper() { ObjectMapper mapper new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); mapper.setDateFormat(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss)); return mapper; } }这样整个项目里的RedisTemplate、消息队列、HTTP消息转换器都会统一走一套序列化规则。推荐把这个配置放在项目公共模块里而不是每个微服务都单独写一份不然各自为政很容易出现A服务写入的格式跟B服务读出来的格式对不上。3.4 跨语言/跨模块读数据的注意点如果你的Redis数据要被多个服务甚至多语言栈共享Java序列化字节流就会成为一个灾难。Python、Go、Node.js都没法直接解析Java的序列化内容它们拿到的就是一坨字节还得手工剥。所以任何在服务间共享的数据我强烈建议统一JSON格式。另外一个小坑是泛型擦除。GenericJackson2JsonRedisSerializer反序列化时是靠写入的class字段来还原类型的你读的时候再指定泛型其实没太大意义。但是如果你用的是Jackson2JsonRedisSerializer并且指定了对象类型比如new Jackson2JsonRedisSerializer(User.class)那么存入ListUser的时候反序列化出来就会变成一个LinkedHashMap集合然后你在强转的时候直接ClassCastException。这个中文社区里被问烂了用Generic版本能绕过去。4. 实际业务里怎么把Redis用好缓存击穿、分布式锁与其它Redis在SpringBoot里除了当缓存做分布式锁和限流是高频场景。这一部分不给你抄代码就讲怎么设计我贴几个能直接落地的方案。4.1 商品详情页的缓存更新与过期策略典型场景是商品详情页。数据放Redis里key形如product:info:{id}缓存时间能定30分钟。但商品修改了价格那就得做到先更新数据库再删除Redis缓存。public void updateProduct(Product product) { // 1. 先更新数据库 productMapper.updateById(product); // 2. 再删除缓存 stringRedisTemplate.delete(product:info: product.getId()); }为什么要删除而不直接更新缓存因为更新缓存要处理并发写入顺序容易跟库里的数据不一致删了缓存下次请求进来自然会重新加载数据库最新值。这个套路叫Cache Aside Pattern简单可靠。但这里又有新的问题读到热点数据并发高时大量请求同时发现缓存失效全部打到数据库就叫缓存击穿。可以用逻辑过期结合互斥锁去玩简单的做法是加锁只放一个请求去DB里拉数据并回填缓存其余请求等待或走降级。public Product getProduct(String id) { String json stringRedisTemplate.opsForValue().get(product:info: id); if (json ! null) { return JSON.parseObject(json, Product.class); } String lockKey lock:product: id; String lockValue UUID.randomUUID().toString(); Boolean locked stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // double check json stringRedisTemplate.opsForValue().get(product:info: id); if (json ! null) { return JSON.parseObject(json, Product.class); } Product product productMapper.selectById(id); stringRedisTemplate.opsForValue().set(product:info: id, JSON.toJSONString(product), 30, TimeUnit.MINUTES); return product; } finally { // 防止误删别人的锁 if (lockValue.equals(stringRedisTemplate.opsForValue().get(lockKey))) { stringRedisTemplate.delete(lockKey); } } } else { // 没抢到锁等一下再取缓存也可以降级返回旧值 try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return getProduct(id); } }这段代码里会注意到我在finally里做了一次“值匹配再删除”。如果直接stringRedisTemplate.delete(lockKey)存在一个经典场景线程A加锁处理时间超过锁的过期时间锁自动释放线程B拿到锁然后线程A处理完把B的锁给删了这时候并发就彻底失控了。所以删除锁之前要先确认持有者是自己。这个场景真的是面试高频题实际项目里也真实存在。锁的过期时间要设置多久我一般遵循“预估最大执行时间的3到5倍”这个原则。比如我预估数据库查询和回填最多500ms我就设2到3秒。设太短业务还没跑完锁就没了设太长万一宕机锁一直挂着别的请求全都过不来。4.2 Redisson不想手写锁就让别人帮你踩完坑如果觉得上面那个写法工程量大直接用Redisson。Redisson是一个Redis客户端框架它对分布式锁做了非常多底层细节处理包括看门狗自动续期。dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.27.2/version /dependency基本用法RLock lock redissonClient.getLock(product: id); boolean locked lock.tryLock(2, 10, TimeUnit.SECONDS); if (locked) { try { // 业务操作 } finally { lock.unlock(); } }Redisson做的最好的一点是看门狗机制只要你的业务线程还活着默认锁的有效期30秒但Redisson会在锁快过期时自动续期避免业务执行超过锁时间导致锁提前释放。这是手写Redis分布式锁时特容易踩的坑。但不建议为了用一个锁就全局引入Redisson。如果你的项目只需要简单的短任务互斥用原生RedisTemplate配合原子操作就够了Redisson的功能确实多但是会跟着额外开销和分布式场景之外的隐形复杂度。4.3 不同环境切换dev、test、prod和配置文件我们最早讲过SpringBoot 2.x和3.x的配置前缀问题。那当你从开发环境切到生产环境密码、地址、数据库编号全都变了怎么管理用spring.profiles.active来做多环境配置是最干净的做法# application.yml spring: profiles: active: dev# application-dev.yml spring: data: redis: host: 127.0.0.1 port: 6379 database: 0# application-prod.yml spring: data: redis: host: 10.0.0.15 port: 6379 password: ${REDIS_PASSWORD} database: 0生产密码从环境变量里读取不要明文写在git里。这个习惯可以救你一命。我遇到过最揪心的就是同事把生产Redis密码硬编码到配置文件里然后整个配置文件的模板在内部仓库里漂了两年换个领导就能看到。可以用${REDIS_PASSWORD}这种占位符在部署平台上注入环境变量这样配置文件即使泄露也没有密码原文。4.4 那种复杂查询的结果还需要Redis吗事务型操作、复杂SQL查询结果能塞Redis缓存吗当然能但缓存之后缓存一致性成本需要理清楚。查出结果集二十行缓存TTL设30分钟业务能容忍30分钟以内的延迟。可业务要求实时性非常强的话缓存就变成负担了。我的经验是能当缓存用的数据天然容忍脏读一段时间。比如文章阅读量、商品库存准确说库存不能只看缓存这种就适合放。如果每个请求都要求读到实时的数据你老老实实走数据库别硬上Redis。Redis是锦上添花不是雪中送炭。5. 进阶Redis常见故障场景和联合治理5.1 Redis连接瞬间爆掉连接池参数进阶策略真实生产环境中经常会遇到Redis连接数突然飙升到连接池上限然后报RedisConnectionFailureException: Unable to connect to Redis。这种问题的根源多半不是Redis本身挂了而是连接池看门狗没起作用或者上游一个慢查询拽了整个服务。排查思路分几步走先把连接数情况查看一遍redis-cli info clients看看connected_clients最大多少。看最大连接数配置redis.conf里的maxclients默认是10000。排查你的客户端是Lettuce还是Jedis有没有开连接池验证Lettuce底层可以复用同一个连接如果某个命令很慢这个连接的后续命令全部排队一堵全堵。当你确认是应用侧连接池配置不合理时再调整池参数。但也要注意Lettuce的共享连接模式跟连接池参数并不完全等同它是对Netty事件循环的抽象。我之前有个项目并发很高默认值也够用但后来突然挂了一次最后一查是有一个定时任务每30秒扫描一次全量keys命令阻塞了Redis单线程其他请求全部排队。这是Redis单线程模型最怕的场景慢命令。5.2 Keys命令的替代方案生产环境的SLA要靠Scan保证先强调一个核心认知Redis是单线程架构所有的命令都是串行执行的。KEYS *会把所有满足条件的key一次性扫描出来当key数量达到几百万这条命令就会阻塞Redis事件循环从几十毫秒到几秒不等期间所有客户端请求都会排队。线上这么玩一次你的业务方就要来敲你桌子了。线上如果要查哪些key匹配某个前缀用SCAN命令。它采用的是游标方式每次返回一小部分key不会阻塞整个服务。redis-cli SCAN 0 MATCH user:* COUNT 100这个COUNT 100是提示命令每次大概扫描的桶数不是返回100条。用客户端脚本循环调用直到游标返回0为止。包括SpringBoot里也推荐配合ScanOptions去做扫描而不是直接keys()。Java侧的正确写法SetString keys new HashSet(); ScanOptions options ScanOptions.scanOptions().match(user:*).count(100).build(); try (Cursorbyte[] cursor stringRedisTemplate.getConnectionFactory().getConnection() .scan(options)) { while (cursor.hasNext()) { keys.add(new String(cursor.next())); } }这个方式的优点是每次拉一批、不阻塞适合生产环境安全地做数据清理或者模糊匹配。5.3 大key和热keyRedis变慢的两大元凶Redis里单个key的value特别大超过1MB或者集合元素超过1万就称作大key。像直接将用户一整年的流水全部塞进一个list访问一次就要传输大体积数据网络IO和内存IO一起遭殃。而且删除大key也可能卡顿。实践中对集合类大key可以拆成多个分片key或者转移部分数据到数据库。热点key的典型场景是秒杀商品、微博热搜同一个key被海量请求读Redis单实例CPU突然暴涨。这时候可以做本地缓存兜底比如在SpringBoot应用里加一层Caffeine缓存短时间的重复请求直接在应用内命中Redis的压力一下就能降下来。5.4 缓存穿透Redis和数据库双重重压的防御方案穿透和击穿是两兄弟穿透是说每次请求查一个根本不存在的数据Redis拿不到、数据库也没有然后每次请求都打到数据库上。如果有人故意用不存在的ID批量刷数据库会被拖垮。常用防御手段空值缓存数据库查不到的时候在Redis里存一个空对象TTL设短一点比如60秒这样后续相同请求直接命中空值不会穿透到数据库。布隆过滤器在缓存前面加一道过滤器把不存在的key直接挡在门外。SpringBoot里可以用Redisson自带的RBloomFilter也可以自己用BitMap做。RBloomFilterString bloomFilter redissonClient.getBloomFilter(productFilter); bloomFilter.tryInit(1000000L, 0.01); boolean mightExist bloomFilter.contains(product:99999);布隆过滤器说“不存在”就一定不存在它的误判只会发生在“存在”这一侧所以拿这个来挡绝对穿透是很可靠的。不过布隆过滤器的维护有一个点就是数据新增时要记得往过滤器里加对应的key不然新数据会被自己误伤。6. 从开发到上线改完配置之后验证手段和常见问题排查6.1 本地自测的五个动作每次改完Redis配置个人建议最少跑一遍下面的验证redis-cli -h host -p port ping确认Redis可达。redis-cli -h host -p port auth password确认密码正确。启动SpringBoot应用看日志里有没有报连接错误。用StringRedisTemplate写一个测试接口写入一个key再读出来确认读写正常。用内存监控工具如INFO memory查看used_memory确认没写怪东西。6.2 连接超时、密码错误和序列化失败的三类现场连接超时报错特征RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException排查顺序Redis服务是否真的在运行ps -ef | grep redis。端口是否被占用ss -lntp | grep 6379。防火墙是否放行云服务器的话安全组规则也别忘了。SpringBoot配置的host是不是指向了错误的地方在IDEA里跑SpringBoot然后连不上最常见原因是localhost和127.0.0.1的区别以及本机Redis用了非默认端口但配置里写了6379。密码错误报错特征ERR Client sent AUTH, but no password is setRedis没设密码但你发AUTH了或者WRONGPASS invalid username-password pair密码真的不对。这两种情况都看一眼配置文件和Redis端requirepass配置。另外SpringBoot 3.x的spring.data.redis.password如果没设置Lettuce会尝试AUTH吗不会只有你显示配置了才发。所以这个报错一般是配置了对不上。序列化错误报错特征java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.xxx.User这其实就是之前讲的读取时用Jackson2JsonRedisSerializer指定了类型但实际集合类型丢了。解决方案要么用GenericJackson2JsonRedisSerializer重新序列化要么读取时不要直接强转集合泛型手动用ObjectMapper去转。6.3 一个完整项目里Redis相关代码的目录结构设计有些兄弟把Redis操作全写在Controller里看起来感觉很爽。但是项目一大了之后更建议沉淀出单独的RedisService甚至按业务域拆RedisKey常量类。这样集中管理key前缀、过期时间、序列化方式后面改起来不痛苦。一个参考的目录结构com.example.project ├── common │ ├── redis │ │ ├── RedisConfig.java │ │ ├── RedisKeyConstants.java │ │ └── RedisService.java ├── service ├── controllerRedisKeyConstants里把key的设计统一管理起来public class RedisKeyConstants { public static final String PRODUCT_INFO product:info:; public static final String USER_TOKEN user:token:; public static final String LOCK_PREFIX lock:; }这样至少能保证key的格式在交付时是一致且有规律的排查效率也高很多。7. 我踩过最贵的几个坑全贴在这里最后分享几个我实际遇到过、网上教程通常不会教你的细节。RedisTemplate的afterPropertiesSet不能少。自定义RedisTemplate时如果只是setConnectionFactory然后直接返回没有调用template.afterPropertiesSet()在部分Spring版本下序列化器不会初始化成功后面读写数据就会以null serializer运行结果是什么运行时IllegalArgumentException: RedisConnectionFactory must not be null。这个错特别唬人其实你明明设置了connectionFactory但生命周期方法没触发导致产物校不过去。bigkey的思想要用到所有字段。不只是Redis单个key大一个业务里所有key的总数量、单个key的过期时间管理、内存占比都需要做治理。线上Redis内存可用INFO memory去看used_memory_human如果达到了maxmemory配置Redis会按淘汰策略默认noeviction拒绝写入请求返回OOM command not allowed when used memory maxmemory。然后整个缓存系统就开始雪崩新值写不进去老值还在慢慢过期。不要在事务型场景里依赖Redis事务。Redis的MULTI/EXEC事务没有回滚也没有乐观锁它只是简单的排队执行。你要是想保证账户余额不超扣老老实实用原子操作DECR、INCR或者用Lua脚本。SpringBoot的Transactional管不了Redis很多刚入行的同事以为开了事务数据库和缓存就能一起回滚这是误解Redis根本不参与数据库事务。配置文件的空格和缩进是YAML最大的敌人。我见过不止一次同事从网页复制了一段Redis配置缩进是一个Tab加四个空格SpringBoot直接启动不了报错mapper.parser.ParseException: while parsing a block mapping。YAML对缩进要求极度严格要么统一用空格要么统一用Tab而且嵌套层级必须对齐。IDEA装一个YAML插件马上就能看见格式问题。Redis的淘汰策略选择要明白优化目标。缓存框架只默认noeviction意思是内存满了不淘汰直接拒绝新写入。这是一个安全策略但对大多数缓存场景不合适。如果只是做缓存建议配allkeys-lru这样所有key参与淘汰最近最少使用的先被移除能极大降低冷数据堆积。这个配置要在redis.conf里体现或者启动时用命令行参数传入。关于选型我要再提一嘴。如果你的项目是Spring Boot 3.x开始的新项目客户端选型上直接考虑用Lettuce而不是Jedis这不是说Jedis不好而是Lettuce的响应式支持、连接复用能力更适合现代应用架构。Jedis的优势在于简单、API直观、同步阻塞模型好理解但SpringBoot默认配料就是Lettuce没必要非要去换。真遇到并发上不去先用Redis慢日志查慢命令SLOWLOG GET再回头调客户端配置。最后所有Redis相关的配置变更我的习惯是做一次记录一次尤其是从SpringBoot 2.x迁移到3.x的版本spring.redis改spring.data.redis这种替换最容易被当成无关紧要的升级忽略。你用Value(${spring.redis.host})这种写法在3.x里取不到值默认值就把你带到本地去了一个不小心测试环境连的就是本地的Redis数据奇奇怪怪排查两三天才发现是配置前缀没跟上版本。升级框架这种事配置文件的隐性变动一定要放到checklist里。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。