接手一个Java老项目翻代码的时候最烦的不是业务逻辑复杂而是满屏的redisTemplate.opsForValue().set(...)、redisTemplate.opsForHash().put(...)翻着翻着就分不清哪个方法是操作哪种数据类型的。说实话RedisTemplate是Spring生态里用得非常高频的一个工具类但很多人用了两三年翻来覆去就只会set和get一旦遇到批量操作、过期时间、序列化乱码、管道命令就抓瞎。这篇东西就是把RedisTemplate的核心方法做一个系统性的梳理从opsForValue到execute从序列化到管道把每个方法背后的使用场景、注意事项、踩坑经验一次讲透。适合刚接触Spring Data Redis的初学者也适合用了很久但一直停留在会用不深阶段的老开发。1. RedisTemplate的定位它是怎么把Jedis和Lettuce藏起来的1.1 连接工厂与模板类的关系先花点时间把RedisTemplate的定位说清楚。RedisTemplate本身不直接连接Redis服务器它依赖一个RedisConnectionFactory。这个工厂在Spring Boot的自动配置里默认是LettuceSpring Boot 2.x之后以前老项目里常见的是Jedis。你可以把RedisConnectionFactory理解为创建数据库连接的入口而RedisTemplate是基于这个连接做二次封装的门面。为什么要多包一层因为直接用Jedis写代码你要自己管理连接、自己处理序列化、自己拼接命令参数代码非常啰嗦。比如用Jedis原生写法Jedis jedis new Jedis(localhost, 6379); jedis.set(name, 张三); byte[] bytes jedis.get(name.getBytes(StandardCharsets.UTF_8)); String name new String(bytes, StandardCharsets.UTF_8); jedis.close();这还只是最简单的字符串存取如果是Hash你要自己处理Map的序列化要自己把Java对象转成字节数组这一套下来代码量直接翻倍。而RedisTemplate帮你把连接管理序列化命令封装这三件事全干了你只需要关心业务数据本身redisTemplate.opsForValue().set(name, 张三); String name redisTemplate.opsForValue().get(name);1.2 一个核心模板对应五类操作入口RedisTemplate最核心的设计就是opsForXxx()这一组方法它把Redis的五种基本数据类型分别封装成了五个操作接口方法返回类型对应Redis类型典型场景opsForValue()ValueOperationsString缓存、计数器、分布式锁opsForHash()HashOperationsHash对象/字典存储opsForList()ListOperationsList队列、消息列表opsForSet()SetOperationsSet去重、标签、好友关系opsForZSet()ZSetOperationsZSet排行榜、按分数排序的数据每个Operations接口里都是一堆方法但命名规律很统一基本上就是你脑子里的命令名。比如set、get、delete、increment。除了这五个入口还有opsForStream()、opsForHyperLogLog()等扩展类型但日常业务里前五种占了九成以上。下面我按类型逐个拆每个都加实际场景和注意事项。2. 字符串与值操作opsForValue()使用率最高的操作2.1 从set到setIfAbsent不同方法解决不同问题opsForValue()是RedisTemplate里最常被调用的入口。基础方法不多但每个都有讲究。set(K key, V value)是最基础的写法存一个键值对。set(K key, V value, long timeout, TimeUnit unit)是带过期时间的写法。这个我非常推荐在实际项目里用因为大多数缓存场景都需要过期时间与其先set再expire分两步两步之间如果程序崩溃就永远没有过期时间了不如一步到位。setIfAbsent(K key, V value)很多人不知道怎么用它在业务里太有用了。这个方法对应Redis的SETNX命令只有key不存在时才写入。这就是实现分布式锁的核心。下面是一个简单的分布式锁实现Boolean locked redisTemplate.opsForValue() .setIfAbsent(lock:order:123, 1, 30, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // 执行业务逻辑 } finally { redisTemplate.delete(lock:order:123); } }注意这里setIfAbsent返回的是Boolean对象不是基本类型boolean所以判空很重要。实际项目中曾经踩过坑Redis连接超时时这个方法返回null直接if (locked)会把null拆箱成NPE正确写法是Boolean.TRUE.equals(locked)。2.2 get、increment、append别小看这几个方法get(Object key)没啥好说的取数据。getAndDelete是取完顺手删掉做一次性读取很方便。increment(K key, long delta)是原子自增对应Redis的INCRBY命令做计数器、库存扣减都用它。这里要特别说明increment是原子操作不需要担心并发问题。很多新手用get拿到数字在Java里加1再set回去这在并发下必出问题。正确姿势永远是让Redis帮你算Long count redisTemplate.opsForValue().increment(order:count:2024, 1);append(K key, String value)是追加字符串对应APPEND命令。这个用的不多但在记录日志、拼接长字符串时有奇效比get回来拼接再set回去省一次网络往返。get(K key, long start, long end)是截取字符串子串的方法对应GETRANGE偶尔用得上。放一张表方便收藏方法等效命令用途注意点set(key, value)SET普通赋值默认不设过期时间set(key, value, timeout, unit)SET EX赋值过期推荐优先使用setIfAbsent(key, value)SETNX不存在才赋值返回Boolean注意判空get(key)GET取值无值返回nullgetAndDelete(key)GETDEL取值并删除一次性操作increment(key, delta)INCR/INCRBY原子自增并发安全append(key, value)APPEND字符串追加注意编码格式get(key, start, end)GETRANGE截取子串与substring语义不同2.3 批量操作multiGet的用法与限制multiGet(CollectionK keys)一次传入多个key返回一个ListV。这个方法的坑是返回的List顺序和传入key的顺序不一定完全对应而且在Spring Data Redis的某些版本里如果某个key不存在List里对应位置是null位置关系基本可以放心但还是要养成拿到结果后先判null的习惯。批量get最大的价值是减少网络往返次数。比如查用户信息列表遍历调用get要N次网络请求multiGet一次搞定。但注意它没法保证原子性只适合通用查询场景不适合需要强一致的逻辑。3. Hash操作opsForHash()处理对象与字段变更3.1 put、get、entries最简单也最常用的三角组合Hash在Redis里是字段-值的映射结构最适合存对象。put(H key, HK hashKey, HV value)往Hash里塞一个字段get(H key, Object hashKey)取一个字段entries(H key)取整个Hash。比如存用户信息我们一般有两种方式。第一种是把整个对象序列化成JSON字符串塞给opsForValue().set()第二种是拆成Hash每个字段单独存。两种各有优劣方案优点缺点JSON字符串实现简单整体读写改一个字段要读整个对象、改完整再写回Hash字段字段级读写灵活代码繁琐数据乱实际项目中我的经验是读多写少的快照型数据用JSON字符串字段频繁更新的数据用Hash。比如用户资料页头像、昵称、签名都可能被单独修改用Hash就很舒服redisTemplate.opsForHash().put(user:1001, name, 张三); redisTemplate.opsForHash().put(user:1001, age, 28); String name (String) redisTemplate.opsForHash().get(user:1001, name); MapObject, Object entries redisTemplate.opsForHash().entries(user:1001);3.2 putIfAbsent与incrementHash里的并发控制putIfAbsent(H key, HK hashKey, HV value)对应Redis的HSETNX字段不存在才写入。这个在防重、初始化场景很实用。举个例子做电商活动时每个用户的领取记录放在Hash里用putIfAbsent保证一个人只能领一次Boolean first redisTemplate.opsForHash() .putIfAbsent(campaign:spring, user:1001, claimed);如果返回false说明之前已经领过了。increment(H key, HK hashKey, long delta)对应HINCRBY对Hash里的数字字段做原子自增。比如统计商品分类下的访问量用Hash的increment比用String的increment更有结构性因为多个商品的计数器都挂在同一个Hash key下面管理起来很方便。3.3 delete、hasKey与批量取值别忘的收尾操作delete(H key, Object... hashKeys)可以删一个或多个字段对应HDEL。注意第一个参数是大key后面可变参数是要删除的字段千万别搞反了。hasKey(H key, Object hashKey)判断字段是否存在对应HEXISTS做幂等判断非常常用。multiGet(H key, CollectionHK hashKeys)批量取字段值返回List。这里有个隐藏问题它和opsForValue().multiGet()一样返回结果里不存在的字段对应null所以拿到结果后最好做一次空值过滤。values(H key)是取所有字段的值keys(H key)是取所有字段名size(H key)是取字段数量。这几个方法在大Hash上要慎用Redis是单线程模型取超大Hash的keys或values会阻塞其他命令执行生产环境大key必须拆分。4. List、Set和ZSet队列、去重与排行榜的三兄弟4.1 List操作leftPush和rightPush别搞混方向opsForList()对应的是一系列List命令。最容易搞混的就是leftPush和rightPush。以leftPush(K key, V value)来说它对应当前Redis的LPUSH意思是从头部插入rightPush对应RPUSH从尾部插入。取的时候leftPop从头部弹出rightPop从尾部弹出。画个图理解一下List就像一根管子左边是头右边是尾。leftPush是往管子左边塞东西rightPop是从管子右边掏东西——组合起来就是一个先进先出的队列FIFO。反过来leftPush配合leftPop就变成了先进后出的栈LIFO。实际项目里用得最多的是用它做简单的消息队列。比如异步处理任务// 生产者 redisTemplate.opsForList().rightPush(task:queue, taskJson); // 消费者 String taskJson redisTemplate.opsForList().leftPop(task:queue);rightPush入队leftPop出队天然的FIFO。leftPop(K key, long timeout, TimeUnit unit)是带阻塞的版本对应BLPOP队列为空时最多等timeout超时返回null。这个在消息队列里非常实用避免空轮询消耗CPU。range(K key, long start, long end)对应LRANGE取一段范围内的元素。注意这里的范围是闭区间range(key, 0, -1)是取全部。size(K key)是取List长度。trim(K key, long start, long end)是裁剪List只保留指定区间做只保留最近N条记录很方便。4.2 Set操作add、remove、isMember与多个集合间运算opsForSet()处理的是无序去重集合。add(K key, V... values)添加一个或多个元素remove(K key, Object... values)移除members(K key)取全部成员isMember(K key, Object o)判断是否存在size(K key)取数量。Set独特的价值在于集合运算。intersect是交集union是并集difference是差集。比如做社交功能计算共同好友SetString commonFriends redisTemplate.opsForSet() .intersect(user:1001:friends, user:1002:friends);这个方法别看是Set类型实际应用特别广用户标签去重、UV统计、抽奖去重、推荐系统里的物品相似度计算都用得上。还有两个方法容易被忽略randomMember(K key)随机返回一个成员适合做抽奖pop(K key)弹出并移除一个随机成员适合做限量抢购的虚拟库存扣减。4.3 ZSet操作sorted set的分数概念与常用方法ZSet有序集合是带有分数的Set每个成员关联一个double类型的分数按分数从小到大排序。add(K key, V value, double score)添加成员并设置分数score(K key, Object o)查分数rank(K key, Object o)查排名从0开始分数最低的排名为0rangeWithScores(K key, long start, long end)按排名取一段带分数的成员。排行榜是ZSet最经典的场景。比如游戏积分排行// 玩家得分 redisTemplate.opsForZSet().add(rank:game:2024, player_1001, 9800.0); // 取前10名 SetZSetOperations.TypedTupleString top10 redisTemplate.opsForZSet().reverseRangeWithScores(rank:game:2024, 0, 9);注意这里用了reverseRangeWithScores分数从大到小排列因为排行榜通常取分数最高的。如果直接rangeWithScores得到的是分数最低的排名。incrementScore(K key, V value, double delta)是给分数加法对应ZINCRBY做实时积分更新不用先取分数再写回原子且高效。count(K key, double min, double max)是按分数区间统计数量remove(K key, Object... values)是移除成员。5. execute与序列化这两个点决定了你的上线质量5.1 execute方法家族绕过模板直达底层execute方法族是RedisTemplate最底层的接口它能让你拿到RedisConnection直接操作命令。它有几个重载execute(RedisCallbackT)最简单的形式回调里给你一个RedisConnectionexecute(SessionCallbackT)在同一个Session连接里执行多条命令保证连接复用executePipelined(RedisCallbackT)管道模式executePipelined(RedisCallbackT, RedisSerializer? resultSerializer)管道并指定结果反序列化器日常开发中executePipelined是性能优化的利器。它对应Redis的Pipeline机制把多条命令一次性发给服务器减少RTT往返时延。如果一个请求里要写20个key一条条写要20次网络往返pipeline一次搞定。看一个例子redisTemplate.executePipelined((RedisCallbackObject) connection - { for (int i 0; i 1000; i) { connection.stringCommands().set( (batch:key: i).getBytes(StandardCharsets.UTF_8), (value: i).getBytes(StandardCharsets.UTF_8)); } return null; });用管道之后测试1000条写入从原来的几十毫秒降到几毫秒提升非常明显。SessionCallback的价值在于保证多条命令在同一个连接执行。Redis的事务MULTI/EXEC就得通过它配合connection.multi()和connection.exec()来实现。RedisTemplate本身没有提供transaction()这样的高亮方法要执行事务就得走SessionListObject results redisTemplate.execute(new SessionCallbackListObject() { Override public ListObject execute(RedisOperations operations) { operations.multi(); operations.opsForValue().set(tx:key1, v1); operations.opsForValue().set(tx:key2, v2); return operations.exec(); } });5.2 序列化乱码问题的根源在哪RedisTemplate的序列化是绕不开的话题。默认情况下Spring Data Redis用JdkSerializationRedisSerializer这个序列化器有两个问题一是存储的key和value会带一串二进制头直接进Redis客户端看全是\xAC\xED\x00\x05t\x00...这样的乱码二是序列化后的体积大占内存。解决方法是显式配置序列化器。最常见的组合是key用StringRedisSerializervalue用GenericJackson2JsonRedisSerializer或者Jackson2JsonRedisSerializer。在配置类里写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; }这里有个容易忽视的细节key和hashKey通常用String序列化value和hashValue用JSON序列化。key用JSON序列化会存成带引号的字符串看着别扭还容易出错。另外还有一个特殊的模板类StringRedisTemplate它继承自RedisTemplate但key和value都强制用String序列化。如果你只存字符串直接用StringRedisTemplate省事如果你要存对象用RedisTemplate加Jackson配置。5.3 序列化不一致导致的坑实际项目里最常见的报错是这样的java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.xxx.User。原因是value用了GenericJackson2JsonRedisSerializer存了一个User对象取出来反序列化时Jackson不认识这个具体类型只能先反序列化成LinkedHashMap。这个问题的根源是GenericJackson2JsonRedisSerializer会把类型信息存在JSON的class字段里但如果你在代码里用redisTemplate.opsForValue().get(user:1)取模板只知道返回Object不知道要转成User所以拿到LinkedHashMap。解法有两种一种是用Jackson2JsonRedisSerializerUser明确指定泛型类型另一种是取出来之后再手动转换Object obj redisTemplate.opsForValue().get(user:1); User user objectMapper.convertValue(obj, User.class);我的建议是搞一个CacheService封装层在封装层里统一做类型转换别让业务代码直接跟RedisTemplate裸奔。6. 过期时间、删除与批量业务里天天踩的小细节6.1 expire、delete、hasKey的正确姿势expire(K key, long timeout, TimeUnit unit)设置过期时间。很多人图省事在set之后单独调expire其实set(key, value, timeout, unit)带过期时间就一步到位了。但有一个例外有些场景是先创建key等业务处理完再设置过期时间比如等待支付30分钟这时候才需要单独调expire。delete(K key)删除单个keydelete(CollectionK keys)批量删除。注意hasKey(K key)返回的也是Boolean对象同样要用Boolean.TRUE.equals()判断。6.2 批量写入的正确姿势别再循环set批量写入最差的方案是for循环里一条条set最好的是Pipeline。Pipeline上面已经介绍了。另一种是opsForValue().multiSet(MapK, V map)对应MSET命令一次性写入多个key。但multiSet没法带过期时间所以如果你需要批量写入并且要过期时间还得用Pipeline逐条set(key, value, timeout, unit)。6.3 缓存穿透、雪崩与空值缓存方案实际项目中用Redis做缓存绕不开三个问题穿透、击穿、雪崩。穿透是指请求了一个不存在的key每次都要打数据库。最常用的方案是空值缓存Object value redisTemplate.opsForValue().get(key); if (value null) { value queryFromDb(key); if (value null) { // 缓存空值过期时间设短一点 redisTemplate.opsForValue().set(key, , 60, TimeUnit.SECONDS); } else { redisTemplate.opsForValue().set(key, value, 600, TimeUnit.SECONDS); } }击穿是指一个热点key过期瞬间大量请求同时打到数据库。解决思路是互斥锁用前面提到的setIfAbsent来实现。雪崩是大量key同时过期解决思路是给过期时间加一个随机抖动避免同一时刻集体失效long timeout 600 ThreadLocalRandom.current().nextInt(300); redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);7. 我的实操体会把这些方法串成一个可用的工具类最后分享一点自己的使用心得。我在项目里习惯封装一个RedisCacheService把RedisTemplate的常用方法再包一层。好处有三个一是业务代码不用直接依赖RedisTemplate的泛型地狱二是序列化、过期时间这些策略在封装层统一处理三是以后要换缓存中间件改一个类就行。封装的时候有几点经验第一模板的key统一用String类型不要用对象当key。对象当key意味着每次都要算hashCode而且序列化方式不同算出来的hashCode不一样同一个业务key换个对象就丢了。第二value的类型统一用JSON字符串或者泛型T。不要一会儿存String一会儿存List一会儿存对象时间长了你会忘了这个key里到底是什么。第三所有获取操作都要判空。Redis没有数据的时候所有get都返回null不判空就NPE。第四设置过期时间必须做。不留过期时间的key就是定时炸弹等Redis内存被打满重启、淘汰策略都是灾难。第五能批量就批量。单个key操作多了RTT是很大的开销Pipeline能大幅提升吞吐量尤其在高频写入场景下收益非常明显。这个方法汇总说白了就是把RedisTemplate的常用API捋了一遍但真正的价值不在API本身而在你什么时候选哪个方法、怎么避坑。像setIfAbsent做分布式锁、increment做原子计数、opsForList做队列、opsForZSet做排行榜、executePipelined做批量写、GenericJackson2JsonRedisSerializer解决乱码——这些组合拳才是项目里真正值钱的东西。业务代码好写但把Redis用对、用稳、用快是要靠一次次踩坑换来的。希望这份汇总能让你少踩几个我踩过的坑。