Mysql - 百万级数据查询优化笔记 (PHP Script) ③
说明:上篇介绍Mysql脚本优化的博客竟然突破了1w点击量,这周又加班熬夜渡过了一段不为人知的黑暗,成长都是苦涩的,愿我能行。

1.PHP脚本配置

1.1 memory_limit设置脚本内存

看一下 PHP官网对memory_limit的解释:

This sets the maximum amount of memory in bytes that a script is
allowed to allocate. This helps prevent poorly written scripts for
eating up all available memory on a server. Note that to have no
memory limit, set this directive to -1.

memory_limit 的值是越大越好么?

当然不是,memory_limit 主要是为了防止程序 bug, 或者死循环占用大量的内存,导致系统宕机。在引入大量三方插件,或者代码时,进行内存限制就非常有必要了。

memory_limit 会使每个 PHP process 都占用固定的内存?

memory_limit 只是限制了每个 PHP进程的内存占用上限,而不是为每个进程分配了固定的内存。
所以,并不会因为 memory_limit 设置越大,导致并发数出现降低。

1.2 set_time_limit设置脚本执行时间

在php中set_time_limit函数是用来限制页面执行时间的,如我想把一个php页面的执行时间定义为5秒
就可以set_time_limit(5)了,规定从该句运行时起程序必须在指定秒数内行结束,0代表不限制。

PHP脚本配置设置代码如下:

set_time_limit(0);
ini_set('memory_limit', '1024M');

2.字典初始化使用Nosql

在执行脚本之前进行初始化加载,把需要处理的数据先从Mysql中取出,用Hash的结构,存储到Redis中,读取时条数不宜过大,不超过1千条,服务器性能高可以使用hGall,建议最好不要使用,Redis单线程一次取出过大,会卡掉服务。

private function _initNovelDict()
{
    // 作品
    echo '初始化小说字典数据' . PHP_EOL;
    $novel_id = 0;
    while (true){
        $novelData = ChangpeiModule_Cpwxw_Novel_Novel::getInstance()
            ->getAllByPrepareSql(['novel_id', 'novel_name', 'author_id'], [
                'where' => [
                    'novel_id' => ['>',$novel_id],
                    'novel_status' => 1,
                ],
                'limit' => 5000
            ]);
        if(!$novelData)  break;
        $novel_id =  end($novelData)['novel_id'];
    }
    $cacheKey = $this->_GetNovelDictKey();
    ChangpeiLib_Redis_Hash::hMSet($cacheKey, $novelData);
    echo '初始化小说数据完成' . PHP_EOL;
}

2.复杂的sql使用PHP数组去组装

对于复杂的计算,要减少Mysql之间的聚合,先全量取出然后使用PHP处理。

SELECT SUM(t.`gift_number` * g.`gift_price`) total, `nid` FROM `{$table}` t
LEFT JOIN cpwxw2_db_v2.`wm_gift` g ON g.`gift_id` = t.`gift_id` 
WHERE `delete_time` = 0 AND `nid` > 0 AND `uid` = {$uid} AND
g.`gift_id` > 0 AND t.`create_time` BETWEEN {$total_start_time}
AND {$total_end_time} GROUP BY nid ORDER BY total DESC LIMIT 3

以下是PHP代码,$mostRewardNovelData是开始取出的结果集:

$giftData = array_column($mostRewardNovelData, null, 'gift_id');
            $gift_ids = array_keys($giftData);
            $gift_sql = " SELECT `gift_id` , `gift_price` FROM  `wm_gift` WHERE  `gift_id` in (" . implode($gift_ids, ',') . ") ";
            $giftPriceData = $this->mysqlCpMain->query($gift_sql);
            $giftPrice = array_column($giftPriceData, 'gift_price', 'gift_id');
            $tempArr = [];
            foreach ($mostRewardNovelData as $value) {
                $nid = $value['nid'];
                $aid = $value['aid'];
                $uid = $value['uid'];
                $gold = (int)$value['gift_number'] * (int)$giftPrice[$value['gift_id']];
                $tempArr[$uid]['nid'][$nid] += $gold;
                $tempArr[$uid]['aid'][$aid] += $gold;
            }

3.关于内存

很多时候我们都在说内存,其实内存的概念很笼统,内存分为CPU上的缓存(catch)和内存条上的内存(Memory),平时说的服务器8G内存指的是内存条上的内存。Cpu已经从Lmp总线型,升级成NUMP的方式,Nginx就是利用NUMP很好的例子。

其实有一个问题特别迷惑,Mysql数据库里的数据就是以特殊结构存储(B-tree)的文件,Redis中的数据也是用(list、hash)结构存储的数据,存储上没有什么太大的不同,只是有快慢的区别。

而高性能就是尽最大的程度上利用Cpu和内存,提高吞吐量,词很贴切。

4.解决方法和教训

所有的大块要分而治之,最大的难点在于阅读记录表有上亿条数据,最后根据业务的不同,把大的脚本拆成6块,分着跑,这样最大的表的执行时间大概两个小时跑完,其他的都很快,最后解决。

这一周的时间过的真的是炼狱一般,想了好多个方法,都是架构师之路的荆棘,每一天为明天。