恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Jedis 实战指南:从连接池到 Spring Boot 整合的 Redis 客户端入门
首页
资讯中心
/
Jedis 实战指南:从连接池到 Spring Boot 整合的 Redis 客户端入门
Jedis 实战指南:从连接池到 Spring Boot 整合的 Redis 客户端入门
发布时间:2026/10/9 20:54:24
1. 为什么 Jedis 是理解 Redis 客户端的最佳起点很多人学 Redis 的路径是这样的装好服务端用命令行敲几个SET、GET觉得挺简单然后打开项目准备用 Java 连一下结果第一步就卡住了——到底该用 Jedis、Lettuce 还是 Redisson这三个名字都听过但它们的定位差别其实挺大。如果你刚开始接触 Redis 客户端我建议你从 Jedis 入手原因很直接它最贴近 Redis 的原生命令API 设计几乎是一个方法对应一条命令你写jedis.set(k,v)的时候脑子里能清楚知道服务端执行了什么。这种透明感对建立正确的认知特别重要。Jedis 在 Java 生态里算是一位老将了早期几乎所有的 Java Redis 项目都用它。它的核心价值在于简单直接引入一个依赖new一个对象就能开始操作。它不像 Redisson 那样封装了大量分布式对象分布式锁、布隆过滤器、分布式集合等也不像 Lettuce 那样基于 Netty 做异步和响应式。Jedis 就是老老实实地把 Redis 命令搬到了 Java 方法上。对于入门者来说这种没有魔法的设计反而降低了理解成本——你不需要先搞懂响应式编程、事件循环这些概念就能把数据存进去、取出来。不过简单不等于没有坑。Jedis 的连接管理是新手最容易翻车的地方。早期很多人直接在方法里new Jedis()用完不关跑一段时间连接数就爆了还有人把 Jedis 实例做成 static 全局共享结果多线程环境下出现数据错乱。这些问题的根源在于Jedis 实例本身不是线程安全的而连接又是稀缺资源。所以真正要掌握 Jedis重点不在于记住多少个 API而在于理解它的连接模型、资源管理方式以及如何和连接池配合。这篇文章我会从实际使用的角度把 Jedis 拆开讲清楚它到底怎么工作、依赖怎么引、五种数据类型怎么操作、连接池怎么配、事务和管道怎么用、和 Spring Boot 怎么整合以及我在实际项目里踩过的那些坑。不管你是刚学 Redis 的新手还是用过但一直没搞明白连接池参数的老手应该都能从里面找到有用的东西。下面我们直接进入正题。2. Jedis 的依赖引入与连接建立的完整过程2.1 Maven 依赖的选择与版本匹配引入 Jedis 本身很简单在pom.xml里加一段依赖就行。但这里有个容易被忽略的点Jedis 版本和 Redis 服务端版本之间存在兼容性关系。Jedis 3.x 和 4.x 的 API 差异不小比如 4.x 之后很多方法签名变了JedisPoolConfig的继承关系也调整过。如果你照着网上的老教程写代码用的却是新版本依赖编译就会报错。dependency groupIdredis.clients/groupId artifactIdjedis/artifactId version4.4.3/version /dependency选版本的时候我一般遵循两个原则一是优先选稳定的小版本不要一上来就用刚发布的大版本二是看你的 Redis 服务端版本如果服务端是 6.x 以上Jedis 用 4.x 比较合适能支持 ACL、RESP3 等新特性。另外Jedis 内部依赖了commons-pool2来做连接池如果你项目里已经引入了别的版本的 commons-pool2注意排查冲突否则可能出现NoSuchMethodError这种运行时才暴露的问题。提示如果你用的是 Spring Boot其实可以不用手动指定 Jedis 版本直接引入spring-boot-starter-data-redis然后在配置里排除 Lettuce、引入 Jedis让 Spring Boot 帮你管理版本。这样能避免大部分版本冲突问题。2.2 最基础的连接方式与它的致命缺陷先看最朴素的写法这也是很多教程的开篇示例Jedis jedis new Jedis(127.0.0.1, 6379); jedis.auth(yourpassword); jedis.set(name, tom); String name jedis.get(name); System.out.println(name); jedis.close();这段代码能跑通但它只适合本地测试。放到生产环境里问题立刻暴露每次操作都新建一个 TCP 连接用完关闭连接的创建和销毁开销非常大。Redis 的单次操作可能只要几十微秒但建立一个 TCP 连接加上认证可能要几毫秒。也就是说连接开销比实际操作开销高出一两个数量级。在高并发场景下这种方式会把大量时间浪费在连接建立上吞吐量根本上不去。更严重的是如果代码中间抛了异常jedis.close()没被执行连接就泄漏了。跑一段时间Redis 的connected_clients指标会一路飙升最后服务端拒绝新连接。所以直连方式只能用于验证环境是否通任何正式代码都不应该这么写。2.3 用 try-with-resources 保证连接释放在讲连接池之前先纠正一个习惯问题。即使你暂时用直连也应该用 try-with-resources 来管理try (Jedis jedis new Jedis(127.0.0.1, 6379)) { jedis.auth(yourpassword); jedis.set(name, tom); System.out.println(jedis.get(name)); }Jedis实现了Closeable接口所以可以放进 try-with-resources。这样无论中间是否抛异常连接都会被正确关闭。这个习惯看起来小但在排查连接泄漏问题时能帮你省下大量时间。我见过不少线上事故根源就是某个方法里忘了关连接平时流量小看不出来一到高峰期连接数瞬间打满。3. 五种数据类型的 Jedis 操作与命令对照3.1 String 类型最常用也最容易用错String 是 Redis 里最基础的类型Jedis 对应的方法也很直观try (Jedis jedis pool.getResource()) { jedis.set(user:1:name, 张三); jedis.setex(sms:code:138xxxx, 300, 8866); String name jedis.get(user:1:name); Long count jedis.incr(article:1:views); jedis.mset(k1, v1, k2, v2); ListString values jedis.mget(k1, k2); }这里有几个实际使用中的要点。第一setex是设置带过期时间的键做验证码、临时缓存时非常常用比先set再expire更原子少一次网络往返。第二incr系列做计数器很方便但要注意它只能对整数字符串操作如果键对应的值不是数字会直接抛异常。第三mset和mget是批量操作能显著减少网络往返次数但要注意单次批量不要太大否则会阻塞 Redis 主线程一般建议控制在几百个键以内。注意set方法在 Jedis 4.x 里返回的是StringOK而不是旧版本的void如果你从 3.x 升级上来方法返回值的变化可能导致编译错误需要留意。3.2 Hash 类型存储对象结构的首选Hash 适合存对象比如用户信息、商品详情。相比把对象序列化成 JSON 存成 StringHash 的好处是可以单独读写某个字段不用整体反序列化。try (Jedis jedis pool.getResource()) { jedis.hset(user:1001, name, 李四); jedis.hset(user:1001, age, 28); MapString, String user new HashMap(); user.put(email, lisiexample.com); user.put(city, 杭州); jedis.hmset(user:1001, user); String name jedis.hget(user:1001, name); MapString, String all jedis.hgetAll(user:1001); SetString fields jedis.hkeys(user:1001); Long delCount jedis.hdel(user:1001, city); }实际项目里我一般用 Hash 存那些字段会单独更新的对象。比如用户积分只需要hincrBy更新积分字段不用把整个用户对象读出来再写回去。但要注意Hash 的字段数量不宜过多如果一个 Hash 有几万个字段hgetAll会一次性拉取全部数据既占内存又慢这种情况应该考虑拆分或者换用其他结构。3.3 List 类型消息队列与最新列表List 是双向链表结构适合做简单的消息队列或者最新 N 条列表。try (Jedis jedis pool.getResource()) { jedis.lpush(msg:queue, msg1, msg2, msg3); String msg jedis.rpop(msg:queue); jedis.lpush(news:latest, 新闻A); jedis.ltrim(news:latest, 0, 9); ListString latest jedis.lrange(news:latest, 0, -1); }用 List 做队列时lpushrpop是经典的先进先出组合。但这里有个坑rpop在队列为空时返回 null如果你的消费者用轮询方式不断rpop空队列时会疯狂空转浪费 CPU。更好的做法是用brpop它会阻塞等待直到有元素或者超时ListString result jedis.brpop(5, msg:queue);ltrim是做最新列表的关键每次插入后裁剪保证列表长度固定避免无限增长。这个技巧在实现最近浏览记录最新公告这类功能时特别实用。3.4 Set 与 ZSet去重与排行榜Set 是无序去重集合ZSet 是有序集合两者在社交、统计场景里用得很多。try (Jedis jedis pool.getResource()) { jedis.sadd(article:1:likes, user1, user2, user3); Boolean liked jedis.sismember(article:1:likes, user1); Long likeCount jedis.scard(article:1:likes); jedis.zadd(rank:score, 100, playerA); jedis.zadd(rank:score, 200, playerB); SetString top10 jedis.zrevrange(rank:score, 0, 9); Long rank jedis.zrevrank(rank:score, playerA); }ZSet 做排行榜几乎是标配zadd更新分数zrevrange取前 N 名zrevrank查某个成员的排名。要注意的是ZSet 的分数是 double 类型如果分数相同Redis 会按成员字典序排序这个细节在做并列排名逻辑时要考虑进去。另外zadd在 Jedis 4.x 里返回long表示新增成员的数量更新已有成员分数时返回 0这个返回值可以用来判断是新增还是更新。4. JedisPool 连接池的参数配置与调优逻辑4.1 为什么必须用连接池前面说过直连方式每次都要建连接开销大。连接池的核心思路是预先创建一批连接复用它们用完归还而不是关闭。这样既避免了频繁建连的开销又能通过池的大小控制对 Redis 的并发压力。Jedis 的连接池基于commons-pool2实现核心类是JedisPool。JedisPoolConfig config new JedisPoolConfig(); config.setMaxTotal(50); config.setMaxIdle(20); config.setMinIdle(5); config.setMaxWaitMillis(2000); config.setTestOnBorrow(true); JedisPool pool new JedisPool(config, 127.0.0.1, 6379, 2000, yourpassword);4.2 关键参数的含义与设置依据连接池参数不能随便填每个参数背后都有实际含义填错了要么浪费资源要么在高峰期拖垮服务。参数含义设置建议maxTotal池中最大连接数根据并发量和 Redis 承受能力定一般 20~100maxIdle最大空闲连接数略小于 maxTotal避免空闲连接占资源minIdle最小空闲连接数保证有预热连接避免突发流量时临时建连maxWaitMillis获取连接最大等待时间建议 1000~3000ms超时快速失败testOnBorrow借出时是否检测连接可用生产建议 true但会增加一次 ping 开销testWhileIdle空闲时是否检测配合 evictor 使用清理失效连接maxTotal是最关键的参数。设太小高并发时线程都在等连接吞吐上不去设太大可能把 Redis 的连接数打满反而影响其他应用。我的经验是先估算峰值 QPS再乘以单次操作的平均耗时得到理论并发连接数然后留 1.5 倍余量。比如峰值 5000 QPS单次操作 1ms理论并发是 5但考虑到网络抖动和慢查询实际设 20~50 比较稳妥。maxWaitMillis决定了拿不到连接时等多久。设成 -1 表示无限等待这是很危险的一旦池被占满所有线程都会卡死。设成 0 表示不等待拿不到直接抛异常适合对延迟极度敏感的场景。一般设 2000ms 左右既能容忍短暂抖动又不会让请求堆积太久。4.3 连接池使用中的常见陷阱第一个陷阱是忘记归还连接。用pool.getResource()拿到连接后必须调用close()归还而不是真的关闭。用 try-with-resources 能自动处理try (Jedis jedis pool.getResource()) { jedis.set(key, value); }第二个陷阱是在连接上执行阻塞命令。比如brpop会阻塞连接如果池子小、阻塞命令多连接很快被占满。这类命令要么单独用一个池要么严格控制超时时间。第三个陷阱是连接池配置和实际负载不匹配。我遇到过一个小项目maxTotal设了 8平时没事一到促销活动请求全部卡在获取连接上接口响应时间从几十毫秒涨到几秒。后来把maxTotal调到 50问题立刻消失。所以连接池参数一定要结合压测来定不能拍脑袋。提示可以通过pool.getNumActive()和pool.getNumIdle()监控连接池状态这两个值能直观反映池子是否吃紧。如果getNumActive()长期接近maxTotal说明该扩容了。5. 事务、管道与 Lua 脚本的实战用法5.1 Jedis 事务multi/exec 的封装Redis 的事务通过MULTI、EXEC实现Jedis 用Transaction对象封装try (Jedis jedis pool.getResource()) { Transaction tx jedis.multi(); tx.set(k1, v1); tx.incr(counter); ListObject results tx.exec(); }要注意Redis 事务不支持回滚。如果exec之前某条命令入队失败比如语法错误整个事务会被放弃但如果命令入队成功、执行时出错比如对字符串执行incr其他命令依然会执行出错的那条返回错误信息。这一点和数据库事务完全不同不能想当然地以为要么全成功要么全失败。5.2 管道批量操作的性能利器管道Pipeline解决的是网络往返次数问题。普通情况下发一条命令等一个响应100 条命令就是 100 次往返。管道把命令一次性发出去再一次性读回响应往返次数降到 1 次。try (Jedis jedis pool.getResource()) { Pipeline pipeline jedis.pipelined(); for (int i 0; i 1000; i) { pipeline.set(key: i, value: i); } ListObject results pipeline.syncAndReturnAll(); }实测下来1000 次set用管道比逐条执行快 5~10 倍具体倍数取决于网络延迟。管道适合批量写入、批量读取场景比如初始化缓存、批量导入数据。但要注意管道里的命令不是原子的中间可能插入其他客户端的命令如果需要原子性得用事务或 Lua。5.3 Lua 脚本原子性与复杂逻辑当需要读-判断-写这种复合操作且要求原子性时Lua 脚本是最佳选择。Redis 执行 Lua 脚本时是单线程原子的不会被其他命令打断。String 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(lock:key), Collections.singletonList(uniqueValue));上面这段是经典的比较并删除逻辑常用于释放分布式锁。用 Lua 的好处是判断和删除在服务端一次完成不会出现判断通过后、删除前锁过期的竞态问题。写 Lua 脚本时要注意脚本里不要做耗时操作否则会阻塞整个 Redis因为它是单线程执行的。6. Spring Boot 整合 Jedis 的配置要点6.1 依赖调整与自动配置Spring Boot 默认用的是 Lettuce要换成 Jedis需要两步排除 Lettuce引入 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然后在application.yml里配置连接信息spring: redis: host: 127.0.0.1 port: 6379 password: yourpassword database: 0 timeout: 2000ms jedis: pool: max-active: 50 max-idle: 20 min-idle: 5 max-wait: 2000msSpring Boot 会自动读取这些配置创建JedisConnectionFactory和RedisTemplate。这里要注意spring.redis.jedis.pool下的参数名和JedisPoolConfig的属性名不完全一样比如max-active对应maxTotalmax-wait对应maxWaitMillis别搞混了。6.2 RedisTemplate 的序列化配置默认的RedisTemplate用 JDK 序列化存进去的 key 和 value 都是二进制乱码用命令行看根本认不出来。实际项目里一般改成 String 序列化Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate( RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); StringRedisSerializer stringSerializer new StringRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.afterPropertiesSet(); return template; } }key 用 String 序列化方便在命令行里查看和排查value 用 JSON 序列化可读性好跨语言也能解析。这个配置几乎是 Spring Boot Redis 项目的标配建议直接抄。6.3 直接注入 JedisPool 的灵活用法虽然RedisTemplate封装得很好但有时候你就是想用原生的 Jedis API比如执行一些RedisTemplate没封装好的命令。这时候可以直接注入JedisPoolService public class CacheService { Autowired private JedisPool jedisPool; public void doSomething() { try (Jedis jedis jedisPool.getResource()) { jedis.set(key, value); } } }Spring Boot 在检测到 Jedis 依赖后会自动配置JedisPoolBean直接注入即可。这种混合使用的方式在实际项目里很常见常规操作走RedisTemplate特殊命令走原生 Jedis。7. 我在实际项目里踩过的 Jedis 坑7.1 连接泄漏的排查过程有一次线上告警Redis 的connected_clients持续上涨最后服务端开始拒绝连接。排查过程是这样的先看应用侧的连接池监控发现getNumActive()一直等于maxTotal说明连接全被占用了。然后 dump 线程栈发现大量线程卡在pool.getResource()上等待。最后定位到一段代码里面用了pool.getResource()但没放在 try-with-resources 里中间一个if分支直接return了连接没归还。这个坑的教训是只要用了getResource()就必须保证close()一定被执行。try-with-resources 是最省心的方式没有之一。如果代码结构复杂至少要在finally里关闭。7.2 大 key 导致的超时另一个坑是往 Redis 里写了一个几 MB 的大 key。平时读写都正常但某次网络抖动读取这个大 key 超时了触发了重试重试又超时形成雪崩。后来复盘发现这个 key 存的是一个列表随着业务增长越来越大没人注意到。避免大 key 的办法一是控制单个 value 的大小一般建议不超过 10KB二是定期用redis-cli --bigkeys扫描发现大 key 及时拆分三是对于集合类型如果元素数量可能很大考虑分片存储比如list:1、list:2这样拆开。7.3 序列化不一致导致读不到数据还有一次两个服务共用一个 RedisA 服务用 JDK 序列化写入B 服务用 JSON 序列化读取结果 B 读出来全是乱码。这个问题的根源是序列化方式不统一。解决办法很简单团队内约定统一的序列化规范key 用 Stringvalue 用 JSON所有服务都遵守。如果历史数据已经用了别的序列化方式迁移时要做好兼容处理。7.4 连接池参数在容器环境下的适配在容器化环境里连接池参数还要考虑容器副本数。比如你设了maxTotal50部署了 10 个副本那对 Redis 的最大连接数就是 500。如果 Redis 的maxclients配置是默认的 10000看起来够用但如果还有其他应用共用就要算总账。我一般会按单副本连接数 × 副本数 Redis maxclients 的 70%来规划留出余量。8. 从 Jedis 出发下一步该学什么把 Jedis 用熟之后你会发现它的定位很清晰同步、阻塞、贴近原生命令。这既是优点也是局限。在高并发、低延迟场景下同步阻塞模型会成为瓶颈每个请求都要占用一个线程等待响应。这时候就该了解 Lettuce 了它基于 Netty支持异步和响应式一个连接可以处理多个并发请求连接利用率高得多。但我不建议一上来就学 Lettuce。原因很简单如果你连 Jedis 的连接模型、资源管理都没搞明白直接上异步框架出了问题根本不知道怎么排查。Jedis 帮你建立的是Redis 操作的本质认知——命令怎么发、响应怎么收、连接怎么管。这些认知是通用的换成任何客户端都用得上。我的建议路径是先用 Jedis 把五种数据类型、连接池、事务管道都跑一遍理解每一步在做什么然后遇到性能瓶颈时再去研究 Lettuce 的异步模型最后如果需要分布式锁、分布式集合这些高级功能再看 Redisson。这样每一步都有明确的动机学起来不会飘。最后分享一个我自己的习惯不管用哪个客户端我都会在本地起一个 Redis用命令行配合客户端一起调试。客户端写进去的数据立刻用命令行GET、HGETALL看一眼确认存进去的到底是什么。这个习惯帮我发现了无数次序列化问题、编码问题和大 key 问题。工具再高级最终落到 Redis 里的还是那些字节亲眼看到它们心里才踏实。