前言
众所周知,SQL文化博大精深,SQL优化那阵算得上百花齐放,啥样的优化方式都有。而我虽然天天干着CURD的工作,和SQL天天打交道,其实说句实话,对SQL优化确实所会不多,只能算的上是皮毛。
这不,什么不会就来什么,刚入职还在试用期的时候,老大派来一个任务
:“小陈,我们有个开户的流程,从其他平台迁移过来,你对接下”
:“好的,老大”
:“对了,还有一个开户的动作,特别慢,要半小时甚至更久才能完成,你顺便优化一下”
:“好的”
什么开户?什么流程?什么优化,刚接到这个问题的我也是一头雾水,后面再深入了解了一下,大概明白了这个问题的来龙去脉
背景介绍
我们是多租户系统。每签一个新客户,合同落地之后,使用我们产品之前,在我们部门这边都要为这个客户创建一个数据库,为客户使用系统保存业务数据用。
每一个客户都有一个独立的数据库。大部分客户的数据库都在一个数据库云服务上,只有那种数据量特别大的客户我们会单独为他开通一个云服务器,这个数据库云服务上只有他自己。
我在接收这个问题之前,物业这边的客户数据库开库动作都是在其他部门的平台上完成(因为物业部门都是从其他部门独立出来的,有些业务依赖原先部门的系统,这个开户的流程就是),那现在要做的事情其实有两个
原先的开户流程迁移到物业系统中
优化开户流程(缩短开户时间
那在着手操作之前,首先要熟悉下代码了,因为物业这边的系统是从原先部门迁移出来的,虽然经过了不同的迭代周期,但是核心代码应该是差不多的。
那就好办了,首先联系对接的对方工程师,然后了解业务代码路径,自己跟着链路走一遍,没问题把代码复制粘贴过来就好了,这一步还是很快的,迁移过来之后,就需要跑通一遍流程,跑流程的时候,发现果然很慢,开户要半个多小时
代码拆解
那现在就要进行开户全流程代码拆解了,看看究竟是哪一步出现的问题,整理开户流程如下:
从配置库获取空库信息
将空库下载到本地,形成SQL文件
file_get_content获取到文件内容
字符串函数preg_replace替换指定内容,大概是删除空库名、DEFINER相关信息
然后将替换过的SQL文件内容重新写入file_put_content到SQL文件中
再将处理过的SQL文件导入到新指定租户数据库中
流程大概如上,具体跑一遍流程观察一下时间...
利用跑的时间去查个bug...
查完bug过来一看还在跑,那就再等等吧(等等党>...
最终时间定格在30多分钟跑完,为什么这么慢呢?这么慢的速度,理论上来说肯定是有优化空间的。
在每个可能得节点打了日志,定位到步骤4耗时最长
分析步骤4为什么耗时这么久,当时想的是从这么几点来看
数据表有多少张?
导出的SQL文件有大
导出效率问题
preg_replace效率问题
回写到租户库的效率问题
逐一确定下:
数据表大概有2000多张
导出SQL文件50M左右
导出方式是mysqldump
preg_replace适用的是正则替换字符串,执行复杂度O(n)到O(n²)取决于正则复杂度
通过分析以上几点来看,怎么优化就清晰多了
优化点
1、导出/导入方式优化
之前的导出代码是mysqldump -P -u -p tenant > tenant.sql
,可以优化为压缩导出mysqldump -P -u -p tenant | gzip > tenant.gz
这样有什么好处呢?
磁盘I/O时间大幅减少
压缩后的gz文件是是原始文件(.sql)文件体积的10-25%大小
写入更小的文件到磁盘所需要的时间更少
因为我们的数据表很多,大概在2000多张,用压缩的效率就要比mysqldump导出sql的方式更好了
管道操作效率
gzip压缩可以和mysqldump同时进行(管道并行处理)
CPU的多核能力可以同时处理dump和压缩
同时创建新的租户库的时候也可以用这种方式提高效率。
2、替换指定字符串方式优化
这一点,其实可以在dump的时候利用linux命令sed可以同步进行,优化后的代码如下
sed -e 's/DEFINER[ ]*=[ ]*[^*]*\*/\*/' | sed -e 's/DEFINER[ ]*=[ ]*[^*]*PROCEDURE/PROCEDURE/' | sed -e 's/DEFINER[ ]*=[ ]*[^*]*FUNCTION/FUNCTION/
sed是Linux/Unix系统中的流编辑器命令,通常用于处理MySQL导出的SQL文件。具体使用用途如下:
修改SQL导出文件:对
mysqldump
导出的文件进行批量修改移出DEFINER信息:删除存储过程、视图、函数中的DEFINER=字句
替换字符集声明:修改数据库的默认字符集设置
调整SQL语法:使导出的SQL文件兼容不同版本的MySQL
通过这两步调整之后,再来跑一下,时间很快来到了3分半就执行完了租户库的创建流程。
完整导出/处理SQL伪代码如下:
mysqldump -h -P -u -p --set-gtid-purged=OFF -R empty | sed -e 's/DEFINER[ ]*=[ ]*[^*]*\*/\*/' | sed -e 's/DEFINER[ ]*=[ ]*[^*]*PROCEDURE/PROCEDURE/' | sed -e 's/DEFINER[ ]*=[ ]*[^*]*FUNCTION/FUNCTION/' | gzip > empty.gz
导入SQL伪代码:
gunzip < empty.gz | mysqldump -h -P -u -p new_tenent
现在来看?
是否可以更快?
答案是肯定的,可以进行库对库复制,且可以适用perl替换sed来处理正则操作
mysqldump --single-transaction -h source_host -u user -p empty_db \
| perl -pe 's/DEFINER\s*=\s*\S+//g; s/\/\*!50013 DEFINER=\S+\*\///g' \
| mysql -h target_host -u user -p new_tenant_db
如何更安全、更稳定?
事务保障机制
try {
$db->beginTransaction();
// 1. 创建新库
$db->exec("CREATE DATABASE new_tenant CHARACTER SET utf8mb4");
// 2. 导入结构
exec("gunzip -c empty_db.gz | mysql -u user -p new_tenant");
// 3. 初始化租户数据
$db->exec("INSERT INTO new_tenant.tenant_info (...) VALUES (...)");
$db->commit();
} catch (Exception $e) {
$db->rollBack();
// 清理失败残留
$db->exec("DROP DATABASE IF EXISTS new_tenant");
throw $e;
}
增量同步方案
试想如果在导出到本地之后,原始空库发生了变化,在本地SQL文件导入创建新的租户库的时候,那原始空库的信息是不会同步到新的租户库的,所以需要增加一个同步方案来解决这种问题。解决方案如下:
为原始空库创建一个版本表
记录对原始空库的任何一个变更
CREATE TABLE schema_version (
id INT AUTO_INCREMENT PRIMARY KEY,
version VARCHAR(50) NOT NULL,
sql_file VARCHAR(255) NOT NULL,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
每次修改原始库时记录变更
INSERT INTO schema_version (version, sql_file)
VALUES ('1.0.1', 'alter_table_add_column_xxx.sql');
导出前获取最新原始空库版本号
$currentVersion = $db->query("SELECT MAX(version) FROM `empty_db`.schema_version")->fetchColumn();
导出后导入前获取空库版本表最新的执行数据
1. 获取最新的版本数据
$newVersions = $db->query(
"SELECT version, sql_file FROM `$baseDb`.schema_version
WHERE version > '$baseVersion'
ORDER BY installed_on ASC"
)->fetchAll(PDO::FETCH_ASSOC);
// 2. 在新的租户库按版本顺序执行更新
foreach ($newVersions as $update) {
try {
$db->beginTransaction();
$db->exec($update['sql_file']);
// 记录版本变更
$db->exec("INSERT INTO `new_tenent`.schema_version
(version, sql_file)", [
':version' => $update['version'],
':sql_file' => $update['sql_file'])
]);
$db->commit();
} catch (Exception $e) {
$db->rollBack();
throw new Exception("应用更新 {$update['version']} 失败: " . $e->getMessage());
}
}
以上应该就能解决增量同步问题了。
总结
优化不可能只简单的分为某个部分,既要考虑效率、又要考虑安全,世界上没有完美的方案,只有最适合业务场景的解决方案。
与君共勉~