个人一个低级错误,致使数据库崩溃半小时!!

1、反常的SQL语句java

某周四午休时分,我正在工位上小憩,睡梦中仿佛看到了本身拿着李白在荣耀峡谷里大杀四方的情景,就在我刚拿完五杀准备带领队友推对面水晶的时候,一句慌乱急促的“糟了”把我从睡梦中惊醒。sql

我眯开朦胧的双眼,才发现刚才的发声来源于个人组长庄哥,看到他在紧张的点开日志系统查看日志,我预感到有什么不妙的事情发生,仔细一问才知道,原来就在我眯眼的期间,线上数据库服务器的CPU被打满,同时触发了生产数据库只读延迟的限定时间而且发出告警,并且告警的过程持续了半个小时。数据库

这让我倒吸了一口凉气,由于咱们组作的系统不少都用的是同一个数据库服务器,日用户活跃量有好几十万,若是服务器崩溃了将会使全部的系统服务都不可用,因而咱们赶忙经过SQL日志进行问题查找,最后排查出来是由于一张SQL的高量查询没有走索引致使,日志列表显示,这条sql语句的扫描行数达到了上百万,基本就是全表扫描的状况,并且半个小时的时间查询了达上万次,每条SQL查询的耗时都在3000ms以上。服务器

个人天啊,难怪服务器会CPU打满,这么一条耗时的SQL语句查询量这么大,数据库的资源固然是直接就崩溃了,这是当时那条SQL的查询状况:性能

图片

2、临时处理 优化

看了这条语句,我又倒吸一口凉气,这不就是我写的系统调用的SQL语句吗?完了,这回逃不掉了,真是人在睡梦里,锅从天上来。ui

图片

固然,由于是我本身写的SQL,因此我一看就知道这条语句是有问题的。spa

根据个人代码处理,这条sql的调用还少了个重要的参数user\_fruit\_id,这个参数没有传的话是不该该走这条sql查询的,在个人设计里,该参数是数据表里一个联合索引的最左侧字段,若是该字段没有传值的话,那么索引就不会生效了。设计

KEY \`idx\_userfruitid\_type\` (  
  \`user\_fruit\_id\`,  
  \`task\_type\`,  
  \`receive\_start\_time\`,  
  \`receive\_end\_time\`  
) USING BTREE

虽然定位到了SQL语句,可是线上的问题刻不容缓,总不可能找出bug改完再上线吧,因此,咱们只能作了一个临时处理,就是在原来的表上多加了一个联合索引,其实就是去掉了user\_fruit\_id 字段,让这些高量的查询都能走新的索引,就像下面这样:日志

KEY \`idx\_task\_type\_receive\_start\_time\` (  
 \`task\_type\`,  
 \`receive\_start\_time\`,  
 \`receive\_end\_time\`,  
 \`created\_time\`  
) USING BTREE

加上索引后,sql的扫描行数就大幅度的下降了,重启实例后就又能正常运行了。

3、最左匹配原则

那么为何最左侧的字段没传索引就不生效了,这是由于MySQL的联合索引是基于“最左匹配原则”匹配的。

咱们都知道,索引的底层是B+树结构,联合索引的结构也是B+树,只不过键值数量不是一个,而是多个,构建一颗B+树只能根据一个值来构建,所以数据库依据联合索引最左的字段来构建B+树。

例如咱们用两个字段(name,age)这个联合索引来分析,

图片

当咱们在where条件中查找name为“张三”的全部记录的时候,能够快速定位到ID4,而且查出全部包含“张三”的记录,而若是要查找“张三,10”这一条特定的数据,就能够用 name = "张三" and age = 10 获取。

由于联合索引的键值对是两个,因此只要前面的name肯定的状况下就能够进一步定位到具体的age记录,可是若是你的查询条件只有age的话,那么索引就不会生效,由于没有匹配最左边的字段,后面全部的索引字段都不会生效,因此我以前写的sql语句才会由于少了最左边的user\_fruit\_id字段而走了全表扫描的查询方式。

正常来讲,假设一个联合索引设计成(a,b)这样的结构的话,那么用a and b做为条件,或者a单独做为查询条件都会走索引,这种状况下咱们就不要再为a字段单独设计索引了。

但若是查询条件里面只有b的语句,是没法使用(a,b)这个联合索引的,这时候你不得不维护另一个索引,也就是说你须要同时维护(a,b)、(b) 这两个索引。

4、找出Bug

虽然临时作了处理,但问题并不算解决,很明显是系统出现了bug才会有走这样的查询条件。

由于是我本身写的代码,因此知道是哪条SQL后我就立刻定位到了代码里的具体方法,后来才发现是由于我对user\_fruit\_id字段的判空处理不生效所致。

由于该字段是从调用方传过来的,因此我在方法参数里对该字段作了非空限制的注解,也就是javax包下的@NotNull。

public class GardenUserTaskListReq implements Serializable {  
  
    private static final long serialVersionUID = -9161295541482297498L;  
  
    @ApiModelProperty(notes = "水果id")  
    @NotNull(message = "水果id不能为空")  
    private Long userFruitId;  
    /\*\*如下省略\*/  
    .....................  
}

虽然加上该注解来作非空校验,但我却没有在参数加上另外一个注解@Validated,该注解若是没加上的话,那么调用javax包下的校验规则就都不生效,正确的写法是在controller层方法的参数前面加上注解:

图片

除此以外,由于user\_fruit\_id这个字段是另外一张表的主键,我在代码里也没有对这张表是否存在这个id作查询判断,这样一来,不管调用方传什么值过来都会直接触发sql查询,而且在不跑索引的状况下直接走全表扫描。

不得不说,这真是个低级错误,说真的,我对这个缘由真是感到嘀笑皆非,再怎么说也工做几年了,怎么还犯一些新手级别的错误呢,这脸打得真是让我至关惭愧。

图片

5、总结

虽然是低级错误,但形成的后果也算挺严重了,此次事件也让我更加的警醒,在之后的开发工做中必需要遵照该有的原则,大概有这么几点:

一、不能相信调用端

重要的参数都要先作验证,即便是非空值也须要作验证,不符合条件的就要直接返回或抛异常,不能参与业务sql的查询,不然频繁的访问也会对服务形成负担。

二、SQL语句要先作性能查询

对于数据量大的表,建好索引后,全部的SQL查询语句要用explain检测性能,而且根据结果来进一步优化索引。

三、代码必需要review

以前我没有放太大的精力在代码的review上,虽然说跟迭代排期的紧凑也有关系,但无论怎么说,bug确实是个人疏忽形成的,尤为是像空值这种细小的错误在Java里能够说屡见不鲜。

千里之堤毁于蚁穴,有时一个小bug很容易就引起整个系统的崩盘,这一次的问题也让我更加深入的认识到了review代码的重要性,无论业务开发的工做量有多麻烦,这一步操做绝对不能忽视。

图片




相关文章
相关标签/搜索