对TP5数据库缓存cache的一些思考

最近在优化代码的时候,突然想起来TP5的数据库操作中有个cache,之前也用过,印象里就是在缓存时间内,请求的速度会大大加快,但是修改数据会导致不能及时更新。当初还比较年轻,没有深入去搞清楚,只是不再使用cache了而已,现在刚好有机会,就来稍微学一学吧。

很可惜,不论是官方文档还是网上搜索出来的结果,基本上都只是告诉我们如何去使用它,完全没有说到它的工作原理之类的,无奈,只能去慢慢读源码了。

首先让我感到疑惑的是,这个cache和我们平常用的缓存Cache有什么区别?

如果单单从功能上看的话,好像两者没有任何区别,都能设置key值和有效期,以及打标签也都支持。随后我做了个实验,手动设置key名,然后用cache()助手函数去读取,结果如我所料,读取出来的结果果然是select的结果。

$result = db('my_table')->where($where)->cache('key',10)->select();
var_dump(cache('key'));  //结果和$result一样

不过实验归实验,还是得去源码里看看具体是如何实现的。

进入/thinkphp/library/think/db/Query.php中,找到cache方法,可以看到,这里只是设置了属性,真正的使用还不在这里,还得去select、find、value、column里面看。

//源码太长了,就不一一复制了
public function cache($key = true, $expire = null, $tag = null)
{
    // 增加快捷调用方式 cache(10) 等同于 cache(true, 10)
    if ($key instanceof \DateTime || (is_numeric($key) && is_null($expire))) {
        $expire = $key;
        $key    = true;
    }
    if (false !== $key) {
        $this->options['cache'] = ['key' => $key, 'expire' => $expire, 'tag' => $tag];
    }
    return $this;
}

public function select($data = null)
{
    ......
    if (empty($options['fetch_sql']) && !empty($options['cache'])) {
        // 判断查询缓存
        $cache = $options['cache'];
        unset($options['cache']);
        $key       = is_string($cache['key']) ? $cache['key'] : md5($this->connection->getConfig('database') . '.' . serialize($options) . serialize($this->bind));
        $resultSet = Cache::get($key);
    }
    ......
    if (isset($cache) && false !== $resultSet) {
        // 缓存数据集
        $this->cacheData($key, $resultSet, $cache);
    }
    ......
}

protected function cacheData($key, $data, $config = [])
{
    if (isset($config['tag'])) {
        Cache::tag($config['tag'])->set($key, $data, $config['expire']);
    } else {
        Cache::set($key, $data, $config['expire']);
    }
}

结论:可以很明显地看出,不论是写入缓存还是从缓存中读取,都是与我们常用的Cache一样的,唯一不同的是,如果你不指定key名的话,他会根据操作的数据库名、表名以及主键ID等信息,帮你生成一个32位密文,你也不用担心万一key名重复导致缓存覆盖了。

疑惑二当数据更新时,缓存会怎么样呢?

按照文档上的说法,要么手动在update等更新操作中添加cache,来实现手动更新缓存;要么使用find方法并且使用主键查询,就会自动清理缓存。手动指定缓存倒没什么问题,除了不触及缓存操作的新增之外,在数据更新后缓存都会被清除,然后在查询时重新被写入。PS:增删改查中,新增操作是不触及缓存的,这也是缓存要谨慎使用的原因,虽然能够极大地增加效率,但是不能反映数据的及时更新。

然后就是什么情况下更新数据能够自动清除缓存的问题了。比较麻烦的是,涉及到缓存操作的话,是否使用主键作为查询条件还不一样。也就是说,会有以下2*2+2*2共八种组合。

  • 用主键做条件进行查询+用主键做条件进行修改——清除
  • 用主键做条件进行查询+用主键做条件进行删除——清除
  • 用主键做条件进行查询+不用主键做条件进行修改——不清除
  • 用主键做条件进行查询+不用主键做条件进行删除——不清除
  • 不用主键做条件进行查询+用主键做条件进行修改——不清除
  • 不用主键做条件进行查询+用主键做条件进行删除——不清除
  • 不用主键做条件进行查询+不用主键做条件进行修改——不清除
  • 不用主键做条件进行查询+不用主键做条件进行删除——不清除

虽然还有很多情况没有测试到,比如更新操作的数据是否为缓存的数据、查询和更新操作的条件是不是一样等等,即使测试的结果和文档上描述的一样,但是还是感觉说服力不够强,还得再去源码里找找原因。这里以update操作为例。

//如果有设置缓存名,则直接从cache中读取
if (isset($options['cache']) && is_string($options['cache']['key'])) {
    $key = $options['cache']['key'];
}
......
//如果没有手动设置缓存,则只能依靠主键ID以及操作的表来识别,否则没有办法识别出来
} elseif (!isset($key) && is_string($pk) && isset($options['where']['AND'][$pk])) {
    $key = $this->getCacheKey($options['where']['AND'][$pk], $options, $this->bind);
}

结论:只有当查询和修改操作都使用主键ID作为条件时,才能实现自动清除缓存。

所以说,数据库缓存并不是随便用的,如果使用不当,很容易影响数据的时效性和用户的体验。如果真的有必要使用的话,最好还是不要偷懒用自动清除缓存,还是手动设置缓存名字,以及在更新操作时指定清除哪个缓存。

好了,不知不觉又花了半个晚上,今天就这样吧,洗洗睡了。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注