Laravel缓存系统的设计

遇到的问题

一个业务流量上来以后,直接查询MySQL的压力是很大的,如果稍有不慎出现扫表行为就容易阻塞MySQL,轻则导致业务请求响应慢重则导致业务卡壳彻底无法使用。

Laravel 的缓存系统只设计了通用的缓存API,某些场景下使用缓存的时候,需要自己去构建缓存Key,等业务逻辑多了以后,对于Key的管理就成了一个很大的问题。

解决方式

想到这里其实我们需要的是一个通用的缓存系统,对于业务层只需要记着调用ORM提供的方法存取数据即可,不需要去理解处理缓存Key的生成、过期机制及缓存介质。

Laravel现成解决方案

使用安个家的开源方案:angejia/pea,下面几点是本人阅读了源码后做的一些笔记,有意了解实现原理的可以完整阅读源代码。

为了描述方便下文中的Eloquent Builder和Query Builder为下面两个类的简称

illuminate\Database\Eloquent\Builder.php
illuminate\Database\Query\Builder.php

如何在Eloquent上挂勾子

继承illuminate\Database\Model.php
重载newEloquentBuilder和newBaseQueryBuilder方法

这个方法主要是用来返回一个Eloquent Builder实例,看源码可以知道它其实是对Query Builder的一个封装。

所在文件:illuminate\Database\Eloquent\Model.php
/**
* Create a new Eloquent query builder for the model.
*
* @param \Illuminate\Database\Query\Builder $query
* @return \Illuminate\Database\Eloquent\Builder|static
*/
public function newEloquentBuilder($query)
{
return new Builder($query);
}
/**
* Get a new query builder instance for the connection.
*
* @return \Illuminate\Database\Query\Builder
*/
protected function newBaseQueryBuilder()
{
$conn = $this->getConnection();
$grammar = $conn->getQueryGrammar();
return new QueryBuilder($conn, $grammar, $conn->getPostProcessor());
}
所在文件:pea\src\Model.php
public function newEloquentBuilder($query)
{
$builder = new Builder($query);
$builder->macro('key', function (Builder $builder) {
return $builder->getQuery()->key();
});
$builder->macro('flush', function (Builder $builder) {
return $builder->getQuery()->flush();
});
return $builder;
}
protected function newBaseQueryBuilder()
{
$conn = $this->getConnection();
$grammar = $conn->getQueryGrammar();
$queryBuilder = new QueryBuilder(
$conn, $grammar, $conn->getPostProcessor());
$queryBuilder->setModel($this);
return $queryBuilder;
}
我们先看继承的newBaseQueryBuilder方法,其实就多了一句`$queryBuilder->setModel($this);`,虽然原码里头是写了这句,去掉其实问题也不大,数据照样能出来。[TODO 这里要写明下原因]
实现自己的Query Builder类

Pea这个类库的缓存判断逻辑就在这个文件里头 pea/src/QueryBuilder.php,主要就是get、insert、update方法。

缓存Key的生成逻辑

复杂查询

  1. 含有 max, sum 等汇聚函数
  2. 包含 distinct 指令
  3. 包含分组
  4. 包含连表
  5. 包含联合
  6. 包含子查询
  7. 包含原生(raw)语句
  8. 包含排序 TODO 优化此类情形

缓存Key的构造规则如下所示:

md5('前缀:数据库名:表名:行级版本号:表级版本号:原始SQL:binding值')
md5('pea:doutula:photo:0:0:select max(`out_id`) as aggregate from `photo`:[]')

简单查询

  1. 以主键或简单字段做查询的SQL语句

缓存Key的构造规则如下所示:

md5('前缀:数据库名:表名:行级版本号:查询字段值')
md5('pea:doutula:photo:0:2')

其中需要注意的是版本号是单独存储在redis内的一个key内,生成规则有两种如下所示:

行级缓存:md5('前缀:schema_version:数据库名:表名')
表级缓存:md5('前缀:update_version:数据库名:表名')

这个版本号的主要作用就是,方便一次性清除所有的缓存(利用redis的过期机制自动清理)

利用redis的LRU规则

LRU(LRU全称是Least Recently Used,即最近最久未使用)实际上只是Redis支持的内存回收策略中的一种。有兴趣的可以了解一下,这里就不多述了。

增删改逻辑

主要做两件事

  1. 过期所有的表级缓存
  2. 过期涉及到的行级缓存

Laravel代码跟踪的两种小技巧

利用Xdebug和phpstorm配合单步调试

这个可以参考以前整理的一篇文章 phpstorm远程调试

直接在代码处手动抛异常

然后就能在Laravel提供的报错页面上看到清晰的调用顺序及参数情况。具体如下所示:
PHP调用栈
以上图为例可以从第10步->第1步清晰地看出 Photo::find(2) 这种语句的调用过程。