尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Jedis与Spring Data Redis核心差异及RedisTemplate实践
第一次在项目里翻到RedisTemplate这个 Bean我本能地以为它跟 Jedis 是一回事只是换了个名字。直到某个夜里排查一批缓存写进去了读不出来的问题把 key 贴进redis-cli一查前面挂着一串\xac\xed的乱码前缀才意识到自己一直把两个不同层次的东西当成了同一个东西在用。Jedis是 Java 世界里最直接的 Redis 客户端实现它把 RESP 协议一层层剥开递到你手上你写什么命令它就发什么字节Spring Data Redis则是 Spring 生态里的门面层它根本不关心底下接的是谁换成 Lettuce 照样跑。这篇内容就是围绕这两个客户端展开的它们各自解决什么问题、调用链路长什么样、连接池和序列化这些真正会让人熬夜的细节怎么处理、以及在一个具体项目里到底该选哪一套。适合已经能连上 Redis、写过几行set/get但一碰到连接池耗尽、乱码 key、Pipeline 写法就卡住的同学也适合准备把老项目从 Jedis 迁到 Spring 体系的人先摸个底。1. 先把层次掰开Jedis 是协议驱动Spring Data Redis 是上层门面1.1 两条截然不同的调用链路把这两者的调用链路画出来很多困惑会自己消失。纯 Jedis 的路径非常短业务代码直接调jedis.set(k,v)Jedis 内部把命令编码成 RESP 数组写进 socket 输出流然后阻塞读输入流解析出返回值。中间没有任何抽象层你写的就是 Redis 收到的。这条链路的好处是透明、可控、性能开销极小代价是所有东西都得自己管——连接的开闭、异常处理、超时、重试一样不落。Spring Data Redis 的路径要长一截业务代码调的是redisTemplate.opsForValue().set(k,v)ValueOperations把 key 和 value 交给配置好的RedisSerializer序列化成字节数组再交给RedisConnectionFactory取一个RedisConnection最后由JedisConnection或LettuceConnection去调用真正的驱动。这中间多出来的每一层都带副作用序列化器决定了你存进去的字节长什么样连接工厂决定了连接从哪来、能不能复用模板类决定了异常怎么转换。这里有个容易被忽略的事实RedisTemplate之所以能换底层是因为RedisConnection这层抽象。用 Jedis 的时候JedisConnection内部持有一个Jedis实例而这个实例来自你在配置里指定的JedisConnectionFactory工厂背后其实还是一个JedisPool。所以哪怕你全程只用RedisTemplate连接池参数该调还是得调只是配置位置从 Java 代码挪到了application.yml里。1.2 依赖坐标里藏着的默认值新起一个 Spring Boot 项目加spring-boot-starter-data-redis大多数人不会去看它到底拉了什么进来。实际默认带的是 Lettuce不是 Jedis。这个默认值从 Spring Boot 2.x 一直延续到 3.x很多人以为自己在用 Jedis其实跑的是 Lettuce直到某天要设置一个只有 Jedis 才有的参数才发现对不上。想切回 Jedis得显式排除再引入这一步的写法在版本升级时经常被漏掉dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId exclusions exclusion groupIdio.lettuce/groupId artifactIdlettuce-core/artifactId /exclusion /exclusions /dependency dependency groupIdredis.clients/groupId artifactIdjedis/artifactId /dependency版本这块有个硬约束值得单独说Spring Boot 2.7 管理的 Jedis 是 3.8.xSpring Boot 3.x 管理的是 4.3.x 往上。Jedis 4 开始引入了UnifiedJedis、JedisPooled这一套新 API老的Jedis、JedisPool还留着但被标记为维护状态。如果你在 Boot 3 项目里照抄 Boot 2 时代的 Jedis 配置代码编译能过但某些方法签名已经变了比如JedisPoolConfig的setMaxWaitMillis在新版对象池里推荐用setMaxWait(Duration)虽然旧方法还在但心里要有数。另一个常被踩的坑是配置前缀。Spring Boot 2.x 写spring.redis.host到 3.x 改成了spring.data.redis.host。升级时如果只改了依赖没改配置启动不会报错因为 Redis 客户端会退化到默认的localhost:6379本地开发环境往往还真就连上了一上测试环境才炸。这种本地正常、线上异常的问题最难查。对比项JedisSpring Data Redis定位Redis 协议的原生 Java 驱动Spring 的 Redis 操作抽象层底层依赖自己就是底层可接 Jedis 或 Lettuce连接管理手动获取与归还由 ConnectionFactory 托管序列化自己处理字节数组由 RedisSerializer 统一处理命令覆盖全量跟随 Redis 版本全量另有 Session/事务封装学习成本低会 Redis 命令就会用中需理解模板与序列化机制2. Jedis 的两种用法单连接与 JedisPool线程安全是硬约束2.1 为什么一个 Jedis 实例绝不能跨线程共享新手最容易犯的错是把Jedis对象存成类的成员变量或者静态变量觉得这样省事。测试环境单线程跑得好好的一上并发就开始出现各种诡异的返回值错位A 线程发的get user:1收到了 B 线程set user:2的OK甚至直接抛JedisConnectionException: Unexpected end of stream。根子在 RESP 协议本身。Redis 的单条连接是严格的一问一答顺序模型客户端发一条命令服务端回一条响应客户端再去读。Jedis 内部维护的是一个RedisInputStream和一个RedisOutputStream两个线程同时往同一个输出流写命令字节流会交错同时从同一个输入流读响应谁先读到哪条响应完全不可控。这不是 Jedis 的设计缺陷而是协议本身决定的换任何同步驱动都一样。正确的写法只有两种。第一种是短生命周期用完就关try (Jedis jedis new Jedis(127.0.0.1, 6379, 2000)) { jedis.set(greeting, hello); String v jedis.get(greeting); }第二种也是生产环境唯一推荐的做法是从池里借、用完还try (Jedis jedis jedisPool.getResource()) { jedis.set(greeting, hello); }getResource()返回的Jedis是一个池化对象close()不等于断开连接而是把连接归还给池。这点非常关键——很多人误以为close()会关掉 TCP 连接所以不敢关结果就是连接泄漏池子很快被掏空。顺带提一句 Jedis 4 之后新增的JedisPooled它内部就是池化加同步声明成单例给多线程用是安全的写法上接近RedisTemplate的味道。但如果你要继续用 Spring 的JedisConnectionFactory走的还是JedisPool那条老路没必要混着来。2.2 JedisPool 参数不是拍脑袋填的连接池参数这件事见过太多项目直接抄一段maxTotal100, maxIdle100就上线了。参数背后的逻辑其实不难推。先看maxTotal也就是池里最多有多少个连接。它决定的是同时最多有多少个请求能拿到连接。按 Little 定律粗算需要的连接数 ≈ 峰值 QPS × 单次占用连接的时长秒。举个具体例子某个商品详情接口峰值 2000 QPS每次请求在 Redis 上执行 5 条命令单条往返 0.5ms那么单次请求占用连接的时长是 2.5ms也就是 0.0025 秒。2000 × 0.0025 5理论上 5 个连接就够。但网络抖动、慢查询、GC 停顿都会让这个数字翻倍所以工程上一般给 3 到 5 倍冗余maxTotal落在 20 到 30 区间比较合理。反过来如果你给到 500Redis 服务端单线程处理那 500 条连接的命令上下文切换和内存开销反而更高。maxIdle和minIdle管的是空闲连接的上下限。maxIdle设得比maxTotal小太多会导致高峰期扩出来的连接在空闲时被大量销毁然后又要在下一个高峰重建反复抖动。通常建议maxIdle maxTotalminIdle取maxTotal的三分之一左右保持一个常驻的热连接池。maxWaitMillis是借连接的最长等待时间默认是 -1也就是无限等。这个默认值在生产环境非常危险——一旦池子耗尽所有业务线程会在这里挂住线程池很快被打满整个服务雪崩。必须显式设一个值一般 500ms 到 2000ms超时后抛异常快速失败让上游能感知到。参数含义常见取值建议设错的后果maxTotal池内最大连接数峰值 QPS × RT × 3~5过小排队过大压垮服务端maxIdle最大空闲连接数与 maxTotal 持平频繁创建销毁延迟抖动minIdle最小空闲连接数maxTotal 的 1/3冷启动时建连慢maxWaitMillis借连接最大等待500~2000ms默认无限等导致线程堆积testOnBorrow借出时校验一般关闭开启会多一次 PING 往返testWhileIdle空闲时校验建议开启关闭后可能借到死连接testOnBorrow和testWhileIdle的组合也值得说说。前者每次借出都发一次 PING代价是每次操作多一个 RTT高 QPS 场景下这个开销不能忽略。后者由后台线程定期检查空闲连接代价小得多还能把被服务端踢掉的死连接清理出去。我自己的习惯是关前者、开后者配合timeBetweenEvictionRunsMillis设个 30 秒左右的巡检周期。2.3 连接池耗尽的排查链路Could not get a resource from the pool这个异常几乎每个用 Jedis 的人都遇到过。看到它的第一反应不应该是调大maxTotal而应该先确认连接为什么没回来。第一步看池的实时状态。通过 JMX 或者自己写个健康检查接口把JedisPool.getNumActive()、getNumIdle()、getNumWaiters()打出来。如果NumActive长期贴着maxTotal而NumIdle是 0说明连接确实被占满了问题出在使用侧。第二步翻代码找没关闭的连接。典型写法有这么几种在try块外面拿连接finally里忘了close()用了jedis.pipelined()之后只关了 pipeline 没关连接在RedisCallback里手动getConnection()却没走模板的自动释放。Spring Data Redis 的模板方法内部会自动关连接但只要你绕开模板直接操作RedisConnectionFactory就得自己兜住。第三步查慢命令。一个keys *在百万级 key 的库上能跑好几秒这几秒里那条连接一直是被占用的状态。池子只有 20 个连接几个慢命令并发打进来池子秒空。monitor命令或者慢查询日志slowlog get 10能直接定位到这类问题。大 key 的hgetall、smembers也是同类。第四步看网络。TCP 层的重传、服务端timeout配置默认 0 表示不主动断开、中间的连接跟踪表超时都可能让客户端以为连接还活着实际对端已经没了。表现是借出来的连接一用就报错然后这个坏连接被销毁池子容量持续下降。这时候testWhileIdle就能帮上忙。3. RedisTemplate 的装配细节自动配置给了你什么又藏了什么3.1 两个模板 Bean 的分工Spring Boot 的RedisAutoConfiguration在满足条件时会注册两个 BeanredisTemplate和stringRedisTemplate。前者泛型是Object, Object后者是String, String。很多人看到两个都能用随手挑一个就开始写结果在跨语言、跨服务的场景里踩了坑。StringRedisTemplate的序列化器是StringRedisSerializerkey 和 value 都按 UTF-8 直接转成字节数组。你存set user:1 jackredis-cli里get user:1拿到的就是jack干净利落。如果你的 Redis 还要被其他语言的客户端读、被运维用命令行查、被监控系统扫描这个模板是首选。RedisTemplate的默认序列化器是JdkSerializationRedisSerializer也就是把对象扔进ObjectOutputStream。这个默认值放在今天看基本不能直接用但它是历史遗留官方一直没改因为改成 JSON 会破坏老项目的兼容性。3.2 JDK 序列化带来的三个后遗症第一个后遗症是 key 不可读。你set(user:1, userObj)实际存进 Redis 的 key 是\xac\xed\x00\x05t\x00\x06user:1这一串。用redis-cli敲keys *出来全是乱码运维想帮你清个缓存都无从下手。更麻烦的是监控告警按 key 前缀配规则一条都匹配不上。第二个后遗症是跨语言不通。Java 序列化的字节流带着类描述信息Python、Go、Node 的客户端读出来是一堆二进制根本解析不了。现在稍微像样一点的系统都是多语言混布这一条几乎直接判了 JDK 序列化的死刑。第三个后遗症是存储膨胀。一个只有三个字段的小对象JDK 序列化出来动辄两三百字节JSON 可能只要八十字节。按千万级 key 算这部分浪费是实打实的内存和带宽。所以结论很明确RedisTemplate的默认序列化器必须改除非你的项目从第一天起就只用 Java 读写、且永远不打算用命令行排查。3.3 换一套序列化器的完整写法最常见的方案是 key 用 Stringvalue 用 JSON。配置类的写法如下Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); StringRedisSerializer keySerializer new StringRedisSerializer(); GenericJackson2JsonRedisSerializer valueSerializer new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(keySerializer); template.setHashKeySerializer(keySerializer); template.setValueSerializer(valueSerializer); template.setHashValueSerializer(valueSerializer); template.afterPropertiesSet(); return template; } }GenericJackson2JsonRedisSerializer会自动在 JSON 里写入一个class字段记录全限定类名反序列化时靠它还原类型。好处是opsForValue().get(k)能直接拿到原始对象不用手动转。代价是多存了一个字段并且对包名重构不友好——你把类挪个包历史数据就反序列化失败了。如果不需要自动还原类型用Jackson2JsonRedisSerializerObject配合明确的ObjectMapper会更干净。这里有个细节直接new Jackson2JsonRedisSerializer(Object.class)反序列化回来的是LinkedHashMap而不是原对象很多人第一次遇到会懵。解决办法是构造时传入具体的实体类或者老老实实用GenericJackson2JsonRedisSerializer。ObjectMapper的配置也要交代清楚否则LocalDateTime一序列化就抛InvalidDefinitionExceptionObjectMapper om new ObjectMapper(); om.registerModule(new JavaTimeModule()); om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); om.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);顺带说一句Spring Boot 3 之后GenericJackson2JsonRedisSerializer的构造器可以接受一个ObjectMapper把上面这个配置好的om传进去两边的行为就对齐了。这个改动不大但对排查Redis 里的 JSON 格式和接口返回的 JSON 格式不一致这类问题很有帮助。4. 批量与原子Pipeline、事务、Lua 在两套 API 下的写法4.1 Jedis 的 Pipeline 与关闭时机单条命令一个 RTT一万条命令就是一万个 RTT。在跨机房场景下单次 RTT 按 1ms 算一万条就是 10 秒这是纯浪费。Pipeline 的作用是把命令攒起来一次性发出去再一次性读回所有响应把网络往返压缩成一次。Jedis 的写法很直观try (Jedis jedis jedisPool.getResource()) { Pipeline pipeline jedis.pipelined(); for (int i 0; i 10000; i) { pipeline.set(batch:key: i, v i); } ListObject results pipeline.syncAndReturnAll(); }这里有个必须记住的限制Pipeline 里不能有依赖前一条结果的命令。比如你先get拿结果再决定set什么值这在 Pipeline 里做不到因为命令是在发送阶段就确定好的你根本拿不到中间结果。真要这种逻辑得用 Lua 脚本。还有个更隐蔽的坑syncAndReturnAll()之后Pipeline 对象本身要处理。老版本 Jedis 里Pipeline继承了MultiKeyPipelineBaseclose()的语义在不同版本有差异有的版本close()会把底层连接一起关掉导致归还到池里的是个废连接。安全做法是用sync()只发送不接收或者干脆把Jedis放在 try-with-resources 里让连接无论如何都正常归还。4.2 executePipelined 的回调形态与返回值陷阱Spring Data Redis 的 Pipeline 走的是executePipelined参数是一个RedisCallback返回值是ListObjectListObject results redisTemplate.executePipelined((RedisCallbackObject) connection - { for (int i 0; i 10000; i) { connection.stringCommands().set( (batch:key: i).getBytes(StandardCharsets.UTF_8), (v i).getBytes(StandardCharsets.UTF_8)); } return null; });注意回调里必须返回null返回别的东西没有意义反而会干扰结果收集。返回值results的顺序和命令发送顺序一致但类型不一定是你要的——SET命令返回的是byte[]形式的OKGET返回的是byte[]需要自己用序列化器转。这也是很多人第一次用executePipelined时卡住的地方明明get了字符串results里拿到的却是一串字节。如果回调里要做到类型安全可以用SessionCallback配合opsForValue()Spring 会在内部帮你做命令和序列化的转换代码可读性好很多ListObject results redisTemplate.executePipelined(new SessionCallbackObject() { Override SuppressWarnings({unchecked, rawtypes}) public Object execute(RedisOperations operations) { for (int i 0; i 10000; i) { operations.opsForValue().set(batch:key: i, v i); } return null; } });4.3 Lua 脚本在 Spring 里的执行方式需要读-判断-写原子执行的时候Lua 是标准答案。释放分布式锁那段经典脚本两边写法都不复杂。纯 JedisString script if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; Object result jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));Spring Data Redis 走DefaultRedisScriptDefaultRedisScriptLong script new DefaultRedisScript(); script.setScriptText(if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end); script.setResultType(Long.class); Long released redisTemplate.execute(script, Collections.singletonList(lockKey), requestId);setResultType(Long.class)这行不能省不设的话返回的是Long还是byte[]取决于底层版本容易出类型转换异常。另外脚本对象建议做成单例 Bean 缓存起来DefaultRedisScript在第一次执行时会算 SHA1 并尝试EVALSHA重复 new 会让缓存失效每次退化成EVAL白白多传一遍脚本体。5. 选型不是站队三种组合各自的适用场景5.1 纯 Jedis、Spring Data Redis Jedis、Spring Data Redis Lettuce 的横向对比市面上常见的组合其实是三种而不是两种。第一种是裸用 Jedis 加JedisPool第二种是RedisTemplate底层走 Jedis第三种是RedisTemplate底层走 Lettuce。它们的差异不只在 API 长相上更在并发模型上。组合并发模型优点短板典型场景纯 Jedis JedisPool同步阻塞一连接一请求透明、可控、开销最小全手写无 Spring 集成工具类、批处理任务、老系统Spring Data Redis Jedis同步阻塞池化模板便利 连接池可控同步模型高并发需大池中等并发、代码规范优先Spring Data Redis LettuceNetty 异步单连接多路复用连接数少天然支持异步与响应式调试链路长参数需理解高并发网关、响应式栈Lettuce 的核心优势是基于 Netty 的多路复用一个连接上可以并发跑多个命令靠请求 ID 匹配响应。这意味着同样 2000 QPSLettuce 可能只需要 1 个连接就够了而 Jedis 需要十几个。连接少了服务端的负担和客户端的线程开销都跟着降。但多路复用不是没有代价。一旦某个命令特别慢它会阻塞这个共享连接上的后续命令——这就是所谓的队头阻塞。Jedis 因为一连接一请求慢命令只影响自己那条连接隔离性反而更好。所以有些团队会在 Lettuce 上配置spring.data.redis.lettuce.pool开一个连接池用commons-pool2做连接复用把隔离性补回来。注意这个池默认是不生效的必须显式引入commons-pool2依赖。5.2 从业务形态倒推技术选择选型这件事我一般按三个问题来问。第一个问题Redis 是纯缓存还是有业务语义如果只是给数据库挡一层key 过期就过期那StringRedisTemplate配 JSON 序列化就够了用哪个底层驱动影响不大按团队熟悉度选。如果是分布式锁、计数器、排行榜这类有语义的场景那就要格外关注原子性和超时设置客户端的选择反而次要。第二个问题并发峰值和延迟要求。QPS 在几千以内、单次 Redis 操作在毫秒级Jedis 池化完全撑得住而且排查问题直观——jstack一看就知道线程卡在借连接还是等响应。QPS 上万、或者要接响应式编程WebFlux ReactiveRedisTemplate那就只能用 LettuceJedis 没有响应式实现。第三个问题存量代码和技术债。如果项目里已经有大量JedisPool的封装硬迁到RedisTemplate的收益往往抵不上回归测试的成本。反过来如果项目是新建的 Spring Boot 应用直接上spring-boot-starter-data-redis用StringRedisTemplate加 JSON 序列化是最省心的路径以后要换驱动改个依赖就行业务代码不用动。有一点需要提醒混用是有代价的。同一个项目里既有裸 Jedis 又有RedisTemplate意味着有两套连接池、两套序列化规则key 的格式很可能对不上。我见过一个项目里A 模块用 Jedis 存的是 UTF-8 字节B 模块用默认序列化器的RedisTemplate读怎么都读不出来最后查了两天才定位到序列化器不一致。要么统一要么至少在 key 命名上加前缀区分。6. 真机上最容易翻车的几个点与处理办法6.1 序列化不一致导致的写进去了读不出来这是所有问题里排查成本最高的一个因为现象很迷惑redis-cli里明明能看到 key业务代码就是读不到。核心原因是 key 的字节序列不一样。纯 Jedis 用jedis.set(user:1, ...)key 的字节是75 73 65 72 3a 31。用默认序列化器的RedisTemplateString,Object存key 经过JdkSerializationRedisSerializer变成了ac ed 00 05 74 00 06 75 73 65 72 3a 31。两个 key 在 Redis 眼里完全不同自然互相读不到。定位手法很简单用redis-cli --scan --pattern *看实际字节或者干脆在代码里打一行日志把keySerializer.serialize(user:1)的十六进制打出来跟redis-cli里object encoding的结果对一遍。对不上就是序列化器的问题。修复方式有两种一是统一序列化方案把所有读写都收口到同一个模板 Bean二是如果你必须和外部系统共享 key那就全部用StringRedisSerializer值也用字符串复杂对象自己writeValueAsString手动转。后一种做法牺牲了便利性但可移植性最好跨语言、跨版本都不会出问题。6.2 超时缺失与线程堆积的连锁反应Jedis 的JedisPool构造函数有一堆重载最容易被忽略的就是timeout这个参数。默认值是2000毫秒但很多人在写配置的时候直接new JedisPool(config, host, port)把后面几个参数全丢了结果连接超时和读写超时都用默认。单看 2 秒不算长但连锁反应是Redis 那边某个主从切换的瞬间所有请求都卡在等响应每条连接占满 2 秒。池子 20 个连接2 秒内能处理的请求数就是 20 个剩下的全部在maxWaitMillis里排队超时后抛异常。上游如果没做降级线程池迅速被打满接口整体超时。所以三组超时都要显式配连接超时连不上 Redis 多久放弃、读写超时发出去多久没响应放弃、借连接超时池里没空闲连接等多久。Spring Boot 里的对应配置在spring.data.redis.timeout读写、spring.data.redis.connect-timeout连接连接池的借出超时则在spring.data.redis.jedis.pool.max-wait。除了超时还要考虑重试。Jedis 本身不做自动重试Redis 集群模式下由JedisCluster处理重定向。如果你在业务层自己加重试一定要控制次数并且加退避否则 Redis 抖动时你的重试会把抖动放大成雪崩。6.3 批量清理时 KEYS 与 SCAN 的选择缓存清理脚本里出现jedis.keys(prefix:*)是很常见的写法在测试环境毫无问题因为 key 只有几百个。生产环境上百万 key 的时候KEYS是 O(N) 全量遍历而且 Redis 是单线程的这条命令执行期间整个实例阻塞所有其他请求排队。几秒钟的阻塞足以让上游的熔断器全部打开。替代方案是SCAN用游标分批遍历每次只返回一小批不会长时间占用主线程String cursor ScanParams.SCAN_POINTER_START; ScanParams params new ScanParams().match(prefix:*).count(500); do { ScanResultString result jedis.scan(cursor, params); ListString keys result.getResult(); if (!keys.isEmpty()) { jedis.del(keys.toArray(new String[0])); } cursor result.getCursor(); } while (!ScanParams.SCAN_POINTER_START.equals(cursor));这里有两个注意点。第一SCAN是弱一致性的遍历过程中新增或删除的 key 可能被漏掉或重复做清理任务时不必追求精确多跑一轮就行。第二del一次传太多 key 同样会阻塞一次 500 个是相对安全的量count参数只是提示实际返回数量可能更多所以del之前最好再切一刀。Spring Data Redis 里对应的是redisTemplate.scan(ScanOptions)返回Cursorbyte[]用起来更顺一些但同样要注意别把游标结果一次性全collect到内存里——百万级 key 拉进 List堆内存直接给你颜色看。6.4 连接池泄漏的几种隐蔽写法最后补充一个我实际遇到过的情况连接池慢慢被耗尽但代码里每处getResource()都写了close()肉眼查不出来。问题出在异常路径上。有一种写法是Jedis jedis pool.getResource(); jedis.set(k, v); jedis.close();如果set抛了异常close()就永远执行不到连接泄漏。必须用 try-with-resources 或者finally。还有一种更隐蔽的是在finally里close()之前又调了一次 Redis 操作那个操作抛异常close()同样被跳过。排查这类问题的有效办法是盯着池的NumActive指标做趋势观察正常业务下它应该围绕均值上下波动如果看到它阶梯式上涨、只增不减那就是泄漏。配合jstack抓一下线程栈看看哪些线程持有连接没释放基本能锁定范围。另外对象池的abandon相关配置setRemoveAbandonedOnBorrow可以在开发环境临时打开把借出超过一定时间的连接强制回收虽然不能作为生产方案但定位问题时很好用。一路写下来我自己在 Jedis 和 Spring Data Redis 之间来回切换过好几次最深的一点感受是这两个东西的复杂度都不在 API 本身API 看半小时文档就能上手真正耗时的是序列化规则、连接生命周期、超时与重试这些不写在快速入门里的细节。上手一个新项目我现在的习惯是先花十分钟把序列化器和连接池参数这两块配置看清楚再动手写业务代码——这十分钟通常能省掉后面好几个晚上的排查。
RELATED

