goldendb中并发 Update 大量锁超时场景

注意:本文档内容涉及内部技术细节,仅供内部使用,不得外传。

1. 多节点复制表 - 并发更新同一条记录

1.1 问题描述

在多节点复制表(Multi-Node Replicated Table)场景下,当多个客户端并发更新同一条记录时,可能引发分布式假死锁,导致锁超时报错。

场景流程:

  1. Client 1 发起 update row 1 -> Proxy -> 下发至分片 g1g2
  2. Client 2 发起 update row 1 -> Proxy -> 下发至分片 g1g2
  3. Proxy 在循环内下发指令(先 g1g2),逻辑上看似串行,但实际网络传输中是并发的。
  4. 受网络延迟、CPU 负载等影响,数据库分片收到请求的顺序可能不一致。

1.2 原因分析

若出现以下时序,将形成死锁:

  • Client 1 的更新请求:g1 先收到并加锁成功,g2 后收到。
  • Client 2 的更新请求:g2 先收到并加锁成功,g1 后收到。

结果:

  • Client 1 持有 g1 锁,等待 g2 锁。
  • Client 2 持有 g2 锁,等待 g1 锁。
  • 结局:形成分布式死锁,必须等待其中一个事务锁超时(innodb_lock_wait_timeout)才能解开。

对比说明:在 CW(特定版本/模式)下不会出现此问题,因为 Proxy 做了优化:更新多节点复制表时,会先下发 select for update 到第一个分片,锁定成功后再下发到其他分片。这会将单条 update 拆分为:start transaction -> select for update (g1) -> select for update (others) -> update -> commit

1.3 解决方案

  • 架构调整:对于需要高频并发更新相同记录的表,严禁使用多节点复制表。
  • 替代方案
    • 改用 单节点复制表
    • 采用其他合适的分片规则。
  • 适用场景限制:多节点复制表仅适用于参数库、配置库等几乎没有写操作的场景。

创建单节点复制表的语法示例:

   CREATE TABLE dbmt.cdr_xxx_log (
     `id` bigint NOT NULL,
     `table_name` varchar(200) NOT NULL,
     `partition_name` varchar(200) NOT NULL,
     `xxx` bigint DEFAULT NULL,
     `begin_time` datetime DEFAULT NULL,
     `end_time` datetime DEFAULT NULL,
     `FLAG` int NOT NULL DEFAULT '0',
     `owner` varchar(200) NOT NULL
   ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin
   distributed by duplicate(g1);

查看表的完整创建语句:

   SHOW CREATE TABLE schema_name.table_name storagedb g1\G

2. 多节点 Hash 表 - 范围更新涉及相同记录

2.1 问题描述

在多节点 Hash 分片表中,执行范围更新(Range Update)时,如果更新的数据集在不同分片间存在逻辑上的“相同记录”依赖,或更新语句导致每个分片都需要参与且存在资源竞争,其本质与上述多节点复制表的死锁原因相同。

2.2 原因分析

  • update 涉及多条记录且跨越多个分片时,如果有相同的记录逻辑或资源竞争,相当于在模拟多节点复制表的更新行为。
  • CW 优化无效:由于这不是严格定义的“多节点复制表”,Proxy 不会触发“先锁第一个分片”的优化逻辑,因此无法避免死锁。

2.3 解决方案

  • 修改分片规则:针对有范围更新需求的业务,调整分片策略。
    • 改为 单节点复制表
    • 改为 Range 分布(范围分片),确保范围更新尽量落在单个分片内或按序处理。

3. 特殊模型 + DDL 导致的分布式死锁

3.1 问题描述

在业务压测或特定场景下,长事务更新与 DDL 操作并发执行时,可能引发复杂的分布式死锁。

业务压测模型:

begin;
update t1 set money = money + 1 where id = 1;       -- 操作分片 g1
update t1 set money = money - 1 where id = 100001;  -- 操作分片 g2
commit;

同时后台执行 ALTER TABLE 等 DDL 操作。

3.2 原因分析

  1. 事务交叉持锁
    • 事务 A:先更新 g1,再更新 g2
    • 事务 B:先更新 g2,再更新 g1
  2. DDL 介入
    • 在两个事务分别持有部分分片锁的时刻,DDL 语句下发到所有分片。
    • DDL 需要获取元数据锁(MDL X 锁),但被未提交的事务阻塞。
  3. 死锁形成
    • 事务 A 的 update g2 等待 DDL 释放锁。
    • 事务 B 的 update g1 等待 DDL 释放锁。
    • DDL 在两个分片上均等待事务 A 和 B 提交以获取 MDL-X 锁。
  4. 最终表现
    • 系统陷入僵局,直到 innodb_lock_wait_timeoutlock_wait_timeout 触发,强制释放锁。
    • 若业务端设置的 socket_timeout 小于数据库锁超时时间,业务会先报超时断链。

3.3 解决方案

配置 CN 组件使 DDL 串行执行,避免并发 DDL 加剧锁竞争。

在配置文件中进行如下设置:

# 指定 ddl 的执行顺序
# 默认为 0: 并行执行
# 修改为 1: 串行执行
# unit: NA, range: 0,1, default: 0, dynamic: no;
ddl_execute_serial = 1

总结与建议

场景 核心问题 推荐解决方案
多节点复制表并发写 网络无序导致分布式假死锁 禁用多节点复制表用于写场景;改用单节点复制或合适分片
Hash 表范围更新 跨分片更新逻辑类似复制表,且无 Proxy 优化 修改分片规则为单节点复制或 Range 分布
事务更新 + DDL 并发 事务持锁与 DDL 争抢 MDL 锁形成死锁 设置 ddl_execute_serial = 1 开启 DDL 串行执行