PHP执行耗时优化

一、耗时纪录方式

在函数执行前后分别调用 microtime() 函数获取当前时间,相减后为本次执行时间。当然也可以使用封装好的计时器函数,但本质还是 microtime()

二、耗时优化方法

1、合理使用 fastcgi_finish_request() 函数

根据 PHP 手册的说明:此函数冲刷(flush)所有响应的数据给客户端并结束请求。 这使得客户端结束连接后,需要大量时间运行的任务能够继续运行。

也就是说,执行该函数后,客户端就会收到本次请求的结果,但是服务端会继续执行本次请求剩余的逻辑。这时即使执行一些耗时操作,客户端也不会有任何感知,达到了提高响应速度的目的。

实际中接触到了下面几种应用场景:

  • “预读”下次请求数据
    根据业务逻辑,判断用户下一次最可能进行的请求(如流程下一步等)。先调用 fastcgi_finish_request() 返回数据,然后读取下一步的数据并存入 Redis 中,下次请求直接从 Redis 中获取结果。局限是仅适用于数据不易变化的场景。如帖子下一页内容会频繁变动,就不适用于此方法。

  • 检查执行错误并记录日志
    Yii 框架中可以在 onEndRequest 回调中使用 error_get_last() 检查错误、记录日志等,如果每次日志量较多,可能会拖慢响应速度。所以可以在程序入口中(如 main.php 配置文件)加入下面代码:

if (Yii::app() instanceof CConsoleApplication) {
} else {
    // 使onEndRequest中的耗时操作在客户端结束连接后执行
    // 这段代码必须放在所有 onEndRequest 事件监听器的最前面
    Yii::app()->attachEventHandler('onEndRequest', function ($event) {
        @ob_end_flush();
        if (!function_exists("fastcgi_finish_request")) { // 兼容非CGI方式调用
            function fastcgi_finish_request()
            {
            }
        }
        fastcgi_finish_request();
    });
    Yii::app()->attachEventHandler('onEndRequest', function ($event) {
        // 收集日志等耗时操作
        // 由于前一段代码已经将数据返回,此处逻辑不会增加响应时间
    });
}

2、使用合理的 Redis 数据类型

一些常见场景需要两张表关联查询,例如先从一张表中查出某分类下的项目,再从另一张表中查出这些项目的数据。但这种场景下一般不使用 JOIN,而是分别进行查询并对结果进行缓存,此时为 Redis 选择合适的数据结构很重要。

这种场景有两种缓存方式:

  1. 使用 Redis 字符串缓存:Key - 项目id,Value -数据
  2. 使用 Redis Hash 缓存:Hash Key - 项目id ,Value - 数据

如果每次查询数据量较大,比如每次需要获取 10000 条项目数据,则必须使用 Redis Hash 缓存,这样只写入一次 Redis,相反如果使用 Redis 字符串,则需要写入 10000次。实际使用证明,后者使用 Redis 后反而会拖慢查询速度!

在实际项目中,不使用 Redis 接口访问速度为 3s 左右,使用错误的 Redis 缓存后接口访问速度为 10s!使用正确的缓存后访问速度 150ms 左右。

3、SQL优化

首先需要知道用到了哪些 SQL,正常利用 Yii 框架正常没办法打印出 SQL,即使开启 enableProfiling、enableParamLogging 参数也只能打印出带有占位符和参数的“准” SQL。利用一下方法可以在日志中打印出执行的SQL:

1) 与 CDbCommand 同级添加 MyCDbCommand:



class MyCDbCommand extends CDbCommand
{
    public $myParam = array();

    public function addLog()
    {
        $params = array_merge($this->params, $this->myParam);
        $sql = $this->getText();
        if (!empty($params)) {
            foreach ($params as $key => $value) {
                $sql = str_replace($key, $value, $sql);
            }
        }
        $sql = preg_replace("/\r\n|\n\r|\r|\n/", ' ', $sql);
        Yii::log($sql, CLogger::LEVEL_INFO, 'system.db.CDbCommand');
    }

    public function bindValues($values)
    {
        foreach ($values as $name => $value) {
            $this->myParam[$name] = $value;
        }
        return parent::bindValues($values);
    }

    public function bindValue($name, $value, $dataType = null)
    {
        $this->myParam[$name] = $value;
        return parent::bindValue($name, $value, $dataType);
    }

    public function query($params = array())
    {
        $this->addLog();
        return parent::query($params);
    }

    public function queryAll($fetchAssociative = true, $params = array())
    {
        $this->addLog();
        return parent::queryAll($fetchAssociative, $params);
    }

    public function queryColumn($params = array())
    {
        $this->addLog();
        return parent::queryColumn($params);
    }

    public function queryRow($fetchAssociative = true, $params = array())
    {
        $this->addLog();
        return parent::queryRow($fetchAssociative, $params);
    }

    public function queryScalar($params = array())
    {
        $this->addLog();
        return parent::queryScalar($params);
    }
}

2) 与 CDbConnection 同级添加 MyCDbConnection:



class MyCDbConnection extends CDbConnection
{

    public function createCommand($query = null)
    {
        $this->setActive(true);
        return new SoYoungCDbCommand($this, $query);
    }
}

3) 在 YiiBase::$_coreClasses 数组中增加下面两个元素:

'MyCDbCommand' => '/db/MyCDbCommand.php',
'MyCDbConnection' => '/db/MyCDbConnection.php',

(虽然数组的注释写明了不应该手动修改,但是考虑到我们新添加的类十分重要,算作核心类也是可以接受的…)

4) 然后应用的配置文件中配置 DB 时,手动指定使用的类为 MyCDbConnection 即可。

'db'=>array(
    'connectionString' => 'xxx',
    'tablePrefix' => 'tbl_',

    'class' => 'MyCDbConnection',
),

这样,执行的 SQL 就会被写入 system.db.CDbCommand 分类的日志里(当然 application.log 里也会有),注意打印日志可能会降低性能(并未测试过),建议只在开发环境下使用。

接下来分析 SQL 性能:
1) 查看 MySQL 版本: show variables like "%version%";
低于 5.0.37 的没有该功能。

2) 查看 性能分析 开关状态: show variables like "%pro%";
profiling 变量默认为 OFF,开启后为 ON
(不必担心该操作会影响性能,每次退出数据库后会自动关闭)

3) 开启/关闭 性能分析 开关: set profiling = 1;set profiling = 0;

4) 尝试执行某个 SQL

5) 然后查看该 SQL 耗时:

show profiles; --总耗时
show profile for query 1; --查询指定编号SQL耗时
show profile all for query 1; --查询指定编号SQL性能相关的全部信息

利用本方法,再结合 EXPLAIN 进行针对性 SQL 优化。

4、注意以下 PHP 内置函数

password_hash
password_verify

检查代码,这两个方法耗时较长。在符合业务需求的前提下,注册登录接口中的方法避免反复重复校验账号和密码。

你可能感兴趣的:(服务端)