ARTICLE DETAIL

资讯详情

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

从零搭建Redis生产级应用:RedisTemplate序列化与连接池配置实践

从零搭建Redis生产级应用:RedisTemplate序列化与连接池配置实践 1. 先搞清楚为什么默认配置的RedisTemplate不能直接用我先说一个很多项目里出现过的真实场景新服务上线内存缓存用的是Redis开发阶段一切正常结果灰度一放量Redis里出现大量形如\xAC\xED\x00\x05t\x00\x04name的乱码键GC压力升高内存翻倍。查了半天定位到原因——默认的RedisTemplate用的是JDK序列化。这个坑几乎每个从零搭Redis的团队都会踩一次。标题里说的从零搭建Redis生产级应用绝不是把spring-boot-starter-data-redis加进依赖就完事。真正要做的是把连接工厂ConnectionFactory、序列化器Serializer、RedisTemplate这三层彻底吃透并且根据自己的业务场景做定制。这篇教程就按这个顺序一层一层拆开来讲每一步都给到可以直接抄的配置、代码和理由。先给一个整体认知框架。我们用Java操作Redis整个链条大概是这样的Lettuce/Jedis底层连接Redis服务器的客户端驱动。RedisConnectionFactory负责创建和管理这些底层连接就像数据库连接池。RedisTemplateSpring Data Redis提供的顶层操作封装。你写redisTemplate.opsForValue().set(key, value)它内部会走连接工厂拿连接然后通过序列化器把key和value变成字节再发给Redis服务器。很多人只关注第三层忽略了前两层结果生产环境一压测就出问题。所以本文的章节安排是先解决序列化家族的问题因为这是数据能不能正确读写的根本再深入连接工厂的每个参数这是高并发下系统稳不稳的关键最后把RedisTemplate的生产级封装完整落地。期间会穿插大量我在生产线上的排查经验。2. 序列化器家族全景选错一个数据就废一半2.1 四个主流序列化器的性格特点与适用边界Redis本身只存字节不关心你存的是String还是对象。所有可读性跨语言内存占用的诉求全都要靠序列化器实现。我先把Spring Data Redis中最常见的四个序列化器列出来用表格做一个直观对比序列化器存储格式可读性跨语言占用空间推荐场景JdkSerializationRedisSerializerJDK二进制极差极差很大不推荐除非只做临时缓存StringRedisSerializer纯字符串好好小key以及纯字符串valueJackson2JsonRedisSerializerJSON好较好中等value为对象能明确指定类型GenericJackson2JsonRedisSerializerJSON含class类型好一般较大value为对象类型多且不想逐个配置默认的RedisTemplate使用的是JdkSerializationRedisSerializer。它最大的问题有三个第一序列化后的字节里带类全限定名和内部结构非常占空间实测一个大对象比JSON方式能多出两到三倍的体积第二只有Java程序才能反序列化接口如果被Go、Python服务调用数据直接没法解析第三类结构一旦变化反序列化容易报错。当你看到Redis里出现\xAC\xED开头的key或者value时不用怀疑就是JDK序列化的产物。StringRedisSerializer是最简单的直接按UTF-8编码转字节。它通常用来序列化key这样在Redis Desktop Manager这类可视化工具里能直接看到user:info:123这样清晰的键名而不是\xAC\xED乱码。这也是很多团队约定俗成的做法key一律用String序列化器value再根据业务形态选择。Jackson2JsonRedisSerializer和GenericJackson2JsonRedisSerializer是我们处理value的主力选手。它俩的区别要重点说Jackson2JsonRedisSerializer在构造时必须明确指定一个对象类型比如new Jackson2JsonRedisSerializer(User.class)反序列化时它就按这个固定类型去解析而Generic版本会在JSON里额外写一个class字段把原始类路径记录下来反序列化时根据这个字段动态还原。前者性能略好、代码更可控后者灵活但多存了一些类型元数据而且存在安全风险后面我细讲。2.2 为什么key和value必须分开配置序列化器新手最容易犯的一个错误是给RedisTemplate四个方法keySerializer、valueSerializer、hashKeySerializer、hashValueSerializer全部塞同一个序列化器。绝大多数情况下key用String、value用JSON才是最优组合。原因在于读写逻辑的语义天然不同。key是检索的入口必须稳定、简洁、可读。试想一个场景运营同学要手动去Redis里删一批缓存key如果key是JDK二进制乱码他根本不知道哪个key对应哪个业务如果key是String序列化他可以一眼看出user:login:token:9527是哪个用户的登录态。而value承载的是复杂业务对象用JSON序列化让结构与字段清晰可见排查问题的时候直接读Redis里的JSON比反序列化再Debug高效得多。hash的结构同理。如果一个hash的field非常多field名也要保证可读性。我见过一个项目hashKey用了JDK序列化结果在可视化客户端看hash时field全是一串十六进制转义序列根本没法人工核对数据。所以我的习惯是redisTemplate的keySerializer、hashKeySerializer一律用StringRedisSerializervalueSerializer优先用GenericJackson2JsonRedisSerializerhashValueSerializer视hash内实际存储内容决定——如果存储的是简单字符串用String即可如果也是对象就用GenericJackson2JsonRedisSerializer。2.3 手写一个安全版GenericJackson2JsonRedisSerializer前面我提到Generic版本有安全风险这里得展开讲。GenericJackson2JsonRedisSerializer在反序列化时会根据class字段指定的类去实例化对象。如果Redis被攻击者写入恶意构造的JSON就可能触发不安全的反序列化行为类似Fastjson那类漏洞的原理。Spring官方其实也意识到这个问题默认配置里给了一个BasicJsonObject的允许列表但如果你直接把ObjectMapper换成自定义的等于把这个保护关了。生产级做法是在自定义ObjectMapper时手动配置一个允许反序列化的白名单。下面这段配置可以直接参考Bean public RedisSerializerObject redisSerializer() { ObjectMapper objectMapper new ObjectMapper(); // 允许序列化空对象否则某些getter返回null的对象会序列化失败 objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 反序列化时遇到未知字段不要报错保证兼容性 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 启用class字段允许携带类型信息 objectMapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY ); // 白名单只允许特定包前缀下的类被反序列化 objectMapper.registerModule(new SimpleModule()); // 关键配置白名单校验器防止任意类反序列化 objectMapper.setPolymorphicTypeValidator(polymorphicTypeValidator()); return new GenericJackson2JsonRedisSerializer(objectMapper); }白名单校验器的实现逻辑是检查类型名是否以指定的包前缀开头只有匹配的才放行。这是经验之谈我在一个对外提供缓存服务的中间件里就是这么遏制住的恶意反序列化尝试。3. 连接工厂Lettuce、Jedis和连接池参数的取舍3.1 默认选Lettuce还是Jedis生产依据是什么Spring Boot 2.x默认使用Lettuce作为Redis客户端3.x也是。Jedis则是很多老项目的选择。两者的核心差异在连接模型上。Jedis是阻塞式I/O操作是同步的每个线程持有独立的连接实例所以早期用它必须配连接池来复用连接。Lettuce基于Netty使用共享连接多个线程可以并发使用同一个连接底层I/O是异步事件驱动的吞吐量上限更高。Lettuce还支持Redis Sentinel、Redis Cluster等拓扑的自动节点发现和拓扑刷新这对生产环境的故障转移很关键主节点挂了Lettuce能感知到新主节点并自动切换连接而Jedis需要手动处理。所以在新建项目时我不会犹豫直接用Lettuce。除非是历史遗留项目强制用Jedis否则Lettuce是更省心的选择。3.2 连接池到底配不配配多少合适这里有一个反转认知Lettuce本身是共享连接模型默认情况下Spring Boot Data Redis给的Lettuce连接工厂是不启用连接池的。但生产环境我的建议是连接的线程安全性不等于免费无限并发高峰期的并发操作落在一条共享连接上仍然会出现排队和阻塞。尤其是执行pipeline、transaction这些批量操作时单连接无法提供足够的吞吐隔离。Lettuce官方其实提供了连接池的支持只是需要额外引入commons-pool2依赖。配置好后Spring Boot的LettuceConnectionFactory就能感知到GenericObjectPoolConfig。dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependency核心配置如下spring: data: redis: host: 192.168.1.100 port: 6379 password: yourpassword database: 0 timeout: 3s lettuce: pool: max-active: 32 max-idle: 16 min-idle: 4 max-wait: 2s time-between-eviction-runs: 30s参数含义和取值逻辑是max-active32连接池最多同时给出32个连接。这个值不是拍脑袋我一般按应用峰值QPS × 单操作平均耗时 / 1000 × 冗余系数粗算。例如峰值QPS 5000每次Redis操作平均2ms理论上需要10个连接为了应对突发再乘2倍左右32就够用了。max-wait2s等待获取连接的最长时间。超过这个时间如果还没拿到连接直接抛异常而不是无限等待。有些系统在这个参数上配置过大结果Redis抖动时线程全部hang在等待连接上最后拖垮了整个应用。max-idle/min-idle保持空闲连接数量。初始化4个空闲连接避免刚启动时频繁创建连接导致的前几次请求延迟偏高。time-between-eviction-runs30s空闲连接回收的扫描间隔。3.3 生产环境真正该关注的三个隐藏参数很多人配置完上面这些就停了实际上Lettuce还有几个隐藏参数在生产环境很关键。第一个是timeout。这个timeout指的是命令执行的超时时间不是连接建立的超时。我见过一个案例Redis慢查询把某条命令拖了几秒钟应用侧默认超时时间没配置结果这条慢命令一直占着连接后续所有请求都在排队。设置了3秒甚至更短的timeout后慢命令会快速失败并抛出异常至少不会拖垮全体请求。第二个是lettuce.shutdown-timeout。应用优雅停机时如果Lettuce线程池还在处理请求不断开的连接会拖慢下线过程。建议显式设置spring: lifecycle: timeout-per-shutdown-phase: 10s第三个是验证连接是否存活。Lettuce默认不主动发送PING命令检测连接健康长时间空闲的连接可能已经被Redis服务端断开而不自知。虽然Lettuce内部有自动重连机制但定期的validateConnection能更早暴露问题。在自建的Redis连接工厂里可以开启factory.setValidateConnection(true);3.4 自定义连接工厂的完整代码参考把连接池和超时参数都整合进一个自定义配置类生产可以直接拿去改改用Configuration public class RedisConfig { Bean public LettuceConnectionFactory redisConnectionFactory() { RedisStandaloneConfiguration serverConfig new RedisStandaloneConfiguration(); serverConfig.setHostName(192.168.1.100); serverConfig.setPort(6379); serverConfig.setPassword(RedisPassword.of(yourpassword)); serverConfig.setDatabase(0); LettucePoolingClientConfiguration clientConfig LettucePoolingClientConfiguration.builder() .commandTimeout(Duration.ofSeconds(3)) .shutdownTimeout(Duration.ofSeconds(5)) .poolConfig(poolConfig()) .build(); LettuceConnectionFactory factory new LettuceConnectionFactory(serverConfig, clientConfig); factory.setValidateConnection(true); return factory; } private GenericObjectPoolConfig? poolConfig() { GenericObjectPoolConfig? config new GenericObjectPoolConfig(); config.setMaxTotal(32); config.setMaxIdle(16); config.setMinIdle(4); config.setMaxWait(Duration.ofSeconds(2)); config.setTestOnBorrow(true); config.setTestOnReturn(false); return config; } }4. RedisTemplate封装API执行逻辑与生产级代码落地4.1 我们为什么很少直接用RedisTemplate的原生方法直接注入RedisTemplateString, Object然后用opsForValue().set()是最简单的写法但在一个真正的大型项目里你会发现原生方法有两个别扭的地方。第一opsForValue()、opsForHash()这些方法返回的操作器是每次临时获取的大量散落在业务代码里导致后续如果要统一加缓存穿透保护、统一加监控埋点需要改的地方非常多。第二RedisTemplate原生方法抛出的异常是Spring Data的DataAccessException体系业务方需要额外catch不然异常信息不太友好。所以生产项目的标准动作是在RedisTemplate之上再做一层轻量封装提供一个CacheService内部统一处理序列化、异常转换、空值标记。4.2 带有序列化配置的Template定义方式第一种方式直接以Bean形式定义RedisTemplateConfiguration public class RedisTemplateConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(connectionFactory); // key使用String序列化器 StringRedisSerializer stringSerializer new StringRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); // value使用GenericJackson2JsonRedisSerializer RedisSerializerObject jsonSerializer redisSerializer(); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }注意这里有个小细节setConnectionFactory不会立刻真正创建连接配置的生效得靠afterPropertiesSet()触发。如果省略这个方法序列化器和连接工厂的初始化顺序可能不对运行时会报奇怪的NPE。第二种方式是使用Spring Boot自动配置的RedisTemplateObject, Object但它的泛型是Object实际使用时要强转很不方便。所以我更推荐上面这种手动定义的模板泛型直接限定为String和Objectkey的类型安全在编译期就保证了。4.3 StringRedisTemplate和RedisTemplate的关系以及数据互通问题Spring Boot里还自动配置了一个StringRedisTemplate它其实是RedisTemplate的子类内部把key和value都固定用StringRedisSerializer序列化。很多项目的踩坑点由此而来一个服务用StringRedisTemplate写入数据另一个服务用自定义RedisTemplate读取结果读不到——因为key的序列化方式一样问题不大但value一个存了纯字符串StringRedisTemplate一个存了带引号的JSON字符串GenericJackson2JsonRedisSerializer格式完全不同读出来要么强转失败要么内容对不上。解决这个问题的唯一办法是在一个服务内统一缓存读写组件的序列化策略。多个服务共享同一套Redis时更要在接口文档或公共SDK里明确约定key和value的格式。我见过最稳妥的做法是抽取一个公共的CacheModule所有服务引用同一个模块序列化配置、key命名规范、过期策略全部统一定义哪种Template都不允许绕过。4.4 在Template之上封装统一缓存服务核心代码下面这段封装覆盖了最常见的场景也是我个人项目的标配Service public class CacheService { private final RedisTemplateString, Object redisTemplate; public CacheService(RedisTemplateString, Object redisTemplate) { this.redisTemplate redisTemplate; } public void set(String key, Object value, long timeout, TimeUnit unit) { redisTemplate.opsForValue().set(key, value, timeout, unit); } public boolean setIfAbsent(String key, Object value, long timeout, TimeUnit unit) { return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit); } public Object get(String key) { return redisTemplate.opsForValue().get(key); } public void delete(String key) { redisTemplate.delete(key); } public void hSet(String key, String field, Object value) { redisTemplate.opsForHash().put(key, field, value); } public Object hGet(String key, String field) { return redisTemplate.opsForHash().get(key, field); } public void expire(String key, long timeout, TimeUnit unit) { redisTemplate.expire(key, timeout, unit); } public Long increment(String key, long delta) { return redisTemplate.opsForValue().increment(key, delta); } public boolean tryLock(String key, String requestId, long timeout, TimeUnit unit) { return setIfAbsent(key, requestId, timeout, unit); } public void unLock(String key, String requestId) { // 使用Lua脚本保证原子性删除避免误删其他线程的锁 String script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; // 这里用DefaultRedisScript执行 } }这段代码同时实现了分布式锁的基础版setIfAbsent加过期时间作为互斥锁释放锁时用Lua脚本比对requestId防止线程A把线程B的锁删了。这是生产环境必踩的坑如果释放锁不判断持有者一个慢请求过期后后来者拿到锁结果前一个请求执行完把锁误删锁就完全失效了。5. 实战中的序列化冲突与类型转换陷阱以及监控兜底5.1 经典事故LocalDateTime序列化失败如果你把Java 8的LocalDateTime直接塞进RedisTemplate存对象用GenericJackson2JsonRedisSerializer序列化时十有八九会报错。原因是Jackson默认不支持Java时间类型必须注册JavaTimeModule。在自定义ObjectMapper时记得加上objectMapper.registerModule(new JavaTimeModule()); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);第二行配置是为了把日期序列化成可读的ISO字符串而不是数字时间戳。不加的话LocalDateTime会变成[2024, 5, 20, 14, 30, 25]这种数组服务端解析虽然也能对应上但阅读性极差排查数据问题的时候看得一头雾水。5.2 经典翻车BigDecimal反序列化后变成Integer或Double用GenericJackson2JsonRedisSerializer存BigDecimal反序列化时经常出现类型不匹配。比如数据库里某个字段是BigDecimal你写入Redis后变量声明也是BigDecimal但读出来却发现是Double一强转就ClassCastException。原因是JSON序列化会把BigDecimal按照数值类型输出比如100.00存成100.00时还可以但100就会输出成100反序列化时Jackson把它当作Integer处理类型信息在JSON里丢失了。解决这个问题的思路有两个一是尽量用字符串表示金额字段在业务对象里把BigDecimal属性的getter/setter改成String存取或者加一个JsonSerialize(using ToStringSerializer.class)注解强制序列化为字符串public class Product { JsonSerialize(using ToStringSerializer.class) private BigDecimal price; }二是严格要求所有涉及金额的核心数据不落地缓存只用Redis做临时读取。这个思路简单粗暴但很有效我倾向于推荐它——金额字段越少经过各种中间件出大问题的概率越低。5.3 连接池耗尽时的现象、定位与处理连接池耗尽在生产上是会真实发生的现象非常典型应用日志里大量出现Unable to connect to Redis和RedisConnectionFailureException同时接口响应时间从几十毫秒飙升到几秒。这时候很多人的第一反应是Redis服务器挂了但实际去redis-cli info clients一看连接数并正常运行。真正的问题往往出在应用侧的慢查询。某条命令执行了很长时间占着连接不释放在max-active32的池子里32个连接都被慢命令占满后续请求全部阻塞在等待连接上直到2秒的max-wait超时后抛出异常。定位手段有三个用redis-cli slowlog get查看慢命令重点看哪些key的操作耗时超过了slowlog-log-slower-than阈值。查应用线程栈jstack看线程是阻塞在pool-2-thread-1上的连接租借代码还是真的进入了网络I/O等待。检查是否有大key操作比如一次性HGETALL一个几十万字段的hash或者直接SMEMBERS一个千万级别的set。这种命令会把网络、序列化、Redis单线程处理全部拖慢。处理方案是把大key拆成多个小key分片或者把批量读取改成SCAN游标分批取。同时把max-wait设短一些宁可快速失败也不要拖垮整个服务。5.4 生产环境建议开启的Redis监控指标最后聊一下兜底。连接池、序列化、超时配置再完善也没法避免Redis集群本身出故障所以监控必须配好。Spring Boot Actuator天然支持Redis的健康检查在application.yml里开启management: endpoints: web: exposure: include: health,info,metrics health: redis: enabled: true同时接入Micrometer后可以采集lettuce.command.measurements这类指标。更细粒度的监控还应该包括Redis实例的hit_rate缓存命中率、connected_clients客户端连接数、used_memory_rss内存占用、instantaneous_ops_per_sec每秒操作数。配合告警规则比如命中率低于80%或内存超过maxmemory的80%就触发告警很多问题可以在用户感知之前处理掉。6. 大流量场景下再进阶管道、二级缓存和分布式锁6.1 管道Pipeline解决批量操作的性能瓶颈如果业务中经常需要一次性写入大量key比如初始化用户标签、批量导入配置逐条set是非常低效的。每条命令都要经历一次网络RTTRound-Trip Time在远程Redis上这个损耗非常明显。用RedisTemplate执行管道核心代码是这样的ListObject results redisTemplate.executePipelined(new SessionCallbackObject() { Override public Object execute(RedisOperations operations) throws DataAccessException { for (int i 0; i 10000; i) { operations.opsForValue().set(bulk:key: i, value- i); } return null; } });管道内部会把多条命令一次性打包发给Redis服务端再将结果一次性读回减少了大量网络交互。实测在局域网里批量写1万条数据逐条写耗时约12秒管道方式耗时约0.5秒差距接近一个数量级。这里要注意的是管道操作会占用一个连接较长的时间如果连接池较小建议控制批量条数或者分批执行。6.2 二级缓存Redis和本地缓存Caffeine的协作模式生产级应用在高并发下有一种经典组合Caffeine本地缓存 Redis远程缓存。流程是先查Caffeine未命中再查Redis再未命中才查数据库然后把结果回填到本地和远程两级缓存。为什么这么做因为Redis再快一次网络RTT也有0.1ms到1ms级别的开销。而在应用内直接读内存只要纳秒级别。对于热点数据比如首页推荐位、秒杀商品详情这一层本地缓存能挡掉大量Redis请求。简单封装思路Service public class CacheAsideService { private final CacheString, Object localCache Caffeine.newBuilder() .maximumSize(10000) .expireAfterWrite(Duration.ofMinutes(10)) .build(); private final CacheService redisCacheService; public Object get(String key) { Object value localCache.getIfPresent(key); if (value ! null) { return value; } value redisCacheService.get(key); if (value ! null) { localCache.put(key, value); } return value; } public void evict(String key) { localCache.invalidate(key); redisCacheService.delete(key); } }这里有个一致性问题要特别留意更新数据时先删缓存还是先更新数据库顺序不同会导致不同的脏数据窗口。我的习惯是先更新数据库再删除缓存而不是先删除缓存再更新数据库。因为前者只有在删除缓存失败时才会出现短暂脏读后者在更新数据库的窗口期内容易把旧数据又写回缓存造成更长的脏数据。配合缓存的TTL兜底最终会收敛。6.3 从手写锁到Redisson分布式锁的正确进化路径前面的CacheService里我写了一个基于setIfAbsent的基础分布式锁它够用但不完善。比如锁没有自动续期机制如果持有锁的线程执行时间超过了锁的TTL锁会被其他线程抢走原来的线程还以为自己安全持有锁。生产级方案是使用Redisson它内部的看门狗机制会自动续期默认每10秒检查一次如果锁还在被当前线程持有就自动延长过期时间RLock lock redissonClient.getLock(order:pay: orderId); boolean locked false; try { locked lock.tryLock(3, 30, TimeUnit.SECONDS); if (locked) { // 业务逻辑 } } finally { if (locked) { lock.unlock(); } }Redisson还在底层处理了主从切换时锁丢失的问题提供了RedissonRedLock等多锁方案。但从成本与复杂度平衡来看大多数业务用到单体Redis加Redisson的锁就够了。Redis集群的RedLock方案只有在极其严苛的分布式一致性要求下才需要考虑。6.4 Sentinel集群与主从方案在生产中的定位标题相关热词里有redis主从redis哨兵集群这些关键词我在这里说个定调如果你的Redis已经在生产环境承担了核心缓存职责千万不要只用单机至少要上主从加Sentinel的架构。主从提供了数据冗余和读写分离的可能Sentinel解决的是故障自动切换。在Java侧Lettuce支持Sentinel拓扑spring: data: redis: sentinel: master: mymaster nodes: 192.168.1.101:26379,192.168.1.102:26379,192.168.1.103:26379这样配置后某个Sentinel节点挂了Lettuce感知拓扑变化的能力会自动切换到新的Sentinel节点Redis主节点故障时Sentinel选主后客户端也会自动重建连接。这也是我在前文反复强调选Lettuce的重要原因——它在客户端层面就具备拓扑感知能力生产环境省心很多。我踩过一次让人很难忘的坑当时只做了主从没有配Sentinel晚上主库宕机从库数据读到一半应用侧报错堆满日志直到第二天早上人工切换。那次之后我就把所有核心链路都迁移到了Sentinel模式。所以如果你问生产环境最底线的配置是什么我的答案很直接主从加Sentinel或者直接Cluster单机只配做开发环境。7. 最后的几个经验补充踩过这么多坑之后有几个细节值得再说一遍。一个是Redis的maxmemory-policy淘汰策略。没设淘汰策略的话内存满了新数据写不进去线上会直接报OOM错误。生产环境中我一般设置allkeys-lru因为缓存场景下热度数据保留价值远大于冷数据LRU最适合大多数业务。没有特殊理由不要用noeviction。另一个是key命名规范。我强烈建议在项目启动时就定好统一前缀比如biz:module:func:id用冒号分隔。既方便按业务模块排查也方便用SCAN按前缀批量清理。不过要注意KEYS user:*这种命令在生产大库上执行会阻塞Redis单线程清理缓存一定要用SCAN游标。还有一个是关于连接工厂初始化的时机问题。有些代码在Spring容器还没完全初始化时就调用了RedisTemplate导致RedisConnectionFailureException。标准的做法是在ApplicationRunner或PostConstruct里做预热连接比如启动时执行一次PING。虽然是个小动作但能有效避免业务流量刚进来时第一批请求撞上连接未就绪的窗口。到这一步从连接工厂的参数、序列化器的选择、Template的定制封装到监控、锁、二级缓存一个能扛住生产流量压力的Redis接入方案就算真正成型了。这套配置我在多个线上项目中反复打磨过你照着做大概率不会再碰到那些让人半夜爬起来排查的基础性问题。
返回列表