恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Spring 4.0.3整合Redis:spring-data-redis与Jedis配置实战
首页
资讯中心
/
Spring 4.0.3整合Redis:spring-data-redis与Jedis配置实战
Spring 4.0.3整合Redis:spring-data-redis与Jedis配置实战
发布时间:2026/10/14 17:39:16
接手一个还跑在 Spring 4.0.3 上的老项目时领导给的任务很具体把 Redis 接进来先把热点数据的缓存扛住后面还要支持计数器、简单限流和跨节点的任务互斥。和同事商量了一圈技术选型几乎没有悬念——spring-data-redis 配 jedis这是这个阶段最主流、文档最多的组合。整完这套东西后回头整理这份记录版本怎么配、配置文件怎么写、序列化器怎么选、业务代码怎么落以及我亲身踩过的那些坑给同样在维护老项目的朋友一个参考。1. 为什么我要往 Spring 4.0.3 里塞 Redis1.1 Redis 在老项目里要扮演的角色这个业务系统的数据库压力已经很明显了。最典型的问题是几个列表查询接口每次请求都要走 MySQL条件复杂一点就慢查询前端一刷新就重复计算几百毫秒。短期内不可能重构数据层也不可能上微服务所以在不动整体架构的前提下引入 Redis是性价比最高的办法。我把 Redis 在这个项目里的定位分成四类热点缓存用户信息、商品详情、推荐列表这类读多写少的数据计数器访问量、日活、订单号段这类需要自增的场景临时队列异步任务、消息暂存用 List 结构做轻量级的生产者消费者模型分布式互斥多个应用节点同时触发定时任务时需要一把简单的锁挡住重复执行。这里有个原则要提前说清楚Redis 是加速器不是数据库。凡是能通过 MySQL 落地的强一致数据不要为了省事全塞进 Redis否则后面就是缓存和数据库一致性的大坑。1.2 为什么不直接写 Jedis 代码可能有人会问项目里直接引一个 jedis 包自己封装一个 RedisUtils 不就行了最初我也这么想过但看了一眼要做的功能列表就放弃了。裸写 Jedis 意味着你自己要处理连接池管理、每次操作的连接获取与归还、异常的包装、序列化逻辑。更麻烦的是一旦后面要接 Spring Cache 注解、要在事务边界里控制 Redis 行为裸写 Jedis 基本等于把 Spring 那套抽象全部重新发明一遍。Spring Data Redis 提供的核心价值是RedisTemplate。它把底层连接细节封装掉了同时又保留了 Redis 的完整操作能力。你在业务代码里只需要redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS); Object cached redisTemplate.opsForValue().get(key);连接是自己管理的序列化是自己配置好的异常转换也是统一的。拿它和裸 Jedis 的代码一比改动量差一个数量级。1.3 本次整合的范围与优先级当时项目周期只有一周我列了一个非常克制的清单目标实现方式优先级热点接口缓存RedisTemplate Spring Cache 注解高访问量计数StringRedisTemplate 的 increment高接口简单限流incr expire中定时任务互斥SETNX Lua 脚本低优先级高的先做优先级低的在版本稳定后再补。这个顺序帮我避免了一个常见问题一上来就把所有功能都堆在一起结果出了问题分不清是配置问题还是业务代码问题。实际上我最后连限流和互斥都做了但因为前面是有序推进的后面两个功能几乎是顺手接上的。2. 版本组合先对齐Spring Data Redis 和 Jedis 的兼容边界2.1 Spring Data Redis 1.x 是老项目的合理落点Spring 4.0.3 是 2014 年发布的版本对应配套的 Spring Data Redis 必然是 1.x 系列。这里最大的教训是不要手滑引入 Spring Data Redis 2.x。Spring Data Redis 2.x 内部依赖 Spring 5 的 API而你的项目还锁在 Spring 4.0.3 上。一旦 2.x 进入依赖树最直接的结果就是启动时抛NoSuchMethodError或者ClassNotFoundException然后你被迫在全局升 Spring 版本那整个改造就失控了。我最后锁定的是spring-data-redis 1.4.2.RELEASE。选它的原因有几点1.4 版本支持 Redis 3.x 的服务端能力提供了GenericJackson2JsonRedisSerializer这一点对老项目非常重要后面序列化章节会细说与 Spring 4.0.3 配合实测稳定没有遇到方法缺失的问题。2.2 Jedis 版本必须锁死在 2.xSpring Data Redis 1.x 的JedisConnectionFactory是照着 jedis 2.x 的 API 编写的。所以Jedis 版本必须锁死 2.x。我在项目里遇到过这样的场景某个中间件模块为了自己的功能直接依赖了 jedis 3.0然后整个项目的 jedis 版本被顶到 3.x结果 spring-data-redis 里连接池初始化逻辑调用的方法签名对不上运行期直接报NoSuchMethodError。最终我确定的依赖组合是组件版本说明spring-data-redis1.4.2.RELEASE整合核心包jedis2.7.3保持 2.x不要高于 3.xcommons-pool22.4.2Jedis 连接池依赖spring-data-commons1.10.2.RELEASE传递需要显式锁定防止被顶到新版Maven 坐标写法如下dependency groupIdorg.springframework.data/groupId artifactIdspring-data-redis/artifactId version1.4.2.RELEASE/version /dependency dependency groupIdredis.clients/groupId artifactIdjedis/artifactId version2.7.3/version /dependency dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId version2.4.2/version /dependency2.3 用依赖树确认没有隐藏冲突版本问题光靠 pom 里写的版本号不够必须看最终进入 classpath 的版本。我在整合完成后做了一次完整的依赖树检查mvn dependency:tree -Dincludesorg.springframework.data,redis.clients,org.apache.commons重点看两个地方spring-data-commons的版本是否和spring-data-redis1.4.2 匹配。这个包是 Spring Data 系列的公共底座被顶到新版本很容易出现抽象方法缺失是否有其他模块传递引入了 jedis 3.x。如果有需要在dependencyManagement里强制锁定 jedis 2.7.3或者排除那条传递依赖。这个步骤不要省。Spring 系列的整合问题很大一部分不是代码写的错而是 classpath 里的 jar 版本乱套了。3. XML 装配三件套连接池、序列化器与 RedisTemplate3.1 Jedis 连接池参数Spring 4 时代的老项目XML 配置依然是主流。我用一个单独的spring-redis.xml文件承载所有 Redis 相关配置然后在主配置文件里import进来这样结构清晰也方便以后整体替换。第一件事是定义连接池bean idjedisPoolConfig classredis.clients.jedis.JedisPoolConfig property namemaxTotal value50/ property namemaxIdle value10/ property nameminIdle value5/ property namemaxWaitMillis value3000/ property nametestOnBorrow valuetrue/ property nametestOnReturn valuefalse/ /bean这里有个特别容易踩的坑连接池最大连接数的属性名是maxTotal不是maxActive。如果你在网上搜老资料很多文章写的是maxActive因为那是 commons-pool 1.x 风格。而 jedis 2.7.3 的JedisPoolConfig继承的是 commons-pool2 的GenericObjectPoolConfig属性名就是maxTotal。属性名写错最迷惑的地方在于Spring 不会报错它只会认为没有这个属性于是连接池的一堆参数全部保持默认值等到压测时连接数一飞冲天你才发现配置根本没生效。参数取值方面我当时的流量规模大概几百 QPSmaxTotal50已经留出了充足余量。testOnBorrowtrue能防止从池里取出已经失效的连接代价是多一次连接有效性检查对于这种请求量完全可以接受。3.2 连接工厂配置然后是连接工厂bean idjedisConnectionFactory classorg.springframework.data.redis.connection.jedis.JedisConnectionFactory property namehostName value127.0.0.1/ property nameport value6379/ property namepassword value/ property namedatabase value0/ property nametimeout value2000/ property nameusePool valuetrue/ property namepoolConfig refjedisPoolConfig/ /bean几个细节说明usePooltrue必须确认。这个属性默认就是 true但如果你在配置里手滑改成 false每次操作都会新建物理连接QPS 一上来服务端和客户端都会拉垮timeout2000是 socket 超时单位毫秒对业务系统来说 2 秒足够不要设成负数或 0否则某些异常情况下线程会长时间挂起database0是默认库。如果业务上要用多个库建议直接按业务拆分 key 前缀而不是真的去用 Redis 的多 database 特性维护成本更低如果 Redis 需要密码填password属性没有密码就留空字符串。3.3 序列化器配置序列化器是整个整合过程中最需要提前想清楚的环节。Spring Data Redis 默认用的是JdkSerializationRedisSerializer如果你不配置就会出现两个非常难受的结果key 变成\xAC\xED\x00\x05t\x00...这样一串乱码在 redis-cli 里根本无法阅读value 用的是 Java 原生序列化字节数膨胀严重而且跨语言基本读不了。我当时定下的方案是key 用 StringRedisSerializervalue 用 GenericJackson2JsonRedisSerializerhash 结构里的 key 和 value 也分别指定。bean idstringRedisSerializer classorg.springframework.data.redis.serializer.StringRedisSerializer/ bean idgenericJacksonSerializer classorg.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer/ bean idstringRedisTemplate classorg.springframework.data.redis.core.StringRedisTemplate property nameconnectionFactory refjedisConnectionFactory/ /bean bean idredisTemplate classorg.springframework.data.redis.core.RedisTemplate property nameconnectionFactory refjedisConnectionFactory/ property namekeySerializer refstringRedisSerializer/ property namehashKeySerializer refstringRedisSerializer/ property namevalueSerializer refgenericJacksonSerializer/ property namehashValueSerializer refgenericJacksonSerializer/ /bean为什么 value 不用Jackson2JsonRedisSerializer而是用GenericJackson2JsonRedisSerializerJackson2JsonRedisSerializer需要你在构造时指定目标类型写入和读取都靠这个类型信息转换一旦对象字段变化或者多个类型共用同一个 value 空间就会遇到反序列化异常。GenericJackson2JsonRedisSerializer会在 JSON 里写入一个类型标记class字段反序列化时能自动还原原始类型。代价是 JSON 体积稍大一些但对业务系统来说完全值得。提示如果项目里已经存在了大量用默认序列化器写入的历史数据不要直接改序列化器否则旧 key 会全部读不到。稳妥做法是通过 key 前缀做灰度或者用一段临时脚本把旧数据先读出来再重新写入。3.4 接入 Spring Cache 注解如果只是用 RedisTemplate 手动读写缓存那 Spring Data Redis 的价值还只发挥了一半。把它接到 Spring 的 Cache 抽象上业务代码里的缓存逻辑瞬间就干净了。先在 Spring 配置里开启缓存注解驱动cache:annotation-driven cache-managercacheManager/然后注入缓存管理器。注意 spring-data-redis 1.4 的RedisCacheManager是直接基于 RedisTemplate 工作的bean idcacheManager classorg.springframework.data.redis.cache.RedisCacheManager constructor-arg index0 refredisTemplate/ property namedefaultExpiration value1800/ /beandefaultExpiration单位是秒我设成 1800 秒也就是半小时。这个配置对所有没有单独指定过期时间的缓存生效。Spring Cache 在 Redis 里存的 key 会带上cacheName::前缀比如userCache::1001排查时一眼就能看出来是哪类缓存数据。4. 在业务代码里真正用起来缓存、计数、队列、锁4.1 缓存读写和 Spring Cache 注解接上 Spring Cache 之后写缓存代码变成最简单的事Cacheable(value userCache, key #userId) public UserVO getUser(Long userId) { return userMapper.findById(userId); }第一次调用会进入方法体查询数据库结果写入 Redis后续相同参数的调用直接走缓存。这里注意key的写法#userId是 SpEL 表达式引用的是方法参数不要写成字符串userId那是固定 key所有用户会互相串数据。手动读写缓存的场景也保留了一些比如某些数据需要精细控制过期时间String key order:detail: orderId; OrderVO order (OrderVO) redisTemplate.opsForValue().get(key); if (order null) { order orderService.getById(orderId); redisTemplate.opsForValue().set(key, order, 30, TimeUnit.MINUTES); }4.2 计数与限流increment 的关键细节Redis 的自增是原子操作非常适合做计数。StringRedisTemplate的increment方法直接对应 Redis 的INCR命令String key visit:uv: LocalDate.now(); Long count stringRedisTemplate.opsForValue().increment(key);这里有一个非常经典的细节计数器通常需要设置过期时间但increment本身不负责设置 TTL。很多人的第一反应是increment之后立刻expire那会产生一个窗口期如果进程在两次操作之间崩溃这个 key 就变成永不过期。正确做法是Long count stringRedisTemplate.opsForValue().increment(key); if (count ! null count 1L) { stringRedisTemplate.expire(key, 1, TimeUnit.DAYS); }只有第一次自增时才设置过期时间后续自增不会刷新 TTL。这样既避免崩溃导致永不过期又不会因为每天被频繁刷新 TTL 而让过期时间失效。限流也是同样的套路窗口内计数超过阈值就拒绝请求。比如某个接口限制每分钟 100 次调用String key rate:api: apiName : minuteBucket; Long count stringRedisTemplate.opsForValue().increment(key); if (count ! null count 1L) { stringRedisTemplate.expire(key, 60, TimeUnit.SECONDS); } if (count 100) { throw new RateLimitException(请求太频繁); }4.3 简单队列List 结构的生产者消费者Redis 的 List 结构非常适合做轻量级队列。生产端用leftPush消费端用rightPop就是一个典型的 FIFO 队列// 生产者 stringRedisTemplate.opsForList().leftPush(task:queue, taskJson); // 消费者阻塞等待最多 30 秒 String taskJson stringRedisTemplate.opsForList().rightPop(task:queue, 30, TimeUnit.SECONDS);rightPop带超时的重载会转换成 Redis 的BRPOP没有数据时阻塞等待不会空转。这个机制很适合任务量不稳定的场景。提示阻塞 pop 会占用一个 Redis 连接如果消费者线程很多连接池会被长期占住。当时我给消费者单独起了一个专用的 RedisTemplate连接池参数也单独设置避免和普通请求抢连接。4.4 分布式锁SETNX 加 Lua 保证原子性定时任务在多个节点都部署时最朴素的互斥方案就是用 Redis 的SETNX语义。Spring Data Redis 1.x 的setIfAbsent可以完成加锁String lockKey job:lock:reportJob; String token UUID.randomUUID().toString(); Boolean locked stringRedisTemplate.opsForValue().setIfAbsent(lockKey, token);但这样只有锁标记没有过期时间一旦持有锁的进程挂了锁永远不释放所有节点都会卡死。于是我们通常会在加锁成功后补一个expire。可是这又回到了上面那个窗口期问题加锁成功到设置超时之间的崩溃依然会留下死锁。正确的出路是用 Lua 脚本把“设置值 过期时间”合并成一个原子操作-- 加锁 if redis.call(set, KEYS[1], ARGV[1], NX, EX, ARGV[2]) then return 1 else return 0 end解锁时也要用 Lua 脚本判断持有人防止误删别人的锁-- 解锁只有 value 匹配时才删除 if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end在 Java 侧用DefaultRedisScript执行DefaultRedisScriptLong lockScript new DefaultRedisScript( if redis.call(set, KEYS[1], ARGV[1], NX, EX, ARGV[2]) then return 1 else return 0 end, Long.class ); Long result redisTemplate.execute(lockScript, Arrays.asList(lockKey), token, 30); if (result ! null result 1L) { // 拿到锁执行任务 }这个实现已经能覆盖大部分业务场景了。当然如果追求更“正统”的分布式锁可以用 Redisson但在老项目里为一个定时任务引入整套新客户端性价比不高。5. 整合后的坑按排查顺序逐个复盘5.1 坑一spring-data-commons 版本错位现象是项目启动时报了一连串的ClassNotFoundException其中有一个类名是org.springframework.data.keyvalue.core.KeyValueOperations。这个类并不在 spring-data-redis 里而是在 spring-data-commons 里。排查过程很直接mvn dependency:tree显示 spring-data-redis 1.4.2 传递依赖的 spring-data-commons 版本被项目里另一个框架挤成了 1.6.x而 1.6.x 里根本没有那个类。解决办法是在dependencyManagement里显式锁定 spring-data-commons 版本dependencyManagement dependency groupIdorg.springframework.data/groupId artifactIdspring-data-commons/artifactId version1.10.2.RELEASE/version /dependency /dependencyManagement这个坑的价值在于提醒我们Spring Data 是一套系列组件公共底座版本错了哪怕你写的代码一行没变启动都会挂。5.2 坑二Jedis 3.x 混入了依赖树这个坑我在前面已经预告过。现象是应用启动没问题但第一次执行 Redis 操作时抛NoSuchMethodError而且错误消息里的方法签名完全看不懂。当时怀疑是 jar 冲突直接查依赖树mvn dependency:tree -Dincludesredis.clients果然某个内部模块传递引入了 jedis 3.0.1。spring-data-redis 1.x 的JedisConnectionFactory.createPool用的是 2.x 里JedisPool的构造函数签名3.x 里变了于是运行期才炸出来。处理方式是在dependencyManagement中锁定 jedis 版本dependencyManagement dependency groupIdredis.clients/groupId artifactIdjedis/artifactId version2.7.3/version /dependency /dependencyManagementNoSuchMethodError是最隐蔽的一类问题编译期不报错运行期才炸遇到它第一反应就应该是版本冲突。5.3 坑三key 前的乱码前缀Redis 里出现了大量\xAC\xED\x00\x05t\x00...开头的 key。这个现象是使用 RedisTemplate 时没有配置 key 序列化器导致的Java 对象序列化后的开头固定是\xAC\xED所以看 key 就能立刻判断。解决办法就是把keySerializer和hashKeySerializer都换成StringRedisSerializer。配置文件在第 3.3 节已经给了。这里补一个额外建议key 设计一定要有前缀规范比如order:detail:123、user:info:1001不要图省事裸写一个数字或一个单词。前缀带来的可读性优势在线上排查问题时会被无限放大。5.4 坑四Jackson 反序列化变成 LinkedHashMap现象是value 用的是 GenericJackson2JsonRedisSerializer写入的时候是正常 POJO读取时ClassCastExceptiondebug 一看发现实际对象是LinkedHashMap而不是目标类型。这个问题的根子出在我当时的自定义 ObjectMapper 配置上。为了省一点存储空间我把 ObjectMapper 的 default typing 给关了于是 GenericJackson2JsonRedisSerializer 写入 JSON 时没有附带类型信息反序列化时只能当成 Map 处理。解决方式有两个直接用默认的GenericJackson2JsonRedisSerializer不要随便动它的 default typing如果确实要自定义 ObjectMapper必须保证写入时包含类型信息。这里多提醒一句不要为了图省事把 ObjectMapper 的 default typing 在整个项目里全局开启尤其是 value 可能来自外部输入时这会有反序列化安全风险。如果只是内部数据用 GenericJackson2JsonRedisSerializer 这个边界范围内的类型信息就够了。5.5 坑五连接池参数没生效压测时发现 Redis 连接数一路涨完全没有按照我在jedisPoolConfig里配置的maxTotal50收敛最后服务端连接数到了几百触发了max clients限制。排查链路如下先确认JedisConnectionFactory的poolConfig有没有成功注入检查 XML 里 bean 的引用名是否一致检查usePool是否被改成了 false检查JedisPoolConfig的属性名是否写错最常见的就是把maxTotal写成maxActive。这里还有个小坑如果 Spring 容器里有多个同名的 bean后加载的会覆盖前面的且不报错。所以我排查的时候也确认了全局没有其他jedisPoolConfig定义。验证连接池是否真正生效最直接的方法是压测时在 Redis 侧执行redis-cli info clients观察connected_clients是否稳定在你预期的连接池大小附近。5.6 坑六Cacheable 在内部调用时不生效这是 Spring 缓存非常经典的坑但整合后第一次遇到依然很迷惑。现象是从一个私有方法调用同一个类里带Cacheable的方法缓存始终不生效。原因是 Spring 的缓存能力是通过 AOP 代理实现的外部调用者拿到的是代理对象而内部this.xxx()调用的是原始对象绕过了代理注解自然不生效。解决办法是把缓存方法放到另一个 Service 类里让外部调用触发代理或者自己注入代理对象调用自身。更简单的做法直接把缓存逻辑写成显式的 RedisTemplate 操作不要依赖注解。对我这个项目来说热点方法不算多显式写反而更直观。6. 上线前后的观察连接数、内存与回滚预案6.1 压测验证整合完成后我先做了一轮压测重点不是 QPS 有多高而是确认三个指标Redis 连接数是否稳定缓存命中率是否达到预期慢查询是否出现。Redis 侧建议打开慢查询日志CONFIG SET slowlog-log-slower-than 10000 CONFIG SET slowlog-max-len 12810000表示记录超过 10 毫秒的命令。上线后观察SLOWLOG GET如果发现某个 key 频繁出现在慢日志里优先排查是不是 key 过大或者命令复杂度太高。6.2 内存与过期策略缓存上线前我把 key 过了一遍给所有缓存类数据都设置了 TTL。说实话这是最容易忽略的一步一旦 Redis 内存打满默认策略下新写入会失败业务直接影响。另外和运维确认了 Redis 的淘汰策略如果 Redis 还承担业务数据存储比如队列、计数建议allkeys-lru保证内存不足时淘汰最久未用的 key如果 Redis 里只放缓存volatile-lru也行但实际上过期策略这层不能完全依赖TTL 一定要在业务代码里设置。上线前跑一遍redis-cli --bigkeys看看有没有超大 value。队列、缓存里如果存了大对象这个命令能很快暴露问题。6.3 回滚预案Redis 接入业务后最担心的不是 Redis 挂了而是 Redis 挂了导致业务也挂。缓存的作用是加速不应该成为依赖点。我给所有缓存读操作包了一个轻量适配层public class RedisAdapter { public Object getWithFallback(String key, CallableObject fallback) { try { Object cached redisTemplate.opsForValue().get(key); if (cached ! null) { return cached; } Object real fallback.call(); redisTemplate.opsForValue().set(key, real, 30, TimeUnit.MINUTES); return real; } catch (Exception e) { log.error(Redis操作失败, key{}, key, e); try { return fallback.call(); } catch (Exception ex) { throw new RuntimeException(ex); } } } }核心思路是Redis 操作失败时捕获异常记录日志然后降级到原始逻辑。当时上线后确实碰到过一次网络抖动Redis 连接中断了十几秒业务接口只是日志多了几条 ERROR对用户没有任何影响。6.4 一点个人体会如果让我重新做一遍我会把两件事放在编码之前第一把依赖树版本边界确定下来第二把序列化器方案定下来。这两件事决定了整个整合过程的调试成本技术问题反而都在其次。这套 Spring 4.0.3 spring-data-redis jedis 的组合今天看确实很老了但老项目就是这样你不能把底层框架推倒重来只能在不破坏现有架构的前提下把新能力引进来。理解这套手工装配的逻辑后面无论项目是升级到 Spring Boot 还是迁移到别的缓存客户端排查问题的思路都是通的。