起因
一直都没找到二级缓存在php中应用的比较好的资料和案例,由于范凯RobbinWeb 应用的缓存设计模式和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);
}
}
吐槽
在专栏发布界面,好不容易写到这里,结果手贱划了下浏览器刷新的鼠标手势,内心瞬间奔崩,砸电脑的心都有了 :rage:。虽然看提示有自动保存,可只有标题被保存了,内容还是空空如也。转眼想想历史上那些不小心毁了书稿写出世界名著的作家,自己也只有硬着头皮把内容回忆出来,所以后面的内容就一笔带过了。
更新与删除
一级缓存的更新和删除可能通过模型的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;
});
}
Java高架构师、分布式架构、高可扩展、高性能、高并发、性能优化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分布式项目实战学习架构师视频免费获取架构群:854180697
群链接:加群链接
写在最后:欢迎留言讨论,加关注,持续更新!