相关推荐

从OpenSearch迁移到阿里云SLS的架构优势与实践

从OpenSearch迁移到阿里云SLS的架构优势与实践

1. 为什么从OpenSearch迁移到阿里云SLS?在数据量爆炸式增长的今天,企业搜索和日志分析需求正面临前所未有的挑战。OpenSearch作为一款开源的分布式搜索和分析引擎,长期以来被广泛应用于日志分析、企业搜索等场景。但随着业务规模的扩大&#…

📅 2026/10/26 18:49:02
金蝶K3 WISE BOM穿透查询与ECN关联分析技术实现

金蝶K3 WISE BOM穿透查询与ECN关联分析技术实现

1. 项目背景与需求解析 在制造业ERP系统中,BOM(Bill of Materials)管理是核心模块之一。金蝶K3 WISE作为国内主流ERP解决方案,其BOM穿透查询功能直接影响着工程变更、成本核算、生产计划等关键业务流程。近期我们在实施某电子制造…

📅 2026/10/26 18:49:02
C#短信发送程序:PDU编码与串口AT指令实现

C#短信发送程序:PDU编码与串口AT指令实现

简介:面向希望深入GSM短信通信与C#底层编码实践的开发者,这份C#短信发送程序围绕PDU类展开,重点解决短信提交、号码与SMSC地址编码、7位/8位/16位字符转换以及长短信拆分重组等问题。包内共45个文件,以cs源码、exe程序、resources…

