活动中台系统慢 SQL 治理实践

作者:vivo 互联网服务器团队- Zhang Mengtao

活动中台系统作为中台项目非常注重系统性能和用户体验,数据库系统性能问题会对应用程序的性能和用户体验产生负面影响。慢查询可能导致应用程序响应变慢、请求堆积、系统负载增加等问题,甚至引发系统崩溃或不可用的情况,因此,需要在数据库系统中针对执行缓慢的SQL查询进行优化和改进。本文主要介绍活动中台系统针对慢SQL问题的实践治理案例。

一、慢 SQL 的含义

1.1 慢 SQL 的含义

慢SQL是指执行时间较长的SQL查询或操作。真实的慢 SQL 通常会伴随着大量的行扫描、临时文件排序或者频繁的磁盘 flush ,直接影响就是磁盘 IO 升高,让正常的 SQL 变成了慢 SQL ,大面积执行超时。

大家不要被慢查询这个名字误导,以为慢查询日志只会记录 select 语句,其实也会记录执行时间超过了long_query_time设定的阈值的 insert、update 等 DML 语句。

1.2 慢 SQL 的危害

从业务的角度来看:慢 SQL 会导致产品用户体验差,会减低用户对产品的好感度。

从数据库的角度来看:慢 SQL 会影响数据库的性能,每个 SQL 执行都需要消耗一定的 I/O 资源。假设总资源是100,有一条慢 SQL 占用了30的资源共计1分钟。那么在这1分钟时间内,其他 SQL 能够分配的资源总量就是70,如此循环,当资源分配完的时候,所有新的 SQL 执行将会排队等待。

二、慢SQL是怎么产生的?

【缺乏索引】:如果在查询中涉及到的列没有适当的索引,数据库系统可能需要执行全表扫描来找到匹配的行,从而导致查询变慢。

【查询条件不当】:查询条件过于复杂、使用了不必要的 JOIN 操作、存在子查询等,都可能导致查询性能下降。

【数据量过大】:当数据表中的数据量非常庞大时,即使有索引,查询也可能变得缓慢。

【锁等待】:如果查询需要访问被其他事务锁定的资源,就会导致查询阻塞,执行时间变长。

【硬件资源不足】:数据库服务器的硬件资源(如 CPU、内存、磁盘)不足以支撑查询的执行,也会导致查询变慢。

【不合适的数据库设计】:数据库表的设计不合理,如过度范式化、冗余数据等,会导致查询性能下降。

【统计信息不准确】:数据库的统计信息不准确会导致查询优化器做出错误的执行计划,影响查询性能。

三、慢 SQL 治理实践

3.1 问题分析和解决方案

通过监控平台发现,项目的慢 SQL 数量较多,每天可能会产生几千甚至上万的慢 SQL,这对系统性能和用户体验都有不小的影响,因此,优化慢 SQL 问题值得被重视。我们从以下几个方面进行排查和分析:

1.数据量

我们都知道,同样的 SQL 语句,对于不同数据量的库表,查询效率也不一样,当数据量达到千万级甚至上亿,普通的查询语句执行时间可能也会超过一秒,进而出现慢 SQL 问题。

经过排查,发现不少库表的数据量已经达到千万,个别分表的数据量已经达到一亿多,几年前的历史数据仍然保留,需要进行人工清理。

针对数据量对 SQL 执行带来的影响,我们可以从三个方面解决:

(1)清理数据

最直接的方式就是将无效数据清理,很多几年前的数据,几乎没有存储的价值,可以直接提交删除语句进行清理掉,当然,我们在删除时要注意线上影响,避免一次性删除太多数据,容易造成线上数据库效率受到影响,对于单个分表可以采用分批删除,每一批只删除一个时间段;对于多个分表可以按照分表去删除,尽量减小对线上环境的影响。

以活动中台系统的答题活动为例,随着答题活动几年的运行,数据库已经有大量的用户数据,目前线上数据量很多到了千万级,个别到了上亿的数据量,这对线上访问影响很大。

