无主键表并发更新易死锁,因InnoDB自动生成隐藏ROW_ID导致锁粒度大、区间重叠;需用information_schema查询定位,并确认数据唯一性、主键语义合理性及低风险添加。

为什么无主键表在并发更新时容易死锁
MySQL 的 InnoDB 引擎在没有显式主键时,会自动生成一个隐藏的 6 字节 ROW_ID 作为聚簇索引。这个隐式主键不可见、不可控,且所有行共享同一索引结构——导致多事务并发更新(尤其是范围条件或非唯一列)时,锁的粒度变大、锁区间重叠概率飙升。常见现象是两个事务分别执行 UPDATE ... WHERE status = 'pending',却因扫描路径不可预测而互相等待对方持有的间隙锁(gap lock),最终触发 Deadlock found when trying to get lock。
关键点在于:无主键 → 无确定性聚簇顺序 → 优化器可能选择全表扫描 + 随机加锁顺序 → 死锁率显著上升。
如何快速定位当前库中的无主键表
用以下查询直接列出所有缺少主键的 InnoDB 表:
SELECT table_schema, table_name
FROM information_schema.tables t
WHERE t.table_schema NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
AND t.engine = 'InnoDB'
AND NOT EXISTS (
SELECT 1 FROM information_schema.key_column_usage k
WHERE k.table_schema = t.table_schema
AND k.table_name = t.table_name
AND k.constraint_name = 'PRIMARY'
);
注意:information_schema.key_column_usage 是可靠来源;避免依赖 SHOW CREATE TABLE 手动检查,因为某些 ORM 自动生成的表可能含注释但无实际 PRIMARY KEY 定义。
添加主键前必须确认的三件事
- 确认该表当前无重复行(尤其在业务已写入数据后):
SELECT COUNT(*) FROM t GROUP BY col1, col2 HAVING COUNT(*) > 1 —— 若存在重复,ALTER TABLE t ADD PRIMARY KEY (col1, col2) 会失败
- 避免使用
AUTO_INCREMENT 列作为主键,除非该列天然具备业务唯一性且写入方向稳定(否则高并发插入易引发页分裂和锁竞争)
- 优先选业务语义明确、低变更率、高区分度的列组合,例如订单表用
(order_id),日志表用 (log_id) 或 (trace_id, created_at, seq_no)
线上表加主键的低风险操作建议
对大表(千万级以上),直接 ALTER TABLE ... ADD PRIMARY KEY 会阻塞 DML 并耗时较长。推荐分两步:
SELECT COUNT(*) FROM t GROUP BY col1, col2 HAVING COUNT(*) > 1 —— 若存在重复,ALTER TABLE t ADD PRIMARY KEY (col1, col2) 会失败AUTO_INCREMENT 列作为主键,除非该列天然具备业务唯一性且写入方向稳定(否则高并发插入易引发页分裂和锁竞争)(order_id),日志表用 (log_id) 或 (trace_id, created_at, seq_no)
ALTER TABLE ... ADD PRIMARY KEY 会阻塞 DML 并耗时较长。推荐分两步:
第一步,创建新主键列(如尚无合适候选列):ALTER TABLE t ADD COLUMN id BIGINT UNSIGNED NOT NULL FIRST(不带 AUTO_INCREMENT,避免初始化开销)
第二步,用 pt-online-schema-change 填充并设为主键:pt-osc --alter "ADD PRIMARY KEY (id)" D=db,t=table。它通过影子表+触发器同步,全程不锁原表。
若无法引入 Percona Toolkit,则至少启用 ALGORITHM=INPLACE, LOCK=NONE(需 MySQL ≥5.6,且表无全文索引、无外键等限制),并提前在低峰期执行。
WHERE non_pk_col = ? 且该列无索引,InnoDB 仍可能回退到全表扫描加锁——死锁隐患只是转移,没消失。


















