ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MySQL千万级表全量更新实战:从锁机制到分批方案

MySQL千万级表全量更新实战:从锁机制到分批方案 我有一个习惯每次接到“全量更新”的需求第一反应不是马上写SQL开干而是先问一句“这条SQL明天再跑行不行”。为什么这么谨慎因为我在生产环境吃过一次大亏。那年某业务方提了个需求要把一张7000万行的订单表里某个状态字段统一改成新值。当时觉得简单一条UPDATE下去结果不到半分钟整个实例的CPU被打满连接数暴涨业务侧下单、支付全部报错最后我手动KILL了这条SQL清理了将近二十分钟undo日志才恢复过来。那个晚上之后我就意识到MySQL里对百万级、千万级表做全量更新绝对不是一个“会不会写UPDATE”的问题而是一个涉及锁机制、日志落盘、索引维护、主从延迟、回滚段膨胀的综合性工程问题。这篇就把我这些年处理过的真实案例、踩过的坑、以及最终沉淀下来的可落地方案完整梳理一遍给正准备对大数据量表做全量更新的同学一个参考。内容不局限于某一种写法会覆盖分批更新、参数调整、临时表替换和在线变更工具这几条路线并解释清楚每条路线背后的取舍逻辑。1. 一条UPDATE是如何拖垮整个实例的从故障现场还原到锁本质很多人在本地库、测试库上更新几万条数据感觉MySQL也就那么回事。一旦换成几千万行、几百GB的表同样的SQL跑出来的结果完全不一样。这里先复盘一下那次故障把全量更新的风险点一个个挑明。1.1 故障现场我跑了一条“看起来没问题”的UPDATE当时的表结构大致是CREATE TABLE t_order ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(64) NOT NULL, user_id bigint(20) NOT NULL, status tinyint(4) NOT NULL DEFAULT 0, is_sync tinyint(4) NOT NULL DEFAULT 0, create_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_create_time (create_time) ) ENGINEInnoDB;需求是把is_sync 0的老数据全量置为1已同步的is_sync 1的数据不动。我最初写的SQL是这样的UPDATE t_order SET is_sync 1 WHERE is_sync 0;逻辑上完全正确索引上也有is_sync的隐性扫描需求但问题就在于is_sync的区分度太差。表里绝大多数行都是is_sync 0优化器看了一眼估算扫描成本之后直接选择了全表扫描。结果就是InnoDB从聚簇索引的第一条记录开始逐条判断、逐条加锁、逐条更新事务迟迟不提交行锁越积越多。当时show processlist里能看到大量会话卡在Waiting for lock这些会话都是正常的业务读写。更吓人的是SHOW ENGINE INNODB STATUS里显示History list length从几百一路涨到几万回滚段疯狂膨胀磁盘空间也在迅速下降。整个系统陷入了恶性循环锁等待越多堆积的请求越多CPU和内存消耗越大SQL跑得越慢。1.2 InnoDB锁机制为什么行锁会演变成“锁全表”的效果InnoDB的行锁是挂在索引记录上的。听起来很美好只锁你更新的那些行。但这里有个容易被忽略的前提——你得先找到那些行。当UPDATE无法使用有效索引定位到目标行时优化器会选择全表扫描或全索引扫描。扫描过程中InnoDB会逐行读取聚簇索引记录并对满足条件的记录加锁。在RCRead Committed隔离级别下不匹配的记录会在判断后立即释放锁但在RRRepeatable Read隔离级别下为了支持半一致读和间隙锁加锁的范围会被放大甚至会把扫描路径上的记录和间隙一起锁住。所以一条看似只更新“少量行”的SQL在实际执行时可能锁住了大量无关记录。外部会话一旦需要访问这些记录就会立刻进入锁等待队列。这里还要多说一句MySQL的锁等待是有超时时间的默认innodb_lock_wait_timeout 50秒。如果一条业务SQL在50秒内拿不到锁会直接报Lock wait timeout exceeded错误并回滚。结果就是原本只想做一次全量更新的我不小心把线上所有涉及到这张表的请求都“教育”了一遍。提示判断一条UPDATE会不会锁住太多行最直接的办法是先跑一遍等价的SELECT用EXPLAIN看它的扫描类型。如果typeALL或typeINDEX扫描行数又接近全表那这条UPDATE几乎一定会引发大面积锁冲突。2. 全量更新的成本到底花在哪三个你必须算清的账很多开发者的直觉是“反正就是改一列的值数据量虽然大但数据库应该能承受”。这种直觉在百万级以下勉强成立到了千万级你必须要认清全量更新背后的三笔硬成本。2.1 并发账行锁与间隙锁对在线业务的影响第一笔账是并发账。对一个几千万行的表做全量更新哪怕你分批做每一批UPDATE都会持有一批行锁直到事务提交。如果业务对这张表的读写非常频繁这些锁会直接影响线上延迟。举个我后来遇到过的例子某个商品表1200万行每天凌晨有定时任务做价格全量更新。最初用一条UPDATE直接跑耗时接近20分钟。这20分钟里前台商品详情页的读请求虽然走的是主库没做读写分离但只要命中被更新行就会卡在锁等待上。用户打开页面转圈反馈投诉一晚上来了几百条。后来我把更新拆成每批2000行的事务每批执行完立即提交并sleep 0.2秒。整体耗时虽然变长到35分钟但单次持锁时间只有几十毫秒业务侧几乎感知不到异常。这就是“用总时长换可用性”的典型取舍。2.2 日志账binlog、redo log与undo log的三重压力第二笔账是日志账。InnoDB的每一次数据页修改至少会产生三条日志流redo log崩溃恢复用记录物理页变更。全量更新产生的redo会持续刷盘如果innodb_flush_log_at_trx_commit 1每个事务提交都要fsync一次大批量小事务模式下磁盘IO压力会很大。binlog复制和恢复用记录逻辑SQL或行变更。全量更新的binlog体量通常是数据体量的1.5到3倍这些日志要同步给从库会造成主从延迟。undo log回滚和MVCC用记录变更前的数据版本。更新多少行undo里就要存多少份旧版本数据。如果更新中途失败回滚InnoDB就要把这些undo一条条反向应用这个过程可能比正向更新还要慢。我见过一个极端案例有人对一张5GB的表做全量UPDATE跑到一半因为磁盘空间不足直接宕机。崩溃恢复阶段InnoDB必须用redo重放未完成的事务再加上undo回滚已更新但未提交的数据整个恢复过程花了将近一个小时。从那以后我对所有全量更新任务的第一要求就是“估算磁盘余量至少是表体积的2倍”。2.3 索引账二级索引更新是隐藏的CPU杀手第三笔账是索引账。很多人天真地以为“我只更新一列索引不受影响”。这句话只有在被更新列本身不在任何二级索引里时才成立。InnoDB的二级索引叶子节点存储的是索引键值和对应的主键值。如果你更新的列刚好是某个二级索引的组成部分那么MySQL不仅要修改聚簇索引里的记录还要删除旧的二级索引项、插入新的二级索引项。一个二级索引就要做一次删除加插入两个索引就是两次。这背后的随机IO、页分裂、页合并开销远比更新聚簇索引本身高得多。举一个直观的数据我测试过对一个1200万行的表做“全表某列1”的更新。当这列没有二级索引时分批更新总耗时约40分钟当这列上有一个二级索引时同样分批更新总耗时飙升到110分钟。索引维护的隐性成本不实测是很难想象到的。3. 分批更新方案从SQL设计到进度监控如果你评估下来这次全量更新必须走UPDATE路线那么分批更新是目前兼顾安全与工程成本的最优解。核心思路只有一句话把一个大事务拆成无数个小事务每批及时提交减少锁持有时间与undo堆积并通过节奏控制让主从延迟可控。3.1 主键范围分片最稳定也最容易被忽略的写法分批更新最常见、最稳妥的写法是基于主键范围分片。以订单表为例-- 第一次执行前先记录当前主键范围 SELECT MIN(id), MAX(id) FROM t_order WHERE is_sync 0;假设范围是1到70000000每批处理5000行可以这样写UPDATE t_order SET is_sync 1 WHERE id BETWEEN 1 AND 5000 AND is_sync 0;第一批评完后提交接着处理id BETWEEN 5001 AND 10000依此类推。这个方式的优点有两个走主键扫描路径稳定。主键是聚簇索引范围扫描效率极高不会因为数据分布不均产生全表扫描风险。每批之间天然隔离。即使某一批因为特殊情况失败其他批次的状态是已提交的已完成后可以记录断点并从失败批次继续重跑。但是在实际操作中有个细节很容易踩坑WHERE条件里的is_sync 0不能丢。如果某一批里恰好有行已经被其他任务更新过加了该条件就不会重复更新不加就可能出现重复消费、覆盖更新的问题。用存储过程把这些批次串联起来更省心。我常用的模板是DELIMITER $$ CREATE PROCEDURE sp_batch_update_order_sync( IN p_min_id BIGINT, IN p_max_id BIGINT, IN p_batch_size INT, IN p_sleep_seconds DECIMAL(10,2) ) BEGIN DECLARE v_start_id BIGINT DEFAULT p_min_id; DECIMAL v_end_id BIGINT; WHILE v_start_id p_max_id DO SET v_end_id v_start_id p_batch_size - 1; UPDATE t_order SET is_sync 1 WHERE id BETWEEN v_start_id AND v_end_id AND is_sync 0; COMMIT; SET v_start_id v_end_id 1; IF p_sleep_seconds 0 THEN DO SLEEP(p_sleep_seconds); END IF; END WHILE; END$$ DELIMITER ;这里要注意存储过程里COMMIT之前一定要确保没有开启隐式提交干扰事务边界。一般我会在存储过程开头显式执行SET autocommit 0避免连接默认的自动提交导致批次边界失效。3.2 游标循环更新适合业务条件复杂但主键范围不好切分的场景有一种情况不适合主键范围分片更新条件不依赖主键范围而是依赖某个业务字段比如“更新最近30天内注册且等级大于3的用户”。这类条件在主键上不是连续的硬用BETWEEN会导致大量无效扫描。这时候可以用游标逐条或逐小批取主键-- 伪代码示意 DECLARE done INT DEFAULT FALSE; DECLARE v_id BIGINT; DECLARE cur CURSOR FOR SELECT id FROM t_user WHERE register_time DATE_SUB(NOW(), INTERVAL 30 DAY) AND level 3 AND flag 0; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done TRUE; OPEN cur; read_loop: LOOP FETCH cur INTO v_id; IF done THEN LEAVE read_loop; END IF; UPDATE t_user SET flag 1 WHERE id v_id; END LOOP; CLOSE cur;但这种“逐行更新”性能很差可能一次提交一行动辄几百万次交互。更好的做法是先把符合条件的主键插入一张临时表拿到一个稳定的ID集合然后再按临时表里的ID分批UPDATE。这样既绕开了复杂条件重复计算的问题又能回到主键分批的轨道上来。3.3 分批大小与间隔怎么定不要拍脑袋要看三个指标很多博客会直接告诉你“建议每批5000条”。但我做过很多测试后发现分批大小根本不存在固定最优值它取决于三个动态指标行的平均长度。行越长一个数据页能容纳的行数越少同样5000行更新占用的缓冲池页面和锁范围就越大。如果一个表的行平均长度超过2KB建议每批1000到2000行如果行平均长度在300字节以内5000到10000行都可以尝试。主从延迟的容忍度。每批UPDATE都会生成binlog从库SQL线程应用时需要逐条回放。观察SHOW SLAVE STATUS里的Seconds_Behind_Master如果延迟持续上升说明批次太大或提交太频繁需要调小批次或拉长间隔。在线业务的锁等待情况。更新期间监控information_schema.innodb_trx如果发现大量事务处于锁等待状态说明单批持锁时间过长应该减小批次。我自己比较保守的打法是第一批用500行试水观察锁等待和从库延迟正常的话调整为1000行继续观察逐步加到2000到5000行。这种“慢启动”策略虽然前期耗时多一些但能最大程度避免一开始就把数据库压垮。3.4 断点续跑与进度监控像跑数据任务一样管理全量更新全量更新跑到一半最怕的就是数据库重启、连接中断或事务回滚。如果没有断点机制整个任务可能要白跑。推荐做法是维护一张执行进度表CREATE TABLE t_update_progress ( task_name varchar(100) NOT NULL, last_done_id bigint(20) NOT NULL, target_max_id bigint(20) NOT NULL, batch_size int(11) NOT NULL, status varchar(20) NOT NULL, update_time datetime NOT NULL, PRIMARY KEY (task_name) ) ENGINEInnoDB;每完成一批UPDATE就把当前最大的主键值写入这张表。下次任务启动时直接从这个断点继续不需要从头扫描。监控方面我常用的一条SQL是SELECT trx_id, trx_state, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS run_seconds, trx_rows_modified FROM information_schema.innodb_trx;如果能看到当前正在执行的大事务、已经修改的行数就能预估剩余时间。另外SHOW ENGINE INNODB STATUS里的History list length也要盯一下这个值如果持续暴涨基本可以断定undo log堆积失控得紧急降速或暂停。4. 更新前必须做的索引与外键体检尽量减少不必要的索引维护压力很多人把全量更新的准备工作等同于“写SQL”其实SQL之外的索引和外键体检才是决定任务成败的关键。这部分如果忽略后面很容易出现半路跑不动、跑完性能反而下降的尴尬局面。4.1 被更新列涉及的索引到底该留还是该删如果被更新列是二级索引的一部分我的建议是评估该索引在这个更新任务缓解后的价值如果价值不大先删掉等更新完再重建。理由前面说过更新索引列时InnoDB会删除旧索引项再插入新索引项会产生大量随机IO和页分裂。删除索引后再更新只动聚簇索引更新完成后再重建索引可以利用批量排序算法高效构建总体耗时反而更短。举个实际对比我处理过一个1200万行的用户表要更新member_level列而该列上恰有一个二级索引。保留索引分批更新总耗时约2小时DROP索引后分批更新耗时约45分钟更新完重新创建索引耗时约3分钟。整体节约了超过一半的时间。但删索引不能拍脑袋要注意三点确认该索引没有被其他SQL的WHERE或ORDER BY依赖。删掉后可能导致线上慢查询突然暴露。删索引要在低峰期操作。ALTER TABLE本身也会拿元数据锁长时间不提交会阻塞该表的所有DML和DDL。重建索引时同样要分批或选择工具完成或者直接用ALGORITHMINPLACE, LOCKNONE在线方式避免长时间锁表。4.2 触发器、外键和生成列三个隐蔽的“隐藏成本”更新一张千万级表如果这张表上有触发器那么每一次UPDATE都会额外触发触发器里的SQL。我曾经遇到一张只有几百万行的日志表上面挂了一个很复杂的BEFORE INSERT触发器每次全量更新时这个触发器的执行时间比更新本身还长。排查了很久才发现罪魁祸首。实在无法删触发器的场景尽量改成先INSERT到临时表、再统一处理绕开触发器。外键的影响更隐蔽。MySQL的InnoDB外键约束在更新父表时会自动去检查子表是否有匹配记录。这个检查是逐行查找在千万级父表上成本极高。如果更新过程中遇到Foreign key constraint fails那说明数据本身有问题需要先修数据再更新。我的经验是全量更新前把外键检查临时关闭SET FOREIGN_KEY_CHECKS 0更新完重新开启但前提是你确认数据逻辑一致性没问题。生成列Generated Column也要注意。如果你更新的是一个普通列而表里有STORED生成列依赖这个普通列那么每次更新都会级联重新计算生成列的值并写入磁盘。这个计算如果很复杂等于给全量更新额外加了一倍甚至两倍的CPU压力。4.3 备份与灰度验证宁可慢一点也要有后悔药无论你多自信全量更新前一定要做备份。最基本的要求是mysqldump物理备份或至少导出被更新表的关键列mysqldump -uroot -p --single-transaction --quick --no-create-info t_db t_order /backup/t_order_before_update_$(date %Y%m%d%H%M%S).sql如果是千万级表mysqldump会很慢更推荐用mydumper或者直接依赖云厂商的快照备份功能在低峰期打一个文件系统快照。快照的好处是恢复粒度精确到秒不像逻辑备份需要重放SQL。灰度这一步我也强烈建议保留。具体做法是先选一个只影响测试环境的ID范围或者干脆先导出一万行到一个同结构的新表里跑一遍批量更新脚本观察耗时和锁情况再决定是否生产环境全量执行。5. 参数调优与低峰期执行策略让CPU和IO尽量“平滑”分批更新解决了事务和锁的问题但如果你所在的环境参数配置不合理照样可能跑得很痛苦。全量更新期间在确认可以接受短期风险的前提下可以临时调整几个关键参数。5.1 临时调整的三个关键参数flush策略、锁超时与缓冲池下面这几个参数我不建议长期修改只建议在低峰期的全量更新任务窗口内临时调整任务结束后改回来。参数名建议调整值调整理由风险提示innodb_flush_log_at_trx_commit从1临时改为2降低每个事务提交时的fsync频率减少磁盘IO压力数据库异常宕机会丢失1秒左右已提交事务的数据sync_binlog从1临时改为0binlog不强制每个事务持久化缓解磁盘写放大宕机时binlog可能落后于实际数据影响恢复精度innodb_lock_wait_timeout从50临时改为5全量更新期间不希望业务SQL等待太久快速失败让应用更快感知如果一次正常业务操作真的需要超过5秒等锁会误伤这三个参数组合使用能让大批量更新对磁盘的冲击明显下降。但请注意只要业务对数据零丢失有严格约束比如支付、订单核心链路就不要轻易把前两个参数改成0或2否则一次意外宕机的数据丢失责任谁都扛不起。5.2 主从延迟与读写分离的矛盾从库落后半小时怎么办生产环境基本都有主从架构。全量更新生成的海量binlog会让从库SQL线程长时间追不上Seconds_Behind_Master可能从几秒一路涨到几十分钟。如果业务对从库的读一致性有要求这会直接导致线上读不到最新数据。我的处理策略是“让主从延迟可控而不是完全消除”从库所在机器的磁盘如果是机械硬盘强烈建议先把从库的innodb_flush_log_at_trx_commit临时改为2减少从库自身IO压力。控制主库的更新节奏给每个批次之间加sleep。sleep时间不是随便加的我一般是从库延迟从X秒涨到XY秒时sleep从0.2秒提升到0.5秒或1秒等延迟回落再恢复。实在无法控制延迟的场景考虑临时把该从库从负载均衡中摘掉等延迟追平再挂回来。5.3 执行窗口选择没有绝对的“低峰期”只有相对的“低峰期”全量更新最理想的执行窗口是业务请求最少的时间段但这个“最少”每个系统不一样。一个电商系统可能凌晨2点到5点是低谷一个金融系统可能只有凌晨4点到6点才是真正的空窗。建议你在确定窗口之前拉一下监控系统里近7天的QPS和活跃会话曲线找出那个“峰值最低且持续时间最长”的时段。不要在业务方说“随便找个时间”就信了——他们不知道数据库的忙碌曲线你作为执行者必须自己判断。另外执行前发个通知群里同步一下写明更新窗口、预计影响、回滚预案。这个动作在关键时刻能省掉很多不必要的解释成本。6. 千万级重写的终极方案临时表替换与在线变更工具如果全量更新的目标是“把一列的值大面积重算”比如所有用户积分统一乘2、所有订单状态推倒重来其实有一种更快的思路别在原来的表上硬UPDATE而是重建一张新表导入新数据然后改表名替换。这听起来像是DDL但它本质上解决的还是数据更新的问题而且往往比UPDATE快得多。6.1 重建表方案的完整流程与成本对比重建表的核心步骤是-- 1. 创建新表 CREATE TABLE t_order_new LIKE t_order; -- 2. 用INSERT...SELECT把数据写入新表在写入过程中完成字段值转换 INSERT INTO t_order_new (id, order_no, user_id, status, is_sync, create_time) SELECT id, order_no, user_id, status, 1, create_time FROM t_order; -- 3. 原子替换表名 RENAME TABLE t_order TO t_order_old_bak, t_order_new TO t_order;这套方案为什么快因为INSERT...SELECT走的是顺序写入聚簇索引的路径InnoDB的批量插入会自动顺序化不存在逐行修改二级索引项的问题。如果目标表上有二级索引甚至可以采用先建表、后插数据、最后统一加索引的方式让索引构建走批量排序算法速度快得多。我做过一组对比测试同样是1200万行、行平均长度500字节的表方案耗时对在线业务影响单条UPDATE全量执行20分钟锁表业务严重阻塞分批UPDATE每批5000行sleep0.2秒约40-55分钟轻微锁影响临时表INSERT...SELECTRENAME约8-12分钟仅在RENAME瞬间有毫秒级元数据锁影响临时表方案在千万级全量更新场景下往往是最优解。6.2 RENAME的原子性与外键/视图依赖切换时最容易翻车的点表名替换看起来简单但有两个坑必须提前处理外键约束问题。如果原表是其他表的父表RENAME之后子表的外键定义仍指向旧的表名需要同步修改子表外键。更麻烦的是临时表t_order_new上还没有建立外键关系RENAME前要把外键也重建好。这个问题在真实环境非常容易忽略我建议切换前先查一下外键依赖SELECT table_name, column_name, constraint_name, referenced_table_name FROM information_schema.key_column_usage WHERE referenced_table_name t_order;视图与存储过程依赖。视图和存储过程内部引用的表名是硬编码的。RENAME后依赖原表名的对象会立刻失效。切换前需要评估所有视图、触发器、存储过程必要时先DROP再重建。6.3 在线变更工具的思路pt-ost与gh-ost的处理逻辑如果你没有足够的停机时间又必须对在线表做全量更新/重写可以参考两个开源工具的底层逻辑pt-online-schema-changept-ost和gh-ost。pt-ost的做法是创建一个与目标表结构一致的新表然后通过触发器把原表上的增量变更同步到新表再把历史数据按主键分批INSERT...SELECT导过去最后通过RENAME切换。gh-ost的做法更激进直接从binlog里解析增量变更不依赖触发器对原库的侵入更小。这两个工具本意是解决Online DDL但套用在“全量更新”上同样成立。如果你的全量更新需要几小时才能完成而业务又完全不能停你可以用同样的思路自己实现创建新表、开启binlog监听、把增量变更同步到新表、批量回放历史数据、最后原子切换。这套方案我在一个2亿行的核心表上实践过切换过程业务几乎无感唯一的代价是前期代码开发和测试成本比较高。那具体怎么判断该用哪种方案我一般按这个标准来表行数在千万以下更新列不涉及索引业务可接受几十秒锁影响直接分批UPDATE即可。表行数在千万以上更新逻辑是“整列大面积变换”优先考虑临时表替换。表行数在千万以上业务要求全程在线优先使用pt-ost或gh-ost这类工具。7. 一批在千万级表上做全量更新时我反复检验过的执行细节前面几章讲的是框架和路线这一章把我在实操中反复踩过、验证过、最后沉淀下来的一些零碎细节集中整理一下。这些东西单独看都小拼在一起却能显著影响一次全量更新的成败。7.1 唯一索引冲突分批更新时最容易出现的“意外中断”如果你更新的列上有唯一索引分批更新时很可能出现“第一条插入成功、第二条插入冲突”的情况。这种事在全量更新里特别坑因为同一批里两条记录互相冲突或者更新目标值恰好与历史值重复都会导致整个批次回滚。应对方法分批前先跑一遍查重SQL把冲突数据单独拎出来处理SELECT is_sync, COUNT(*) AS cnt FROM t_order GROUP BY is_sync HAVING cnt 1;如果确实存在历史脏数据先清洗再开启全量更新。7.2 SQL中的WHERE条件与LIMIT组合MySQL 8.0下的新玩法MySQL 8.0开始UPDATE语句真正支持了带LIMIT的语法例如UPDATE t_order SET is_sync 1 WHERE is_sync 0 LIMIT 5000;这条SQL会只更新最多5000条符合条件的记录。虽然LIMIT在UPDATE里不保证“每次取的是同一个子集”但结合循环调用可以做到有边界地渐进更新适合那些数据分布非常散、主键不好切分的场景。但用LIMIT时一定要注意不要在同一事务里反复循环执行同一条LIMIT语句。因为已更新的行已经变成is_sync 1下次执行时条件is_sync 0会自动跳过它们倒也不会死循环但性能上会反复扫描全表找“还没被更新的行”效率远不如主键BETWEEN。7.3 更新后的数据校验别只盯着执行成功还要看影响行数全量更新完成后校验环节千万别省。我的校验习惯是执行前后各跑一条聚合SQL对比-- 更新前 SELECT COUNT(*) AS total, SUM(is_sync 1) AS already_sync, SUM(is_sync 0) AS pending_sync FROM t_order; -- 更新后 SELECT COUNT(*) AS total, SUM(is_sync 1) AS already_sync, SUM(is_sync 0) AS pending_sync FROM t_order;如果更新后pending_sync不为0说明有部分行因为条件不满足或其他原因没被更新到需要查一下原因。如果total总数都不一致那问题更严重得立刻查备份和binlog。7.4 别忘了SHOW ENGINE INNODB STATUS排查锁问题的第一抓手最后分享一个排查习惯。无论分批更新还是临时表方案只要我怀疑锁有问题第一件事永远是执行SHOW ENGINE INNODB STATUS\G重点看TRANSACTIONS段的History list length、LOCK WAIT相关的TRX ID和持锁事务的SQL文本。很多锁问题光看报错信息猜半天不如直接看InnoDB事务状态来得直观。那次事故复盘之后我给自己定了一个规矩凡是预估执行时间超过5分钟的全量更新必须写成带进度表、可断点续跑的脚本并配套监控告警。这不是小题大做而是在一张几千万行、承载核心业务流的表上一条鲁莽的UPDATE就可能让整个团队半夜爬起来处理故障。根据我的个人经验最稳妥的路线永远是能重建表就不硬UPDATE必须UPDATE就分批加监控急切的变更先推迟到低峰期再验证。数据量越大越要把“稳”字放在第一位。
返回列表