BLOB字段更新放大行锁争用,因InnoDB对含BLOB的行默认加全记录锁,且优化器易弃用二级索引,导致锁范围扩大、扫描行数激增;垂直拆表需解耦读写路径、新建扩展表并补建索引,同时防止单行高频更新与跨表事务。

为什么BLOB字段更新会放大行锁争用
不是BLOB本身“特别重”,而是InnoDB对含BLOB/TEXT列的行在更新时默认走聚簇索引全记录锁(X lock on clustered index record),哪怕只改其他普通字段。只要该行有BLOB,UPDATE语句就会锁住整行物理记录——包括所有变长字段的外部页指针。更关键的是,BLOB列不参与索引排序,导致优化器容易低估扫描成本,进而放弃使用二级索引,间接扩大锁范围。
常见错误现象:SHOW ENGINE INNODB STATUS\G里看到大量lock_mode X locks rec but not gap但trx_rows_locked远高于实际匹配行数;慢查日志中UPDATE语句rows_examined接近表总行数;QPS上不去但Innodb_row_lock_waits持续上涨。
- 垂直拆表前,
SELECT * FROM t WHERE id = 123也会加载BLOB内容,拖慢查询并加剧锁持有时间 - 应用层ORM若未显式指定
SELECT字段,极易触发隐式全字段读取 - BLOB列即使为NULL,InnoDB仍为其保留20字节指针空间,影响页内行密度和锁竞争粒度
如何用information_schema定位BLOB锁热点行
不能只看INNODB_TRX,得结合INNODB_LOCKS和INNODB_LOCK_WAITS反查被锁的主键值,再关联到具体BLOB所在行。重点不是“谁在等”,而是“等哪一行”以及“那行有没有BLOB”。
实操建议:
- 先执行
SELECT trx_id, trx_mysql_thread_id, trx_query FROM information_schema.INNODB_TRX WHERE trx_state = 'LOCK WAIT'拿到等待事务 - 用结果中的
trx_id去INNODB_LOCK_WAITS查blocking_trx_id,再反查持锁事务的trx_mysql_thread_id - 通过
performance_schema.threads和performance_schema.events_statements_current定位其原始SQL(需提前开启performance_schema) - 最后用该SQL中的WHERE条件(如
WHERE order_id = 45678)去查原表,确认该行是否含非空BLOB字段:SELECT order_id, LENGTH(blob_content) > 0 AS has_blob FROM orders WHERE order_id = 45678
垂直拆表时BLOB字段迁移的关键细节
拆不是简单ALTER TABLE切走字段,要确保新旧路径完全解耦:老业务读写仍走原表(兼容过渡),新逻辑必须绕过BLOB列。否则拆了等于没拆,锁还是卡在原表主键上。
推荐步骤:
- 新建扩展表:
CREATE TABLE orders_blob_ext (order_id BIGINT PRIMARY KEY, blob_content LONGBLOB, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP),注意order_id必须与原表一致且加主键,避免JOIN时额外锁 - 原表删掉BLOB字段:
ALTER TABLE orders DROP COLUMN blob_content(线上操作需评估MDL锁影响,建议低峰期+pt-online-schema-change) - 应用层改造:所有涉及BLOB的
INSERT/UPDATE全部路由到orders_blob_ext;原表SELECT禁止*,明确排除BLOB字段 - 补数据用
INSERT IGNORE INTO orders_blob_ext SELECT order_id, blob_content, NOW() FROM orders_old,避免唯一键冲突
注意:orders_blob_ext表本身也需建INDEX(如按updated_at),否则SELECT ... ORDER BY updated_at LIMIT 10会触发全表扫描+全行锁。
拆完之后还要防哪些隐形坑
垂直拆表解决的是“锁粒度”,但不解决“锁频率”。如果业务仍是高频更新同一order_id的BLOB(比如实时上传图片缩略图),新表照样会因单行争用卡住。
必须同步做的几件事:
- 检查
orders_blob_ext的PRIMARY KEY是否被高频更新语句命中——若存在UPDATE orders_blob_ext SET blob_content = ? WHERE order_id = ?,就仍会锁单行;考虑加冗余字段做分片键,例如shard_id TINYINT,把同一订单的多次BLOB更新分散到不同物理行 - 禁用
AUTOCOMMIT=0下跨表事务:原表orders和扩展表orders_blob_ext更新必须拆成两个独立事务,否则锁范围跨表扩大 - BLOB读取加缓存层:Nginx或应用层缓存
/api/blob/{order_id}响应,降低数据库直接压力 - 监控项要新增:
SELECT COUNT(*) FROM information_schema.INNODB_TRX WHERE trx_query LIKE '%orders_blob_ext%',单独盯这个表的事务堆积
最易被忽略的一点:拆表后EXPLAIN显示type=ALL的查询,往往是因为忘了给orders_blob_ext建索引——它不再继承原表索引,而BLOB表又常被当作“附属表”被忽视。











