老项目维护优化(一)

最近维护一个CRM的老项目,项目由于数据量比较大, 导致查询速度很慢, 经常出现超时的情况, 下面记录一下具体的优化方法和优化效果。

优化老项目,老生常淡的几点:

1. 数据库优化
2. 代码结构优化
3. 缓存优化
4. 资源优化
...

数据库优化

众所周知, MySQL 优化第一步,就是建索引, 看了一下整个系统的表, 发现有大量的表都没有索引, 建了索引的表,索引名称有点花里胡哨, 如下:

1
2
3
4
5
6
7
8
9
10

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索引的命名虽然没有硬性的规范,但是修改一下自己看着舒服, 个人理解:

1
2
3
4
普通索引:idx_字段1_字段2
唯一索引:uk_字段1_字段2
主键索引:pk_字段1_字段2

于是 上面的索引改成了:

1
2
3
4
5
6
7
8
9
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. 这么大,那好办,先分析一下 数据库中的表引擎.
上了一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

<?php
/**
* Author: PFinal南丞
* Date: 2023/12/28
* Email: <lampxiezi@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 information_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 的配置发现:

1
2
3
4
5
6
7
...

key_buffer_size = 512M
innodb_buffer_pool_size = 2048M

...

都知道 innodb_buffer_pool_size 针对的 是 InnoDB的表引擎,key_buffer_size 针对的 是 MyISAM的表引擎. 这配置不得修改一下. 果断打申请, 申请修改线上配置.

1
2
3
4
5
6
7
...

key_buffer_size = 2048M
innodb_buffer_pool_size = 2048M

...

重启服务后,果然比原来快了好多.能撑到 同事不在群里 打报告了.

艰巨的长征路迈出了第一步,接下来,本着 死道友不死贫道的原则, 厚着脸皮,让运维帮忙整了一台mysql 的机器,来做了个主从分离。 速度一下,不影响业务的正常使用了.

接着 开启漫长的 优化之路.

缓存优化

  1. 项目没有开启数据缓存, 只有 代码编译的缓存

所以这一块是一个大的工程, 所以先不动, 只是 给 几个 常用的功能加了一个 数据 的 缓存。后续的思路是:

a. 加一个 redis, 使用 把代码中的统计数据 缓存到 redis 中
b. 把客户信息,客户的关联信息,组合到一起, 然后缓存到 redis中.

….

代码结构优化

开始挖开代码, 看看 查询慢的 功能 代码是咋写的,不看不知道,一看直接上头:

  1. 几乎全是 foreach 中 的 SQL 查询:
1
2
3
4
5
6
7
8
9
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']);

# ......
}

  1. 由于 ORM 的方便复用, 大量的 表关联模型 复用,导致查询的 废字段特别多.比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  <?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);
}
}
?>
  1. 代码中的业务逻辑一直再叠加,导致废代码量特别的大需要重新梳理逻辑

针对以上的代码做修改:

a. 第一点, 把所有foreach 中的 sql拆出来,先去查询到内存中,然后组合减少sql语句 
b. 第二点, 简化 ORM的乱用,比如只需要查询一个字段的 就直接用原生sql或者新的一个不关联的orm 来处理

资源优化

  1. 由于录音文件过大, 找运维 做了一个专门的文件服务器,移到了文件服务器上

最后,给加了个定时任务告警的功能, 方便及时发现异常, 优化的 第一期 勉强交活。剩下的 优化 需要再花点时间了,慢慢来了.