承接前文《一次线上Mysql数据库崩溃事故的记录》,在文章中讲到了一次线上数据库崩溃的事件记录,建议两篇文章结合在一起看,不至于摸不着头脑。
由于时间原因,其中只讲了当时的一些经过以及我当时的一些心理活动,至于原因和后续处理步骤并没有在文章中很清晰的写出来,以致于很多朋友说看得不清不楚的,这里向他们道个歉,主要是上周真的没有足够的时间将两篇文章同时准备好,不然也不会草草结尾了,而且上篇文章中主观因素占了较大的比重,因为回忆起这件事的时候确实有很多想法,因此显得有些个人化、日记化了。
这篇文章就不再讲述事件经过了,主要是把事件的原因和后续的处理步骤整理好。
忆往昔 1有张图,是后来老大发给我的,能够看出当时的数据库情况:
这是数据库宕机后的实例信息,基本瘫痪了,至于事务锁相关的截图,当时没有保存,因此就无法放在文章中了,有朋友给我留言问当时为什么不直接kill掉锁住的进程,我回答是因为我不懂这个知识点,我找了一下那几天的日记确实有这方面的记录,是当时老大发我的几条命令:
show processlist;
//找到锁进程
kill id ;
应该也是做了这些操作的,但是看到上面那张图也应该知道为什么这些都不管用了,事务锁确实存在,导致了部分表无法正常操作,但是主要原因还是因为数据库资源被消耗光了,即使kill了相关的进程也无法解决。
入库功能介绍已经定位到入库是发生这次事故的主要原因,那么为什么频繁的操作会导致这件事的发生呢?
首先来介绍一下当时的功能设计改动及涉及到的SQL语句。
表结构设计及功能设计
入库功能中涉及到的表和实体:
- 仓库信息(tb_storehouse)
- 货架信息(tb_shelf)
- 格子信息(tb_shelf_grid)
- 商品信息(tb_product)
- 商品位置信息(tb_grid_product)
- 入库信息(tb_store_in_record)
释义:
- 由于有多个仓库,因此仓库也独立做了一张表;
- 一个仓库中有多个货架,tb_storehouse与tb_shelf是一对多的关系;
- 一个货架中有多个格子(货架规格不同,有的是8个有的是4个),tb_shelf与tb_shelf_grid也是一对多的关系;
- 商品信息,以商品码作为主键,还有其他属性,但是与入库信息无关就没有罗列出来;
- 商品的位置信息就是一件商品是在哪个格子上,表结构的设计就是四个字段:id,商品码,格子id,number(数量),存储了两个属性的主键id和数量值;
- 入库记录信息,就是哪件商品在哪个时间点由哪个入库员在进行入库,涉及到的字段有:product_id,grid_id,operator_id,create_time,还有其他字段但是与入库操作无关联就不列举出来了。
入库功能 V1.0
在最初的版本中,入库功能的设计较为简单和清晰,入库操作只做一件事,就是入库,页面逻辑也比较简单:进入后台-->仓库管理-->货架管理-->格子管理-->点击入库按钮-->入库,进入想要入库的格子页面点击入库,这时页面会弹出一个弹框,上面有一个input框,然后入库员用扫描枪扫描商品码即可完成入库,入库过程中input框为不可选中状态,成功后方可进行下一次入库,这种方式入库也挺快的,入库员找到想要入库的格子然后一直扫码就可以了。
在最初的版本中需要执行的SQL语句有:
- 根据商品码查询商品,为空则报错并提醒需要完善商品SKU;
- 查询格子信息,为空则报错;
- 查询位置信息,如果已存在,则执行数量number加一,不存在则执行新增操作;
- 添加入库记录信息,完成入库,返回。
共计4条SQL即可完成此一次入库操作,第一个版本是开发自己设计的,功能很明确,不会有其他操作和查询。
入库功能 V2.0
这个版本就是引起数据库崩溃的版本,改动原因很简单,新的"产品经理"觉得入库页面太丑,没有美感,因此要求重做,新的功能如下:进入后台-->仓库管理-->货架管理-->格子管理-->点击入库按钮-->入库-->刷新页面-->点击入库按钮-->入库,新的页面改动如下,原来的格子列表页只会显示格子的信息,但是新页面不同了,要显示格子上有什么商品,商品数量是多少,以及在此仓库中共有多少此商品,分页列表显示,新的功能中要求在入库后执行页面刷新操作,让入库员可以看到变化。
看到这里,你可能觉得不妥或者不合理的地方,暂时先保留一下意见,我们来看一下新的改动执行了哪些SQL语句:
- 根据商品码查询商品,为空则报错并提醒需要完善商品SKU;
- 查询格子信息,为空则报错;
- 查询位置信息,如果已存在,则执行数量number加一,不存在则执行新增操作;
- 添加入库记录信息;
- 查找位置信息列表("产品经理"要求一页20条数据);
- 根据20条记录中的商品码查找对应的真实库存,完成入库,返回。
因为查询真实库存需要另外执行SQL语句,因此新的功能一次入库操作需执行的sql共计25条,除了第一个版本中的几条SQL外,这次功能改动加的SQL语句都是复杂SQL。
答疑
说明:由于牵涉到公司的一些业务,因此入库操作在文章中被简化了,实际的入库操作比文章中描述的过程要复杂一些,当时的功能改动比这个更为糟糕,本来的入库操作是执行大概6条SQL,但是在功能修改后,一次入库要执行60多条SQL。
- Q:为什么要这么设计?
- A:"产品经理"觉得好看。
- Q:仓管需要这种设计吗?
- A:"产品经理"觉得仓管是傻子,不用管他们的想法。
- Q:为什么要商品所有的真实库存数据?
- A:"产品经理"觉得需要全局统筹规划。
- Q:页面好看了但是功能麻烦了,为什么还要这么做?
- A:"产品经理"觉得仓管事情多一点无所谓。
- Q:开发人员针对每个问题反驳了吗?
- A:"产品经理"说要做,反驳没用。
- Q:"产品经理"是谁?
- A:老板的女朋友。
解答完毕!
这种设计其实一眼就知道多此一举,后来跟仓管私聊,他们也气得骂娘,而且在收到流程图就明确问了关于所有真实库存的问题,但是没办法,不做不行啊,原本一次入库可能也就一秒钟不到的时间,新功能一出来,有时可能需要3-4秒钟设置更长时间才行,而且还导致了这次事故。
崩溃原因通过前文的描述,大致也能够知道是什么原因导致了数据库的崩溃,我们公司有一位女黑客!哈哈哈,这个是开玩笑的。
入库操作由原来的6条SQL执行语句增长为60条SQL执行语句,入库时长也随之增长,而且这60条SQL语句中关于查询真实库存的SQL也比较复杂,用到了多表联查及函数操作,性能也比较差,而当天的入库操作也比较密集,因此数据库承受了比原来要重n倍的负荷!资源被逐步耗尽也就不奇怪了。
崩溃原因总结如下:
- 一个业务功能执行了太多的SQL语句,此功能在短时间内又会被多次调用。
- SQL语句中有复杂语句,比如用到了一些函数,比如多表联查,大数量的SQL语句加上复杂SQL语句无疑是雪上加霜。
- 数据库连接池的选择和设置问题,导致出现了大量的数据库连接。
- service层的代码不规范,select语句也加了事务,增加了一些不必要事务的开启和关闭,增加了myslq数据库的开销。
- 部分表没有加索引,或者说索引不完整,导致了慢SQL的出现。
原因列举了这么多,事务出了问题、索引不规范导致查询出了问题、慢SQL的出现、数据库连接爆表,一环扣一环,一个问题牵连着一个问题出现,但是这些其实都不是主要的问题所在,第一环并不是这些,最主要的原因还是功能设计的极不合理,导致短时间内执行了巨量的SQL语句,进而将所有的不足之处都暴露出来,最终将问题引爆,一般情况下,慢SQL和复杂SQL语句并不会拖垮数据库,即使没有索引,也只是查询返回时间会多一些,不可能导致整个应用崩溃掉。
有人可能会说,让老板换个女朋友不就好了吗?
嘘~小声一点。
还有人可能会说,到这里不就简单了吗?直接回退版本就好了吧。
其实问题是多方面的,不仅仅是因为这次功能改动,虽然这次改动是导致问题的主因,但是代码不规范,表结构优化不到位,慢SQL没有处理,这些问题还是存在的,即使这次由于仓管流量的增加没有导致数据库崩溃,说不定下一次商城流量增加或者其他页面流量增加也会打垮数据库,因此,功能修改也是全方位的动刀,而不仅仅是回退版本就行了。
后续处理让老板换女朋友是开玩笑的。
后续处理的步骤比较多,总结如下:
- 入库功能修改,保留页面设计,功能做改动;
- 数据库连接池更改;
- 表结构优化;
- 清理慢SQL;
- 业务代码规范,减少事务开销;
- Mysql参数修改,印象比较深的是
wait_timeout
参数; - 整合缓存功能。
虽然事故发生让人很无奈很沮丧,但是看到处理结果再去想想,如果没有类似这种事故的发生,也不会想着去优化代码,去优化数据库,去整合缓存等等一系列的操作,这些不仅让系统更加健壮,更重要的,是经验!因此不要害怕出现问题,经验就是在磕磕碰碰中增加的。
几年的工作经历,也让我渐渐明白了技术的成长离不开一个又一个的错误,失败中虽然有心酸和不甘,但是也不可否认它也带来了成长,不管是心态的强大,还是效率的提升,经验也是在一次次事故的产生和解决中积累。未来依然如此,还是会遇到一个又一个的难处。
忆往昔 2在这次事件中,我也第一次接触到Mysql宕机,数据库竟然也能被请求到崩溃,以往遇到的是tomcat服务器被请求击垮或者服务器流量被打满,因此关于这件事的记忆比较深刻,可能细节记不太清晰,但是对我的影响还是很大的。这次事件后也是我第一次在项目中用到缓存,这也是为什么在写缓存整合文章前先写了这两篇文章。当然,一开始的整合代码是老大写的,后面又学了很久,才一点点的入门,不仅仅是入库操作,其他的功能中整合缓存对于系统来说也是极有帮助的,在这里,缓存就是负责减轻数据库的压力,转移一部分请求使得其压力不直接落在数据库上。
打个不恰当的比方,一个功能执行6条SQL会运行的很好,而执行60条SQL时,一旦操作比较密集就有可能会崩溃,而缓存就可以避免这一点,尽量的分担掉数据库的压力,不用每次请求都去访问数据库,就像这次事件中的60条SQL一样,如果后面的54条SQL语句返回的结果都放入缓存中,也就不会出现这个崩溃的事件了。
因为如今距离事故发生已经有大概两年的时间,所以可能无法回忆的特别精确,只能根据当时写的几篇日记来还原一下事件的经过,不过也仅仅是个大概,毕竟事件发生的时间点离现在有点远了。其实呢,过程的准确性倒不是特别重要,从这次事件记录里也能看出当时处理事情的不成熟和稚嫩,没想到用缓存去处理,因为压根没有这方面的概念,这件事情以现在的视角去看待的话肯定可以很快的定位到问题并且快速的处理掉,但是对于当时的我来说,还是很麻烦的,在技术角度来说甚至可以说是一件不可能的事情,一开始听到锁表的时候,整个人都蒙了,这是啥,数据库锁是什么?表锁了该怎么办?
结语关于线上Mysql数据库崩溃事故记录的文章到这里就结束啦!如果有问题或者有一些好的创意,欢迎给我留言,也感谢向我指出项目中存在问题的朋友。
新的项目演示地址:perfect-ssm,登录账号:admin,密码:123456
如果你想继续了解该项目可以到我的GitHub仓库或者开源中国代码仓库中查看源码及项目文档。
我曾七次鄙视自己的灵魂:
第一次,当它本可进取时,却故作谦卑;
第二次,当它空虚时,用爱欲来填充;
第三次,在困难和容易之间,它选择了容易;
第四次,它犯了错,却借由别人也会犯错来宽慰自己;
第五次,它自由软弱,却把它认为是生命的坚韧;
第六次,当它鄙夷一张丑恶的嘴脸时,却不知那正是自己面具中的一副;
第七次,它侧身于生活的污泥中虽不甘心,却又畏首畏尾。