Yii2性能优化之:数据库查询缓存完美解决方案

我们在做项目的时候,为了最大化提升项目的执行速度,缓存必不可少,各种开源框架也提供了丰富多彩的的缓存实现方案。比如,HTML整页缓存、片段缓存、程序层面的数据库查询缓存、数据库层面的查询缓存等等。每一种方式都有其适合的应用场景。比如,对于访问量比较大的网站首页,比较适合用缓存整页的方式;比如,某个页面的一个统计表格,不需要每次都进行查询,使用的时候直接从缓存里面取,片段缓存就非常合适;比如,对于一些频繁请求数据库的sql查询,数据库中的值有时候没有改变,却被频繁进行查询明显是不合理的,尤其是对一些比较耗时的查询,如果将这部分查询的数据放入缓存,则会明显缩短程序的响应时间,这种场景下就需要使用数据库查询缓存。与数据库层面的查询缓存相比,程序层面的缓存对于程序员来讲是友好且更容易控制的。本文主要讲述应用程序层面的数据库查询缓存。

实际上,不管对于何种缓存使用方案,都面临两个问题:

  1. 缓存命中率问题。
  2. 缓存过期问题。
    第一个问题不是本文讨论的重点,我们着重解决第二个问题。

缓存一般都需要设置一个过期时间,被缓存的数据过期之后,需要更新一下,设置缓存的示例代码如下:

    /**
     * @param $key 缓存key
     * @param $value 缓存的值
     * @param null $duration 缓存的过期时间
     */
    public function set($key, $value, $duration = null);

注意:对于数据库查询缓存这样的场景来讲,上面这种常规设置缓存的方式有两个问题:

  1. 在缓存有效期内,数据库的数据被更新了,但通过读缓存获取的还是旧数据怎么办?
  2. 数据库数据并没有更新,但缓存过期了,必然需要重新进行一次数据库查询并存入缓存,从某种角度讲,这一次查询也是没有必要的。

一个假设:
是否存在一种设置缓存的方式,可以实时监测数据库表是否更新过,以此来决定是否读取缓存?
答案是肯定的。我们可以通过引入缓存依赖的概念来解决此问题。
缓存依赖是除了缓存时间之外的另一个让缓存失效的条件,它和缓存时间共同决定着缓存是否有效,如果其中一个条件失效,那么缓存会被重新设置。那么对于使用【数据库查询缓存】的场景我们只需要做两件事,一个是获取数据最后被更新的时间,另外一个是Yii2是怎样设置数据库缓存依赖的。

获取数据表最后一次缓存时间,以msyql为例

以mysql数据库user表为例:

##获取user表的最后一次更新时间。
SELECT
    *
FROM
    information_schema.TABLES 
WHERE
    TABLE_SCHEMA = DATABASE ( ) 
    AND information_schema.TABLES.TABLE_NAME = 'user'\G

##或者使用
show table status like 'user';

我们执行第一条sql,获取结果如下:

*************************** 1. row ***************************
  TABLE_CATALOG: def
   TABLE_SCHEMA: zhyc_prod
     TABLE_NAME: user
     TABLE_TYPE: BASE TABLE
         ENGINE: InnoDB
        VERSION: 10
     ROW_FORMAT: Compact
     TABLE_ROWS: 1271
 AVG_ROW_LENGTH: 1250
    DATA_LENGTH: 1589248
MAX_DATA_LENGTH: 0
   INDEX_LENGTH: 0
      DATA_FREE: 4194304
 AUTO_INCREMENT: 1308
    CREATE_TIME: 2019-01-02 15:28:02
    UPDATE_TIME: 2019-01-05 17:44:23
     CHECK_TIME: NULL
TABLE_COLLATION: utf8_general_ci
       CHECKSUM: NULL
 CREATE_OPTIONS: 
  TABLE_COMMENT: 账号表

返回的结果中有两个关键字段
CREATE_TIME:表的创建时间
UPDATE_TIME:表的最后一次更新时间

注意:Innodb引擎在mysql5.7.2版本才开始支持UPDATE_TIME这个字段,在低于这个版本的mysql中,该字段值是空的。

InnoDB: Beginning with MySQL 5.7.2, UPDATE_TIME displays a timestamp value for the last UPDATE, INSERT, or DELETEperformed on InnoDB tables. Previously, UPDATE_TIME displayed a NULL value for InnoDB tables. For MVCC, the timestamp value reflects the COMMIT time, which is considered the last update time. Timestamps are not persisted when the server is restarted or when the table is evicted from the InnoDB data dictionary cache. -----引用自Mysql官网

Yii2框架的缓存依赖实现

yii2框架实现了几种缓存依赖机制。

  1. 文件依赖
  2. 表达式依赖
  3. 标签依赖
  4. 数据库依赖
    对于Yii2的缓存依赖,网上有大量文章。此处不做赘述。这里我们只用到数据库依赖。
    废话不多说,直接上代码:
<?php

namespace common\base\db;

use yii\caching\DbDependency;

class ActiveRecord extends \yii\db\ActiveRecord
{

    /**
     * @var int|true 查询的缓存时长,如果为true,则取[[Connection::queryCacheDuration]]中设置的值,
     * 如果是负数,则表示不查询缓存
     * 如果是正整数,则表示缓存时长,单位是秒
     * 理论上:当缓存依赖于数据库表之后,这个过期时间可以设置为无限大。
     */
    static $queryDuration = 86400 * 10;

    /**
     * 重写父类的find方法,在每一次请求数据库查询的时候,先查询缓存是否过期
     * @return $this
     */
    public static function find()
    {
        $table = str_replace("`", '', static::getDb()->getSchema()->getRawTableName(static::tableName()));
        $sql = "select CREATE_TIME,UPDATE_TIME 
                from information_schema.TABLES 
                where TABLE_SCHEMA=database() 
                and information_schema.TABLES.TABLE_NAME = '" . $table . "'";

        return parent::find()
            ->cache(static::$queryDuration, new DbDependency([
                'sql' => $sql,
                'reusable' => true //对于一个会话,只执行一次查询即可
            ]));
    }

}

注意:本类可以作为项目的自定义的超类,其他的所有model类需要继承它之后才可以用缓存。

通过对find函数的重写,我们达到了所有子类都可以使用缓存的效果,下面来测试一下:

  1. 修改yii框架的log配置,将sql单独写到一个文件里去
'components' => [
      ...
        'log' => [
            'traceLevel' => 0,
            'targets' => [
                [
                    'class' => 'yii\log\FileTarget',
                    'levels' => ['profile'],
                    'logVars' => [],
                    'categories' => ['yii\db\Command*'],
                    'logFile' => '@runtime/logs/sql.log',
                ],
            ],
        ],
      ...
    ],

  1. 运行你的yii2项目,然后再看下sql.log。

我们啰嗦了这么多,实际的代码量并不大。在本人的测试中,发现有两种查询总是不能被缓存。一个是ActiveQuery::viaTable() 函数,另一个是ActiveQuery::via()函数,至于为什么不能,留给小伙伴自己去实践吧。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,711评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,079评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,194评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,089评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,197评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,306评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,338评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,119评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,541评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,846评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,014评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,694评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,322评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,026评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,257评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,863评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,895评论 2 351

推荐阅读更多精彩内容