1. 为什么Java项目里我一直在用Jedis操作Redis
Jedis这个词,只要你做过后端开发,大概率在依赖清单里见过。它是Java生态里最早被广泛使用的Redis客户端,说白了就是一个让我们在Java代码里操作Redis的中间层库。Redis本身是C语言写的服务端,我们Java程序没法直接跟它对话,Jedis就是那根“电话线”,把Java方法调用翻译成RESP协议指令发给Redis,再把结果翻译回Java对象。
我在多个项目里都用它处理缓存、分布式锁、排行榜这类场景。它的好处用一句话概括:接口直观,跟Redis命令几乎一一对应。你熟悉Redis的命令行,就基本会写Jedis代码,比如SET对应set,HGETALL对应hgetAll,没有那么多抽象包装,上手成本非常低。如果你是刚接触Redis的Java工程师,或者项目里需要快速接入缓存功能,这篇文章就是给你准备的。
当然,也有人说Jedis线程不安全,说它过时了,但实际项目中它依然有大量应用场景。所谓线程不安全,是指同一个Jedis实例不能在多线程里共享,解决方式也很简单——用连接池,每个线程从池子里拿一个独立的连接实例,用完归还。这个套路我后面会详细讲。Jedis适合那些想要直接控制连接、代码简单直白的项目,尤其是中小规模系统和内部工具,它比某些重型客户端更省心。接下来,我按从零开始建项目到生产实践的路径,把Jedis操作Redis的完整链路拆给你看。
2. 环境准备与第一个Jedis程序
2.1 引入依赖,别在版本上翻车
操作Jedis第一步,是在项目里加入依赖。我用的是Maven,配置如下:
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.4.3</version> </dependency>版本建议选4.x以上。原因有三个:第一,4.x支持Redis 6.x、7.x新增的命令类型,比如SET带NX和EX参数的原子操作在封装上更顺手;第二,4.x开始改进了连接池内部实现,在高并发下更稳定;第三,旧版3.x在某些场景下存在资源回收不彻底的问题,容易把连接数打满。
如果你用的是Spring Boot,还可以通过spring-boot-starter-data-redis间接引入,但那样默认走的是Lettuce。想强制用Jedis,需要排除Lettuce后单独引入Jedis依赖。我这里讲的是脱离Spring,直接使用Jedis原生API的方式,这样你对底层机制的理解会更深,以后切换框架也不慌。
2.2 建立连接,从最简单的单机模式开始
写代码之前,先确保本机或服务器上Redis已经启动,并且端口是默认的6379。连接方式非常简单:
import redis.clients.jedis.Jedis; public class QuickStart { public static void main(String[] args) { // 指定Redis服务地址和端口 Jedis jedis = new Jedis("127.0.0.1", 6379); // 如果Redis设置了密码 jedis.auth("yourpassword"); // 测试连通性 System.out.println("Ping result: " + jedis.ping()); // 写一个字符串 jedis.set("blog:title", "通过Jedis操作Redis"); // 读出来 String title = jedis.get("blog:title"); System.out.println("Read from Redis: " + title); jedis.close(); } }这段代码是入门标配。new Jedis(host, port)直接创建一个TCP连接,ping()用来确认服务是否存活,set和get就是最基础的读写操作。这里有个细节:jedis.close()不是可有可无的,Jedis的close()方法会释放底层Socket资源。在真实项目里,我们通常不直接new连接,而是从连接池获取,这一点在第4章节具体展开。
2.3 理解Jedis对象模型:命令即方法
Jedis的设计哲学特别朴实——Redis有什么命令,它就有什么方法。比如Redis里SETEX命令表示“设置值并指定过期时间”,到Jedis里就是setex(key, seconds, value)。这种一一映射关系让学习曲线几乎平坦。
用个生活类比:你把Redis当成一个储物柜,Jedis就是你的钥匙串和手写标签。每个标签对应一个操作指令,想存东西就贴标签放进去,想取东西就按标签拿。没有中间商赚差价,所以速度快、心智负担小。
Jedis支持的数据类型操作包括:
- 字符串(String):缓存、计数器、分布式锁的基础
- 哈希(Hash):存储对象字段,读写单个字段
- 列表(List):消息队列简易版、最新消息列表
- 集合(Set):去重、标签系统、随机抽奖
- 有序集合(ZSet):排行榜、延时队列
后面我逐一展示代码示例,每个示例都是我在真实业务中验证过的写法。
3. 五大数据类型操作实战
3.1 字符串类型:缓存与计数器的核心
字符串是Redis最基础也最常用的类型,在Jedis里对应的方法名也最直白。
// 基本读写 jedis.set("user:10001:name", "张三"); String name = jedis.get("user:10001:name"); // 带过期时间的写入,单位是秒 jedis.setex("session:token:abc123", 3600, "login_user_10001"); // 只有当key不存在时才设置成功,常用于分布式锁 Long result = jedis.setnx("lock:order:20001", "1");这里重点说两个容易忽略的点。
第一,setex是原子操作,相当于set加expire两步同时完成。我在项目里遇到过同事先set再expire的写法,如果程序在这两步之间崩溃,key就会变成永不过期的“僵尸数据”,白白占用内存。所以带过期时间的写入一定要用setex。
第二,setnx是“set if not exists”的缩写,它是实现分布式锁的基石。第二个参数的值可以是任意字符串,但如果你要区分不同线程的锁标识,通常会把线程唯一ID放进去,释放锁时校验这个值,防止误删别人的锁。
字符串类型还经常用来做计数器,比如统计每日访问量:
Long visitCount = jedis.incr("stats:visit:20240218"); // incrBy可以指定增加幅度 jedis.incrBy("stats:score:10001", 10);incr是原子自增,在并发场景下不用担心数值错乱。做秒杀库存扣减,也可以用decr操作配合setnx的返回值判断是否扣减成功。
3.2 哈希类型:对象数据的最佳容器
项目里我们经常需要存用户信息、商品信息这样的结构化数据。单独拼字符串能存,但读一个字段要把整个字符串拿回来解析,很浪费。哈希类型就是为这种场景设计的。
// 设置单个字段 jedis.hset("user:10001", "name", "张三"); jedis.hset("user:10001", "age", "28"); jedis.hset("user:10001", "city", "杭州"); // 一次性设置多个字段 Map<String, String> userInfo = new HashMap<>(); userInfo.put("name", "李四"); userInfo.put("age", "32"); userInfo.put("city", "深圳"); jedis.hset("user:10002", userInfo); // 读取全部字段 Map<String, String> allInfo = jedis.hgetAll("user:10001"); // 读取单个字段 String city = jedis.hget("user:10001", "city");哈希类型有个直观的好处——修改某个字段不需要动整个对象。想象一下,用户在个人中心改了头像,底层只用执行一次hset("user:10001", "avatar", "new_url"),而如果用字符串存储整个对象,就得先get再反序列化再修改再序列化再set,性能差距非常明显。
这里有个实操建议:哈希里所有字段的value都统一存字符串。如果存数字,不要存整数类型,而是存字符串形式的数字,这样在跨语言、跨版本时兼容性最好。比如年龄存“28”而不是28。
3.3 列表类型:轻量消息队列的替代方案
列表类型的典型操作是lpush配合rpop,或者反过来rpush配合lpop,形成先进先出的队列效果。
// 从左侧入队 jedis.lpush("queue:email", "email_task_1"); jedis.lpush("queue:email", "email_task_2"); // 从右侧阻塞式出队,最多等待10秒 List<String> messages = jedis.brpop(10, "queue:email");brpop里的b是blocking(阻塞)的意思。没有消息时线程会阻塞等待,直到超时。这在做简易消息队列时非常有用,避免程序用轮询方式不停空转,节省CPU资源。
我在项目里用它处理过一批异步任务:前台上传图片后,把图片路径推入Redis列表,后端消费者用brpop拉取任务,执行缩略图生成。实现简单,无需引入消息中间件,几十万级别的任务量完全扛得住。
列表类型还有个常见用途是“最新动态列表”。例如论坛最新的10条帖子,每发一帖就lpush,读取时用lrange(key, 0, 9)拿前10条,性能远好于数据库ORDER BY id DESC LIMIT 10的查询。
3.4 集合类型:去重与随机抽奖
集合类型的特点是元素唯一、无序。Jedis操作如下:
// 添加元素 jedis.sadd("activity:lottery:users", "user_1", "user_2", "user_3"); // 判断用户是否已参与 Boolean isJoined = jedis.sismember("activity:lottery:users", "user_1"); // 获取集合大小 Long size = jedis.scard("activity:lottery:users"); // 随机弹出1个元素,适合抽奖场景 String winner = jedis.spop("activity:lottery:users"); // 获取集合中所有元素 Set<String> allUsers = jedis.smembers("activity:lottery:users");spop随机弹出一个元素并且从集合里移除,天然适配抽奖不重复的需求。如果需求是“随机取出但不移除”,就用srandmember。
集合还常用来做交并集运算,比如计算两个兴趣标签的重叠用户:
jedis.sadd("tag:java", "user_1", "user_2", "user_3"); jedis.sadd("tag:redis", "user_2", "user_3", "user_4"); // 交集:既是Java爱好者又玩Redis的用户 Set<String> common = jedis.sinter("tag:java", "tag:redis");3.5 有序集合类型:排行榜利器
有序集合在集合基础上给每个元素关联了一个分数(score),Redis按分数排序。排行榜业务是它的经典战场。
// 添加成员及分数 jedis.zadd("rank:game:week", 1500, "player_a"); jedis.zadd("rank:game:week", 2300, "player_b"); jedis.zadd("rank:game:week", 1800, "player_c"); // 按分数从高到低取出前3名 Set<String> top3 = jedis.zrevrange("rank:game:week", 0, 2); // 查看某人的排名,注意排名从0开始 Long rank = jedis.zrevrank("rank:game:week", "player_b"); // 增加分数 jedis.zincrby("rank:game:week", 200, "player_a");zrevrange按分数从大到小返回,zrange则相反。zincrby在原有分数上加值,适合实时更新玩家得分,不用先取出再算再写回。
有序集合还能做延时队列。把任务ID存进ZSet,时间戳当分数,起一个定时任务不断用zrangeByScore取当前时间之前到期的任务执行,实现简单可靠。我在日志系统里用这个方案做过延迟重试队列,效果比睡线程好得多。
4. 连接池配置:生产环境的必修课
4.1 为什么不能每次请求都新建连接
实践初期,我犯过一个新手都会犯的错——每来一个请求就new Jedis(...),用完close()。跑到并发量一高,Redis连接数直接飙到几千,服务端报max number of clients reached,线上告警响个不停。
原因是创建TCP连接需要握手开销,Redis每接受一个连接也要占用内存资源。频繁创建销毁连接,不仅浪费,还容易把Redis的连接数窗口撞破。
正确的做法是使用JedisPool连接池,预先创建一批连接放在池子里,用的时候借,用完归还。
4.2 连接池参数的合理配置
import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; JedisPoolConfig poolConfig = new JedisPoolConfig(); // 最大连接数 poolConfig.setMaxTotal(50); // 最大空闲连接数 poolConfig.setMaxIdle(20); // 最小空闲连接数 poolConfig.setMinIdle(5); // 从池子里获取连接的最大等待时间 poolConfig.setMaxWaitMillis(3000); // 连接空闲多久后校验有效性 poolConfig.setTestWhileIdle(true); // 获取连接时是否校验有效性 poolConfig.setTestOnBorrow(false); // 归还连接时是否校验有效性 poolConfig.setTestOnReturn(false); JedisPool jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379, 2000, "yourpassword");参数怎么定?这取决于你的应用并发模型。
maxTotal:建议设为预估并发峰值的1.5倍左右。比如单机应用最大同时处理200个请求,每个请求只要一次Redis操作,那maxTotal设成50到100足够,不需要贪大。maxIdle:控制空闲连接数量,设得太大浪费资源,太小则高峰期要频繁新建连接。一般设为maxTotal的一半。maxWaitMillis:非常重要。设为3000毫秒表示从连接池取不到连接时最多等3秒,超时抛出异常,避免线程无限挂起。如果业务要求快速失败,可以设成1000毫秒。testOnBorrow:设为false。因为每借一次连接多一次PING检查,在池子质量稳定时是纯浪费。依靠testWhileIdle做后台空闲检测就够了。
4.3 正确使用连接池的方式
try (Jedis jedis = jedisPool.getResource()) { jedis.set("key", "value"); }这里有个重要的细节:从池子里拿到的Jedis对象,用完调用close()不是真的关闭连接,而是归还给连接池。这个机制很多初学者不知道,以为close就是断连,其实Jedis内部做了判断——如果这个连接来自池子,close触发归还逻辑;如果是独立new出来的,则真正关闭。
如果使用的是Spring容器,把JedisPool声明成一个单例Bean,全局共享。高并发场景下,同一个线程内也不要多次getResource(),尽量在一个线程的整个业务逻辑里用同一个连接实例,减少借还开销。
5. 管道与事务:批量操作的性能利器
5.1 管道模式:从RTT到合并发送
业务中有个典型场景:用户首次登录需要初始化多个key,比如缓存用户信息、记录登录日志、添加日活统计。如果你一个一个执行,每执行一个命令都要等待一次网络往返(RTT),假设每条命令2毫秒,执行5条就是10毫秒,100个用户登录就是1000毫秒,损耗非常大。
管道(Pipeline)就是把这些命令打包在一起发送,Redis一次性处理完后批量返回结果。带宽占用大幅降低,总体耗时可以减少一个数量级。
Pipeline pipeline = jedis.pipelined(); for (int i = 0; i < 100; i++) { pipeline.set("batch:key:" + i, String.valueOf(i)); } // 批量执行并获取响应 List<Object> responses = pipeline.syncAndReturnAll();注意管道和普通命令混合使用时要小心。管道内的命令不会立刻执行,必须调用sync()或syncAndReturnAll()触发。sync()不关心返回值,适合纯写入场景;syncAndReturnAll()返回每条命令的结果列表,适合需要校验结果的场景。
我在批量清理缓存时用管道体验尤其明显。有时一个用户下线要删除几十个session key,逐条删除要几十毫秒,用管道一把梭,处理一万个key也就一两秒。
5.2 事务:MULTI、EXEC与DISCARD
Redis事务和数据库事务不同,它没有回滚机制,但能保证一个事务块里的命令按顺序原子执行——所有命令要么都执行,要么因为EXEC前出错都不执行。
Transaction multi = jedis.multi(); multi.set("account:10001:balance", "100"); multi.decrBy("account:10001:balance", 10); multi.incrBy("account:20002:balance", 10); List<Object> results = multi.exec();multi()对应Redis的MULTI命令,之后调用的方法不会立刻发送到Redis,而是缓存在客户端。调用exec()后一次性发送并执行。中途想放弃,调用discard()。
这里有个常见误区:Redis事务只能保证命令连续执行,不能保证业务上的原子性。比如扣减账户A余额再增加账户B余额,如果A扣减成功了,B增加却因为参数错误失败,Redis不会把A回滚。所以事务适合“保证命令不被其他客户端的命令插入”的场景,而不是“全有或全无”的数据库事务。
想要真正的事务回滚能力,需要配合Lua脚本实现。Jedis支持直接执行Lua脚本,这也是分布式场景下复杂操作的原子性保障方案。
String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " + "return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(luaScript, Collections.singletonList("lock:order:20001"), Collections.singletonList("thread_id_abc"));这段Lua脚本实现了“检查锁的值是否属于自己的线程ID,是才删除”,完美解决了分布式锁误删问题。eval的返回值如果是0说明锁已过期或已被别人持有,无需处理。
5.3 管道和事务的执行时机区别
管道和事务经常被混为一谈,但二者解决的问题完全不同。
管道解决的是网络开销问题,把多个命令一次发送,节省的是RTT时间,命令之间没有隔离性保证。事务解决的是原子执行问题,保证一组命令不被其他客户端插入,但不减少网络往返。
实际项目中两者可以结合:用管道包裹事务的入队和提交命令。Jedis里Transaction本身继承自MultiKeyPipelineBase,天然支持管道特性。但大多数场景我建议分开使用,逻辑更清晰。
6. 哨兵模式与集群模式:高可用架构下怎么用Jedis
6.1 哨兵模式:用JedisSentinelPool自动切换主从
单机Redis一旦宕机,整个缓存体系就瘫痪了。生产环境至少要上主从加哨兵。哨兵模式让Jedis能感知当前哪个节点是主节点,主节点挂掉后自动切换到新的主节点。
Set<String> sentinels = new HashSet<>(); sentinels.add("10.0.1.10:26379"); sentinels.add("10.0.1.11:26379"); sentinels.add("10.0.1.12:26379"); JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels, "yourpassword"); try (Jedis jedis = pool.getResource()) { jedis.set("key", "value"); }这里传入的是哨兵节点的地址,而不是Redis数据节点的地址,这一点很多人容易写错。Jedis会通过哨兵获取当前主节点地址,建立连接。主节点挂掉时,哨兵会选举出从节点升级为新主节点,连接池内部会自动更新连接目标。
6.2 集群模式:关注slot的分布
Redis Cluster模式下,数据按照CRC16算法分布到16384个槽位,每个节点负责一部分槽位。Jedis提供了JedisCluster类:
import redis.clients.jedis.HostAndPort; import redis.clients.jedis.JedisCluster; Set<HostAndPort> clusterNodes = new HashSet<>(); clusterNodes.add(new HostAndPort("10.0.1.20", 7000)); clusterNodes.add(new HostAndPort("10.0.1.21", 7000)); JedisPoolConfig poolConfig = new JedisPoolConfig(); poolConfig.setMaxTotal(50); JedisCluster jedisCluster = new JedisCluster(clusterNodes, 2000, 2000, 5, "yourpassword", poolConfig); jedisCluster.set("cluster:key", "cluster_value");JedisCluster只需要配置部分节点地址即可,它内部维护了槽位与节点的映射关系。当操作key时,它计算出key对应的槽位,再定位到正确的节点,整个过程对调用方透明。
集群模式有几个限制,写代码时要注意:
- 不支持
select库操作,集群只有db 0 - 不支持多key操作(除非所有key都在同一槽位)
- 事务和Lua脚本中的多key操作也必须保证key在同一个槽位
业界通用的做法是用大括号定义hash tag,比如{user:10001}:name和{user:10001}:age,这样两个key会被分配到同一个槽位,允许在一个事务或脚本里操作。
6.3 高可用场景的选型思路
如果项目规模还小,单机足够,直接JedisPool。如果要求高可用但数据量不大,哨兵模式最简单。如果数据量巨大、需要水平扩展,集群模式是必然选择。
我的经验是不要过度设计。某早期项目Redis数据总量不到100MB,硬是搭了集群,运维复杂度翻了几倍,收益却为零。先从单机或哨兵开始,当单机内存不满足、需要水平扩展时,再迁移到集群。
7. 常见问题排查与避坑指南
7.1 连接超时与阻塞排查
Jedis操作中报JedisConnectionException: java.net.SocketTimeoutException是最常见的故障之一。
排查步骤按顺序来:
第一,确认Redis服务端的socketTimeout配置是否过小。Jedis构造时有个超时参数,比如new JedisPool(config, host, port, timeout),这个timeout是连接超时加读写超时的总和。如果业务上有大value的读写(比如几百KB的JSON),默认2000毫秒可能不够,适当调高到5000或8000。
第二,确认是否为慢查询拖住了Redis服务端。用Redis的SLOWLOG GET命令查看最近的慢命令,如果有很多KEYS *这类命令,全表扫描会阻塞服务端,导致所有请求变慢。强烈建议生产环境禁用KEYS *命令,改用SCAN。
第三,检查网络层面是否存在跨机房访问。Redis的RTT(往返时间)通常要求在1毫秒以内,如果客户端和服务端跨地域机房访问,RTT可能到几十毫秒,这时候怎么优化客户端代码都没用,必须从架构上让客户端和服务端同机房部署。
7.2 连接池耗尽与timeout等待
报错Could not get a resource from the pool时,大多数人第一反应是调大maxTotal。但正确做法是先分析为什么连接被占满。
用redis-cli CLIENT LIST查看服务端实际连接数,如果连接数远小于maxTotal,说明有连接泄漏——代码里从池子获取连接后,业务抛异常时没有执行close()归还连接,导致池子里可用连接越来越少。
这里的一个深刻教训:连接归还必须放在finally块,或者用Java 7的try-with-resources语法。
// 推荐写法:try-with-resources try (Jedis jedis = jedisPool.getResource()) { jedis.set("key", "value"); } catch (Exception e) { // 业务异常处理,连接会被自动归还 }7.3 大Key与热Key问题
有些业务把用户的所有动态信息拼成一个大JSON塞进一个key里,value大小可能超过1MB。这种大key有几个危害:网络传输慢、Redis阻塞风险高(比如DEL一个超大key会阻塞服务端)、内存碎片增多。
解决方案是数据拆分。我处理过的一个案例:某团队把用户头像、昵称、等级、积分全部塞进一个key,结果单key最大达到3MB。拆分成用户基本信息哈希和用户统计信息哈希多个key之后,单个key控制在几十KB,性能提升了一个量级。
热Key问题则体现在某个key被超高并发读取,比如微博热搜第一名。单一节点承受不住,解决思路有本地缓存兜底、把热key复制为多份(如hotkey:1到hotkey:10分散读压力)、或者引入多级缓存架构。
7.4 序列化方式不一致导致的数据读不出来
Jedis本身只负责传输字节,它不关心你的对象怎么序列化。如果写入时用JSON序列化,读取时忘了反序列化,拿到的就是一串JSON字符串;如果写入时用的Java原生ObjectOutputStream,读取时类定义变了,直接抛ClassNotFoundException。
我的建议是统一在项目里封装一个工具类,操作Redis时自动完成序列化和反序列化。不要直接暴露Jedis原生的set(byte[])接口,这样容易在团队里造成序列化方式混用。
public class RedisCacheUtil { private static final JedisPool POOL = new JedisPool(); public static void setString(String key, String value) { try (Jedis jedis = POOL.getResource()) { jedis.set(key, value); } } public static void setObject(String key, Object obj) { try (Jedis jedis = POOL.getResource()) { String json = JSON.toJSONString(obj); jedis.set(key, json); } } public static <T> T getObject(String key, Class<T> clazz) { try (Jedis jedis = POOL.getResource()) { String json = jedis.get(key); if (json == null) return null; return JSON.parseObject(json, clazz); } } }7.5 常见问题速查表
| 报错现象 | 可能原因 | 解决方案 |
|---|---|---|
| SocketTimeoutException | 超时配置过小、网络延迟高 | 调大超时参数,检查网络环境 |
| Could not get a resource | 连接池耗尽、连接泄漏 | 检查finally归还逻辑,观察CLIENT LIST |
| NOAUTH Authentication required | Redis设置了密码但客户端未传入 | jedis.auth(password)或在池化配置中指定 |
| ERR max number of clients reached | 服务端连接数达到上限 | 调大maxclients或减少连接池maxTotal |
| JedisDataException: WRONGTYPE | 对key使用了错误的数据类型命令 | 确认Redis中该key的实际类型 |
| OutOfMemoryError | 读取了超大key | 拆分大key,设置Jedis的maxBuffer限制 |
8. 从Jedis到更高级的抽象:我的实践体会
操作Redis到最后,绕不开一个工程化问题:代码里到处散落Jedis调用,key的命名不规范,团队里你写userName我写user_name,同一个key某个模块写入字符串另一个模块当哈希读,线上排查时非常痛苦。
我的经验是建立统一的缓存管理模块。核心做三件事:
第一,集中管理所有key的命名规则。用业务域:实体:ID:字段的规范,比如user:profile:10001:basic。把常量集中在一个类里,避免字符串散落各处。
第二,针对高频业务做统一封装。比如缓存读取使用“读缓存-未命中-回源数据库-写回缓存”的模板方法:
public <T> T getWithLoad(String key, Supplier<T> loader, int expireSeconds) { T cached = getObject(key, ...); if (cached != null) return cached; T value = loader.get(); // 从数据库加载 if (value != null) { setObject(key, value, expireSeconds); } return value; }这个模板方法还能加上一个随机过期时间,防止批量key同时到期导致缓存雪崩。
第三,监控Redis的命令耗时。可以继承Jedis类重写关键方法记录耗时,更优雅的方式是用AOP切面拦截缓存方法的调用。我在自己的项目里只是简单地给每个缓存方法打了日志,就能发现问题——某个接口的Redis耗时突然从5毫秒涨到80毫秒,顺着日志排查,发现是有人写了个KEYS *命令。
使用Jedis操作Redis这件事,本身并不复杂,但把细节打磨好、把工程规范建立起来,才是让稳定性和性能拉开差距的地方。这个库长寿不是没有原因的,它把复杂留给了自己,把简单交给了开发者。等你用顺手了,再回头看那些报错和调优记录,会发现每一步坑都踩得值得。