Jedis入门到实战:Java操作Redis的完整指南与连接池坑解析

发布时间:2026/10/22 4:52:08
Jedis入门到实战:Java操作Redis的完整指南与连接池坑解析 很多朋友第一次接触 Redis拿到的资料基本都是 Jedis 的示例代码但真正动手跑一个测试 Demo 的时候又会遇到各种莫名其妙的问题。我这篇就把 Jedis 从基本概念到可运行的测试 Demo 完整过一遍顺便把那些文档里不会写清楚的坑也一并说了。1. 先别急着敲代码搞清楚 Jedis 在 Redis 生态里的位置1.1 Jedis 到底是什么Jedis 是 Java 语言里最老牌、最轻量的 Redis 客户端它的名字就是 Jedis——跟 Redis 就差一个字母。你可以把它理解成 Redis 和 Java 程序之间的翻译官你的 Java 代码想要让 Redis 存一个值、取一个值、删一个 key都是通过 Jedis 把请求发过去再把结果带回来。很多初学者的误区是以为 Jedis 本身是一个独立的数据库或者缓存组件。不是的。Redis 服务端是 C 语言写的它自己跑在一个端口上默认 6379提供了一套基于 RESP 协议的命令接口。而 Jedis 只是把发送命令、解析响应这些琐碎的底层工作封装成了 Java 方法调用让你写jedis.set(key, value)就能完成一次操作而不用自己去拼字符串、维护 socket 连接。1.2 为什么我建议入门阶段优先用 Jedis做一个测试 Demo选哪个客户端其实是有讲究的。市面上主流的 Java Redis 客户端有好几款但我个人一直建议刚接触 Redis 的朋友从 Jedis 入手理由很直接它足够简单。Jedis 的 API 几乎和 Redis 原生命令一一对应你学SET、GET、HSET在 Jedis 里就是set、get、hset几乎没有学习成本。它足够透明。不像某些封装很重的客户端Jedis 的报错和连接行为非常直白出问题的时候你能很快定位到是网络问题、权限问题还是数据类型问题。它的资料最多。社区里关于 Jedis 的踩坑文章和示例代码随便一搜就是一大堆对于初学者来说遇到问题能找到答案本身就是最大的优势。当然最流行和最好用是两回事。Jedis 的缺点是线程不安全而且在高并发场景下需要靠连接池来管理连接。这个我会在第 4 部分详细讲这里先留个印象。1.3 Jedis、Lettuce、Redisson 怎么选这里我简单列个对照表方便你有个全局认识。记住一句话你写测试 Demo 的时候用 Jedis搞清楚了 Redis 本身后面再按项目需求换客户端也不迟。客户端线程安全性核心特点适合场景Jedis线程不安全需连接池轻量、API 直白、命令对应度高入门学习、小型项目、对性能要求不极致的场景Lettuce线程安全天然支持并发基于 Netty连接复用能力强是 Spring Data Redis 的默认客户端Spring Boot 项目、长连接高并发场景Redisson线程安全封装了大量分布式数据结构分布式锁、队列、信号量需要使用分布式特性的项目一句话总结只是做测试 Demo、学习 Redis 命令选 Jedis打正式项目且项目基于 Spring Boot多数时候你会用 Lettuce需要分布式锁、分布式集合这些高级特性再考虑 Redisson。2. 搭一个能跑通的测试环境从零开始写第一个 Demo2.1 环境准备清单在写代码之前你要确保下面这些环境是齐的可用的 Redis 服务端。本地装一个 Redis 也好用 Docker 起一个也好或者连公司测试环境的 Redis 也行。重点是你得知道它的 IP、端口和密码。JDK 8 及以上。Jedis 3.x 之后要求 JDK 8这个对大多数开发环境来说都不是问题。Maven 或 Gradle。用来管理 Jedis 依赖我这里用 Maven 做示例。一个 Java IDE。你可以在本地这样启动一个最简单的 Redis# 如果已经通过 Homebrew 安装 redis-server /usr/local/etc/redis.conf # 如果用的是 Docker docker run -d --name my-redis -p 6379:6379 redis启动之后用redis-cli ping能返回PONG就说明 Redis 服务已经跑起来了。2.2 引入 jedis 依赖时最容易踩的版本坑Maven 里引入 Jedis 很简单在pom.xml里加dependency groupIdredis.clients/groupId artifactIdjedis/artifactId version5.1.2/version /dependency但这里我要特别提醒一个坑Jedis 版本和 Redis 服务端版本之间没有强绑定关系但和 JDK 版本有绑定关系。有同学用 JDK 8 却引入了一个只支持 JDK 11 的 Jedis 新版本结果跑起来直接报UnsupportedClassVersionError。另一个常见问题项目本身是 Spring Boot但你手动引入了 Jedis 依赖同时又没有排除 Spring Data Redis 自带的 Lettuce结果两个客户端共存在 classpath 里。虽然不一定报错但后续排查连接问题时很容易自己把自己绕晕。做测试 Demo 的时候尽量用干净的独立项目别往大项目里塞。2.3 第一段能跑的代码连接、写入、读取下面这段代码是完整的第一个 Demo。我故意没有做任何多余封装就是为了让你看清 Jedis 最基本的使用套路import redis.clients.jedis.Jedis; public class FirstJedisDemo { public static void main(String[] args) { // 1. 创建连接 // 如果你的 Redis 有密码用 new Jedis(host, port) 之后再 jedis.auth(password) Jedis jedis new Jedis(127.0.0.1, 6379); try { // 2. 验证连通性 String pong jedis.ping(); System.out.println(连接成功Redis 返回 pong); // 3. 写入一个 key jedis.set(demo:name, zhangsan); // 4. 读取一个 key String name jedis.get(demo:name); System.out.println(读取到的 name name); // 5. 检查 key 是否存在 Boolean exists jedis.exists(demo:name); System.out.println(key 是否存在 exists); // 6. 设置过期时间单位秒 jedis.expire(demo:name, 60); System.out.println(key 剩余存活时间 jedis.ttl(demo:name)); } finally { // 6. 关闭连接 jedis.close(); } } }这段代码跑起来控制台应该输出连接成功Redis 返回PONG 读取到的 namezhangsan key 是否存在true key 剩余存活时间60到这里你的第一个 Jedis 测试 Demo 已经通了。别看这段代码简单它已经包含了连接、写入、读取、过期时间设置这些最核心的操作。一个很容易忽略的细节是finally里的jedis.close()。这个调用不只是关闭连接如果你后面改用了连接池在池子里拿到的是借用的连接调用close()其实是在归还连接给连接池。这种习惯要从一开始就养成。3. 把五种常用数据类型都测一遍API 实操对照3.1 String 类型缓存一个字段的全过程String 是 Redis 里最简单也最常用的数据类型什么缓存计数器、临时状态存储全都能用它。在 Jedis 里String 的操作非常直观Jedis jedis new Jedis(127.0.0.1, 6379); // 基本读写 jedis.set(user:1:email, helloexample.com); String email jedis.get(user:1:email); // setnx只在 key 不存在时才设置常用于分布式锁的雏形 Long result jedis.setnx(lock:order:123, 1); // 自增自减常用于做计数器比如网站访问量 jedis.incr(counter:pageview); jedis.incrBy(counter:pageview, 10); jedis.decr(counter:pageview); // 批量操作 jedis.mset(k1, v1, k2, v2, k3, v3); ListString values jedis.mget(k1, k2, k3);测试的时候我特别建议你把setnx和incr这两个命令多跑几次。很多初学者对这两个方法的返回值不敏感实际上它们常常是业务逻辑的关键setnx返回 1 表示设置成功返回 0 表示 key 已经存在incr的返回值则是自增后的值。3.2 Hash 类型把 Java 对象拆开存Hash 类型是一个非常实用主义的设计。你可以把一个 Java 对象的多个字段分别存到一个 Redis key 的不同 field 里而不需要把整个对象序列化成一个大字符串。这在做用户信息缓存的时候特别常见。// 向 hash 中添加字段 jedis.hset(user:1, name, zhangsan); jedis.hset(user:1, age, 28); // 一次设置多个字段 MapString, String profile new HashMap(); profile.put(city, beijing); profile.put(job, developer); jedis.hset(user:1, profile); // 读取单个字段 String age jedis.hget(user:1, age); // 读取全部字段 MapString, String all jedis.hgetAll(user:1); // 删除字段 jedis.hdel(user:1, age); // 判断字段是否存在 Boolean hasName jedis.hexists(user:1, name);这里要提醒一点Hash 的 field 数量不宜过多。虽然 Redis 的 Hash 类型在字段少的时候做了内存优化ziplist 编码但如果你一个 key 下面塞了几万个 field读写性能会明显下降。做测试的时候无所谓但脑子里要有这根弦。3.3 List、Set、ZSet 的常见用法与返回值细节List 类型适合做消息队列、最新动态列表这种有顺序要求的场景// 从右边推入 jedis.rpush(queue:notify, msg1, msg2, msg3); // 从左边弹出模拟队列的先进先出 String msg jedis.lpop(queue:notify); // 读取列表某一范围 ListString list jedis.lrange(queue:notify, 0, -1); // 获取列表长度 Long length jedis.llen(queue:notify);Set 类型适合做去重、交集并集运算比如共同好友jedis.sadd(tags:java, spring, redis, jvm); jedis.sadd(tags:backend, redis, mysql, linux); // 求交集 SetString common jedis.sinter(tags:java, tags:backend); // 判断成员是否存在返回 true/false Boolean isMember jedis.sismember(tags:java, redis); // 随机弹出一个成员 String randomMember jedis.spop(tags:java);ZSet 带分数能做排行榜// 添加成员及分数 jedis.zadd(ranking:score, 100, player1); jedis.zadd(ranking:score, 80, player2); jedis.zadd(ranking:score, 95, player3); // 按分数从高到低取出前三名 SetString top3 jedis.zrevrange(ranking:score, 0, 2); // 获取某个成员的分数 Double score jedis.zscore(ranking:score, player1); // 给某个成员的分数加分 jedis.zincrby(ranking:score, 5, player1);我建议你把zrevrange和zrange的输出对比一下它们一个从高到低、一个从低到高。用错一个排行榜顺序就反了。这种问题在测试 Demo 里最容易暴露。3.4 关于 key 过期和事务的补充测试除了五种数据类型你可能还需要测一下过期时间和事务。过期时间前面提过expire但有个非常容易踩坑的细节重新设置一个 key 的值并不会自动清除它之前设置的过期时间。比如你先set key v1然后expire key 60之后又set key v2这个 key 依然会在 60 秒后过期。因为你用set覆盖的是 value而不是 TTL。如果你希望覆盖后永不过期需要显示地persist key或者expire key -1。事务在 Jedis 里的写法比较特别它是把命令先塞到一个队列里最后才统一执行Transaction tx jedis.multi(); tx.set(balance:1, 100); tx.incrBy(balance:1, -30); tx.set(operation:last, deduct); ListObject results tx.exec();注意exec()的返回结果是一个ListObject里面的每一项对应事务里每一条命令的执行结果。而且这个事务执行期间不会被打断保证了一组操作要么全部执行要么在中间出错时不会出现执行了一半的状态注意 Redis 事务没有数据库那种回滚机制命令如果语法错误不会执行但运行时错误如类型不匹配前面的命令还是会生效。4. 生产环境的正确姿势连接池与线程安全的细节4.1 为什么高并发下不能用单个 Jedis 实例我见过很多刚接触 Redis 的同学写出的代码长这样一个类里private Jedis jedis new Jedis(host, port);然后所有方法都用这个字段。这个写法在测试 Demo 里没问题但在高并发场景下会出大问题。因为 Jedis 实例本身不是线程安全的。多个线程同时通过同一个 Jedis 连接发命令就会出现响应错乱、数据覆盖、甚至连接直接断掉的情况。为什么 Jedis 做不到线程安全因为它的实现里复用了同一个 socket 连接发送命令和读取响应的过程不是原子化的。想象一下两个人同时对着同一个对讲机说话你说一句、我也说一句对讲机那头的人根本分不清谁是谁。多线程共享一个 Jedis 连接就是这种混乱状态。所以生产环境的正确做法是用JedisPool连接池来管理一组 Jedis 连接。每个线程从池子里借一个连接用完再还回去这就避免了并发争抢同一个 socket 的问题。4.2 JedisPool 的连接参数调优JedisPool 的使用并不复杂但参数配置值得细抠import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class JedisPoolDemo { public static void main(String[] args) { // 连接池配置 JedisPoolConfig poolConfig new JedisPoolConfig(); // 最大连接数 poolConfig.setMaxTotal(50); // 最大空闲连接数 poolConfig.setMaxIdle(20); // 最小空闲连接数 poolConfig.setMinIdle(5); // 从池中获取连接的最大等待时间毫秒 poolConfig.setMaxWaitMillis(3000); // 借出连接时是否做有效性检测 poolConfig.setTestOnBorrow(true); // 归还连接时是否做有效性检测 poolConfig.setTestOnReturn(false); // 创建连接池 JedisPool pool new JedisPool(poolConfig, 127.0.0.1, 6379, 5000, yourpassword); // 使用 try (Jedis jedis pool.getResource()) { String result jedis.set(pool:demo, ok); System.out.println(result); } // 使用完毕关闭连接池 pool.close(); } }关于参数我给一个测试环境的参考值参数建议初始值说明maxTotal50连接池最多容纳的连接数QPS 高且 Redis 响应快时可以适当调大maxIdle20空闲时保持的连接数太大会浪费资源太小会导致频繁建连minIdle5连接池最低保持的空闲连接适合避免流量突增时瞬间建连maxWaitMillis3000获取连接的等待超时超过就抛异常避免线程无限等下去testOnBorrowtrue借出时 ping 一下确保拿到的连接是活的读多写少的场景建议开启有个新手很容易忽略的问题pool.getResource()返回的 Jedis 对象用完必须要close()。但如果直接调用close()会关闭连接吗不会。在连接池场景下close()的语义是归还连接只有连接池本身关闭时里面的连接才会真正被销毁。这一层逻辑理解透了你就不容易写出连接泄漏的代码了。4.3 连接池用完一定要归还finally 的必要性用连接池时最标准的写法是配合 try-with-resources 或 finallyJedis jedis null; try { jedis pool.getResource(); jedis.set(key, value); } catch (Exception e) { // 处理异常 } finally { // 注意不是断开连接而是归还连接 if (jedis ! null) { jedis.close(); } }如果你不归还连接会发生什么连接池里的连接会被借完之后就出现JedisConnectionException: Could not get a resource from the pool这个经典报错。这个错误在测试 Demo 里很少遇到但一上生产环境遇到流量稍大的情况就特别常见。通常排查下来的结果都是某处代码只创建了连接没有 close。5. 测试 Demo 跑通之后我踩过的那些坑5.1 ping 不通先按这个顺序排查如果你在跑 Jedis Demo 时发现jedis.ping()一直超时或者报连接拒绝不要着急改代码按下面这个顺序逐项排查Redis 服务端真的启动了吗用redis-cli ping测一下能返回 PONG 才说明服务端没问题。端口对不对默认是 6379但有同学会在云服务器上把 Redis 映射到别的端口或者本地 Docker 做了端口映射。防火墙和云安全组放行了吗本地测试可能没这个问题但如果你连接的是远端的 Redis一定要确认安全组规则里允许了你的来源 IP 访问 6379 端口。Redis 配置限制了绑定地址吗Redis 默认只监听127.0.0.1如果你要用局域网 IP 连接需要修改bind配置。另外如果 Redis 开启了protected-mode yes远程连接也会被拒绝。你可能会说我本地测试Redis 就装在本地前三个都不可能是问题。那多半是第四个——因为很多 Redis 发行版默认的配置文件里bind 127.0.0.1是写死的。解决办法是在配置里把 bind 改为0.0.0.0或者按需绑定你的网卡 IP然后重启 Redis。做测试 Demo 的话直接改成bind 0.0.0.0最省事但生产环境不要这么干这是个安全风险。5.2 Jedis 与 Redis 版本不兼容的典型表现有一种报错很误导人明明连接没有超时ping也通了但执行某些命令客户端就报ERR unknown command。我遇到过一次排查到最后发现是版本问题。新版的 Redis 引入了不少新命令比如SET命令的某些可选参数GET选项、KEEPTTL选项如果 Jedis 版本太老拼出来的协议格式不对Redis 端可能直接认不出这个命令。另外还有一种情况你用了 Jedis 3.x 去连 Redis 2.x 的老服务端某些新命令会报错反过来你用很老的 Jedis 去连 Redis 7.x旧版客户端不支持新版协议同样会有诡异的问题。我的建议是做测试 Demo 时尽量都用相对新的版本。Redis 服务端和 Jedis 客户端版本之间差异越大遇到怪问题的概率越高。你不需要完全同步但至少别用一个五六年前的老版本。5.3 常见报错信息速查表最后整理一张我在测试和排查过程中遇到过的报错对应表方便你遇到问题时对照报错信息可能原因解决办法ConnectException: Connection refusedRedis 没启动或端口不对确认服务端状态检查端口JedisConnectionException: Could not get a resource from the pool连接池连接被借完或者连接泄漏检查close()是否执行适当调大 maxTotalJedisDataException: NOAUTH Authentication required没有提供密码或密码错误用jedis.auth(password)或在连接池构造时传入密码JedisDataException: ERR unknown command客户端版本过老/过新与服务端命令集不匹配升级或降级客户端版本JedisConnectionException: java.net.SocketTimeoutException连接超时检查网络连通性调整连接超时参数UnsupportedClassVersionErrorJDK 版本太旧升级 JDK 或换用低版本 Jedis这张表覆盖了我见过的大部分初级问题。如果你遇到的不在上面那大概率是代码逻辑问题比如在finally里还没判断连接是否存在就直接调用方法、或者事务执行前后去操作了已经关闭的连接。这种问题就只能靠断点调试一个命令一个命令地看了。5.4 另一个隐蔽的功能使用 Pipeline 批量提交等你把基础 Demo 跑通了我建议再试一下 Pipeline 和 Lua 脚本这两个功能。Pipeline 的意义在于你执行十条 Redis 命令如果一条一条发网络往返就是十次如果用 Pipeline 打包一次性发过去再一次性拿回结果网络开销能省下一大截。Pipeline pipeline jedis.pipelined(); pipeline.set(batch:1, value1); pipeline.incr(batch:counter); pipeline.lpush(batch:list, a, b); // 提交并获取结果 ListObject results pipeline.syncAndReturnAll();注意 Pipeline 并不是原子性的。它只是把一组命令一次性发过去执行过程还是逐条执行的。如果你需要一批命令要么全成功要么全失败那要用的是事务multi()而不是 Pipeline。测试 Demo 阶段把这两个概念的边界搞清楚后面写复杂逻辑时会少走很多弯路。Lua 脚本则能让你在服务端一次执行一段复杂的原子逻辑。比如扣减库存前先检查库存够不够这种读写组合操作用简单命令很容易出现并发问题但用 Lua 脚本可以做成原子操作。Jedis 里执行 Lua 脚本的命令是// 示例一个简单的 Lua 脚本先取值判断后再递减 String script local current redis.call(get, KEYS[1]) if tonumber(current) tonumber(ARGV[1]) then return redis.call(decrby, KEYS[1], ARGV[1]) else return -1 end; Object result jedis.eval(script, Arrays.asList(stock:1001), Arrays.asList(5));第一次看到这段代码的人可能会觉得 Lua 脚本很难但实际用起来逻辑很简单Redis 把脚本当作一个命令去执行脚本里可以回调 Redis 自己的命令。它的执行过程是原子的这就解决了先查后写这类操作在高并发下的竞态问题。6. 最后补一个我自己的实测建议Jedis 的测试 Demo 看起来就是几行set/get但你真的把它跑通、再把连接池、Pipeline、事务这几个点都试一遍之后对 Redis 客户端的理解会有一个明显的提升。有一点我在实际使用中体会特别深不要因为 Jedis 的 API 简单就在代码里到处new Jedis()每一个连接对象都有真实的 socket 开销尤其在测试环境同时起几十个线程的时候这种问题会被瞬间放大。另外你自己练习的时候可以在 Redis 客户端里用info clients命令观察当前连接数。配合 Jedis 的测试代码你会直观看到连接是怎么被创建、复用、释放的。这个过程比看十篇理论文章都管用。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询