员工写了个比删库更可怕的Bug!

想必大家都听说过删库跑路吧,我之前一直把它当一个段子来看 。
可万万没想到,就在昨天,我们公司的某位员工,竟然写了一个比删库更可怕的 Bug!
给大家分享一下(不是公开处刑),希望朋友们引以为戒 。
一、Bug 起因
事情是这样的,昨天中午 11 点左右,突然用户群里的小伙伴反?。鹤约褐苯映晌??鱼聪明 AI 网站 的管理员!

员工写了个比删库更可怕的Bug!

文章插图
接下来,陆续有更多同学反?。捍蠹叶汲晒芾碓绷耍?
员工写了个比删库更可怕的Bug!

文章插图
看到这里,我立刻就去查了下数据库,结果看到的是:
员工写了个比删库更可怕的Bug!

文章插图
好家伙,早起脑供血不足的我立刻高血压上来了,怎么所有的用户都变成管理员了?!
我赶紧问下我所有的员工,这特么是谁干的?。。?
然后员工小 A 大叫:“我 X , 是我今天执行单元测试更新数据的时候,少加了个 where 条件!”
本来的预期:update user set userRole = 'admin' where id = 1
实际上执行:update user set userRole = 'admin'
于是导致整个库里的所有用户都变成了管理员 , 大家可以愉快地薅鱼毛了 。
二、紧急处理
后来据这位写 Bug 的同学的回忆,由于她之前没有遇到过类似的情况,第一时间脑袋是一片空白、头嗡嗡的,完全不知道接下来要怎么做 。
不过我是很冷静的,因为之前在公司处理过类似的情况,毕竟曾经凌晨 4 - 5 点的时候都被叫起来过 。。。
所以立刻就给他发了一段处理方式:
员工写了个比删库更可怕的Bug!

文章插图
解释一下,就跟我们在路上看到一起交通事故一样,第一时间要么是保护现?。?乓桓鲂∨婆撇蝗么蠹医?绞鹿史⑸?兀灰?淳褪欠乐估┐笥跋?nbsp;, 人工疏导不让更多人围观、阻塞交通 。
一般这两件事情是同时执行的 , 由于我知道怎么能够判定哪些用户本来是 VIP(比如通过 VIP 信息)、而且程序又有详细的日志,所以第一时间是让员工先把 user 表的所有角色设置为普通用户权限,防止有人继续利用管理员权限去做一些不好的事情 。
接下来就是立刻停止了线上的前后端服务,一方面是为了后面好恢复数据,另外也是防止一些同学发现自己突然从会员变成了普通用户,增加大量的人工咨询成本 。
所以当时很多同学访问鱼聪明时,看到了这样的截图:
员工写了个比删库更可怕的Bug!

文章插图
稳定现场后,接下来就是想办法恢复数据到正常的状态,好在我给数据库设置了分钟级别的备份,可以直接把数据恢复到事故发生前的最近正常的时间点 。
员工写了个比删库更可怕的Bug!

文章插图
有了备份后的老数据,还要考虑恢复这个时间点后新增的用户数据 。
有很多种恢复策略,我优先选择了逻辑最简单的策略:直接更新用户 updateTime > '2023-07-20 10:00:00' 的数据,根据 id 点对点覆盖除了 userRole 之外的数据列;如果没有对应的 id , 新增一条数据 。也就是使用类似 saveOrUpdate 的方法 。
理想很丰满 , 现实很残酷 。万万没想到,由于 updateTime 是一个发生数据修改时自动更新的字段,导致所有的数据 updateTime 全是最新的,相当于要把数据库全量的数据都去比较一遍 。
于是我的员工呢,写了类似下面这样的程序:
员工写了个比删库更可怕的Bug!

文章插图
然后就开始执行了,结果执行了很久很久,数据都没更新完 。
看来单线程还是太慢了,于是我用并发编程的方式改进了同步的过程 。先把所有用户分组,然后多线程同时执行 saveOrUpdateBatch 方法 。
示例代码如下:
void restoreUserTable() {
List<User> userList = userService.list();
List<UserBak> userBakList = userList.stream().map(user -> {
user.setUserRole(null);
UserBak userBak = new UserBak();
BeanUtils.copyProperties(user, userBak);
return userBak;
}).collect(Collectors.toList());
int batchSize = 1000;
// 使用 lambda 表达式将 userList 每1000个元素分为一组
List<List<UserBak>> groupedBakUsers = IntStream.range(0, userList.size())
.boxed()
.collect(Collectors.groupingBy(index -> index / batchSize)) // 将索引按组分组


推荐阅读