MySQL 8.0 JSON字段更新本身不直接加剧锁冲突,真正原因是默认全值覆盖写入:InnoDB将JSON列视作LONGBLOB,即使只改一个键也重写整条LOB记录、锁整行并生成大量undo/redo;启用JSON_SET等函数+动态行格式+变更≤100字节二进制差异方可触发原地小更新。

MySQL 8.0 的 JSON 字段更新本身不直接“更频繁触发锁冲突”,真正导致锁冲突加剧的,是默认的全值覆盖写入行为 —— 即使只改一个键,InnoDB 仍按整条 LOB 记录重写,锁住整行并生成大量 undo/redo。关键不在“JSON”,而在“怎么更新”。
UPDATE JSON 时为什么锁整行而不是只锁字段
MySQL 的 InnoDB 存储引擎不识别 JSON 内部结构;对它来说,JSON 列就是 LONGBLOB。在 8.0 之前,任何 JSON 更新都等价于:
- 标记原 LOB 页为“待删除”
- 分配新 LOB 页写入完整新值
- 更新聚簇索引中指向 LOB 的指针
这个过程必须加行级 X 锁(排他锁),且持续到事务结束。哪怕你只执行 JSON_SET(jdoc, '$.status', 'done'),只要没启用部分更新优化,就和 UPDATE ... SET jdoc = ? 全量赋值无异。
如何启用真正的 JSON 部分更新(8.0.4+ 必须项)
MySQL 8.0.4 起支持 LOB 小更新的原地修改(≤100 字节),但需同时满足三个条件,缺一不可:
- 使用
JSON_SET()、JSON_REPLACE()或JSON_REMOVE()函数,不能用=直接赋值 - 更新后 JSON 二进制差异 ≤ 100 字节(不是字符数!注意空格、引号、转义符)
- 表使用
ROW_FORMAT=DYNAMIC或COMPRESSED(REDUNDANT和COMPACT不支持)
验证是否生效:执行更新后查 INFORMATION_SCHEMA.INNODB_METRICS 中 innodb_n_rows_updated 增量是否远小于 innodb_n_rows_inserted + innodb_n_rows_deleted;若后者显著更高,说明仍在走全 LOB 替换路径。
JSON 更新锁冲突的隐藏放大器:生成列索引未覆盖 WHERE 条件
很多人以为建了 GENERATED COLUMN + 索引就能避免锁冲突,但实际常踩坑:
-
WHERE preferences->"$.notify_on_updates" = true不会命中生成列索引,仍全表扫描加锁 - 必须显式写成
WHERE notify_on_updates_flag = true(假设生成列为notify_on_updates_flag) - 生成列定义里不能含函数副作用(如
NOW())、不能是STORED类型以外的表达式(VIRTUAL不可索引)
否则 EXPLAIN 里 access_type 仍是 ALL,WHERE 条件失效 → 扫描全表 → 每行加记录锁 → 锁冲突概率指数上升。
真正能压低锁冲突的组合拳
单靠 JSON 函数或单个索引解决不了问题。生产环境有效方案是三层收敛:
- WHERE 层:用生成列 + 普通索引把查询收敛到千行内(避免扫描)
- UPDATE 层:用
JSON_SET()+ 动态行格式 + 小变更(控制 diff ≤100 字节)触发原地更新 - 事务层:拆成小批量(
LIMIT 50),每次COMMIT后再取下一批,不让锁持有时间跨批次
最容易被忽略的是第三点:即便启用了部分更新,如果一次 UPDATE 语句更新 10 万行 JSON,仍会持有 10 万行锁几十秒 —— 这不是 JSON 的锅,是批量逻辑没切开。batch_size 不是越大越好,而是要让单次执行耗时稳定在 200ms 以内。


