📅 2026/10/26 18:49:02
MORE NEWS

更多资讯

📰

给AI对话加跨会话记忆:从语义提取到向量召回的设计与实践

做AI辅助开发的朋友应该都有同感:同一个项目聊到第三周,新开一个会话,AI完全不记得你两周前让它梳理的那套模块边界,也不记得你已经拍板过的技术选型。你只能重新粘贴一遍背景,重新交代一遍约束,时间全耗在…

📰

智慧城市三维可视化多端协同技术解析

1. 项目背景与核心挑战 在智慧城市建设的浪潮中,"城市安全大脑"作为中枢神经系统,承担着实时监控、预警分析和应急指挥的重任。而指挥大屏则是这套系统的"眼睛"——决策者通过它直观掌握城市运行态势。但传统方案面临一个尴尬的技术…

📰

一氧化碳报警器法规解析与安装指南

1. 项目概述:一氧化碳报警器的强制安装背景 去年冬天,我邻居家发生的一起事故让我至今心有余悸。半夜两点接到电话说整栋楼在疏散,原因是某户燃气热水器泄漏导致一氧化碳浓度超标。当时消防员破门而入时,屋内两位老人已经出现中毒…

📰

claude-mem:为Claude CLI注入长期记忆的开源工具

你有没有过这种体验:用官方终端版 Claude 干活,今天聊完的项目背景、技术选型、代码规范,明天新开会话它全忘了,你又得把同样的话重复一遍。这不是你操作有问题,而是这类对话工具天然的“无状态”特性——每次会话都是…

📰

Web开发中的随机图片访问技术实现与优化

1. 项目概述"访问随机图片地址"这个看似简单的需求,实际上涉及到了网络请求、资源管理、性能优化等多个技术领域。作为一名长期从事Web开发的工程师,我发现很多初级开发者容易低估这类功能的实现复杂度。本文将从一个实际项目经验出发&#xf…

📰

agency-agents:面向复杂业务的多智能体协同决策架构

1. 项目概述:这不是一个“代理”,而是一套协同决策系统“agency-agents”这个词组乍看像拼写错误,或是某个被截断的英文短语,但放在当前技术演进语境下,它其实指向一个正在快速成型的新范式——具备自主目标感知、环境…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