所以我们进行手动清理数据,目前线上一共10张分表,考虑到对线上用户业务的影响,我们通过两方面减小影响

一是分五次进行清理,每次只清理2~3张分表;

二是将删除语句转化为根据主键删除,大大提升 SQL 语句执行的效率。

最终清理掉大量的活动数据,我们采用的删除策略是将一年以前的历史数据进行删除,这部分数据已经不会用到,这个时间范围清理掉了大量的无效数据。

(2)分库分表

手动清理数据虽然能解决问题,但是治标不治本,而且对于一些特殊的活动,可能留下的数据量仍然很大,这个时候如果库表的数据量仍然较大,且不能进行删除,就要考虑分库分表是否足够,可能之前出于业务考虑没有进行分表或者分表数量较少,这个时候就要考虑进行更多的分表,以提升数据库访问的效率。这里如果可以的话最好设置分表策略,建立配置项,灵活扩充分表,避免直接发版。

活动中台系统会有一张单独的路由表,记录分表路由,便于快速查询分表。

同时可以把路由相关配置直接建立在配置中心,无须发版就可以快速扩分表。可以根据数据量大小创建10、20、50、100张分表。

(3)大数据量查询移步ES数据库

当然,对于一些响应要求比较高的业务需求,MySQL 数据库的性能可能无法达到要求,这个时候可以考虑将数据存储在 ElasticSearch 数据库或者缓存当中。

2.SQL 语句

数据库的数据量是重要的影响因素,但是 SQL 语句本身更会影响执行的效率,规范的 SQL 语句是避免慢 SQL 的前提。根据下图可以看出,SQL 的执行顺序为:

  • 首先执行 from、join 来确定表之间的连接关系,得到初步的数据。

  • 然后利用 where 关键字后面的条件对符合条件的语句进行筛选。

  • from & join&where:用于确定要查询的表的范围,涉及到哪些表。

那么根据这个执行顺序,我们来看下 SQL 执行过程中常见的问题有哪些?

(1)查询字段

我们在项目中查询最常用的是返回整个 DO 层对象,但其实很多时候我们只需要其中几个字段甚至一个字段,这个时候查询整个对象是很不划算的,比如下面这个例子,该语句在数据库管理器中的执行结果如下,执行时间为9269ms。

csharp 复制代码
select * from a where id = 0;

然而我们只是想查询指定活动下的单个字段,这个时候可以不需要返回其他字段,只返回需要的字段,优化后示例如下,执行时间为4104ms,明显执行时间变短了。

sql 复制代码
select result from a where id = 0;
(2)索引问题

索引在数据库查询中是一个很重要的影响因素,走不到索引和走到索引是有很大的区别的。但是索引有弊也有利:

优点

  • 提高查询语句的执行效率,减少 IO 操作的次数

  • 创建唯一性索引,可以保证数据库表中每一行数据的唯一性

  • 加了索引的列会进行排序,在使用分组和排序子句进行查询时,可以显著减少查询中分组和排序的时间

缺点

  • 索引需要占物理空间

  • 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加

  • 当对表中的数据进行增删改查时,索引也要动态的维护,这样就降低了数据的更新效率

  • 所以合理的设置索引并利用索引是高效执行SQL的重要因素。

以下面这个案例来分析:

sql 复制代码
CREATE TABLE `table_test` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`test` varchar(128) NOT NULL DEFAULT '' COMMENT '索引测试字段',
............
`result` varchar(128) NOT NULL DEFAULT '' COMMENT '结果测试字段',
PRIMARY KEY (`id`),
KEY `test` (`test`),
) COMMENT='记录表';

这是活动平台系统纪录的数据表,其中【result】是唯一的,这里可以看到索引没有涉及【result】,如果我们根据这个字段去查询唯一记录,看下执行结果如何:

耗时4286ms,这里根据建表语句可以看到有索引【KEY test (test)】,如果我们根据索引字段来查询该条记录的话,看下执行结果如何:

