💥 千万级大表需要批量更新字段,如何避免全表锁?有哪些高效方案?
作者:天天摸鱼的java工程师
关键词:MySQL、批量更新、全表锁、分批更新、MyBatis 动态 SQL、异步更新
🧨 一次“简单更新”带来的线上事故
事情起因很简单:
产品提了个需求,要在用户表中新增一个字段 user_tag,然后我们需要对历史的 1000 万条用户数据进行初始化更新。
于是有同事写了这么一句 SQL:
UPDATE user SET user_tag = 'normal';
结果呢?
线上数据库 CPU 飙升,接口响应变慢,最终被 DBA 秒回滚。
这就是传说中的 —— 全表锁 + 大事务灾难。
🧠 问题本质:大表更新为什么危险?
在 InnoDB 引擎下,虽然 UPDATE 是行级锁,但触发条件不当时,也可能演变成全表锁:
- 没有使用索引字段作为 WHERE 条件
- 更新范围过大,一次性影响全表
- 表上有触发器、外键、级联
- 事务未提交,长时间持锁
全表锁的后果就是:所有写入阻塞,读请求异常慢,严重时直接打挂数据库。
🤔 那么,如何安全、高效地更新千万级大表字段?
作为一名写了 8 年 Java 的开发,踩过坑也总结了一些可复用方案,以下是我推荐的几种 高可行性解决方式。
✅ 方案一:分批更新(推荐)
💡 思路:
将全表更新拆分为若干“小批次”,每次只更新几千或几万条,避免长事务和锁表。
🛠️ 举个例子:
-- 每次更新 5000 条
UPDATE user SET user_tag = 'normal'
WHERE id >= 100000 AND id < 105000;
配合主键或索引字段进行范围控制,通过循环执行实现全量更新。
✅ 优点:
- 避免锁全表
- 易于控制事务大小
- 能配合业务低峰进行调度
❗注意事项:
- 批量大小要根据数据库性能调优(建议 1000~10000)
- 要有断点续传机制避免中断失败
- 可配合分页工具如
limit + offset,但注意 offset 过大会性能下降
✅ 方案二:CASE WHEN 批量更新(适合少量字段差异)
💡 思路:
如果每条数据的更新值不同,可以通过 CASE WHEN 一次性更新多个字段,减少 SQL 提交次数。
sql
UPDATE user
SET user_tag = CASE id
WHEN 1001 THEN 'vip'
WHEN 1002 THEN 'normal'
WHEN 1003 THEN 'black'
END
WHERE id IN (1001, 1002, 1003);
✅ 优点:
- 一条 SQL 更新多条数据,减少连接次数
- 写在程序里的批量更新逻辑更清晰
❗注意事项:
- 不适合单次更新量太大(建议单次 500~1000)
- SQL 长度受限,不宜上万条数据合并
✅ 方案三:读写分离 + 异步更新(适合不强一致场景)
💡 思路:
如果业务允许最终一致,可以先将更新请求写入消息队列(如 Kafka),再由消费端异步执行更新。配合读写分离架构,先写入变更记录到缓存/副表,延后更新主表。
✅ 实战流程:
- 用户发起更新请求
- 后端将更新写入 Kafka 消息队列
- 消费者服务异步批量消费
- 分批、带索引字段更新主表
- 更新成功打日志、发监控
✅ 优点:
- 解耦主业务流程,提升响应速度
- 更新压力平滑,易于做限流控制
- 不易锁表,适合数据量特别大的场景
❗适用场景:
- 用户画像更新、积分刷新、定时同步等非强一致性业务
✅ 方案四:MyBatis 动态 SQL + 批处理操作优化
💡 MyBatis 的强项就是构建动态 SQL,结合批处理可以更高效地操作数据库。
示例:批量构造 CASE WHEN SQL
UPDATE user
user_tag =
CASE WHEN id = #{item.id} THEN #{item.tag}
END
WHERE id IN
#{item.id}
✅ 优点:
- 写法灵活,适配业务需要
- 结合连接池参数优化(如批量提交、预编译)能提升性能
✅ 补充优化点:连接池参数调优
在高并发批量写场景中,数据库连接池配置也需要合理调整,例如:
spring.datasource.hikari:
maximum-pool-size: 50
minimum-idle: 10
connection-timeout: 30000
max-lifetime: 1800000
auto-commit: false
注意开启批量支持:
# MySQL 开启批处理
rewriteBatchedStatements=true
✅ 可选方案:建临时表 + 替换(适合离线更新)
如果业务可停机或做离线处理,也可以:
- 建一个结构一致的临时表
- 批量 insert + update 临时表
- 替换主表或 merge 数据
适合数据架构演进、历史数据修复等场景。
🚨 最容易踩的坑
- ❌ 使用非索引字段更新,触发全表扫描
- ❌ 一次性更新全表,造成大事务阻塞
- ❌ 忽略连接池配置,导致连接耗尽
- ❌ 异步更新无幂等控制,重复消费造成脏数据
✅ 推荐组合方案(实战总结)
| 场景 | 推荐方案组合 |
|---|---|
| 强一致性、实时更新 | 分批更新 + 主键范围控制 |
| 数据差异大、更新值不同 | MyBatis 动态 SQL + CASE WHEN |
| 非强一致性、允许延迟 | 异步更新 + 消息队列 |
| 离线处理 | 临时表构建 + 替换合并 |
🧩 总结:大表更新,稳比快更重要
千万级大表的字段更新,一定要谨慎设计更新策略,不要一时贪快:
- 合理分批,控制事务大小
- 利用索引,避免全表扫描
- 善用异步,提高系统可用性
- 提前评估风险,做好回滚和监控
写 SQL 不难,难的是在生产环境下安全、稳定、可控地执行更新操作。
🔧 附:开源推荐工具
MyBatis Plus的批量更新方法(封装好 CASE WHEN)Canal + Kafka做异步增量同步ShardingSphere分库分表场景下的批量更新支持ShedLock定时任务执行控制,避免重复更新
📌 如果你也踩过类似的坑,欢迎留言分享经验!
我是一个 Javaer,写了 8 年代码,从 CRUD 到架构演进,越写越觉得稳定比炫技更重要。
如果本文对你有帮助,别忘了点赞 + 收藏 + 转发给团队的 DBA 或后端同事!