RedisTemplate使用指南:序列化、连接池与缓存实战坑位解析
发布时间:2026/10/16 8:24:25 锦皓数字建站

干活这么多年凡是SpringBoot项目里接Redis基本都会碰见RedisTemplate。我见过太多项目Redis装上、依赖引进来结果跑着跑着出现乱码、反序列化报错、key异常、数据查不到排查一圈发现全是在RedisTemplate的使用细节上栽了跟头。这工具看起来就是一个封装好的模板类但它的序列化机制、连接管理、操作接口和事务行为每一层都有讲究。这篇文章不聊那些空泛的理论直接把我实际用RedisTemplate过程中验证过的配置、踩过的坑、总结出来的经验和排查套路整理出来给正在用或者准备用RedisTemplate的人做参考。1. RedisTemplate的结构与定位先搞清楚它到底包了什么1.1 为什么项目里用RedisTemplate而不是直接用连接很多同学刚接触Redis时用的是Jedis或Lettuce的客户端直连方式写出来的代码长这样Jedis jedis new Jedis(localhost, 6379); jedis.set(name, 张三); jedis.close();这种方式其实没有错但放到Spring框架体系里就显得很笨重。每个方法都要自己管理连接、自己处理异常、自己判断资源释放一多起来代码里全是重复样板。而且一旦你在代码里到处new连接连接数会成为一个大隐患。Spring Data Redis的出现就是来解决这个问题的。它把连接管理和操作封装起来对外提供一个统一的入口RedisTemplate。我们用的时候不需要关心底层是Jedis还是Lettuce也不需要手动管理连接池的获取和释放。更重要的是RedisTemplate设计了一套清晰的操作分发机制不同的数据类型对应不同的操作器调用起来非常直观redisTemplate.opsForValue().set(user:1, user); redisTemplate.opsForHash().put(cart:1, sku:1001, 2); redisTemplate.opsForList().leftPush(news:list, news);这就是Spring框架一贯的思路把复杂的东西包起来把常用的东西暴露出来。从代码结构上看RedisTemplate相对于直接用客户端主要有三个价值连接生命周期由框架管理用完自动归还连接池不需要开发者操心。内置序列化机制Java对象可以直接存取出来自动转回对象。操作接口按数据类型划分清晰语义明白团队协作时不容易写乱。1.2 从RedisTemplate到RedisConnection的调用链我最初使用RedisTemplate的时候一直有个困惑RedisTemplate操作数据的时候到底是怎么把命令发给Redis的后来翻了一下源码整个调用链大概是这样RedisTemplate调用opsForValue()拿到ValueOperations实际实现这个实现内部通过execute()方法从ConnectionFactory拿到RedisConnection再通过connection执行真正的Redis命令。这里的RedisConnection就是底层客户端的包装。如果你用的是Lettuce那么RedisConnection的底层是LettuceConnection如果用Jedis底层就是JedisConnection。无论底层是谁对上层RedisTemplate来说暴露的接口是一致的。这个设计有几个实际影响需要明白RedisTemplate本身是线程安全的可以在多个线程间共享使用因为它内部不持有具体连接每次操作都是从连接工厂申请连接。RedisTemplate和StringRedisTemplate是两个不同的Bean它们的序列化方式不同混用会出问题。同一个连接下事务、管道这些操作是有状态的连接归还连接池之前状态会被清理但如果你手动开启了事务一定要记得提交或回滚。我见过不少项目为了图省事直接注入RedisTemplate然后满世界用也不配置序列化结果存进去的key是乱码取数据全是一堆奇怪的二进制。所以理解调用链还不够序列化才是真正的第一道坎。2. RedisTemplate序列化90%的异常都跟它有关2.1 默认的JdkSerializationRedisSerializer有多坑Spring Boot在自动配置RedisTemplate时如果没有显式指定序列化器默认用的就是JdkSerializationRedisSerializer。这个序列化器的问题非常明显。第一它序列化出来的二进制数据很长占用大量内存。一个简单的字符串经过JDK序列化后会带上一长串类定义、序列化版本号、对象头之类的元信息存到Redis里非常臃肿。第二也是更恶心的一点JDK序列化要求对象实现Serializable接口而且对类结构变更非常敏感。你存进去的是一个User对象哪天在User类里加了一个字段老数据反序列化就可能直接报InvalidClassException线上缓存全部变成不可读。第三它序列化出来的内容在Redis客户端里可见性极差。你用Redis Desktop Manager或者其他可视化工具查看时看到的是\xAC\xED\x00\x05t\x00...这种二进制乱码排查数据时根本不知道key是什么。还有最隐蔽的一点使用默认序列化器时key和value都会经过JDK序列化如果你用redisTemplate.opsForValue().set(name, 张三)这个name也会被序列化。你看着代码里存的是name实际Redis里存的key是一串乱码你用命令行redis-cli get name去查什么都查不到。这种问题对刚接触的人杀伤力极大排查一上午都不知道是自己存进去的方式不对。2.2 推荐的序列化组合方案实际项目里我用的最多也是最稳的一套组合是key使用StringRedisSerializervalue使用GenericJackson2JsonRedisSerializerhash的key使用StringRedisSerializerhash的value使用GenericJackson2JsonRedisSerializer为什么要这么配理由分两层说。key用String序列化是为了可读性和可控性。业务key大部分都是拼接出来的字符串比如user:1001、order:20250101:12345用String序列化后Redis里面看得清清楚楚命令行能直接get可视化工具能直接看到出了问题排查起来轻松。StringRedisSerializer还有一个好处是跨语言兼容其他系统比如Go、Python写的服务也能基于同一套key规则访问缓存数据。value用GenericJackson2JsonRedisSerializer是为了把Java对象存成JSON。JSON的优点是体积比JDK序列化小很多而且可读性好。GenericJackson2JsonRedisSerializer和普通的Jackson序列化器还不一样它会在序列化时保存一个class类型信息这样反序列化时就能知道要把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.setValueSerializer(jsonSerializer); template.setHashKeySerializer(stringSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }配置完之后Redis里存的key就是明文value就是JSON字符串。比如存一个用户对象你会在Redis里看到这样的数据{class:com.example.entity.User,id:1001,name:张三,age:25}其中class字段就是GenericJackson2JsonRedisSerializer自动添加的类型标识取数据的时候它靠这个来还原对象。2.3 GenericJackson2JsonRedisSerializer的局限性和备选这套方案也有需要注意的地方。class字段会暴露类的完整类名如果Redis数据被外部拿到相当于泄露了一点项目结构信息对于安全要求极高的场景需要评估是否可用。另外反序列化时如果类的结构发生了不兼容的变化比如删除字段、修改字段名会抛异常。但这比JDK序列化要好很多至少字段的增删大部分情况下不影响已有数据。如果不想在JSON里带class还有一种选择配置ObjectMapper使用JavaType自动推断。但这种方式要求取数据的时候明确指定类型代码写起来麻烦一些。还有一种思路是全用StringRedisTemplate把对象手动转成JSON字符串再存。手动转JSON的优势是完全可控不会出现类名暴露问题缺点是每次存取都要自己写转换代码项目里十几个地方都有类似代码重复度太高。我在一些项目里也见过FastJson序列化器、Kryo序列化器各有各的适用场景。但从稳定性和社区活跃度来看GenericJackson2JsonRedisSerializer仍然是Spring生态里的首选。另外即便同一个项目里如果有存集合、存对象、存字符串等不同种类的数据也可以用不同的RedisTemplate或者通过同一个RedisTemplate配合手动序列化来处理不要死板地一套序列化器用到黑。3. 五种核心操作器的实战用法与细节3.1 ValueOperations缓存最常见的读写场景ValueOperations对应的是Redis的String类型。这类操作在缓存场景里用得最多比如用户登录状态、验证码、接口数据缓存、计数器。基本用法// 存储数据 ValueOperationsString, Object ops redisTemplate.opsForValue(); ops.set(user:login:token:1001, token, 30, TimeUnit.MINUTES); // 读取数据 Object tokenObj ops.get(user:login:token:1001); // 自增适用计数场景 Long count ops.increment(visitor:count:20250119);有一个非常实用的细节set方法可以带超时时间这个一定要用起来。很多人存数据只调set(key, value)用完忘了设置过期时间Redis内存一天天涨最后OOM或者达到maxmemory触发淘汰线上出事故。我在团队里强制要求过任何缓存写入必须带过期时间除非你有非常明确的理由让它永久存在。另一个细节是setIfAbsent方法这个天然支持分布式锁。比如要加一个防重复提交的锁Boolean locked redisTemplate.opsForValue() .setIfAbsent(lock:order:12345, 1, 10, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // 执行业务逻辑 } finally { redisTemplate.opsForValue().delete(lock:order:12345); } }注意这里有一个经典坑setIfAbsent返回的是Boolean对象不是boolean基础类型如果你直接写if(locked)会存在自动拆箱空指针的风险。Redis网络异常时这个方法可能返回null。所以比较稳妥的写法是if(Boolean.TRUE.equals(locked))。还有一个是getAndDelete这个在一次性读取消费数据的场景很有用。比如获取一个临时凭证读取后立刻删除。常见用法Object value redisTemplate.opsForValue().getAndDelete(temp:qrcode:1001);这种组合操作避免了先get再delete两个步骤之间的时间窗口问题。3.2 HashOperations对象属性维度管理的利器Redis的Hash类型对应Java里的Map特别适合存对象属性比较多、并且经常要更新部分字段的场景。比如购物车、用户资料、配置项。基本用法HashOperationsString, Object, Object hashOps redisTemplate.opsForHash(); hashOps.put(cart:1001, sku:20001, 2); hashOps.put(cart:1001, sku:20002, 1); Integer count (Integer) hashOps.get(cart:1001, sku:20001); MapObject, Object entries hashOps.entries(cart:1001);如果配置了HashKey和HashValue都使用String序列化器那Redis里看到的hash结构非常清晰。但如果没有配置又会回到那个老问题hash的field和value全是JDK序列化乱码。很多人只配了keySerializer和valueSerializer忘了配hashKeySerializer和hashValueSerializer存储hash类型时还是有乱码。这个细节我建议反复检查。Hash还有一种用得比较巧妙的地方是结合increment做精细化计数比如记录某个用户购买某个商品的次数hashOps.increment(buy:count:1001, sku:20001, 1);这比把整个对象取出来改完再存回去高效得多而且天然支持并发。3.3 List、Set、ZSet的典型应用与注意点ListOperations对应Redis的List类型。常见的用法是做消息队列、最新消息列表、操作日志。操作接口有leftPush、rightPush、leftPop、rightPop等。我自己更常用rightPush配合leftPop或者leftPush配合rightPop按业务需要确定方向。队列场景下需要注意如果不阻塞地pop数据为空时会返回null要结合业务判断是没数据还是数据异常。SetOperations对应Redis的Set类型。适用于去重统计、标签系统、共同好友这类场景。比如统计某活动的参与用户去重集合redisTemplate.opsForSet().add(activity:20250119:users, user_id_1001); Long size redisTemplate.opsForSet().size(activity:20250119:users);Set的交集、并集、差集运算在业务里也经常用到比如通过交集算出两个分组里的共同用户不需要把数据拉回内存里再算直接让Redis算完返回结果。ZSetOperations对应Redis的ZSet有序集合。它的核心是每个成员有一个scoreRedis根据score排序。这个特性在排行榜场景里非常契合。比如积分排行榜ZSetOperationsString, Object zsetOps redisTemplate.opsForZSet(); zsetOps.add(rank:score:2025, userId, 100.0); // 增加积分 zsetOps.incrementScore(rank:score:2025, userId, 10.0); // 获取前10名 SetZSetOperations.TypedTupleObject top10 zsetOps.rangeWithScores(rank:score:2025, 0, 9);ZSet还有一个常用场景是延时队列。把任务的执行时间戳作为score用zsetOps.rangeByScore从小到大取出当前时间之前的任务处理完成后remove掉。这种方案实现延时任务比定时轮询数据库优雅不少。4. 连接工厂、连接池与性能调优4.1 Lettuce与Jedis怎么选Spring Boot 2.x之后默认集成的是Lettuce。Lettuce基于Netty连接是异步的、可以在多个线程间共享一个连接在高并发场景下连接数占用少这也是Spring官方推荐Lettuce的原因之一。Jedis则更简单直接每次操作都是同步的需要连接池来管理多个连接在高并发场景下连接数会更多。实际项目里我建议除非有特殊理由否则直接用默认的Lettuce就好。它自带连接池的概念是通过commons-pool2实现的需要额外引入依赖并在配置里开启spring: redis: host: localhost port: 6379 password: lettuce: pool: max-active: 50 max-idle: 20 min-idle: 5 max-wait: 3000ms这里要区分一下Lettuce本身在非阻塞模型下可以共享连接但如果你想控制并发峰值、防止某个操作把连接资源全部占满还是建议配置连接池。尤其是你的Redis服务端设置了maxclients限制客户端连接池的合理配置能有效避免连接数超限。4.2 连接池参数设置的参考逻辑连接池参数不能照搬网上的模板要根据自己的业务并发量来估算。我给一个简单的推算方法假设某个接口的QPS是500平均每次操作Redis耗时为2ms那么同时处于活跃状态的连接数大约是500×2/10001个连接的平均并发。但真实业务会有毛刺所以一般按峰值QPS的5到10倍来预留连接数。max-active设多大合适一个经验值是取应用实例数乘以单实例并发上限的乘积。比如你有4个实例每个实例峰值并发操作Redis约200那max-active给50到100就够。这里不要贪大连接池太大不仅浪费连接资源Redis服务端维护连接本身也有内存开销。max-wait很关键它决定了当连接池耗尽时线程是等待还是立刻抛异常。如果不设置或者设为-1表示无限等待极端情况下线程会全部堵在获取连接上应用表现为接口大量超时而且这种问题很难定位。我建议设置为1000到3000ms超过这个时间直接抛异常让监控系统报警至少你能知道系统在哪个环节出问题了。4.3 请求超时与命令超时的设置Spring Redis配置里有一个timeout参数这个指的是连接建立和命令执行的超时时间。如果Redis服务端负载很高或者网络有延迟命令执行超过timeout时间就会抛异常。默认值是60秒对大多数接口来说太长了一个Redis操作如果真的等60秒你的接口基本已经拖垮了。我通常设置为1到3秒spring: redis: timeout: 3s配置了这个之后Redis操作超过3秒就会抛出异常虽然业务会报错但至少不会让线程长时间阻塞。线上环境快速失败比无限等待要好得多。还有一个进阶玩法针对单个操作单独设置超时。lettuce的客户端可以设置commandTimeout这个不在Spring的配置里暴露但可以通过自定义ConnectionFactory来设置。对那种不允许长时间阻塞的Redis操作这个配置很有用途。5. 事务、管道与批量操作的应用细节5.1 RedisTemplate事务的正确打开方式RedisTemplate支持事务使用方法是在execute方法里调用setEnableTransactionSupport(true)然后通过SessionCallback来执行多个操作。切到事务模式下所有的命令会被缓存起来直到exec提交时一次性发给Redis。一个标准的写法redisTemplate.execute(new SessionCallbackListObject() { Override public ListObject execute(RedisOperations operations) { operations.multi(); operations.opsForValue().set(key1, value1); operations.opsForValue().set(key2, value2); operations.opsForValue().set(key3, value3); return operations.exec(); } });这段代码的执行逻辑是multi开启事务之后set命令不会立刻执行而是进入命令队列exec时一起执行。整个过程保证原子性不会出现key1成功key2失败的情况。但是要特别注意Redis事务和关系型数据库事务完全是两回事。Redis事务不支持回滚如果exec前有命令入队失败会报错但如果exec执行过程中某个命令执行失败其他命令依然会执行不会回滚。所以不要把业务逻辑的强一致性完全寄托在Redis事务上。另一个实际问题是开启事务后如果在同一个连接里执行get命令返回的并不是真实值而是null因为get在事务队列里执行时返回值需要等exec之后才能拿。很多初次使用RedisTemplate事务的人会在这里栽跟头在multi()和exec()之间不该去依赖读取结果。5.2 管道Pipeline大幅提升批量性能管道和事务在命令发送方式上类似但目的不同。事务关心的是原子性管道关心的是性能。管道的核心思想是把多条命令一次性发给Redis服务端服务端依次执行最后一次性返回结果。省去了每一条命令都要走一次网络往返的开销。批量写入的场景比如一次性初始化几千条数据用逐条set的速度很慢网络往返是主要瓶颈。用管道的话可以把这几千条命令打包可能只需要几次网络往返就完成了。RedisTemplate管道写法有几种。一种是直接用executePipelinedListObject results redisTemplate.executePipelined(new SessionCallbackObject() { Override public Object execute(RedisOperations operations) { for (int i 0; i 1000; i) { operations.opsForValue().set(batch:key: i, i); } return null; } });这里有一个注意点executePipelined执行完返回的List里保存的是每条命令的执行结果。但在管道模式下如果你使用配置了序列化器的RedisTemplate返回结果可能和你预期的类型不同因为管道结果是基于连接层的返回序列化器可能不生效。所以如果管道里执行的是简单的set操作返回结果不需要过多关注重点是把命令批量发出去。再说一个我自己踩过的坑管道里绑定的命令数量不能太大。一次性发送几万条命令到RedisRedis服务端虽然能处理但会长时间占用一个连接阻塞其他命令的响应。我一般把单次管道命令数控制在500到1000条以内如果数据量大分批执行。5.3 delete与批量删除要注意的又是一个大坑批量删除在RedisTemplate里不是原生支持的。你以为可以类似redisTemplate.delete(keys)这样传一个集合其实这个方法确实支持传入Collection参数但如果你有一万个key要删除一次性delete所有key性能并不好。更常见的批量删除场景是你要删除某个前缀的所有key但Redis的KEYS命令在线上环境是禁用或慎用的因为它会扫描全库导致阻塞。实际推荐的做法是如果业务允许给每个需要批量管理的key设置一个可枚举的过期时间让它们自然淘汰。如果必须主动删除可以结合SCAN命令和无损的方式分批扫描出key再逐批删除。在Spring里写一个基于RedisTemplate的简单版本SetString keysToDelete new HashSet(); ScanOptions options ScanOptions.scanOptions().match(user:cache:*).count(200).build(); try (Cursorbyte[] cursor redisTemplate.executeWithStickyConnection( connection - connection.scan(options))) { while (cursor.hasNext()) { keysToDelete.add(new String(cursor.next())); if (keysToDelete.size() 500) { redisTemplate.delete(keysToDelete); keysToDelete.clear(); } } } if (!keysToDelete.isEmpty()) { redisTemplate.delete(keysToDelete); }这个方案里SCAN命令是游标式的不会像KEYS那样一次性阻塞服务端。配合count参数控制每次扫描的数量。执行批量删除时也要记得连接是复用的用完要释放。6. 缓存场景下的一致性、穿透与雪崩应对6.1 缓存和数据库的一致性怎么处理用RedisTemplate做缓存最常见的就是缓存数据库查询结果。我见过很多项目都是这么写的先查缓存缓存没有就查数据库然后回写缓存。这个模式本身没错但一致性问题就出在更新顺序上。举一个经典场景用户信息更新接口。更新数据库后要清缓存如果先更新数据库再删缓存在并发场景下另一个线程可能在删缓存前把旧数据读进来并回写缓存造成这个用户缓存里是老数据。如果先删缓存再更新数据库那么更新过程中有请求进来缓存是空的会去数据库查查到旧数据又把旧数据写进缓存了。两种顺序都有可能出问题。业界比较推荐的方案是延迟双删先更新数据库再删缓存然后过几百毫秒再次删缓存。第一次删是为了快速让缓存失效第二次删是为了处理那个并发窗口期里写入的脏数据。用RedisTemplate实现起来不复杂public void updateUser(User user) { userMapper.updateById(user); String cacheKey user:info: user.getId(); redisTemplate.delete(cacheKey); // 延迟再次删除具体延迟时间看业务容忍度 Thread.sleep(500); redisTemplate.delete(cacheKey); }Thread.sleep阻塞线程的方式不太优雅项目里一般放在异步任务里或者用延迟队列实现。这里想强调的是RedisTemplate的delete操作再简单也要考虑并发场景下的一致性细节不能只关注CRUD本身。另一个思路是缓存的value里带上时间戳或版本号写入缓存时对比版本号低版本不能覆盖高版本。这是乐观锁思路在更新频繁且一致性强要求下用得上。6.2 穿透、击穿、雪崩的RedisTemplate层面防护缓存穿透指的是请求的key在缓存里不存在数据库里也不存在每次请求都会穿透到数据库。防护方法之一是缓存空值。用RedisTemplate写就是查到数据库没有这个数据也把这个key的value设成一个空标记设置较短的过期时间比如30到60秒Object cacheValue redisTemplate.opsForValue().get(key); if (cacheValue null) { Object dbValue userMapper.getById(id); if (dbValue null) { // 缓存空值防止穿透 redisTemplate.opsForValue().set(key, NULL, 30, TimeUnit.SECONDS); return null; } redisTemplate.opsForValue().set(key, dbValue, 30, TimeUnit.MINUTES); return dbValue; }缓存击穿指的是某个热点key在过期瞬间大量并发请求同时打到数据库。防护方法主要是互斥锁用RedisTemplate的setIfAbsent实现拿到锁的线程去查数据库没拿到锁的线程等待后重新查询缓存。缓存雪崩指的是大量key在同一时间段过期导致大量请求打到数据库。防护方法一是过期时间加随机值避免同一批key同时失效二是多级缓存比如本地缓存加一层Redis缓存三是限流降级。用RedisTemplate设置随机过期时间很灵活配合TimeUnit很容易实现long baseExpire 30 * 60; long randomExpire baseExpire new Random().nextInt(300); redisTemplate.opsForValue().set(key, dbValue, randomExpire, TimeUnit.SECONDS);这一套下来虽然还是没有百分之百解决一致性问题但至少把常见的风险点堵住了。6.3 热key问题的初步识别与处理热key是指某个key被极高并发访问导致Redis单分片负载过高。在Spring Cloud场景下很多服务共用同一个Redis一个热key就能拖慢所有依赖它的接口。识别热key可以在Redis服务端用monitor命令做短期分析或者在客户端给RedisTemplate加一个简单的访问计数切面。处理方式相对直接如果key对应的数据不是要求强一致可以考虑在本地缓存如Caffeine里缓存一份短期副本热点访问直接接本地缓存如果是计数类热key可以考虑把key拆成多个子key分担写入压力读取时汇总。用RedisTemplate的场景里要注意opsForValue().get这种简单的读操作在高并发热key下也会成为瓶颈。Redis单分片的处理能力是有限的不能把Redis模板当普通工具类无限压榨架构层面该做拆分就做拆分。7. 高频报错的实战排查备忘7.1 反序列化异常ClassCastException常见的报错之一是取出缓存数据强转时抛出ClassCastException。原因通常是存储时的value类型和读取时期望的类型不一致。比如存储时用了GenericJackson2JsonRedisSerializer存了一个User对象反序列化信息里包含的是旧包名或旧类名后来类的包路径改了再读取就还原不了了。解决办法是如果项目里有类包名调整的重构记得清掉旧缓存或者做兼容迁移。还有尽量不要在value类型上做万能Object设计你把一个Integer存进去取出来却期望是String反序列化结果自然不对。存取类型要保持一致给团队立一条规矩。7.2 Too many connections连接数超限如果Redis报错ERR max number of clients reached说明连接数已经超过Redis服务端的maxclients设置。这个问题的原因有几个客户端连接池配置过大应用实例数量多每个实例的池子加起来超过了服务端上限或者代码里有泄露连接的地方或者Lettuce默认没有配置连接池但Netty事件循环创建的连接过多。排查思路是在Redis服务端用CLIENT LIST命令查看当前连接来自哪些IP结合应用的实例信息判断。如果是池子太大调小max-active如果是连接没归还重点检查SessionCallback使用后是否有异常路径导致连接没有关闭。Spring Data Redis本身对连接管理已经做了很多处理但如果你手动获取了连接比如通过execute方法拿了Connection一定要在finally里关闭。7.3 调用Redis的线程阻塞问题排查还有一个比较隐蔽的问题业务接口明明只执行了一次Redis get操作却耗时几百毫秒。这种情况先看是否在RedisTemplate配置中开启了事务支持setEnableTransactionSupport(true)。开启后所有Redis操作都会走和Spring事务同步的绑定连接机制。如果Spring事务在业务方法上长时间开启Redis连接一直被占有其他请求在获取同一连接时就会等待。另外如果RedisTemplate设置了比较长的timeout且没有连接池上限在高并发下多个线程排队获取连接即使每个命令都很快总体RT也会涨上去。排查RT异常时优先看连接等待时间再去看Redis服务端的慢日志。7.4 Redisson与RedisTemplate并存时的序列化冲突如果项目里同时使用了Redisson做分布式锁和RedisTemplate做缓存要注意Redisson默认的序列化方式和RedisTemplate配置的序列化方式可能不一致。Redisson写入锁相关数据是用它自己的编解码器RedisTemplate读这些数据时用你的序列化器很可能报错或者读到乱码。处理方式是把Redisson的编解码器也设置为和RedisTemplate一致或者在架构上划分清楚锁的key和缓存的key使用不同前缀避免读错。其实这一个点最能说明问题RedisTemplate和项目里的其他Redis组件必须是同一个语言否则你面对的就不是工具问题而是设计问题。8. 几个我深有体会的最终建议RedisTemplate本身不难懂真正的难点在于它的生态细节特别多。序列化器选型、连接池参数、事务语义、管道大小、缓存一致性任何一个环节没想清楚最后都会以线上故障的形式暴露出来。我个人的习惯是在一个新项目初始化阶段就先定好RedisTemplate的配置规范用同样的序列化配置、同样的key命名规范、同样的超时标准并且写进团队的开发手册里。这比后续出了问题再排查效率高得多。配置统一了代码里的使用习惯统一了很多坑根本不会出现。序列化这块再说一个小技巧在配置文件里把RedisTemplate的keySerializer和valueSerializer、hashKeySerializer和hashValueSerializer全部明确配好即使你觉得默认值也行也要显式写一遍。因为默认值是什么版本会变显式配置能让你的项目行为可控也方便后来人快速理解你的设计意图。最后不要忘了Redis集群模式下的差异。如果你用的是Redis ClusterRedisTemplate的连接工厂要对应地配置为LettuceConnectionFactory的集群模式。Cluster模式下key的哈希槽决定了命令路由到哪个节点RedisTemplate的普通操作不需要感知这些但事务、管道、scan这些操作在集群下的支持和单机不完全一样使用前要确认你的Redis环境是什么形态。技术的东西说复杂复杂说简单也简单。把RedisTemplate当成一个熟悉的老伙计摸清它的脾性理解它的设计意图后面用起来就顺了。我也是踩了无数次坑之后才梳理出这些内容希望对你有点帮助。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。