可以看到只需要16ms,索引对数据库效率的提升至关重要。我们可以根据 explain 关键字来解析一下 SQL 语句,比如刚才的案例,我们来看下相应的结果:

可以看到,直接根据非索引字段【result】查询,type 字段值为【ALL】,代表遍历全表,扫描行数为3144273,如果带上索引字段【test】,type 字段值为【ref】,代表匹配到单行记录值,扫描行数为1,显然通过索引查询效率得到了极大的提升。对于索引的使用会出现以下几种常见类型

ALL:全表扫描,表示 MySQL 将遍历整个表以满足查询条件。这通常是效率最低的访问类型,应尽量避免。

index:索引全扫描,表示 MySQL 将遍历整个索引以满足查询条件,而不是遍历整个表。虽然比全表扫描效率要高,但仍然需要遍历索引的每一行。

range:范围扫描,表示 MySQL 使用了索引的一部分来满足查询条件,例如使用了索引的某个范围。这通常发生在有范围查询条件时,例如使用了 WHERE 子句中的 BETWEEN、>、< 等操作符。

index_merge:索引合并,表示 MySQL 使用了多个索引来满足查询条件,然后将结果合并。这通常发生在查询中有多个条件,每个条件可以使用不同的索引来访问数据。

unique_subquery:唯一子查询,表示 MySQL 使用了子查询来获取唯一的结果,并且子查询使用了唯一索引。

const:常量,表示 MySQL 使用了常量表来获取结果,这通常发生在查询条件中包含了常量值。

ref:引用,表示 MySQL 使用了非唯一索引来扫描表,通常发生在查询中使用了单个索引列作为条件。

(3)联表查询

JOIN】关键词在项目的日常查询中可能会遇到,对于一些管理台项目可能要求较低,可以作为日常开发查询语句,但是对于面向用户的项目,由于数据量较大,往往需要避免联合查询的使用。以下面这个案例来分析:

css 复制代码
select * from a left join b on a.id = b.id where a.id = 0;

这个SQL是联合多张表进行联合查询,还有其他查询条件,执行得出结果时间为1432ms,超过1秒则为慢 SQL,这里联合两张表,如果相关的数据量较大,则执行速度会较慢。我们可以将 SQL 拆分为两个语句执行,拆为以下两个 SQL 分步执行:

csharp 复制代码
select * from a where id = 0;
select * from b where id = 0;

分步获取数据库结果后再进行聚合,分步执行结果分别为728ms和744ms,联合查询拆分为简单查询可以有效减少慢 SQL,同时可以提高SQL查询的复用性。

(4)条件查询

在项目的日常开发中,SQL 语句切忌使用复杂查询,这会对数据库造成较大的压力。下面这个例子就是使用了比较复杂的条件查询:

sql 复制代码
select * from a where id in (select id from b) and time > '2024-03-29';

使用查询条件的同时还嵌套了查询语句,除此之外还夹杂了联合查询,语句已经比较复杂,我们来看下查询结果:

可以看到执行时间非常缓慢,达到12648ms,这个SQL的背景是用于业务对账,虽然使用的是离线数据库,但是仍然不能忽视它的风险,每天大量的执行这种复杂 SQL,还是对业务有一定的风险影响。考虑到业务价值单一,复用性不高,我们可以直接冗余一份数据到单独的表里,避免复杂查询,直接一步到位解决对账带来的慢 SQL 问题。

3. 整体策略

清理完数据库的无效数据,优化完 SQL 语句本身,可能还是会出现慢 SQL 问题,这个时候我们要考虑下,是否可以优化整体的数据库交互策略,以活动中台系统的数据清理慢 SQL 为例。

活动中台系统创建了一个定时任务,每天凌晨执行一次,对数据库的无效历史数据进行统一清理,具体删除哪些库表、什么时间段、什么条件都由配置项灵活控制,配置项示例如下:

sql 复制代码
"分表数量":7,
"表名":table,
"条件":condition

包括分表的数量、要删除数据的表名、查询的条件信息,查询条件对应SQL语句中的【where】信息。执行流程如下图:

