ARTICLE DETAIL

资讯详情

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

Jedis、Lettuce、Redisson选型详解:从连接原理到分布式锁实战

Jedis、Lettuce、Redisson选型详解:从连接原理到分布式锁实战 Java 后端聊到 Redis 客户端绝大多数人的第一反应就是 Jedis、Lettuce、Redisson 三选一但真到项目里做选型的时候很多人还是靠感觉——Spring Boot 默认带 Lettuce 就用 Lettuce听说 Redisson 分布式能力强就上 Redisson老项目里躺着一个 Jedis 就一直用下去。这篇我不打算背书而是想把这三个客户端的底层逻辑、适用场景、实战配置和常见坑全部摊开讲帮你在选型的时候能说出“到底为什么”而不是只会念名字。无论你是刚入行的实习生、准备跳槽的 Java 工程师还是正在做中间件治理的后端负责人这篇文章都会对你有一点实际帮助。1. 为什么“选一个 Redis 客户端”会难住这么多人1.1 三个客户端三种时代产物Redis 官方支持的 Java 客户端不止一个但真正被大规模使用的只有三个Jedis、Lettuce、Redisson。它们并不是同一样东西的三种换壳版本而是对应于 Redis 生态不同发展阶段的产品。Jedis 出现得最早API 直接把 redis-cli 的命令搬成 Java 方法用起来最像操作原生命令几乎零学习成本。它的问题是连接模型比较初级走的是传统 Socket 阻塞式 IO一个连接同一时刻只能绑定一个命令请求所以多线程要用连接池来兜底否则线程安全直接给你颜色看。Lettuce 是后来居上的高性能客户端基于 Netty 事件驱动一个连接可以并发处理多个命令请求这在网络通信上也叫多路复用。它线程安全、吞吐高所以从 Spring Data Redis 2.0 开始成为默认客户端直到今天的 Spring Boot 3.x 依然如此。可以说Lettuce 已经占据了“通用客户端”生态位。Redisson 则走了一条完全不同的路它没有老老实实只做命令封装而是把 Redis 包装成一个分布式内存计算平台直接给你提供分布式锁、分布式 Map、分布式 Set、信号量、限流器等工具类。如果你的业务已经用 Redis 解决“协调分布式系统”的问题Redisson 几乎是不可替代的。1.2 选型难的本质并发模型和场景定位完全不同很多新手觉得选择困难是因为只看到了功能列表没有看到三者对网络连接的处理方式差异。这里我画了个对比表你可以对着体会一下维度JedisLettuceRedisson底层 IO 模型阻塞式 Socket同步Netty 多路复用同步 API 但底层异步处理Netty 多路复用封装成分布式对象线程安全性不建议多线程共享实例线程安全可共享实例线程安全连接管理必须用连接池 JedisPool默认单连接复用可选连接池自带连接管理聚焦高级功能可伸缩性高并发下连接数和线程数容易成为瓶颈高并发表现最佳减少连接切换开销分布式场景下最顺手典型使用场景脚本工具、老项目、低并发系统Spring Boot 默认缓存、读写分离、高吞吐服务分布式锁、分布式计数、编排类业务选型难难在你明明是在选一个“连接模型”却经常被“哪个更好”这种问题引导。我的结论很明确如果你做的是标准 CRUD 加缓存的服务用 Spring Boot 的默认 Lettuce 就是最优解如果项目里要处理分布式协调、分布式锁这类高级需求直接考虑 Redisson 的整合而 Jedis 更适合作为内部小工具、测试用例、或者你根本不想引入复杂依赖的场景。2. 三大客户端的核心设计与场景解析2.1 Jedis直连模型的老将现在还能干什么Jedis 的设计最简单粗暴每个命令走一次 Java Socket命令发出去就等着响应回来。这个模型决定了 Jedis 实例本身不是线程安全的——因为 JVM 里多个线程如果同时操作一个 Socket 输出流两个请求的字节会交错在一起服务器返回时根本分不清哪段数据该给哪个线程。所以正确做法是使用 JedisPool每次从池子里借一个连接用完还回去。传统 Vs 连接池风格对比// 错误示范多线程共享一个 Jedis Jedis jedis new Jedis(localhost, 6379); new Thread(() - jedis.set(a, 1)).start(); new Thread(() - jedis.get(a)).start();// 正确的 JedisPool 用法 JedisPool pool new JedisPool(host, port); try (Jedis j pool.getResource()) { j.set(key, value); }需要注意的问题是JedisPool 的连接数上限、最大空闲连接数、获取连接超时时间这些参数在高并发下非常敏感。如果 maxTotal 配得过大反而会因为创建大量空闲连接导致内存浪费和 Redis 端文件描述符耗尽配得过小请求会排队等待拿连接最终表现为接口 RT 上升。不过说句实在话如果你负责的是新项目我不太建议用 Jedis。它缺少 Redis Cluster 节点拓扑自动刷新能力遇到集群节点变更扩容、缩容、故障切换的时候JedisCluster 需要手动触发节点刷新在容器化环境中这是很糟心的事情。但 Jedis 作为轻量工具库写个压测、跑个数据迁移脚本体验很好依赖也干净没有 Netty 那种庞大的间接依赖链。2.2 LettuceSpring Boot 默认选择的真相Lettuce 之所以被 Spring 选中作为默认 Redis 客户端核心原因是它的 Netty 多路复用模型。简单说它维护了一个或少数几个 TCP 连接在这个连接上以“请求编号 响应编号”的对应关系复用通道而不是一个线程一个连接。这样单个 JVM 内可以同时发出几十个 Redis 命令等待响应的同时不用阻塞当前业务线程。一个典型的 Lettuce 配置spring: data: redis: host: localhost port: 6379 timeout: 3s lettuce: pool: max-active: 32 max-idle: 16 min-idle: 4 max-wait: 3s这里要特别提醒Spring Boot 2.0 之后spring.redis变成了spring.data.redis很多从旧项目升级的人在这里踩坑配置写了一大堆结果全没生效连接还是默认 localhost:6379。Lettuce 的优势在于多个线程共享一个连接天然线程安全不用加锁或刻意使用连接池在高 QPS 场景下连接开销几乎可以忽略。曾经有压测数据表明Lettuce 在相同硬件条件下比基于阻塞 IO 的客户端可以支撑更高并发。我在自己的项目中用 Lettuce 跑过几百万次读写的压测连接数稳定在个位数和 Jedis 连接池动辄几十上百个连接比起来确实省心。但 Lettuce 也有自己的脾气。因为它底层是异步多路复用很多底层错误只有在真正发送命令时才会暴露比如网络闪断导致的RedisCommandTimeoutException处理起来比 Jedis 的JedisConnectionException更费眼神。另外Lettuce 原生 API 是 low-level 的比如你需要自己去拼 Lua 脚本来处理复杂业务这一步没有高级封装的话写起来比较原始。2.3 Redisson把 Redis 当分布式平台用的集大成者Redisson 的定位不是“Redis 的 Java API”而是一整套分布式组件库。它自带一套基于 Netty 的客户端再往上封装了很多大家日常都会用到的东西RLock分布式锁、RMap分布式 Map对应 Redis Hash 结构、RSet、RAtomicLong、RSemaphore、RCountDownLatch甚至延迟队列等。Redisson 在分布式锁这块做得尤其突出。原生 Redis 做分布式锁需要自己设计过期时间、释放逻辑、续期逻辑稍不留神就会出漏洞。Redisson 直接用lock()、unlock()底层自动帮你处理了加锁、看门狗续期、可重入、保护性删除这些事看起来就是一个 Java 并发包接口学习成本极低。引入 Redisson 也很简单dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.27.2/version /dependencyspring: data: redis: redisson: file: classpath:redisson.yaml使用的时候只需要注入Autowired private RedissonClient redisson; RLock lock redisson.getLock(order:pay:1001); try { if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { // 业务代码 } } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }Redisson 的缺点是太重。如果你只是用 Redis 做缓存引入 Redisson 会带来很多用不到的分布式组件还要面对它的配置体系另外它和 Spring Cache、RedisTemplate 还是有分工的不能混为一谈。所以我的习惯是Spring Boot 自带 Lettuce 管缓存和常规读写再单独引入 Redisson 管分布式锁和分布式协作两者并存各干各的活。3. Spring Boot 项目里的 Redis 客户端落地玩法3.1 从依赖到配置一次把环境搭对新项目最省心的组合是spring-boot-starter-data-redis它默认引入 Lettuce。如果你想要 Jedis需要排除 Lettuce 再单独引入 Jedis 依赖但既然默认是 Lettuce且性能更好一般情况下没必要折腾。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency连接配置按前面给的 yml 写就行有一点很重要如果你用 Docker 起 Redis 并在容器里开启了密码记得加上密码项不然连接超时问题会非常迷惑。环境准备这里我顺便把几个高频热词的实操一起讲了。本机没装 Redis 的macOS 用户一行命令搞定brew install redis brew services start redis想在服务器上用容器跑的docker pull redis:7 docker run -d --name redis -p 6379:6379 redis:7 --appendonly yes如果要做主从复制最简单的容器方案是建一个自定义 Docker 网络起一个主节点再起一个或多个从节点指向主节点docker network create redis-net docker run -d --name redis-master \ --network redis-net -p 6379:6379 \ redis:7 redis-server --appendonly yes docker run -d --name redis-slave \ --network redis-net -p 6380:6379 \ redis:7 redis-server --replicaof redis-master 6379从节点容器里执行info replication看到master_link_status:up就说明搭建成功。做本地开发时这种方式比直接装多个系统服务要干净很多。3.2 序列化方案对比乱码问题的根源与正确解法用 RedisTemplate 做缓存时最常见的报错不是连接失败而是存进去明明是 String取出来变成一串\xAC\xED\x00\x05t...开头的乱码或者直接把业务对象存成不认识的二进制格式。根因是 Spring 默认的JdkSerializationRedisSerializer会把对象序列化成 JDK 原生二进制流而 Redis Desktop Manager、命令行 redis-cli 看到的必然是一堆乱码。不同序列化方式各有取舍我平时建议这样取舍序列化方案可读性跨语言兼容安全性风险适用场景JDK 默认序列化差差秋马允许的普通风险几乎不推荐业务使用String 序列化JSON 字符串好好低推荐用于缓存和接口数据Jackson JSON 序列化好好有一定反序列化风险需配置白名单项目内部对象缓存方便调试GenericJackson2JsonRedisSerializer好较好需注意类型信息注入Spring Boot 常用同样防乱码推荐封装一个显式配置的 RedisTemplateBean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); template.afterPropertiesSet(); return template; }如果你对数据类型要求更严格也可以直接用StringRedisTemplate业务里用 Fastjson 或 Jackson 序列化成 JSON 字符串再存读取时再反序列化。这种方式最可控也最容易排查问题只是要自己多写一层序列化工具。踩过几次坑之后我反而觉得 StringRedisTemplate 手动 JSON 是很多业务的最优解调试方便不存在隐式的序列化器差异。3.3 Redis 缓存治理缓存注解和 TTL 怎么设置才算合理Spring Cache 集成 Redis 很简单加上EnableCaching然后在方法上标Cacheable即可。但缓存治理的细节都在配置里spring: cache: type: redis redis: time-to-live: 10m cache-null-values: false这里有一个非常容易被忽略的坑cache-null-values默认是 true会把 null 也缓存起来。如果你的业务里大量查询未命中并返回 null这些 key 会被缓存导致后续查询即使数据已经恢复也拿不到最新值。所以我在生产项目里通常把它关闭。缓存雪崩、缓存击穿、缓存穿透也是 Java 面试高频题落到客户端配置层面分别是穿透查询不存在的 key缓存和数据库都落空。解决是在服务层做空值缓存、布隆过滤器。击穿热点 key 过期瞬间大量请求打到数据库。解决是用互斥锁重建缓存锁这块可以结合 Redisson 的 RLock 来实现。雪崩大量 key 同一时刻过期导致数据库压力瞬增。解决是 TTL 加随机时间比如统一 TTL 10 分钟可以在此基础上再加 1-3 分钟的随机偏移。如果你在 Spring 项目里用Cacheable管理热点接口建议给不同业务配置不同 TTL不要把全局 TTL 设成一个值。例如商品信息缓存 5 分钟用户信息缓存 30 分钟这种差异化配置在 Spring CacheManager 里要自定义RedisCacheManager按 cacheName 设置过期时间。3.4 连接工具与可视化客户端的选择很多人在排查问题时会用到可视化客户端。早期的代表是 Redis Desktop Manager也就是热搜词里的 RDM现在已经转型为 RedisInsight官方免费功能也更强——支持数据浏览、命令行、内存分析等。另外还有一个国产的 Another Redis Desktop Manager跨平台界面更符合国人习惯很多团队在用。不过我要多说一句可视化客户端方便归方便最好不要在生产环境里直接浏览 key。两个原因一是很多工具默认扫描会触发KEYS *在 key 数量较大的 Redis 实例上会让服务直接卡顿二是如果误操作把测试库和生产库搞混后果很严重。生产环境建议开只读账号或者用 RedisInsight 的专业模式做限制否则出了事很难追责。4. 分布式锁的正确姿势从手写 SETNX 到 Redisson 的演进4.1 手写分布式锁为什么会翻车提到 Redis 分布式锁很多八股文会告诉你用SET key value NX EX 30。但在真实生产环境里这个方案有一堆隐蔽问题。第一个问题是如果你只给锁设置了 30 秒过期时间但业务代码执行了 2 分钟那前面的线程释放锁时可能已经把它后面线程加的锁删掉了。第二个问题是如果线程 A 锁过期后线程 B 拿到了锁此时线程 A 才执行完并删锁等于把 B 的锁解掉两个线程同时进入临界区这就是典型的多线程安全事件。正确做法是加锁时设置随机唯一值释放锁时先比较再删除这必须用 Lua 脚本保证原子性if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end这段脚本能保证只有持有者才能删锁但依然没有解决业务执行时间超过锁过期时间的问题。当然你可以把过期时间设得足够长比如 30 秒但业务里头有外部接口调用超过几十秒的情况太常见这种粗粒度的时间设置早晚会踩雷。4.2 Redisson 的看门狗到底解决了什么Redisson 的分布式锁会自动给锁加一个看门狗Watchdog机制默认锁的过期时间是 30 秒如果业务还没执行完Redisson 会在锁快要过期时自动把锁的过期时间续到 30 秒。这就解决了“业务时间不确定”的问题。当业务正常执行完lock 会主动释放锁并通知看门狗停止续期。关键点来了你在调用tryLock时必须注意 leaseTime 参数。如果你手动指定了 leaseTime比如tryLock(3, 10, TimeUnit.SECONDS)Redisson 会认为你不需要自动续期10 秒后锁必然过期只有传-1才会触发看门狗自动续期。这个知识点很多背八股文的同学都不清楚反而是在面试现场能拉开差距的细节。正确的使用姿势RLock lock redisson.getLock(queue:job:save-user); try { boolean locked lock.tryLock(2, -1, TimeUnit.SECONDS); if (locked) { // 业务处理 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } }注意点这个锁是可重入的同一个线程可以重复加锁而不自我阻塞释放锁操作要放在finally里否则异常路径会导致锁永远不释放。另外isHeldByCurrentThread()这个判断不能省否则你可能会尝试释放一个自己根本没拿到的锁。4.3 单机锁、哨兵模式和 RedLock 的现实边界很多人看到 Redisson 分布式锁就去搜 RedLock 算法想把多节点锁的一致性也做起来。我的态度很明确绝大多数项目不需要 RedLock。原因有两层一是 RedLock 本身存在争议。它在分布式系统层面并不是绝对安全比如遇到时钟跳变、GC 暂停、网络分区时同样存在丢锁窗口。二是实际业务场景里单机 Redis AOF 持久化 看门狗续期已经能覆盖 99% 的需求。真正需要 RedLock 的通常是金融级场景而且这类系统一般会选择更成熟的方案而不是自己搞多节点锁。如果非要给生产建议我认为单 Redis 节点做主从复制锁写在 master业务运行期间即使从节点延迟同步对锁的读取也没有影响锁过期后有看门狗兜底这种组合是性价比最高的。在高并发扣减库存、幂等下单这种场景配合本地事务和业务幂等表已经能保证不超卖、不重复支付。5. 高频问题与排查技巧实录5.1 RedisCommandTimeoutException最常见的连接超时搜索热词里那条 “Redis command timed out; nested exception is io.lettuce.core.RedisCommandTimeoutException” 应该是很多人的噩梦。这个异常表面上是命令超时背后往往是这几个原因第一种是慢命令阻塞。Redis 是单线程执行命令一旦执行KEYS *、对大 Hash 的HGETALL、或者大批量的SMEMBERS执行时间可能直接超过客户端的 timeout后面排队的命令全部超时。排查方法是开 Redis 慢日志# 设置超过 10 毫秒的命令记录 CONFIG SET slowlog-log-slower-than 10000 # 查看最近 10 条慢命令 SLOWLOG GET 10第二种是客户端线程池耗尽。如果用 Lettuce 开启了连接池max-active 太小、max-wait 太短请求会在没有可用连接时直接超时。处理方式是适当调大 max-active但同时要监控 Redis 端连接数避免连接数爆炸。第三种是网络抖动。尤其在微服务容器环境下跨宿主机访问 Redis 时偶尔的网络包丢失会导致底层连接重建这段时间内命令就会超时。如果业务允许把 timeout 从 3 秒稍微调到 5 秒能显著降低偶发超时率。但我更推荐的是完善重试策略Spring Boot 里可以给 Lettuce 配置重试次数让单次超时变成可容忍的抖动。5.2 序列化乱码的三种排查思路序列化乱码我见得太多了这里直接给排查路径第一先用 redis-cli 查看某个 key 的值如果打印出来是一堆\xAC\xED说明写入时用了 JDK 默认序列化第二确认是不是 RedisTemplate 和 StringRedisTemplate 混用一个用 JDK 序列化写一个用 String 读必然乱码第三如果项目里有多个模块都往 Redis 写数据一定要统一全局的序列化规范别让 A 工程写入 JSONB 工程却使用 JDK 序列化读取。注意一旦生产环境的 key 已经被 JDK 序列化写入光改代码是不能对存量数据“自动修复”的。需要写一个迁移脚本用旧版本反序列化读出原对象再按新序列化格式重新写入。所以序列化方案一定要在项目初期就定死后期改序列化的成本远比你想的大。5.3 大 key 问题的排查工具Redis 缓存治理中大 key 是一个常年存在的隐患。一个 String 类型的 value 超过 10MB或者一个 Hash 里面有几十万个字段都会在读取时把所有数据一次性拉进内存网络带宽和客户端内存双双爆炸。redis-cli 自带了一个大 key 扫描工具redis-cli --bigkeys它能按类型统计占用空间最大的 top key但要注意这个命令同样会对线上 Redis 有一定压力建议在业务低峰期执行。在生产项目里更精细的做法是用 SCAN 命令配合分段统计而不是一次性 HGETALL。另外对大 Hash 的读取可以考虑拆成多个小 Hash或者用 Hash 中的 sub-field 做局部读取避免全量加载。5.4 排查问题时的日志和监控很多同学遇到 Redis 问题只会重启服务、加大超时时间这治标不治本。我更推荐按这个顺序排查先看 Redis 端监控再看客户端日志最后做代码审查。Redis 端可以开启INFO commandstats看命令级的耗时分布也可以看INFO clients检查连接数是否异常。客户端这边Spring Boot 里把logging.level.io.lettuce.coreDEBUG打开能看到连接创建和命令发送的详细信息但线上千万别长期开着否则日志量非常大。正规项目还是建议接 Prometheus 监控 Redis 指标连接数、慢命令数、内存使用率、过期 key 数这些数据摆在监控面板上比临时抱佛脚查日志高效得多。6. Java 面试里最常见的 Redis 客户端问题怎么答6.1 高频考题的答题框架Redis 在 Java 面试中的地位不用多说基本是必问。很多人以为面试官问的是数据类型和缓存穿透其实他们更想通过客户端选型观察你是否真的写过生产级代码。整理几个高频题和对应的答题框架第一个是“Redis 支持哪些数据类型”。基础答案是 String、Hash、List、Set、ZSet。但你要主动提到 Redis 5.0 新加的 Stream、以及 Bitmap、HyperLogLog、Geo 这些底层仍基于 String 却具备特有语义的功能。加分点是说明你用什么客户端类型去操作它们——例如 Redisson 的 RMap 对应 Hash 结构RScoredSortedSet 对应 ZSet这种跨层映射能证明你真的用过不只是背了八股。第二个是“缓存穿透/击穿/雪崩怎么解决”。答题时建议把客户端层面的措施和业务层面的措施分开客户端可以做的包括设置空值缓存、随机 TTL、互斥锁重建缓存业务层面还有布隆过滤器、限流降级、热点 key 本地缓存。你提到“局部热点缓存”时可以顺带带出 Caffeine说明你有多级缓存意识。第三个是“分布式锁怎么实现”。不要只背 SETNX你要分两个层次回答原生 Redis 怎么手写锁、怎么解决误删除和过期续期再讲 Redisson 的看门狗机制如何自动续期可重入如何实现。如果能说出tryLock传参-1才会触发看门狗面试官基本能确认你实际上手过。第四个是“Lettuce 和 Jedis 的区别”。这是客户端层面的经典题。答题公式是底层模型阻塞 VS Netty 多路复用、线程安全Jedis 需要连接池Lettuce 共享连接、Spring Boot 默认选择。还可以补充一句Lettuce 在 Redis Cluster 节点变更时能自动刷新拓扑JedisCluster 在这方面要手动干预这是很多团队选 Lettuce 的实际原因。6.2 从客户端选型看出系统思考能力面试官问客户端选型真正想了解的往往是你是否有全局判断力。如果你只答“Lettuce 性能最好”那是背书如果你能说“先看场景再定选型”这是思路。我自己的回答思路可以分享给你先说明项目类型——如果是高并发读写、缓存持久化为主的系统默认用 Lettuce因为它是 Spring 默认、线程安全、连接复用优秀如果业务里有跨模块的分布式协作、秒杀扣库存、任务调度抢单等场景会额外引入 Redisson 处理分布式锁和信号量如果只需要一个轻量脚本或者存量代码还在用 Jedis不追求重写那就继续用 Jedis 连接池避免为了换而换。这个回答没有贬低任何一个框架没有盲目追逐技术热点逻辑上能自洽面试官通常会认可。另外如果你在项目里实际踩过坑——比如序列化乱码、大 key 阻塞、连接池耗尽能把这些经历讲成一段有前因后果的排障故事比任何背诵都更有说服力。我个人在实际项目里的体会是Redis 客户端选型没有银弹最好的方式就是把 Lettuce 当默认主力在合适的地方用 Redisson 补位对 Jedis 保持理解但不滥用。真正让缓存系统稳定的不是客户端本身而是你对连接模型的理解、对序列化方案的敬畏、对锁场景的克制和合理的监控治理手段。每个配置项都不是摆设每行序列化代码都值得认真写把这些细节做好后面出问题的概率会低很多。
返回列表