他山之石 使用二级缓存提高缓存命中率和内存使用效率
起因
一直都没找到二级缓存在php
中应用的比较好的资料和案例,由于范凯Robbin
Web 应用的缓存设计模式和Hibernate
二级缓存的启示,记下这篇二级缓存在Eloquent ORM
中的应用。
过程
比如博客的首页调用最新的20
篇文章,相信不少同学在刚开始使用缓存的时候,会写下如下代码:
# 控制器
public function index()
{
$articles = Article::latestArticles(20);
return view('articles.index', ['articles' => $articles]);
}
# 模型
class Article extends Model
{
public static function latestArticles($amount = 20)
{
return Cache::remember('articles:latest', 10, function () use ($amount) {
return static::latest('id')->take($amount)->get();
});
}
}
当然,模型中还能预加载每篇文章的分类,作者和tag
信息,看起来没有任何问题,而且非常符合人类直觉。但是,放大到全站缓存来看,还是有很大的改善空间。
首先,首页缓存的是一个包含20
个article
对象的集合,集合的每一个单独的article
对象除了在首页出现,还会在分类、作者和tag
等列表页出现,还有文章详情页,而缓存的集合数据没办法在这些页面间共用,重复缓存大量相同的article
对象是对内存资源的很大浪费,要是article
中的text
字段content
没有单独拆分出去,内存浪费得就更严重了。
其次,不像详情页数据改动很少,首页作为列表页来说,更新频率很高,设置的缓存时间比较短,一般是分钟级别,缓存命中率并不高。
为了有效解决这两个问题,二级缓存就派上用场了,先说下自己对二级缓存的理解。
一级缓存可以看成是数据库里存的数据的一个镜像,只不过把数据从数据库搬到内存,一个key
对应一条记录。key
一般为表的标识符,比如key
为articles:1
存的value
就是id=1
的article
对象。一级缓存时间可以设得比较长,甚至forever
也行,对象修改删除时,只要删除对应的key
就行。
二级缓存可以看成业务逻辑的缓存,首页最新20
条文章 就属于业务逻辑,只缓存这20
条文章的id
,极大地节省了内存占用。等需要用到具体的数据再去一级缓存取,一级缓存没有才去查询数据库,由于都是主键查询,不会造成表的描述,查询效率非常高。即使二级缓存很快过期,一级缓存也不会失效。
个人觉得理解二级缓存最难的是要接受n+1
查询这点,这个问题争议很大,明明各种ORM
为了避免n+1
使用了预加载,我们反而要抛弃它。包括我当初阅读范凯的《Web 应用的缓存设计模式》也心存疑惑,直到去了解了Hibernate
二级缓存机制和自己在项目中的实践发现,还真是他说的那样。
拆分n+1条查询的方式,看起来似乎非常违反大家的直觉,但实际上这是真理,我实践经验证明:数据库服务器的瓶颈往往是磁盘IO,而不是SQL并发数量。因此 拆分n+1条查询本质上是以增加n条SQL语句为代价,简化复杂SQL,换取数据库服务器磁盘IO的降低 当然这样做以后,对于ORM来说,有额外的好处,就是可以高效的使用缓存了。
使用二级缓存来重构latestArticles
方法
public static function latestArticles($amount = 20)
{
// 二级缓存
$ids = Cache::remember('articles:latest:ids', 10, function () use ($amount) {
return static::latest('id')->take($amount)->pluck('id');
});
return $ids->map(function ($id) {
// 一级缓存
return static::findById($id);
});
}
public static function findById($id)
{
return Cache::rememberForever("articles:{$id}", function () use ($id) {
return static::find($id);
});
}
除了返回Collection
,还可以返回Generator
。
public static function latestArticles($amount = 20)
{
// 二级缓存
$ids = Cache::remember('articles:latest:ids', 10, function () use ($amount) {
return static::latest('id')->take($amount)->pluck('id');
});
foreach ($ids as $id) {
// 一级缓存
yield static::findById($id);
}
}
更新与删除
一级缓存的更新和删除可能通过模型的updated
和deleted
事件来清除对应的缓存。二级缓存由于缓存时间比较短,影响不大。
关联关系
关联模型的缓存可能通过accessor
来设置一个虚拟的属性来设置,比如在Article
模型与Content
模型是一对一的关系。
在Article
中:
// 一对一关联
public function content()
{
return $this->hasOne(Content::class);
}
// contents表字段: article_id, body
public function getContentAttribute()
{
return Cache::rememberForever("contents:{$this->id}", function () {
return $this->content->body;
});
}
本作品采用《CC 协议》,转载必须注明作者和本文链接
我测试了一下,刷新是还有本地缓存的?你还能重现吗?什么浏览器?
我的测试流程:
@Summer 世界之窗7.0 Chrome太占内存 平时只开发和翻墙时候用 以防万一 以后还是先用有道云(支持markdown)写完再粘贴过来吧 :relaxed:
其他浏览器不保证兼容性哦,LC 百分之 82 的用户使用的是 Chrome 浏览器,供你参考哈。
分享很赞
正好用到,非常棒