来自公众号:新世界杂货铺

程序猿最大的悲哀是什么!

经历了这两次事故后,笔者觉得最大的悲哀莫过于半夜打电话给DBA请求帮忙恢复数据。程序猿和PM之间的战斗往往还有来有回,而笔者碰上DBA之后,那可真是求人办事,怎么怂怎么来,只要DBA大爷高兴!

为了以后尽量少跪舔DBA大爷,笔者将亲身经历的两次事故记录下来以提醒自己。

第一次数据回滚

PM是需求的生产者,程序猿是需求的消费者,这二者就是典型的生产者与消费者模型。因此本次事故的根因还是PM提出了需求,故笔者认为只要PM不再提需求就不再有事故。

唉!快醒醒,别做梦了!

image

回到事故的本身,笔者先描述一下当时的背景。

PM有大量的数据需要紧急更新到线上。这需求有多紧急呢?PM要绕过QA验证,直接在线上先用少量数据进行测试,少量数据验证通过后就更新所有剩余的数据。

结合笔者所在公司的业务场景,笔者按照以下步骤完成了本次数据更新。

1、将需要更新的数据使用mysqldump进行备份。

mysqldump --replace -f --single-transaction -t \
-h hostname -u user -P 3936 -p dbname tablename  \
--where="id in (1,2,3)"  > tablename.sql

2、开发一个脚本直接调用线上已有更新数据的接口(开发时笔者已经在测试环境自测)。

3、在线上先更新少量数据,并更新修改数据部分的缓存,PM对少量数据进行验证。

4、PM确认该部分数据验证通过后,开始对剩余数据进行线上更新操作。

初看上面的步骤好像没什么大问题,但实际结果却是狠狠地打了笔者的脸。下面,笔者就好好掰扯掰扯到底是哪些原因造成了本次事故。

1、更新接口逻辑没有理清楚,导致线上数据更新错误。

该接口是一个比较老的服务且相关文档少,笔者因为没有梳理清楚所有逻辑,调用接口时部分数据参数传递有误,导致线上数据更新错误。

2、更新接口实现有问题,调用服务后,删除了关联表的数据,所以需要恢复。

如果只是上述第一个问题,笔者自己备份的replace into语句就可完成数据的恢复,但很明显问题不止于此。当事故发生后笔者开始对该服务逻辑进行二次梳理,发现此接口对主表的关联表也进行了更新而且更新逻辑为先删除关联数据然后插入新的关联数据。只是如此倒也罢了,关键是该接口的实现者将所有请求参数作为一个关联数组并将此关联数组传递给所有函数。好家伙,各个具有不同业务功能的函数传递的参数都是一样的,这导致笔者第一次梳理逻辑时无法完全理清楚各个业务函数真实需要的数据到底是什么。

关联数据被删除笔者也没有备份,最后只好跪舔DBA大大帮忙进行数据回滚。

警告⚠️:代码不清晰,程序猿泪两行!

3、未经过QA的保证,就直接在线上测试。

笔者自己虽然在测试环境进行了简单测试,但是程序猿的本职还是开发不能耗费过多的精力去完成QA的工作,而PM很明显也不够专业,这才在质量保证环节出了错并扩大了线上的错误范围。

4、测试时未在无缓存环境下进行验证。

初始,PM对少量数据的验证结果是没有问题的,但是当所有数据更新完成后缓存已经开始逐步重建,数据有误和数据被删的问题就开始暴露了。这是因为笔者只更新了PM想要验证的数据的缓存,却没更新关联数据部分的缓存,因此只有等这部分缓存自然失效问题才逐渐显现。

后续

DBA对数据进行回滚后,批量更新数据还得继续啊!狠心的PM愣是逼着笔者大半夜修好问题继续验证,唯一值得高兴的可能就是这次仅更新少量数据第二天继续更新剩余数据。最后,笔者修好问题并成功地更新完全部数据。

第二次数据回滚

PM又又又提出批量更新数据的需求了,不过这次笔者信心满满,毕竟这次需求和第一次需求几乎一样,唯一的区别是PM指定部分数据不需要更新(这部分PM给到的数据是有问题的,所以不更新)。

但是人怎么可能不犯错呢,笔者忘记了部分数据不需要更新这个点,最后正确的和不正确的数据都更新至线上。万万没想到,经历了第一次数据回滚之后还能遭遇第二次数据回滚,笔者心态是真的崩了。

image

事情已经发生,笔者也只能想办法解决了,下面是笔者基于实际业务场景想到的两个数据恢复方案:

方案一

1、先通过数据ID确认哪些数据需要修复(笔者在执行脚本时记录了数据ID的log日志)。

2、解析备份SQL中需要恢复的数据并拼接为新的恢复SQL。

3、调用服务删除新增的数据(数据更新接口在修改数据的同时会新增其他关联表的数据)。

4、执行步骤2中生成的SQL恢复数据。

方案二

寻求DBA大爷的帮助恢复数据。

方案一可以自己恢复数据,而且正确的数据会保留,但操作麻烦且恢复过程可能产生新的问题,所以最后还是厚颜无耻地去找DBA恢复数据。

DBA恢复数据后还给笔者发了下面恢复线上数据的SQL:

alter table table_a rename to table_a_bk_2;
alter table table_a_bk rename to table_a;

好家伙,DBA暗示已经这么明显了嘛,笔者二话不说默默地发了一封邮件准备申请一个具有DDL权限的账号。笔者现在想的十分清楚,以后再有这种批量更新线上数据的操作一定好好全表备份数据而不是使用仅有读权限的账号备份replace into语句。

-- 全表备份sql语句
CREATE TABLE table_a_bk AS SELECT * FROM table_a;

总结

下面是这两次事故发生后笔者的一些心得,希望可以给大家提供参考。

1、代码逻辑要清楚,函数参数命名要语义清晰。一个参数就包含了所有需要的数据是十分不正确的行为同时代码中尽可能多些注释。

2、对线上数据充满敬畏,操作数据时要理清楚业务逻辑。

3、准备操作线上数据前,尽量先在无缓存环境下进行数据预验证。

4、人都有可能会犯错,所以还需要QA进行双重保证。

5、笔者就是吃了紧急需求的亏才导致这两次事故,其他情况请务必按照正常流程进行数据操作。

6、备份真的很重要!这两次事故后笔者认为前文提到的全表数据备份方案相对合理且易恢复。

最后,衷心希望本文能够对各位读者有一定的帮助


Gopher指北
158 声望1.7k 粉丝