ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java操作Redis:客户端选型、序列化与分布式锁实战指南

Java操作Redis:客户端选型、序列化与分布式锁实战指南 最近在折腾 Redis 相关的项目发现不少同行在 Java 操作 Redis 客户端这块还在能用就行的阶段。Jedis、Lettuce、Redisson 到底怎么选连接池参数调多少合适序列化器的坑踩了也不知道怎么排查这些都是实际开发里绕不开的问题。这篇文章我会从客户端选型讲起到环境搭建、核心 API 实操、序列化方案再聊到分布式锁这类高级玩法最后分享一些排查手段和效率工具。适合刚接触 Java 操作 Redis 的入门读者也适合用了一段时间但想系统梳理的开发者。1. 客户端三选一Jedis、Lettuce、Redisson到底该用哪个很多初学者第一个问题就是Java 操作 Redis 用什么客户端去网上搜答案五花八门有人推荐 Jedis有人推荐 Lettuce还有人直接上 Redisson。我得先说清楚一件事这三个东西虽然都是客户端但定位和适用场景差别很大选错后续会很被动。1.1 Jedis直来直去的经典派Jedis 是老牌客户端API 设计跟 Redis 命令几乎一一对应set就是sethgetAll就是hgetAll几乎没有学习成本。以前 Jedis 在并发场景下有个明显短板它本身不是线程安全的每个线程都要拿自己的连接实例所以必须依赖连接池。你在用 Jedis 的时候心里得时刻想着用完要把连接还给池子否则线程一多很容易把连接耗尽。不过话说回来连接池模式本身很成熟只要配置合理吞吐量完全够用。Jedis 的另一个特点是用起来直接你能清楚地看到我在用连接这个过程。对新手来说这其实是好事情因为 Redis 连接的本质一目了然不会像某些封装得太狠的库那样出了问题根本不知道连接是怎么管理的。1.2 Lettuce异步原生的现代化选手Lettuce 基于 Netty 实现核心卖点是一个连接可以供多个线程共享底层靠异步事件循环。这意味着在并发量上来的时候你不需要像 Jedis 那样创建几十上百个连接一个连接就能扛住很可观的 QPS。从 Spring Boot 1.x 到 2.x 的演进里默认的 Redis 客户端也从 Jedis 切换到了 Lettuce这其实代表了业界对连接管理理念的变化。Lettuce 默认也不开连接池因为它自己就能很好地复用连接。但要注意的是如果服务端 Redis 的maxclients配置很小或者你有大量阻塞命令在跑单一连接反而可能成为瓶颈。1.3 Redisson为分布式场景而生的老大哥Redisson 严格来说不只是一个客户端它更像是一个基于 Redis 的分布式框架。分布式锁、延迟队列、信号量、布隆过滤器这些复杂数据结构它都直接提供了现成的 API。举个例子你想实现一个分布式锁用 Jedis 的话得自己写SET lock uuid NX EX 30这种命令还要配合 Lua 脚本处理释放逻辑稍有不慎就容易出并发问题。用 Redisson 的话一行redissonClient.getLock(orderLock).lock()就搞定了锁的自动续期机制它内部已经封装好了。1.4 三个客户端的适用场景对比我在日常项目里的选型经验是这样的客户端线程安全连接池需求学习成本适合场景Jedis否必须低简单业务、新手学习、需要精确控制连接时Lettuce是可选中Spring Boot 默认项目、高并发读多写少场景Redisson是可选中高分布式锁、分布式集合、需要高级数据结构的场景我个人不太推荐在复杂项目里裸用 Jedis因为连接管理太费精力。Spring Boot 项目直接用默认的 Lettuce 就好如果需要分布式锁再引入 Redisson。用 Redisson 不冲突它也可以直接连 Redis 实例。2. 环境准备与依赖配置不借助 Spring Boot 把客户端跑起来很多教程上来就讲怎么在 Spring Boot 里操作 Redis搞得大家以为离开 Spring Boot 就没法用 Redis 了。其实 Java 操作 Redis 的根本很简单一个 Java 工程、一个 Redis 服务端、一个客户端依赖就够了。2.1 基础设施安装与验证我本地的实验环境是 Windows 11装的是 Redis 7.x 的 Windows 移植版。装完以后一定要把 Redis 服务跑起来然后打开命令行验证一下redis-cli -h 127.0.0.1 -p 6379 -a yourpassword注意Redis 7.x 以后也支持ACL权限控制了如果你在配置文件里启用了 ACL用-a传密码的方式可能不生效得先认证默认用户。但本地测试一般没那么复杂有个密码就够了。如果你的 Redis 在远程服务器上记得检查防火墙是否放行了 6379 端口还有bind配置是否允许外部访问。我见过太多人配置文件里保持默认的bind 127.0.0.1然后远程连不上折腾半天才发现是 bind 的问题。2.2 依赖引入Maven 与 Gradle 两种方式以 Maven 为例如果使用 Jedis在pom.xml中加入dependency groupIdredis.clients/groupId artifactIdjedis/artifactId version5.1.5/version /dependency如果是 Lettucedependency groupIdio.lettuce/groupId artifactIdlettuce-core/artifactId version6.4.2.RELEASE/version /dependency如果要用 Redissondependency groupIdorg.redisson/groupId artifactIdredisson/artifactId version3.42.0/version /dependencyGradle 对着翻译成implementation redis.clients:jedis:5.1.5这种格式就行没什么好说的。版本这里我要多提醒一句不要随手拿最新版最好看一眼该版本对应的最低 Java 编译版本。比如 Jedis 5.x 要求 Java 8 以上如果你项目还在 Java 7那只能用 Jedis 2.x。另外Lettuce 的版本命名里有RELEASE后缀别去掉否则可能拉不下来。2.3 用 Jedis 写出第一个连接我把三种客户端的连接方式都写一遍先看 JedisJedisPoolConfig config new JedisPoolConfig(); // 最多 50 个连接 config.setMaxTotal(50); // 最多 10 个空闲连接 config.setMaxIdle(10); // 池子中最小空闲连接数 config.setMinIdle(1); // 最多等待 5 秒 config.setMaxWaitMillis(5000); // 拿到连接后要不要验证可用 config.setTestOnBorrow(true); // 还回去时验证可用 config.setTestOnReturn(true); JedisPool pool new JedisPool(config, 127.0.0.1, 6379, 2000, yourpassword); try (Jedis jedis pool.getResource()) { jedis.set(hello, world); System.out.println(jedis.get(hello)); } pool.close();注意到try-with-resources了吗因为 Jedis 实例内部持有一个 Socket 连接用完不还池子最终会被耗尽。用try-with-resources可以保证连接一定会归还这是使用 Jedis 的核心纪律。2.4 Lettuce 和 Redisson 的连接方式对比Lettuce 的简单版不需要连接池RedisClient client RedisClient.create( RedisURI.create(redis://:yourpassword127.0.0.1:6379) ); StatefulConnectionString, String conn client.connect(); RedisCommandsString, String commands conn.sync(); commands.set(hello, world); System.out.println(commands.get(hello)); conn.close(); client.shutdown();Redisson 更简洁它用配置对象构建客户端Config config new Config(); config.useSingleServer().setAddress(redis://127.0.0.1:6379).setPassword(yourpassword); RedissonClient redisson Redisson.create(config); RBucketString bucket redisson.getBucket(hello); bucket.set(world); System.out.println(bucket.get()); redisson.shutdown();三种客户端各自用起来都不复杂真正复杂的是理解每种连接模式的原理。Lettuce 为什么不需要池子因为它内部的 Netty 连接是异步复用的。Redisson 为什么操作对象化因为它把 Redis 的数据结构都映射成了 Java 对象的视角。这些理念差异决定了你在对应生态里写代码的方式。3. 核心 API 实操从 String 到 Hash 逐个过一遍连接建立起来之后就是正式操作了。Redis 有五种基本数据类型每个都有对应的 Java API。我按 Jedis 的写法来展示思路清晰Lettuce 和 Redisson 的 API 差异稍后对比。3.1 String 类型不只是 set/getString 的使用频率最高但很多人只知道set和get其实里面可以做的优化操作很多。try (Jedis jedis pool.getResource()) { // 基础 set/get jedis.set(user:1:name, 张三); String name jedis.get(user:1:name); // 不存在才设置常用于分布式锁占位 Long setnx jedis.setnx(user:1:lock, 1); // 设置过期时间单位秒 jedis.setex(sms:code:13712345678, 60, 482913); // 先取值再重新赋值 String oldValue jedis.getSet(counter, 100); // 数值自增 jedis.incr(page_view); jedis.incrBy(page_view, 10); }incr这种原子操作在做计数器时很有用但你得知道它的底层类型是字符串Redis 内部把它当成整数来解析所以你不能往这个 key 塞非数字的字符串否则会报错。关于setex有个隐蔽的注意点如果你后面还要给这个 key 再次调set过期时间会被覆盖掉因为set是直接赋值不会保留之前的 TTL。Redis 的过期策略是整键过期不是键值更新后重新计时。一旦set了新的 value原来设的过期时间就消失了这是高并发场景下常踩的坑。3.2 List 类型队列还是栈取决于你 push 和 pop 的方向List 在 Java 客户端里对应lpush、rpush、lpop、rpop这一套命令。它的典型用法是实现简单消息队列。try (Jedis jedis pool.getResource()) { // 从右边入队从左边出队 FIFO先进先出 jedis.rpush(task:queue, task-1); jedis.rpush(task:queue, task-2); String task jedis.lpop(task:queue); // 查看列表长度 Long length jedis.llen(task:queue); // 取元素但不弹出 ListString range jedis.lrange(task:queue, 0, -1); }如果你用lpush和lpop那这个 List 就是一个栈LIFO先放进去的后取出来。队列模式建议消费端用brpop而非lpop因为brpop是阻塞式的队列空的时候会一直阻塞等待避免在空队列上空转消耗 CPU。要注意lrange key 0 -1会把整个 List 全取出来如果列表特别大这种操作不仅慢还会阻塞 Redis 主线程。真要全量遍历应该用lscan这类命令分批处理或者考虑用其他数据结构替代。3.3 Hash 类型Java 的 Map 放进 RedisHash 对应 Java 里的MapString, String在存对象属性时非常实用。拿用户信息举例try (Jedis jedis pool.getResource()) { jedis.hset(user:1, name, 张三); jedis.hset(user:1, age, 30); jedis.hset(user:1, city, 上海); // 一次性设置多个字段 MapString, String user new HashMap(); user.put(email, zhangsanexample.com); user.put(phone, 13712345678); jedis.hset(user:1, user); // 取单个字段 String name jedis.hget(user:1, name); // 取所有字段 MapString, String all jedis.hgetAll(user:1); // 获取所有字段名 SetString fields jedis.hkeys(user:1); // 字段不存在时才写入 Long hsetnx jedis.hsetnx(user:1, nickname, 阿三); }Hash 比 String 在存储对象上有个明显优势你可以只修改某一个字段不需要把整个 JSON 都拿出来重新序列化再放回去。但要注意Hash 的底层编码是ziplist或hashtable当字段数量多或者字段值大的时候会从ziplist转成hashtable性能特点会变化。这个一般不用你操心理但如果你追求极致性能可以去了解一下这个转换机制。3.4 Set 和 ZSet去重、交集、排行榜Set 用于去重和集合运算ZSet 多了一个 score 字段用于排序所以排行榜功能天生就是为 ZSet 设计的。try (Jedis jedis pool.getResource()) { // Set 操作 jedis.sadd(tag:java, article:1); jedis.sadd(tag:java, article:2); jedis.sadd(tag:redis, article:2); // 交集 SetString inter jedis.sinter(tag:java, tag:redis); // 并集 SetString union jedis.sunion(tag:java, tag:redis); // 差集 SetString diff jedis.sdiff(tag:java, tag:redis); // ZSet 排行榜 jedis.zadd(ranking:hot, 10, product:1001); jedis.zadd(ranking:hot, 20, product:1002); jedis.zadd(ranking:hot, 15, product:1003); // 按分数从高到低取前 3 名 SetString top3 jedis.zrevrange(ranking:hot, 0, 2); // 给某个成员加分 jedis.zincrby(ranking:hot, 5, product:1003); }ZSet 的zadd可以同时添加多个成员zrevrange是按分数倒序zrange是按分数正序。注意如需获取分数的值要学会用zrevrangeWithScores或者用zscore查询单个成员的分数。Redis 的 6.0 以后新增了 Bitmap、HyperLogLog、GEO 等更高级的数据结构Java 客户端也都支持。比如地理位置相关的应用可以直接用geoadd、geodist这些比自己在内存里计算经纬度距离靠谱得多。3.5 Pipeline 与事务减少 RTT 的利器这是一个很多初学者不知道的慢查询优化点。假设你要插入 1000 个订单号到 Redis逐个执行set命令意味着要发起 1000 次网络请求每次都有一次 RTT。用 Pipeline 可以一次性发送给服务端try (Jedis jedis pool.getResource()) { Pipeline pipeline jedis.pipelined(); for (int i 0; i 1000; i) { pipeline.set(order:202501: i, String.valueOf(i)); } pipeline.sync(); }Pipeline 的优势在批量操作时极为明显性能差距可以达到一个数量级。但你要学会区分 Pipeline 和事务Pipeline 只是减少网络往返并不保证多条命令的原子性如果你需要一个原子性的批量操作应该用multi和exec或者直接使用 Lua 脚本。Redis 的eval命令天然是原子的这也是分布式锁和复杂操作的底层基石。try (Jedis jedis pool.getResource()) { Transaction tx jedis.multi(); tx.set(account:1001, 100); tx.decrBy(account:1001, 30); tx.incrBy(account:1002, 30); // 原子性执行 ListObject results tx.exec(); }注意事务里的decrBy如果让余额变成负数Redis 不会报错它只保证执行不保证业务合法性。真正要校验业务逻辑还得配合 Lua 脚本或乐观锁方案。4. 序列化是最大的坑JDK 序列化和 JSON 序列化的取舍很多人用 Redis 操作字符串很顺手一存对象就开始各种报错RedisDesktopManager 里看到一堆乱码。这些乱码十有八九是序列化方案的问题。4.1 Redis 里只能存字节Redis 本质上只认字节流。你存什么进去它存的就是字节你取什么出来它返回的还是字节。所谓对象直接存入其实是 Java 客户端帮你做了一层序列化。如果你用的是 Spring Boot 的RedisTemplate默认的序列化器是JdkSerializationRedisSerializer它会把 Java 对象序列化成带类型信息的二进制格式。结果就是你在终端工具里看这个 key看到的是一串\xAC\xED\x00\x05t\x00...这种乱码。这个坑有一个非常实际的影响跨语言读取的时候。如果你的数据要被 Python、Go 或 Node.js 的应用读取JDK 序列化后的数据是几乎无法被其他语言解析的。更麻烦的是如果你的 Java 类结构升级了比如字段改名或删除反序列化可能直接报InvalidClassException或者类型不兼容的错误。4.2 JSON 序列化跨语言、可读性强的选择我个人的建议是除非有特殊要求否则不要用 JDK 默认序列化。业界更常用的方案是GenericJackson2JsonRedisSerializer它把对象序列化成 JSON 字符串既有可读性也能做跨语言解析。以 Spring Boot 为例Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); // 使用 GenericJackson2JsonRedisSerializer 替换默认序列化 GenericJackson2JsonRedisSerializer jsonRedisSerializer new GenericJackson2JsonRedisSerializer(); StringRedisSerializer stringRedisSerializer new StringRedisSerializer(); // key 用 String 序列化方便肉眼识别 template.setKeySerializer(stringRedisSerializer); template.setHashKeySerializer(stringRedisSerializer); // value 用 JSON 序列化 template.setValueSerializer(jsonRedisSerializer); template.setHashValueSerializer(jsonRedisSerializer); template.afterPropertiesSet(); return template; } }这里有个细节key 为什么要用StringRedisSerializer因为如果你 key 也用 JSON 序列化那么同一个字符串 key 存进去会带一层引号包装你在运维工具里看到的 key 名会变成\user:1\排查问题极其痛苦。业界普遍做法是key 用 Stringvalue 用 JSON。4.3 反序列化时的类型信息JSON 序列化有一个逃不开的问题反序列化时需要知道目标类型。比如你存了一个User对象取出来的时候 JVM 怎么知道它应该还原成User而非MapGenericJackson2JsonRedisSerializer会在 JSON 里写入一个额外的类型字段默认是class。这样反序列化时它会照着这个字段来还原类型。但这个方案也有隐患如果你在前后端接口上使用了动态代理类、内部类或 Lambda 相关类型可能序列化时会带上奇怪的类名反序列化就会失败。如果你确定你的 value 类型是固定的比如某类业务里就是存Order对象我更推荐用针对性更强的Jackson2JsonRedisSerializer把ObjectMapper配好并且显式指定目标类型。这样能减少class带来的安全隐患和冗余字段。当下安全控制比较严格的环境里反序列化白名单和class滥用是安全测试的重点关注项这点要提前考虑好。4.4 字符串与对象不要混用还有一种很常见的坑一个 key 存的是 String另一个 key 存的是序列化后的对象而你在代码里用了同一个RedisTemplate去读。因为你 value 序列化器统一了可能 String 类型的 value 在序列化以后也变成带引号的 JSON 字符串取出来的时候多了一对引号或者类型转换直接抛出异常。我的经验是String 类型的缓存尽量用StringRedisTemplate对象类型的缓存用配置好的RedisTemplate两者区分开不要混在一个视角里操作。这样不同的 key 使用不同的序列化器互不干扰。如果真要排查序列化问题用Redis Desktop Manager这类可视化工具直接看原始字节会更直观。看到\xAC\xED开头的基本可以确定是 JDK 序列化什么时候存进去的、用的哪个模板是什么能立刻有个方向。5. 高级玩法与典型坑从分布式锁到防止缓存穿透连接操作熟练了序列化搞明白了就到了上生产环境的时候。生产环境里最常遇到的问题就是并发场景下 Redis 的数据一致性问题以及各种缓存异常。5.1 分布式锁的正确写法分布式锁是 Java 操作 Redis 的高频面试题也是生产环境的高频需求。很多人的写法有问题// 有问题设置值和过期时间不是原子的 if (jedis.setnx(lock:order:1001, 1) 1) { jedis.expire(lock:order:1001, 10); // 执行业务逻辑 // ... jedis.del(lock:order:1001); }这个写法有什么问题如果setnx成功之后在expire之前进程崩溃了锁就永远没有过期时间会一直占用。解决方式是用一个命令完成设置值和过期时间// 正确SET key value NX EX seconds 是原子的 String result jedis.set(lock:order:1001, requestId, NX, EX, 10); if (OK.equals(result)) { try { // 处理业务 } finally { // 释放锁时要校验持有者 String lockScript if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; jedis.eval(lockScript, Collections.singletonList(lock:order:1001), Collections.singletonList(requestId)); } }这个requestId可以是 UUID它的作用是确保只有锁的持有者才能释放锁。如果不用requestId判断可能会出现线程 A 拿到锁执行时间超过过期时间锁自动释放了线程 B 拿到锁然后 A 执行完了调del把 B 的锁给删了。这就是经典的删锁删到自己头上问题。用 Lua 脚本保证判断删除是原子的是分布式锁的标准实践。如果你直接用 Redisson它会自动通过一个看门狗机制给锁续期解决业务没执行完锁就被过期的问题省心不少。5.2 缓存穿透、缓存击穿、缓存雪崩的应对这三个概念面试天天问但真正落地的方案值得展开说说。缓存穿透查询一个不存在的数据Redis 查不到数据库也没有每次请求都直接打 DB。解决方案是布隆过滤器拦截或者把空值也缓存起来。空值缓存要注意设置较短的过期时间防止大量无效 key 堆积占内存。缓存击穿某个热点 key 突然过期高并发同时去打 DB。解决方案是互斥锁重建缓存即只让一个线程去查 DB 然后写缓存其他线程等待或者返回旧值。用 Redis 分布式锁就能实现。缓存雪崩大量 key 在同一时间过期或者 Redis 实例挂了导致 DB 压力瞬间飙升。解决方案是过期时间加随机值避免同一时间集体过期。Redis 本身可以配置持久化和高可用架构来对抗挂掉的情况这是运维层面的事但是 Java 开发也需要在代码层面做熔断和降级。5.3 Java 客户端中的连接池参数调优很多人在生产环境直接用默认连接池参数这是有隐患的。以 Lettuce 为例默认是不开启连接池的如果你的代码里大量使用同步命令一定要确认你用的是共享连接还是每条命令一个连接。对于 Jedis 连接池的调优我一般建议参考这几个参数参数推荐值说明maxTotal50~200根据业务 QPS 和 Redis 实例性能调整maxIdle10~20保留的常驻连接数量minIdle1~5低峰期最小连接数减少冷启动maxWaitMillis3000~5000获取连接的超时时间超过则抛异常testOnBorrowtrue借出前校验连接可用性代价是每次校验多一次 PINGtestWhileIdletrue定期校验空闲连接避免 Redis 服务端主动断开后拿到死连接一个很重要的判断如果你在日志里看到JedisConnectionException或Could not get a resource from the pool最可能的原因是连接池满了或者 Redis 服务端把空闲连接断掉了。前者调大maxTotal和maxWaitMillis后者靠testWhileIdle加上合理的超时设置。5.4 Redis 操作的时间复杂度意识Java 操作 Redis 的 API 虽然简单但你要有命令复杂度的意识。比如keys *这个命令线上环境千万别用它会全量扫描所有 key在 key 数量大的时候阻塞 Redis 主线程。Java 客户端对应的 API 是jedis.keys(*)很多初学者喜欢拿这个排查问题。正确的方案是用SCAN命令游标式遍历Jedis 里对应的是jedis.scan相关方法。同样的道理smembers返回全量 Set 成员对大集合也是 O(n)要小心使用。这些经验在常规教程里不会提但生产事故往往就是这种不起眼的命令触发的。Redis 单线程模型决定了它处理每个命令都是排队执行的。一旦碰到keys *或者大集合的全量遍历整个 Redis 实例的响应时间都会被拖累。所以作为 Java 开发者你在封装 Redis 工具类时就应该把这种危险命令挡在门外。6. 排查与运维视角可视化客户端、慢查询和常见异常盘点写代码只是前半段上线之后排查问题才是真正磨练人的地方。这里分享一些我把 Java 客户端跑在生产环境时积累的排查手段。6.1 用 Redis Desktop Manager 看数据是否正常Redis Desktop Manager 是最常用的可视化客户端之一但是我得提醒你它只是一个可视化工具不是生产环境排障的万能药。它的核心价值是快速查看 key 是否存在、TTL 还剩多少、value 是什么尤其是验证序列化器是否得当。打开 RDM你如果发现一堆 key 以\xAC\xED\x00\x05开头大概率是JdkSerializationRedisSerializer干的如果你发现 key 名带了引号大概率是 key 用 JSON 序列化导致的。生产环境建议把 key 命名规范定为业务名:模块名:ID层级清晰不仅在 RDM 里好看也方便后续的运维统计。现在的 Navicat 也支持连接 Redis还有 Another Redis Desktop Manager 这类开源选择。工具你可以随喜好选但记住一个原则工具只能帮助你观察不能替代你理解序列化方案和连接池机制。6.2 利用 Redis 慢查询日志定位问题命令Redis 本身提供了慢查询日志功能。你可以把超过一定执行时间的命令记录下来Java 客户端侧可以通过配置参数来设置阈值。在 Redis 配置文件里搜slowlog-log-slower-than默认是 10000 微秒10ms生产环境建议设到 1000 微秒1ms甚至更低。排查的时候流程一般是用SLOWLOG GET或可视化工具看慢命令列表找出耗时最长的那一批命令结合 Java 代码重新审视是不是用了keys *是不是有hgetAll大 Hash是不是用lrange拉了超大 List在客户端侧做预防禁止危险命令、设置命令超时时间、用 Pipeline 批量处理Java 客户端里的命令超时时间也值得检查。Jedis 默认的soTimeout是 2000 毫秒如果你的网络环境较慢或者 Redis 实例在做持久化时阶段性卡顿命令很容易超时。这时你需要根据业务对延迟的容忍度来合理调整超时时间。设太短会引发大量异常设太长会掩盖整体性能问题。6.3 Java 端的 Redis 异常分类我在实操中把常见的异常归了类这样排查起来方向更明确异常类型典型报错最常见原因连接类异常connect timed out网络不通、防火墙拦截、bind 配置限制认证类异常NOAUTH Authentication required密码没传或传错连接池耗尽Could not get a resource from the pool连接池太小、命令阻塞太久、连接泄漏序列化异常SerializationException/ClassCastException用了 JDK 序列化后又改了类结构或 JSON 类型信息丢失命令执行异常ERR wrong number of argumentsAPI 用错参数个数不对看到异常不要急着问同事或上网搜先按这个分类自查能解决大多数问题。尤其是连接池耗尽这个坑我见过太多团队排查半天网络最后发现代码里某个finally少写了jedis.close()。6.4 在 Java 侧做 Redis 调用的统一封装最后分享一个工程上的建议不要在每个业务代码里直接塞 Jedis 连接管理逻辑。写一个统一 Redis 服务类把连接获取、序列化、异常转换都封装好。业务代码只需要调用redisService.getObject(key, User.class)这种清晰的方法。这样做的收益是将来换客户端从 Jedis 换到 Lettuce或者引入 Redisson时只需要改服务类内部实现业务层代码完全不用动排查问题时也有统一日志入口。这个习惯我在多个项目里验证过确实能省掉不少维护成本。另外如果你的调用方拿不到连接或者连接池满了统一的封装里应该做好快速失败和降级策略。不是所有 Redis 查询都必须成功要根据业务判断这个缓存挂了是直接报错还是返回一个降级值降级到数据库查询这些决策提前定好比在故障现场做决定靠谱得多。
返回列表