ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot整合Redis配置详解:从连接池到序列化避坑指南

Spring Boot整合Redis配置详解:从连接池到序列化避坑指南 1. 先从基础说起Redis装好了后面才不会反复折腾聊到Redis的Spring配置其实很多问题不是出在Spring代码上而是Redis基础环境没搭好。我见过不少团队把代码层面排查了个遍最后发现是本地Redis是Windows老版本配置文件里连密码都没设置或者用了一个特别老的可视化客户端连Redis 6之后引入的RESP3协议都不认识白折腾一晚上。1.1 本地装Redis的几个版本细节如果你是本地开发调试Windows上常见的做法是去GitHub找一个Redis的Windows移植版本比如tporadowski/redis支持到Redis 5.x。但说句实在话建议直接用WSL或者Docker跑官方Linux版本因为线上环境大概率是Linux本地方言和线上越接近越能提前暴露问题。Docker方式最省事一条命令就能拉起来docker run -d --name redis-local \ -p 6379:6379 \ -v /data/redis/redis.conf:/etc/redis/redis.conf \ redis:7.2-alpine \ redis-server /etc/redis/redis.conf这里有个小建议一定要挂载配置文件不要用默认参数直接跑。Redis官方默认配置对本地开发够用但bind 127.0.0.1和protected-mode yes这套组合在Docker映射端口后经常把连接挡住反而让人误以为是Spring配置写错了。如果用的是Linux服务器部署我更偏向直接编译安装或者用apt/yum装稳定版然后单独建redis用户跑避免用root起服务。配置文件里至少要确认几个关键参数bind要么注释掉改成监听所有网卡要么精确指定IP别用0.0.0.0。protected-mode设了密码之后这个可以保持yes不设密码就建议改成no但如果你直接对公网开放6379裸奔状态分分钟被扫描工具盯上。requirepass测试环境我一般也会加一个强密码因为从Redis 6开始有ACL体系生产环境更是要单独建业务账号别用默认的default账号走天下。maxmemory和maxmemory-policy这个很多人忽略本地测试可能无所谓但一旦数据量涨起来没有淘汰策略Redis可能在内存耗尽时直接OOM崩溃。还有一点我得提醒appendonly。本地调试如果不想留持久化文件可以关掉但如果你在开发缓存功能时把Redis当临时存储比如存验证码、存临时token那至少把快照RDB开着省得重启后数据全没了还得重新构造测试数据。1.2 选一个顺手的可视化客户端可视化工具这块现在比较主流的是Redis Insigh原Redis Desktop Manager的继任者、Another Redis Desktop Manager这两款。我个人用下来觉得Another Redis Desktop Manager在Windows环境更轻快支持SSH隧道、集群节点透视、慢日志查询界面也直观。不过要提醒的是可视化工具只是辅助真正排查线上Redis问题命令行才是王道。我习惯在Spring项目里保留一个CommandLineRunner启动时打印Redis连接状态或者用redis-cli -a 密码 ping快速验证网络和认证是否正常。很多连接问题用可视化工具看是连不上用命令行一敲就知道是网络不通还是密码不对。2. Spring Boot整合Redis的第一步依赖和连接参数Spring Boot的Redis自动配置其实已经把大部分事情做完了但这不代表你可以完全不看底层依赖。我在项目里见过太多组合错乱的pom文件比如同时引入了spring-boot-starter-data-redis和jedis又手动塞了一个lettuce-core的旧版本导致连接池参数完全不生效。2.1 依赖怎么选选哪个先明确一个基本概念Spring Data Redis里的底层连接工厂有两种实现一种是Lettuce一种是Jedis。Spring Boot 2.x之后默认用的是Lettuce因为它的响应式支持和连接复用做得更好Jedis是阻塞IO模型API简单直接但多线程下表现不如Lettuce。所以除非你有非常特殊的理由否则直接用默认的Lettuce就行不要额外引入Jedis避免配置文件里写的连接池参数到底归谁管都搞不清楚。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency !-- 如果用连接池一定要显式引入commons-pool2 -- dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependency这里有个极其关键的坑Lettuce默认是不启用连接池的而是每个线程/请求单独获取连接。你以为配置了lettuce.pool.max-active就一定生效不一定Spring Boot官方文档里写得很清楚只有在classpath里有commons-pool2且spring.data.redis.lettuce.pool.enabledtrue实际上官方默认true但要依赖存在时才启用连接池。不引commons-pool2配置了等于白配。2.2 application.yml里的核心配置项下面这份配置是生产中实际用过的我加了注释说明每个参数的含义和常见取值spring: data: redis: host: 127.0.0.1 port: 6379 password: your-redis-password database: 0 timeout: 3s # Lettuce 连接池配置 lettuce: pool: enabled: true max-active: 16 # 连接池最大连接数 max-idle: 8 # 最大空闲连接数 min-idle: 2 # 最小空闲连接数 max-wait: 3s # 获取连接最大等待时间超过抛异常 shutdown-timeout: 200mstimeout这个参数值得展开说说。它指的是连接Redis的超时时间不是读写超时。默认是60秒但在高并发场景下如果你一次请求卡住60秒才报错调用方早就超时了。我建议设成1-3秒快速失败比傻等更有意义。一旦报错业务侧能迅速走降级逻辑而不是把线程池占满。database这个字段也容易被忽略。Redis默认有16个逻辑库很多团队喜欢0-15按业务隔离。但注意如果你后面用Redis Cluster多库是不生效的只能用一个库。我建议要么早点规划好key的前缀策略比如user:info:{userId}要么干脆用多实例隔离别把多个业务的缓存全堆在0库上线后排查数据归属很痛苦。2.3 连接池参数到底怎么调连接池参数不是越大越好。我见过有人为了压测把max-active调到200结果Redis端连接数被占满而且Redis是单线程模型连接数太多反而在线程上下文切换上浪费性能。一个比较实用的估算口径假设你的应用有200个并发请求Redis操作耗时平均3ms那么稳定状态下同时需要的连接数大约是200×3ms/1000ms 0.6个按这个算其实很小。但实际操作有抖动预留3-5倍的余量就差不多了。所以我一般推荐max-active在16-32之间max-idle在8-16之间mini-idle保持2左右保证低峰期也能快速响应。如果业务侧经常拿到连接失败异常别急着调大连接池先看看是不是Redis端timeout参数设得太短服务端把空闲连接提前回收了客户端这边连接池又不知道还在池里复用一个已经被服务端断开的连接。这种问题排查起来很隐蔽通常会在日志里看到RedisConnectionFailureException但Redis明明还活着。3. 序列化方案九成项目这里都会踩坑Spring Boot默认会给RedisTemplate配置两个序列化器key用StringRedisSerializervalue用JdkSerializationRedisSerializer。看到这个组合你就该知道要动手改了——因为JDK序列化在生产里几乎是毒瘤。3.1 默认JDK序列化的问题JDK序列化有几个明显缺点第一存进去的数据肉眼不可读。你要是直接在redis-cli查key看到的value是一串以\xAC\xED\x00\x05t开头的二进制乱码排查数据时特别痛苦。第二序列化体积大。JDK序列化会附带大量类元数据、类信息同样一个对象用JSON存可能只有几十字节JDK序列化后可能几百字节。缓存一多额外浪费的内存相当可观数据落RDB和AOF时体积也会跟着膨胀。第三反序列化版本兼容性差。类的serialVersionUID一旦变化之前的缓存数据全部反序列化失败直接抛InvalidClassException。这在升级实体类时很常见我就踩过属性加了一个字段忘了更新serialVersionUID结果线上缓存大面积报错后来全是靠清缓存才恢复教训深刻。3.2 生产环境推荐的序列化组合我这边推荐一套组合也是目前业界用得最多的keyStringRedisSerializervalueGenericJackson2JsonRedisSerializerkey用String这个没悬念Redis本身就是字符串字典key弄成对象序列化不仅难读而且keys搜索和TTL设置都别扭。value用GenericJackson2JsonRedisSerializer而不是普通的Jackson2JsonRedisSerializer两者区别在于前者会把对象的类信息写到JSON里反序列化时能自动还原成目标类型不需要你手动指定Class。代价是JSON字符串里多了一个class字段体积略大但换来的是极大的便利。有一些团队会用Jackson2JsonRedisSerializer并指定具体的类这样做缓存结构更干净但redisTemplate的方法要绑定类型比如opsForValue().set(user, user, Duration)没问题但opsForHash().put(hash, field, user)时泛型推导容易出问题。个人经验项目里如果缓存对象类型很多用GenericJackson2JsonRedisSerializer省心如果缓存对象就一两个固定类型用Jackson2JsonRedisSerializer指定Class更可控。3.3 自定义RedisTemplate配置类聊到配置类很多文章只给片段我把自己项目里稳定跑了一阵子的完整版本放出来Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); // 使用 GenericJackson2JsonRedisSerializer 序列化 value GenericJackson2JsonRedisSerializer jsonSerializer new GenericJackson2JsonRedisSerializer(); // key 和 hash 的 key 使用 String 序列化 StringRedisSerializer stringSerializer new StringRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }注意一个细节setHashKeySerializer和setHashValueSerializer必须单独设置。因为RedisTemplate默认对hash的key和value使用了JdkSerializationRedisSerializer不覆盖的话你用opsForHash()往Redis里塞数据hash field会出现乱码然后在业务侧执行hashOps.get(user, name)时能查到但直接看Redis里的key就是一堆\xAC\xED开头的东西特别容易引起误会。还有一个经验供参考对纯字符串的缓存操作直接用StringRedisTemplate更顺。StringRedisTemplate的key和value默认就是StringRedisSerializer不会出现序列化八竿子打不着的情况。我一般在代码里两个都注入规整的缓存对象走RedisTemplateToken、验证码、分布式锁这类纯字符串场景走StringRedisTemplate分工清晰。4. 和Spring Cache集成注解式缓存背后的公平与陷阱Spring Cache注解缓存属实好用一个Cacheable就把缓存逻辑塞进去了。但好用归好用配置上还是有不少值得研究的地方。毕竟Spring Cache和Redis结合时缓存过期时间不是直接写Redis的TTL而是通过CacheManager内部去设置稍不留神就出现缓存永不过期的IMO问题。4.1 开启缓存与CacheManager配置首先要加EnableCaching注解放在启动类或者配置类上都行。然后需要定义一个RedisCacheManager的Bean但这里有个巨坑Spring Boot自动配置里是有默认的CacheManager的但默认的RedisCacheManager用的是RedisCacheConfiguration的默认配置value序列化器是JdkSerializationRedisSerializer。也就是说就算你前面自定义了RedisTemplate缓存注解走的那套是另一套配置两者不互通。我见过有人自定义完RedisTemplate然后用Cacheable去存对象结果Redis里value又是乱码了。核心原因就是CacheManager的序列化没跟着改。一个比较规范的自定义写法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(); }entryTtl(30分钟)这个默认超时时间建议按业务来。如果不设置默认就是永不过期等到你缓存数据量越来越大才反应过来Redis内存早就吃紧了。4.2 Cacheable、CachePut、CacheEvict的使用细节这三个注解用起来简单但细节上有些容易踩的坑Cacheable是先查缓存没有再调方法然后缓存结果。适用于查询路径。要注意的是如果方法内部抛了异常缓存是不生效的因为Spring Cache默认只在方法正常返回后写缓存。CachePut是每次调用方法再更新缓存。适用于数据变更场景比如更新用户信息时同步更新缓存。这个注解的返回值会被写进缓存所以方法必须返回需要缓存的数据。CacheEvict是删除缓存。特别要注意beforeInvocation参数默认是false也就是说方法执行成功后才删缓存。如果方法执行抛异常缓存就不删了。有些场景需要先把缓存删掉再执行方法那就要设置beforeInvocation true但这样也有风险方法执行失败后缓存也没了下次查询就要穿透到数据库。还有个细节CacheEvict支持allEntries true清空整个缓存分区。这个操作要慎用如果一个分区下面有大量的key清空操作对Redis其实是原子的但如果并发高清空瞬间可能会有大量请求同时打到数据库。这种情况建议用双删延迟删除的策略或者干脆让缓存自然过期别主动清。4.3 缓存分区与key的设计Spring Cache里的value或者cacheNames对应的是缓存分区在Redis里会以cacheNames::key的格式存储。比如Cacheable(cacheNames user, key #userId)最终Redis里的key是user::123。这里有个常见的坑多个业务模块如果用了同一个cacheNamekey很容易冲突。比如list这种泛泛的名称不同方法都往list分区里写A方法写进去的缓存对象反序列化时可能被B方法的返回值类型解析失败报错现场很难排查。我的经验是cacheName要尽量带上业务含义比如user:info、order:detail、product:list用冒号分隔层级也符合Redis key的组织习惯。key的SpEL表达式也得注意精度。用#user.id而不是#user因为如果用整个对象作为key那对象里任何一个字段变化都会导致缓存key变化缓存命中率会大打折扣。还有一点key拼出来如果是空的或者nullSpring Cache会报InvalidCacheKeyException调用前最好校验一下关键参数。5. 实战问题排查与避坑清单这部分我准备了一份自己平时排查Redis配置问题时的思路按频率从高到低排序每一条都是真实踩过的坑。5.1 连接不上Redis先从这四步查遇到连接异常别先怀疑代码。我这边习惯按这个顺序排查第一步先确认为什么连不上。用telnet 127.0.0.1 6379或者redis-cli -h 你的Redis地址 -p 6379 -a 密码 ping验证网络和端口。如果telnet都连不上大概率是Redis没启动或者防火墙拦了端口Spring配置里的host/port改来改去都是无用功。第二步确认密码。Redis如果设置了requirepass而Spring配置里没填password会报NOAUTH Authentication required。注意这个异常不是连不上是连上了没授权。第三步确认database是否存在。如果你配置了database: 2但Redis的databases参数设置的是16一般默认都有但如果改了databases参数就会出问题。此外默认配置是select 2时不会报错也是坑。第四步确认是否触发了maxclients限制。Redis配置里默认maxclients是10000但如果连接数满了新连接会被拒绝。此时redis-cli info clients一看就清楚。这种情况先看日志有没有max number of clients reached有的话再回头调客户端连接池而不是无脑加大池子。5.2 反序列化异常的一类典型现场如果你在日志里看到ClassCastException或者RedisSerializationException先不用着急大概率是序列化配置不统一导致的。我之前就遇到过一个奇葩问题代码里用RedisTemplate存了对象后来又改用StringRedisTemplate去读取。虽然存的都是字符串形式的值但RedisTemplate默认是JdkSerializationRedisSerializerStringRedisTemplate是StringRedisSerializer两边序列化方式不一致读出来的数据就完全解析不了。还有一种情况是项目从早前的自定义序列化方案切到了GenericJackson2JsonRedisSerializer旧的缓存数据没有清理反序列化时报错。这种情况只能清掉对应key的旧数据或者写一个兼容反序列化器在项目启动时做一段数据迁移。没有一劳永逸的办法所以序列化方案越早定下来越好。5.3 容易被忽略的几个配置细节有几个时钟相关的坑这里统一絮叨一下。第一Spring Boot 2.x里的spring.redis.*配置已经废弃要改用spring.data.redis.*。如果你在网上搜到的是老文章复制了spring.redis.host这份配置在Spring Boot 3.x里根本不生效连接还是默认的localhost。这个坑太常见了升级Spring Boot版本时配置不迁移全是玄学问题。第二RedisTemplate在注入时要注意是否有多个Bean。如果项目里同时有多个RedisConnectionFactory比如本地一个、生产一个那RedisTemplate的Qualifier一定得标注清楚否则启动时直接NoUniqueBeanDefinitionException。第三别忘了Transactional和Redis操作的组合问题。Spring事务默认只控制数据库事务Redis的操作不受事务管理。如果业务里先写数据库再写Redis数据库回滚后Redis里的数据就成了脏数据。一个简单的规避策略是数据库事务提交成功后再去更新Redis或者监听事务提交事件在afterCommit里执行Redis操作。第四关于key的TTL设置。用opsForValue().set(key, value, Duration)设过期时间时Duration不能太长也不能为null。我之前有个需求是缓存不过期结果直接不传Duration结果真的没过期后面一直成了历史包袱。5.4 缓存穿透、击穿、雪崩的初步防御虽然标题是配置但既然做缓存这三个词迟早躲不开。我简单说下配置层面的应对思路细节就不展开了。缓存穿透是查询一个一定不存在的数据。可以在代码里对空值也做缓存缓存一个短的过期时间比如3分钟。Spring Cache里如果方法返回null默认是不写缓存的除非你开启cacheNullValues但这样缓存里大量空值会影响命中率。更推荐用Set集合把存在的id都维护一份启动时加载或者用布隆过滤器挡一下。缓存击穿是某个热点key过期瞬间大量请求打到数据库。可以设置热点数据永不过期或者用分布式锁控制重建缓存的并发。配置层面可做的是把热点key的过期时间打散别让多个热点同时过期。缓存雪崩是大量key在同一时刻过期。可以在设置过期时间时加一个随机偏移量比如10分钟过期时间基础上再加0到2分钟的随机值。Spring Cache里统一配置的TTL没法做随机所以对需要错峰过期的数据建议不走注解而是手动用RedisTemplate设置TTL每个key单独加随机值。6. 分享一个我常用的扩展思路上面这些配置基本够日常项目用了但如果你想更顺手一点可以再往前迈一步自己封装一个缓存工具类把常用操作收敛起来。比如封装一个CacheService里面提供get、set、setIfAbsent、setWithRandomExpire、delete等方法底层注入StringRedisTemplatevalue统一JSON序列化。这样每次写业务代码时不会在RedisTemplate和StringRedisTemplate之间纠结也更方便做统一的异常捕获和降级处理。另一个建议是监控这一层。Redis连接数、命令执行时延、缓存命中率这些指标即使不做专业监控系统也至少要在Redis里开慢日志CONFIG SET slowlog-log-slower-than 10000 CONFIG SET slowlog-max-len 128然后把慢查询定期拉出来看看。缓存出问题通常不是配置瞬间崩掉的而是慢查询、大key、热点key这些问题一点一点拖出来的。早发现、早处理比出了问题再调配置省心得多。就我个人经验来说Redis的配置没有一套放之四海皆准的参数核心是把每个参数背后的原理弄明白再根据自己项目的并发量、数据量去微调。本文这套配置适合中小型项目起步等到规模上去了还要考虑分片、主从、多级缓存这些更复杂的架构决策。到那一步配置的边界会越来越模糊你对Redis本身的理解才是真正可靠的东西。
返回列表