InnoDB 用回滚其中一个事务来解决锁交叉的方案

InnoDB 死锁会回滚整个受害事务

当多个事务形成不兼容锁请求的等待环时,就发生 InnoDB 死锁。开启死锁检测后,引擎选择一个受害者,回滚它的整个事务,使其他参与者能够继续。应用收到错误 1213(SQLSTATE 40001)。若只重试报错的那条语句,就会漏掉同一事务中更早执行、但已经一起被回滚的工作。 坏掉的样子 不能给所有锁错误套用同一种回滚规则。默认关闭超时整笔回滚时,锁等待超时只撤销正在等待的那条语句,事务内此前执行的语句仍然待决。开启对应服务端选项后,超时才会改为回滚整个事务。死锁本来就会回滚整个受害事务,不能把默认超时的语句级行为误套在死锁上。 受害者选择也不保证某个固定会话永远输。应用必须检查真实错误,再以有界次数重试完整逻辑事务。不可逆的外部副作用如果没有单独去重契约,不应跟着数据库事务自动重放。提交回复途中连接断开,也不等于已经得到死锁回滚的确定结论;错误类型与已知事务结果需要分开判断。 快速验证 在可丢弃的 MySQL 8.4 实例上打开甲、乙和观察者三个全新管理客户端,先确认三个会话均处于自动提交模式,关闭客户端自动重新连接,保持会话身份稳定。本次实际执行版本是 MySQL 8.4.11。观察者先创建尚未使用的实验模式,并核对死锁检测开启、超时采用默认语句级回滚: CREATE DATABASE xbcatm_deadlock_lab; USE xbcatm_deadlock_lab; CREATE TABLE accounts (id INT PRIMARY KEY, balance INT NOT NULL) ENGINE=InnoDB; INSERT INTO accounts VALUES (1,100),(2,100); SELECT @@innodb_deadlock_detect,@@innodb_rollback_on_timeout; 两个设置应分别为一和零。甲开始一项给两个账户各增加十的操作,先锁住第一个账户: USE xbcatm_deadlock_lab; SET SESSION innodb_lock_wait_timeout=30; SET @increment=10; START TRANSACTION; UPDATE accounts SET balance=balance+@increment WHERE id=1; 乙开始一项各增加一百的操作,先锁住第二个账户: ...

四月 20, 2028 · 1 分钟 · XB Field Notes
携带足够列、能独自满足整条查询的索引

InnoDB 覆盖索引包含查询所需的列

覆盖索引包含某条查询求值和返回结果所需的列。在 InnoDB 中,二级索引记录还会携带主键列,因此即使二级索引定义没有再次列出主键,某条查询也可能已经被覆盖。覆盖描述的是一条具体查询与某个索引之间的关系,不是给索引贴上标签之后,整张表的所有查询都自动获得同样行为。 坏掉的样子 增加返回列可能使覆盖失效。原本能提供标识和时间戳的索引,不一定包含应用后来开始读取的大载荷。优化器仍然可能沿用这个索引定位匹配记录,再访问聚簇记录取得缺失字段。因此,“用到了索引”和“由索引覆盖查询”是两件不同的事,不能从计划中出现索引名字就直接推导后者。 覆盖也不保证访问范围足够小。一个扫描大量条目的查询,可以完全被索引覆盖,仍然花费很多工作。列顺序、过滤条件、排序要求和行数估计都很重要。不要把 Using index 覆盖标记当作查询很快的证明,也不要把它与 Using index condition 索引条件下推标记混淆;后者描述部分条件在索引层求值,并不证明整个返回投影都在索引里。 InnoDB 的可见性检查还会限制通常所说的“只读索引”解释。二级记录不包含与聚簇记录相同的隐藏事务元数据。遇到删除标记条目,或被更新事务修改过的二级索引页时,可能仍需访问聚簇记录才能获得正确可见版本。因此,在并发多版本活动下,一个覆盖计划并不普遍保证聚簇访问次数永远为零。 快速验证 准备一台新的 MySQL 8.4 实例,使用能够创建测试模式的客户端会话。本次实际运行版本是 MySQL 8.4.11。确认实验模式名尚未使用,遇到非预期 SQL 错误就停止,并保持同一个会话,以保留当前选中的数据库。下面建立一万行数据,其中一百笔已付款订单,并让载荷列明显比索引所需字段更宽: CREATE DATABASE xbcatm_index_lab; USE xbcatm_index_lab; CREATE TABLE orders ( id INT PRIMARY KEY, status VARCHAR(10) NOT NULL, created_at DATETIME NOT NULL, payload VARCHAR(1000) NOT NULL, INDEX by_status_created (status,created_at) ) ENGINE=InnoDB; INSERT INTO orders WITH digits(n) AS ( SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 ) SELECT number+1,IF(MOD(number+1,100)=0,'paid','open'), TIMESTAMP '2026-01-01 00:00:00' + INTERVAL (number+1) SECOND, REPEAT('x',640) FROM (SELECT a.n+10*b.n+100*c.n+1000*d.n AS number FROM digits a CROSS JOIN digits b CROSS JOIN digits c CROSS JOIN digits d) AS numbered; ANALYZE TABLE orders; SELECT COUNT(*),SUM(status='paid') FROM orders; 最后的两个计数应为一万与一百。二级索引明确包含状态与创建时间,InnoDB 隐式提供主键标识。先检查并运行只需要这些值的查询: ...

三月 3, 2028 · 2 分钟 · XB Field Notes