事情发生在两周前,周二凌晨。
我们那个跑批系统,每天两点半自动起,把白天的订单流水做结转。这套东西是我五年前搭的,PHP 写的,丑是丑,稳,五年没出过什么大岔子。那天不一样。
三点多我被电话吵醒,值夜班的小周说单子卡住了,结转跑到一半不走了,日志最后一行是 SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded。我迷迷糊糊听了半天,第一反应是这脚本小子又在讲什么天书。第二反应是穿裤子。
到公司三点四十。机房在七楼,电梯停运,爬上去的时候我脑子里还在想,锁表嘛,杀个进程就完了。结果一看监控,不是锁表,是死锁,两个事务互相等,谁也出不来。这种我熟,当年在上一家公司也碰过。把两个 session 都 kill 掉,理论上就该活了。
问题是我 kill 之前,手抖,多敲了一个逗号。
别问我为什么 kill 命令后面会有逗号,我也想知道。反正回车摁下去,屏幕闪了一下,一张中间表的数据没了。
那张表叫 order_tmp_0930,是当天结转的中间产物,两百万行。没了。备份是隔天凌晨做的,也就是最坏情况丢一天。但这是中间表,丢了当天那批单子就全得重跑,重跑要动源表——而源表那头的同步任务是我年初配的,我自己都记不太清细节。
当时汗就下来了。小周站在旁边问我要不要叫老王,我说不用,我自己来。
要我说,这时候人的第一反应才是真本事。我干了十二年,头一回觉得手是凉的。先看的不是什么高端工具,是 binlog。还好,五年前建这套系统的时候,我把 binlog 开着了,格式是 ROW。当年为什么开的?想不起来,好像是阿里云那个文档里跟着抄的,也没想过真用上。
然后我干的第一件事,是让 Cursur 给我把那个 binlog 的解析脚本写出来。不是我不会写,mysqlbinlog 那套参数我背不熟,而且凌晨脑子跟浆糊一样,让它先出个初稿,我来改。二十分钟,脚本出来了,我扫了一遍,有几个参数它写歪了,改掉,跑。
先是只解析,不恢复。把删除那张表前后的 position 圈出来,导成一个 sql 文件,两百多兆。我没敢直接往里灌,先 head 看了一段,确认是 insert 不是 delete,然后——先恢复到一个测试库。
测试库这事,我得说一句,当年搞这个是给新人练手用的,没想到半夜救了命。
恢复跑了四十分钟。跑完我拿两个库的行数对了一遍,两百万零七行,一致。然后才敢往生产恢复。
弄完六点半,天亮了。小周买了早饭,我坐在工位上吃了半个包子,忽然特别想抽烟,但我戒了三年了。
后怕的地方是,如果 binlog 那天因为什么原因没开,或者中间被 rotate 切了,那两百万行就真得从源表重算,源表那头同步任务的账号我早忘了密码。所以说到底,不是我救回来的,是五年前那个记不清为什么开 binlog 的我救回来的。我有两下子,但主要是运气。
话说回来,那个多敲的逗号,我到今天也没想明白是怎么敲出来的。小周后来问我,白哥你怎么 kill 命令还带逗号,我说你懂什么,那是分页符。他没敢再问。
PHP 也照样能接这套玩法,谁再说 PHP 死了,让他来看看我们这套跑批。虽然差点被我撸了。