最近维护一个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 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 的机器,来做了个主从分离。 速度一下,不影响业务的正常使用了.
接着 开启漫长的 优化之路.
缓存优化
项目没有开启数据缓存, 只有 代码编译的缓存
所以这一块是一个大的工程, 所以先不动, 只是 给 几个 常用的功能加了一个 数据 的 缓存。后续的思路是:
a. 加一个 redis, 使用 把代码中的统计数据 缓存到 redis 中 b. 把客户信息,客户的关联信息,组合到一起, 然后缓存到 redis中.
….
代码结构优化 开始挖开代码, 看看 查询慢的 功能 代码是咋写的,不看不知道,一看直接上头:
几乎全是 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' ]); }
由于 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 ); } } ?>
代码中的业务逻辑一直再叠加,导致废代码量特别的大需要重新梳理逻辑
针对以上的代码做修改:
a. 第一点, 把所有foreach 中的 sql拆出来,先去查询到内存中,然后组合减少sql语句
b. 第二点, 简化 ORM的乱用,比如只需要查询一个字段的 就直接用原生sql或者新的一个不关联的orm 来处理
资源优化
由于录音文件过大, 找运维 做了一个专门的文件服务器,移到了文件服务器上
最后,给加了个定时任务告警的功能, 方便及时发现异常, 优化的 第一期 勉强交活。剩下的 优化 需要再花点时间了,慢慢来了.