靠维护老项目度中年危机
最近靠维护老项目度过中年危机的话题挺火,刚好最近也在维护一个PHP开发的CRM的老项目,项目由于数据量比较大, 导致查询速度很慢, 经常出现超时的情况, 下面记录一下具体的优化过程。
优化老项目,老生常淡的几点:
1. 数据库优化
2. 代码结构优化
3. 缓存优化
4. 资源优化
...
数据库优化
众所周知, MySQL 优化第一步,就是建索引, 看了一下整个系统的表, 发现有大量的表都没有索引, 建了索引的表,索引名称有点花里胡哨, 如下:
contractId `contacts_id` NORMAL BTREE 27599 A 0
customer_id `customer_id` NORMAL BTREE 27599 A 0
--
index_group `role_id`, `callDate` NORMAL BTREE 4359069 A 0
business_id `business_id` NORMAL BTREE 518 A 0
status_id `status_id` NORMAL BTREE 43 A 0
于是,优化第一步,规范一下索引的命名,MySQL索引的命名虽然没有硬性的规范,但是修改一下自己看着舒服, 个人理解:
普通索引:idx_字段1_字段2
唯一索引:uk_字段1_字段2
主键索引:pk_字段1_字段2
于是 上面的索引改成了:
idx_contacts_id `contacts_id` NORMAL BTREE 27599 A 0
idx_customer_id `customer_id` NORMAL BTREE 27599 A 0
--
idx_role_id_callDate `role_id`, `callDate` NORMAL BTREE 4359069 A 0
idx_business_id `business_id` NORMAL BTREE 518 A 0
idx_status_id `status_id` NORMAL BTREE 43 A 0
一下看起来舒服多了, 于是, 优化第二步, 就是给没有索引的表加上索引, 这个工作量比较大, 先把几个 常用功能模块的 表给加上索引, 于是 吭哧吭哧的 分析了 2天的 慢日志, 给需要加索引的表加上索引,本以为 加完索引后, 查询速度会快很多,结果发现, 并没有什么卵用. 一个页面 虽然快了点, 但是 不是太明显.
本着能加 配置 绝不改代码的原则,先去问了一下运维 Mysql 运行的机器内存是多大 64G. 这么大,那好办,先分析一下 数据库中的表引擎. 上了一段代码:
/** * Author: PFinal南丞 * Date: 2023/12/28 * Email: */@163.com>/** 确保这个函数只能运行在 shell 中 **/if (!str_starts_with(php_sapi_name(), "cli")) { die("此脚本只能在cli模式下运行.\n");}/** 关闭最大执行时间限制 */set_time_limit(0);error_reporting(E_ALL);ini_set('display_errors', 1);const MAX_SLEEP_TIME = 10;$hostname = '';$username = '';$password = '';$connection = mysqli_connect($hostname, $username, $password);if (!$connection) { die('Could not connect: ' . mysqli_error($connection));}$query = "SELECT table_name,engine FROM informati0n—schema.tables WHERE table_schema = 'smm';";$result = mysqli_query($connection, $query);if (!$result) { die("Query failed: " . mysqli_error($connection));}$InnoDB_num = 0;$MyISAM_num = 0;while ($process = mysqli_fetch_assoc($result)) { echo $process['table_name'] . " " . $process['engine'] . PHP_EOL; if ($process['engine'] == 'InnoDB') { $InnoDB_num++; } if ($process['engine'] == 'MyISAM') { $MyISAM_num++; }}echo "InnoDB " . $InnoDB_num . " MyISAM " . $MyISAM_num . PHP_EOL;mysqli_close($connection);
得出结果:
表引擎 MyISAM 的表 176 张 InnoDB的表引擎 88张. 要了一份 线上MySql 的配置发现:
...
key_buffer_size = 512M
innodb_buffer_pool_size = 2048M
...
都知道 innodb_buffer_pool_size 针对的 是 InnoDB的表引擎,key_buffer_size 针对的 是 MyISAM的表引擎. 这配置不得修改一下. 果断打申请, 申请修改线上配置.
...
key_buffer_size = 2048M
innodb_buffer_pool_size = 2048M
...
重启服务后,果然比原来快了好多.能撑到 同事不在群里 打报告了.
艰巨的长征路迈出了第一步,接下来,本着 死道友不死贫道的原则, 厚着脸皮,让运维帮忙整了一台mysql 的机器,来做了个主从分离。 速度一下,不影响业务的正常使用了.
接着 开启漫长的 优化之路.
缓存优化
- 项目没有开启数据缓存, 只有 代码编译的缓存
所以这一块是一个大的工程, 所以先不动, 只是 给 几个 常用的功能加了一个 数据 的 缓存。后续的思路是:
a. 加一个 redis, 使用 把代码中的统计数据 缓存到 redis 中
b. 把客户信息,客户的关联信息,组合到一起, 然后缓存到 redis中.
....
代码结构优化
开始挖开代码, 看看 查询慢的 功能 代码是咋写的,不看不知道,一看直接上头:
- 几乎全是 foreach 中 的 SQL 查询:
foreach($customer_list as $key=>$value){ # ...... $customer_list[$key]['customer_name'] = $this->customer_model->get_customer_name($value['customer_id']); $customer_list[$key]['customer_phone'] = $this->customer_model->get_customer_phone($value['customer_id']); $customer_list[$key]['customer_address'] = $this->customer_model->get_customer_address($value['customer_id']); # ...... }
- 由于 ORM 的方便复用, 大量的 表关联模型 复用,导致查询的 废字段特别多.比如:
<?php class CustomerViewModel extends ViewModel { protected $viewFields; public function _initialize(){ $main_must_field = array('customer_id','owner_role_id','is_locked','creator_role_id','contacts_id','delete_role_id','create_time','delete_time','update_time','last_relation_time','get_time','is_deleted','business_license'); $main_list = array_unique(array_merge(M('Fields')->where(array('model'=>'customer','is_main'=>1,'warehouse_id'=>0))->getField('field', true),$main_must_field)); $data_list = M('Fields')->where(array('model'=>'customer','is_main'=>0,'warehouse_id'=>0))->getField('field', true); $data_list['_on'] = 'customer.customer_id = customer_data.customer_id'; $data_list['_type'] = "LEFT"; //置顶逻辑 $data_top = array('set_top','top_time'); $data_top['_on'] = "customer.customer_id = top.module_id and top.module = 'customer' and top.create_role_id = ".session('role_id'); $data_top['_type'] = "LEFT"; //首要联系人(姓名、电话) $data_contacts = array('name'=>'contacts_name', 'telephone'=>'contacts_telephone'); $data_contacts['_on'] = "customer.contacts_id = contacts.contacts_id"; // 检查是否存在部门库字段 $warehouse_id = I('warehouse_id', '', 'intval'); if ($warehouse_id) { $warehouse_id = D('Fields')->isExistsWarehouseTable(1, $warehouse_id); if ($warehouse_id) { $customer_warehouse_data_table = customer_warehouse_table($warehouse_id); $warehouse_data_list = M('Fields')->where(array('model'=>'customer','is_main'=>0,'warehouse_id'=>$warehouse_id))->getField('field', true); $warehouse_data_list['_on'] = 'customer.customer_id = ' . $customer_warehouse_data_table .'.customer_id'; $warehouse_data_list['_type'] = "LEFT"; $this->viewFields = array('customer'=>$main_list,'customer_data'=>$data_list,$customer_warehouse_data_table=>$warehouse_data_list,'top'=>$data_top,'contacts'=>$data_contacts); } else { $this->viewFields = array('customer'=>$main_list,'customer_data'=>$data_list,'top'=>$data_top,'contacts'=>$data_contacts); } } else { $this->viewFields = array('customer'=>$main_list,'customer_data'=>$data_list,'top'=>$data_top,'contacts'=>$data_contacts); } } ?>
- 代码中的业务逻辑一直再叠加,导致废代码量特别的大需要重新梳理逻辑
针对以上的代码做修改:
a. 第一点, 把所有foreach 中的 sql拆出来,先去查询到内存中,然后组合减少sql语句
b. 第二点, 简化 ORM的乱用,比如只需要查询一个字段的 就直接用原生sql或者新的一个不关联的orm 来处理
资源优化
- 由于录音文件过大, 找运维 做了一个专门的文件服务器,移到了文件服务器上
最后
最后,给加了个定时任务告警的功能, 方便及时发现异常, 优化的 第一步 勉强交活。剩下的 优化 需要再花点时间了,慢慢来了.
来源:juejin.cn/post/7353475049418260517