前言:最近要做一次数据访问组件的分享,想着趁此机会结合这几年的工作经历,好好梳理一下数据库相关的开发规范,之前我也写过很多这方面的文章了,且数据库相关的知识也没什么新意可言,但我之所以还是决定提笔再写一篇主要出于几个方面考虑:
一是好多人只对前沿以及看似高深的技术感兴趣,总是高谈阔论,反而轻视、也忽视了CURD的重要性,我想提醒他们,这种思想会为你带来灾难,这并不是危言耸听,而是真实存在的,尤其是做财务、金融相关的,我这么多年就是一直从事这个行业,我亲眼见过太多血淋淋的惨痛案例了。实际上,我们开发的几乎所有业务系统,无论逻辑有多么复杂和高大上,最终还是以数据的落地为准,而基本的数据库操作就是最后一环,Last but not the Least。
二是我深知费曼技巧的重要性,要持续输出,持续用最浅显、最直白的语言将自己所掌握的知识传递出去。理论也好,实践经验也罢,都是可共享、可学习的。
三是本文并不是想把知识点单纯写下来,知识点网上一搜一大堆,没有什么意义,本文的目标是能够引起开发者们足够的重视,能够一定程度提高读者们数据库操作的风险意识。就算是只唤醒一个读者,也算没白写。
备注:我下面所提到的建议,一定是我亲眼见过的真实案例。为了保密性,我不会具体说什么业务、导致了什么结果、当事人受到什么处罚,只单纯从技术角度去阐述。
1、注意查询条件的边界问题
实际应用中,sql的查询参数都是由前端或客户端传过来的,一些经验不足的开发者会想当然认为前端传过来的数据一定是OK的,可极端情况下所有查询参数可能都为空,因为客户端的传参很多情况下都依赖于客户的输入,如果前端本身没做好校验,就会出现这种情况,全空的条件最终导致数据库操作进行了全表扫描。
这个看似很小的边界问题是真实发生的,更糟糕的是,还是在分库分表的场景下进行的,最终引起的后果可想而知。
记得我刚去小米时,我们领导和我们说了一句话:”永远不要相信任何和你对接的上下游系统,你要假想他们一定会犯错,只要这样你才能避免你自己犯错“。这句话是非常受用的,对所有上下游系统保持怀疑态度,开发时要考虑到他们出现异常的所有可能性并做好预案。而sql语句的传参就是一种典型问题。如果所有条件都为空,可以直接向客户端抛出异常。
2、新增字段,且是必要查询条件,记得增加索引
这是实际发生的一起事故,有个哥们在数据表上新增了一个字段,忘了加索引,但还会经常使用该列作为查询条件,上线后就出问题了。没有索引,又是一个典型的全表扫描,并发一旦上来就死翘翘了。
索引就是一把利剑,通过索引可以减少磁盘IO次数,大幅度提高查询效率。用好他,他会给你惊喜。
3、复合条件注意优先级
JAVA开发者经常用mybatis或者mybatis-plus,或者其他语言也都有成熟的ORM框架,我们通常会直接使用框架提供的API拼接sql, 当我们写一个简单sql时可能不会出错,但如果我们在写复合查询时,稍有不慎,就会出错。像这种既存在and,又有or的复合查询,API用不明白的话非常容易出错。如:
where a="haibo" and (b is null or b == "" or b =="0"),
这是预期要写的where语句,可是实际上拼接的sql语句中却没有了括号,即:
where a="haibo" and b is null or b == "" or b =="0"
看到这里,大家都知道问题出在哪里了吧,缺少了括号,这可是差了十万八千里。这个算是我见过的,业务影响最恶劣的一次错误了,金额巨大。
所以,如果开发中一定要用API拼接sql的话,那么就尽量把sql 打印出来,至少在开发、自测以及功能测试阶段,sql语句尽量 debug输出一下,这样你也能检测到自己的sql是不是符合自己的预期。
4、分库分表场景尽量不要做聚合类查询,最好保证必须有分片键
在分库分表场景下直接做聚合查询的风险性是极高的,因为它会把每个子库的数据都查出来,然后进行聚合,这种情况下,对性能、对内存数据存储都是考验。我在第一点中提到了where条件都是空的问题,这个问题也导致sql把每个子库都做了一次全表扫描,且会查询出所有的数据,这引起的后果很严重,一是线程hang在那儿了,如果此时请求量较大,系统肯定崩了;二是过多的数据加载到内存,OOM就出现了,此时如果多次的GC之后内存占用依然很大的话,很有可能被kill掉的,因为Linux操作系统本身会检测内存占用情况,如果发现某个进程内存占用过多,他会根据某种策略kill掉相应进程。
在高并发场景下,慢sql影响的不仅仅是数据库本身,也影响应用,影响整个系统。尤其是分库分表场景下。
对于在线交易,尽量不要做聚合查询,一定要带上分片键。如果想做聚合查询,请采用其他的手段。可以将所有数据归档到一个聚合库,聚合库可以是关系型数据库,可以是列式存储数据库如Hbase,也可以采用目前流行的分布式数据库TiDB等等。这也是当前几乎所有大公司采用的方案。我们之前是以用户id为分片键,同一个用户的所有数据肯定都在一个子库上,ToC的所有操作都是带分片键userId的。
select oid from shop_order where uid=35355
对于像结算系统、数据组这种有聚合类的查询需求时,我们都是把子库数据以增量的方式同步到一个聚合库上的。我们当时用的是Mysql,借助canal和canal-adapter完成了数据聚合。
5、索引失效-避免隐式转换
这是我之前遇到的一个问题,有一天我发现结算系统有个界面变慢了,慢到已经超过了Nginx最大响应时间了,最终定位到了原因是从我们公司的支付网关查询到的流水号是整数,但是我们自己数据库的支付表中流水号的字段是varchar,当我发起查询的时候,直接使用了网关返回的整数,而这导致了索引失效,查询变得异常慢。
具体看下面的示意图,图一没有使用上索引,而是全表扫描;图二使用了索引。
之所以会产生这样的原因是:where的值是整数,但该字段是字符串类型的,它会将数据表里的所有该字段的值都转换为整数,然后再取匹配where条件里面的值,这就导致了全表扫描。
不过这里要强调一下,像Mysql,当遇到类型不一致时,它会把字符串转成数字。也就是说,假如我们的字段是int类型,查询参数是字符串,Mysql会自动把字符串转成int,然后再去查询,此时索引是正常可用的。
6、索引失效-遵循最左匹配原则
在使用联合索引时,往往会因为违背了最左匹配原则导致索引失效。如联合索引 (a,b,c),下面的查询都是无效的:
select a from table where b = "dd" ;select a from table where c= "c";select a from table where b = "b" and c= "c";
这是因为联合索引的原理决定,b是局部有序,即只有确定了a,b在已确定的a的范围内才是有序的。而如果没有a,只拿b查询,全局b是无序的;同理C也一样。
7、其他索引问题
这里就简单列出来,包括索引使用函数、索引字段进行表达式计算、like进行模糊匹配,这些都会导致索引失效。
建议对我们写的sql,可以通过explain执行计划看看是否真的用上了索引。
关于索引,我会再写一篇文章,主要介绍Mysql的各种索引,包括B+tree,哈希索引,全文索引,介绍索引下推、MRR、索引原理等。 文章写的差不多了,只不过我还需要再完善一下之后发布出来:
8、不要一次性查出过多的数据
实际生产发生的是内部的热点账户查询引起的该类问题。账户存在热点账户和非热点账户的,尤其是对公账户。如果我们只拿一个账户id去查,非热点账户把所有账户返回都是OK的,可对于热点账户,只通过一个账户id条件去查,会把所有的数据查询过来,数量级可达到千万级,最终导致OOM。
9、避免大事务
前一阵子开会,有个项目是因为大事务的操作,且同一操作即有批量操作,又有删除语句,这导致在并发的情况下出现了死锁,线程长时间挂住,资源被耗尽。负责人狡辩说他们这不算大事务,每次都只操作一两百行,当场我就把他怼了回去,我和他解释了什么是大事务,告诉他大事务的判断不仅仅是单纯以条数为基准,一说一个不吱声。
我再说一个例子,之前在小米的时候,有一次我们的系统上线之后,大家都轻松了很多,突然负责其中某个微服务的小伙伴收到了DBA的加急消息,他没回,DBA直接联系到了我们领导,领导火急火燎过来说,赶紧看看,DBA说咱们的sql有大事务操作,且大量的同级别sql请求打过去,数据库快受不了了。哈哈,我这人对类似的这种事情记的非常清楚。
10、谨慎使用 group by
分组聚合以及排序如果使用不当,会出现严重的性能问题。
2019年米粉节,有个哥们写了一个根据用户uid进行的分组查询,大促时,N多个同样的查询打到数据库上,当时业务规模小,所有服务还都使用一套数据库,这导致他自己写的sql有问题导致数据库性能下降,间接影响到了其他服务,用户完全下不了单,好在商品服务都有缓存还能看,要不然整个系统都蹦了。他的那封给全员发的检讨邮件我印象深刻!!!
11、严格把关 drop、truncate、delete操作
drop、truncate操作好说,DBA本身就不可能让业务直接操作,CREATE 、ALTER等等DDL、DML语句,业务都是不能直接操作的,都必须要提交工单执行。
然而delete就必须要谨慎了,delete操作是我最忌讳做的事情,所以基本上,我之前写的业务都是能逻辑删除的就逻辑删除,而且在线业务也很少需要我们做删除。删除操作可能常应用于数据归档或者离线的一些任务。
开发过程中,如果有delete操作,一定要格外注意,仔细检查where条件是否正常,千万不要出现 delete all data的问题。如果出现,那就赶紧跑路吧,或者赶紧给DBA磕个头,让他帮你做数据恢复。
12、谨慎使用join联查
如果你看过阿里出版的数据库开发规范,一定会看到里面有一条说尽量不做多表联查,如果用,最好不要超过三张表。join联查底层是需要选择一个驱动表进行的,如果驱动表选择不当,或者是没有索引以及索引失效导致出现笛卡尔积,那性能简直是灾难级别的。
13、注意在线DDL操作
有一天晚上,群里炸了,说用户领不了券了,领导问发生了什么。负责的同学说,我们在做数据表修改操作,由于数据比较多,导致在该阶段进行领券的用户无法领券,客户端提示系统异常(领券意味着数据库会进行Insert操作)。当时DBA的限制比较松,只要领导审批通过,DDL、DML等SQL语句都可以执行,所以出了这么大的事情。这件事情之后,DBA才优化,审批时首先要评估影响的行数,如果影响太大,DBA需要亲自手动操作的。这就涉及到了数据库的Online-DDL操作。感兴趣的可以看我写的文章:Mysql在线DDL操作 。
14、注意order by的性能问题
去年我们去现场帮着一个业务排查系统问题,系统现象是他们在高并发场景下, 接口耗时特别大, 并发量怎么也上不去。后来排查发现他们写的大SQL的order by有问题,order by字段没有索引,当没有索引时会进行filesort,这个过程可能会非常慢。
15、其他开发规范
其他规范暂时有的还没遇到相应的实际问题,就不写了,但其同样重要,比如分页注意性能优化、不要查出太多的列、尽量采用覆盖索引,避免回表等等,感兴趣的可以看我之前写的文章,比较全:Mysql数据开发规范 。
防范措施
1、项目经理、组长、开发人员本身要具备风险意识,自己要对SQL有足够的重视,这是首要条件。如果做到了,大多数问题都不会出现了;
2、公司可以开发一套sql过滤器或者访问组件,对于每个sql在执行之前,Filter或者组件会首先会根据预置的规则对sql进行拦截检测,可根据不同的风险级别,对可能有问题的sql进行实时的告警或者拦截。嗯,这也是我开发数据访问组件的初衷;
3、 可以开发一套sql检测脚本,可让开发或者测试人员主动去检测源码里的sql,就像checkstyle检测我们的代码规范一样。另外,现在我们基本都在用devops来编译和部署应用,在构建流水线任务时,也可以将检测任务加到流水线里;
4、上周我们大部门开了一个研讨会,领导派我参加,有一个其他处室领导说了一个办法,虽然比较low,但简单粗暴最有效,哈哈。他们是把所有sql语句都复制出来,在数据库CLI上手动执行一遍,看一下执行计划,他们主要是为了验证性能;
5、要有一套数据库监控平台,主要是DBA负责。监控平台可对所有sql进行统计,主要监控和统计慢sql。当然,这是一种后置手段,并不是预防手段。
总结
最后还要特别强调,希望大佬们引起足够的重视,CURD不仅不丢人,反而是最重要的,对此不屑的代价是惨痛的,尤其是和钱相关的,一个小失误导致少则几十万,多则几个亿,十几亿的金额影响都是我亲眼见过的。我做了很多年财务结算,我从来没有出现过错误,不是因为自己有多牛,完全是因为我发自内心的高度重视,无他。