MySQL不存在分布式死锁,所谓跨实例死锁实为应用层事务顺序不一致导致的业务逻辑阻塞;全局ID仅在作为稳定分片键时可规避跨实例争抢,关键在于路由一致性而非ID唯一性。

MySQL 多实例间根本不存在“分布式死锁”这个概念
这里有个常见的误解需要先澄清:MySQL本身并不感知其他实例的存在。它的核心引擎INNODB的死锁检测机制,只在单个实例内部生效。所以,我们常说的“跨实例死锁”,本质上是一种**业务逻辑阻塞**,而不是数据库层面会抛出Deadlock found when trying to get lock错误的真死锁。
那么,当你看到两个实例上的事务互相等待、长时间卡住时,背后发生了什么?大概率是这样一个场景:同一笔业务的并发请求,被负载均衡打到了不同的MySQL实例上。每个实例上的事务各自持有了部分资源的锁(比如,实例A锁定了订单1001的用户余额,实例B锁定了同一订单1001的库存),然后它们又试图去获取对方持有的锁。问题在于,MySQL的锁管理器彼此独立,它们压根不知道这两把锁其实关联着同一个业务实体。
为什么全局 ID 不能解决死锁,但能缓解争抢
首先要明确一点:全局ID(无论是snowflake还是UUID)本身并不参与数据库的锁机制,它也无法改变INNODB的加锁行为。它的价值,其实建立在一个关键前提之上:你必须用它作为稳定的分片键或路由依据,从而确保同一个业务实体(比如一个用户、一张订单)**始终被路由到同一个MySQL实例**。
一旦做到了这一点,“跨实例争抢”就从一种难以预测的概率事件,转变为一个可以通过设计来规避的问题。这里的核心,不在于ID是否全局唯一,而在于它能否成为一个稳定的路由因子:
- 用
user_id做分片键 → 同一用户的所有操作都落到同一个实例 → 该用户相关的所有事务在实例内天然串行化。 - 用
order_id(全局ID)做分片键 → 同一订单的支付、发货、退款等操作都去往同一个实例 → 相关的库存、余额更新就不会在不同实例间“打架”。 - 但如果用
request_id(每次请求随机生成)做路由,即使它全局唯一也毫无用处——同一订单的多次操作仍可能散落在各个实例,问题依旧。
真正要防的是“逻辑死锁”,不是等数据库报错
必须认识到,MySQL永远不会因为跨实例的操作而抛出死锁错误。这意味着,你不能指望通过捕获Deadlock found...这类异常来进行重试处理。真正的防线,必须构筑在应用层,主动去约束事务的边界和执行顺序:
- 强制路由与串行化:所有涉及同一
business_key(例如order_id)的操作,应强制路由到同一实例,并在应用层通过同线程处理(或借助synchronized、RedisLock等机制)保证串行化。 - 避免“先查后改”模式:典型的例子是先执行
SELECT ... FOR UPDATE查询余额,再在应用代码中判断并执行扣减。这个时间窗口内,数据可能已被其他实例修改。更优的做法是改用原子更新语句:UPDATE account SET balance = balance - ? WHERE id = ? AND balance >= ?。 - 设置合理的超时时间:
innodb_lock_wait_timeout默认50秒对于线上服务来说太长了。应用层应该设置3–5秒级别的锁等待上限,一旦超时立即放弃并回滚,避免请求堆积引发雪崩。
全局 ID 协调机制容易踩的三个坑
很多团队以为引入了snowflake这类全局ID生成方案就高枕无忧了,结果线上依然出现卡死。问题往往出在协调层没有对齐,细节上翻了车:
- ID生成服务的高可用缺失:如果ID生成服务是单点,一旦故障,部分实例将无法获得新ID进行写入。流量会被迫挤到其他正常实例,瞬间打破原有的路由一致性。
- 分片规则缓存未刷新:应用在启动时缓存了分片映射规则(如
shard_map),但在数据库扩容、分片规则变更后,缓存没有及时刷新,导致新生成的ID被路由到错误的实例。 - 绕过路由的跨库查询:为了便利,有些查询(如订单表JOIN用户表)可能会绕过分片中间件,直接连接主库或使用
FEDERATED表。这会导致事务跨实例开启,是必须严格禁止的SQL模式。
说到底,最棘手的往往不是技术选型,而是业务代码里那些隐式的、未被明确定义的路由假设。举个例子:日志表按create_time分库,但某个数据导出功能却直接用order_id去查询,结果不仅查不到数据,还可能触发全库扫描。这类业务逻辑与数据分布模型的错配,远比选择哪种ID生成算法要关键得多。