整体上看删除策略很通用,条件配置灵活,可以同时应对不同分表、不同查询条件等。但是线上运行发现会产生大量的慢SQL,可能每天就会产生几千条。

主要原因如下:

  • 会出现联表查询的情况,数据量较大的表会出现执行时间超过一秒的 SQL 语句;

  • 删除策略中时间是重要的因素,但是时间往往不会设置为索引字段,所以很难充分利用索引;

  • 删除策略会扫描所有分表,但是很多分表可能全部扫描也没有需要删除的数据,会出现无效执行的情况。

针对以上策略,我们进行了综合改进,主要措施有以下几条:

  • 避免联表查询,尽可能的拆分为简单 SQL 执行;

  • 不要从时间维度直接出发,而是从活动维度出发,如果一个活动结束时间较长,那么这个活动相关的数据自然不再需要,可以直接删除;

  • 利用分表路由信息,从活动本身出发快速路由到相关分表,减少 SQL 语句的无效执行。

修改完的配置项如下:

sql 复制代码
"分表数量":7,
"表名":table,
"条件":condition,
"删除数量":1,
"删除策略":1,
"开始时间":"",
"结束时间":""

包括分表的数量、要删除数据的表名、查询条件、每次删除的数量、开始时间和结束时间,在这个时间范围内的活动都符合条件,整体上看删除策略更为通用,条件配置更加灵活。优化的删除策略流程图如下:

从图中可以看到,我们保留原有的删除策略,避免个别数据表没有活动信息和分表信息,只能单纯根据时间扫描删除;除此之外,对于有活动信息的数据,我们会先查询活动,根据活动去删除,如果数据表是根据活动进行分表,则直接查询路由信息,并删除指定分表的数据即可,也就是图中的第三种策略,线上大部分的数据都会按照第三种策略执行,如果有活动信息但是未按照活动进行分表,可能按照用户等维度进行分表,则可以根据活动信息进行删除,也可以有效避免慢 SQL 的出现。

3.2 治理效果

经过对慢 SQL 的专项治理,活动中台系统的慢 SQL 数量由几千个,稳定在了两位数,有效减少了慢SQL的数量,进一步提升了系统稳定性。

四、经验总结

  • 治理慢 SQL 的根本是从源头避免慢 SQL,项目组内部必须达成高度一致,根据编码规范进行前置避免慢 SQL 的出现。

  • 离线数据库不能成为忽视慢 SQL 问题的原因,仍然会有影响线上业务的风险,数据库实例如果是混合部署的方式,可能离线库所在的机器有其他业务的主库,而且如果从库延迟严重会影响主从故障切换。

  • 数据库的合理设计不能依赖后期重构,一开始就要尽可能的考虑充分。

  • 慢 SQL 治理过程中出现的问题可以及时复盘,避免团队其他成员继续踩坑。

相关推荐
跟着珅聪学java41 分钟前
spring boot +Elment UI 上传文件教程
java·spring boot·后端·ui·elementui·vue
我命由我123451 小时前
Spring Boot 自定义日志打印(日志级别、logback-spring.xml 文件、自定义日志打印解读)
java·开发语言·jvm·spring boot·spring·java-ee·logback
lilye661 小时前
程序化广告行业(55/89):DMP与DSP对接及数据统计原理剖析
java·服务器·前端
徐小黑ACG2 小时前
GO语言 使用protobuf
开发语言·后端·golang·protobuf
·薯条大王4 小时前
MySQL联合查询
数据库·mysql
战族狼魂4 小时前
CSGO 皮肤交易平台后端 (Spring Boot) 代码结构与示例
java·spring boot·后端
xyliiiiiL5 小时前
ZGC初步了解
java·jvm·算法
杉之6 小时前
常见前端GET请求以及对应的Spring后端接收接口写法
java·前端·后端·spring·vue
morris1316 小时前
【redis】redis实现分布式锁
数据库·redis·缓存·分布式锁
hycccccch6 小时前
Canal+RabbitMQ实现MySQL数据增量同步
java·数据库·后端·rabbitmq