答案是伪死锁而非数据库死锁,因jedisclusterpipeline未设超时导致线程无限等待连接池,dubbo线程池耗尽而卡死。

Redis Cluster 迁移本身不会抛出 Deadlock 报错 —— 你看到的这个错误一定来自客户端或应用层,不是 Redis Server 的响应。 真实场景中,它几乎总是由 Jedis(或其他客户端)在 pipeline + cluster 模式下,因连接池耗尽、超时缺失、节点重定向未处理,最终触发线程阻塞级联导致的“伪死锁”现象。下面直击关键操作点。
为什么 Deadlock 报错实际是线程卡死而非数据库死锁
Redis Server 不会返回 Deadlock 错误字符串;MySQL 才有 Deadlock found when trying to get lock 这类原生报错。你在日志里看到的 Deadlock 很可能是 JVM 线程 dump 中的关键词(比如 java.lang.Thread.State: WAITING (parking) 被误读),或是 Dubbo/Netty 层因线程池满而抛出的封装异常名(如 RejectedExecutionException 被日志系统截断为 “Deadlock”)。确认方式很简单:
- 搜索完整堆栈里是否含
org.apache.commons.pool2.impl.GenericObjectPool.borrowObject—— 有则 100% 是连接池取不到连接 - 检查 Redis 日志(
redis-server.log)里是否有CLUSTER SETSLOT相关错误或ASK/MOVED重定向失败记录 - 用
redis-cli -c -h xxx -p xxx手动执行一个迁移中的 key 查询,看是否返回ASK或直接报错
JedisClusterPipeline 必须设超时,否则线程永远等不到连接
默认配置下,JedisClusterPipeline 底层复用 JedisCluster 的连接池,而 GenericObjectPool 的 maxWaitMillis 默认是 -1(无限等待)。一旦某个槽迁移中节点短暂不可达,pipeline 的 syncAndReturnAll() 就会卡住整个调用线程 —— 多个线程同时卡住,Dubbo 线程池迅速耗尽,监控显示 “Deadlock”。解决只需两处硬性配置:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
-
JedisPoolConfig.setMaxWaitMillis(2000):强制连接获取最多等 2 秒,超时抛JedisConnectionException可捕获重试 -
JedisCluster构造时传入connectionTimeout=2000和soTimeout=3000:防止 socket 建连和读响应无限挂起 - 禁用
blockWhenExhausted=false(不推荐):改用快速失败策略,但需配套降级逻辑,否则请求直接 500
降低并发迁移线程数 ≠ 降低业务线程数,要分清作用域
很多人误以为 “把迁移脚本的线程数从 20 改成 2 就能解决 Deadlock”,其实无效 —— 因为出问题的从来不是迁移脚本本身,而是线上业务代码在迁移过程中继续高频访问被移动的 slot。关键区分:
- 迁移工具线程(如
redis-trib.rb或自研迁移器):它只发CLUSTER SETSLOT和MIGRATE,不走 JedisPool,基本不会卡死 - 业务线程(如 Dubbo 接口线程):它们用
JedisClusterPipeline访问数据,才是真凶;必须控制的是这部分并发量 - 真正有效的做法是:在迁移窗口期,对涉及迁移 slot 的业务 key 加一层轻量路由判断,例如缓存
slot → node映射,绕过 Jedis 自动重定向逻辑,避免大量ASK重试堆积
迁移期间最易被忽略的坑:ASK 重定向未触发重试就失败
当 slot 正在迁移时,客户端访问旧节点会收到 ASK 12345 10.0.1.5:7001。Jedis 默认会自动跳转,但前提是:JedisCluster 实例没被多线程共享(它是非线程安全的)、且没有在 pipeline 中混用非迁移命令。常见翻车点:
- 在
JedisClusterPipeline中执行了跨 slot 命令(比如同时查 keyA 和 keyB,它们属于不同 slot),导致 pipeline 内部无法统一重定向,部分请求静默失败 - 迁移过程中客户端缓存了旧的 cluster nodes 信息,没及时刷新,持续往已无权服务的节点发请求,连接池被无效请求占满
- 没监听
ClusterTopologyRefreshOptions的adaptiveRefreshTriggers,导致拓扑变更后 60 秒才感知到,这期间所有请求都打错节点
迁移不是改完配置就完事的动作,它考验的是客户端对重定向协议的理解深度和容错韧性。越想靠“调小线程数+加超时”一招解千愁,越容易在 ASK 和 MOVED 的边界上栽跟头。










