Laravel缓存:基础知识以及提示和技巧

缓存技术使您可以创建更多可扩展的应用程序,将某些查询的结果存储在快速的内存中。 但是,实施不正确的缓存会大大降低用户对应用程序的印象。 本文包含一些有关缓存的基本概念,从过去的几个项目中学到的各种规则和禁忌。


不要使用缓存。


您的项目速度很快,没有性能问题吗?
不用缓存了。 认真地:)


这将极大地复杂化从数据库进行的读取操作,而没有任何好处。


没错,Mohamed Said在本文开头进行了一些计算,并证明了在某些情况下,优化应用程序数毫秒可以为您的AWS账户节省大量资金。 因此,如果您的项目预计节省的资金超过1.86美元,那么缓存可能是个好主意。


如何运作?


当应用程序想要从数据库中获取某些数据(例如,通过其ID的Post实体)时,它会针对这种情况生成一个唯一的缓存键( 'post_' . $id非常合适),并尝试通过此键在快速键值存储(内存缓存, redis或其他)。 如果存在该值,则应用程序将使用它。 如果没有,它将从数据库中获取它,并通过此密钥存储在缓存中以备将来使用。



永远不要将此值保留在缓存中,因为可以更新此Post实体,但是应用程序将始终接收旧的缓存值。
因此,缓存功能通常会询问该值应存储在什么时间。


在此时间到期后,memcache或redis将“忘记”它,应用程序将从数据库中获取新的价值。


一个例子:


 public function getPost($id): Post { $key = 'post_' . $id; $post = \Cache::get($key); if($post === null) { $post = Post::findOrFail($id); \Cache::put($key, $post, 900); } return $post; } 

在这里,我将Post实体放置在缓存中15分钟(自5.8版开始,laravel在该参数中使用秒,然后是分钟)。 对于这种情况, Cache门面也有一个方便的remember方法。 此代码与上一个代码完全相同:


 public function getPost($id): Post { return \Cache::remember('post_' . $id, 900, function() use ($id) { return Post::findOrFail($id); }); } 

Laravel文档中有一个“ 缓存”一章,其中介绍了如何为应用程序和主要功能安装必要的驱动程序。


快取资料


所有标准Laravel驱动程序都将数据存储为字符串。 当我们要求您缓存Eloquent模型的实例时,它使用serialize函数从对象中获取字符串。 当我们从缓存中获取对象时,反序列化功能将其还原为对象状态。


几乎所有数据都可以缓存。 数字,字符串,数组,对象(如果可以正确序列化,请参见前面的链接中的功能说明)。


雄辩的实体和集合可以轻松地进行缓存,并且是Laravel应用程序缓存中最受欢迎的值。 但是,其他类型的使用也很广泛。 Cache::increment方法在实现各种计数器时很流行。 另外,当开发人员在竞争竞争条件时, 原子锁非常有用。


要缓存什么?


缓存的第一个候选对象是经常执行的请求,但是其执行计划并非最简单。 最好的例子是主页上的前5条文章或最新新闻。 缓存这些值可以大大提高主页的性能。


通常,使用Model::find($id)通过id来获取实体非常快,但是如果此表负载了很多更新,插入和删除查询,则减少选择查询的数量将使数据库得到很好的喘息。 每次将加载具有hasMany关系的实体也是缓存的理想选择。 当我从事一个每天有10+百万访客的项目时,我们几乎缓存了所有选择请求。


缓存失效


指定时间后的密钥衰减有助于更新缓存中的数据,但这不会立即发生。 用户可以更改数据,但是一段时间后,他将继续在应用程序中看到它们的旧版本。 关于我过去的项目之一的常规对话:


 :   ,     ! : ,  15 ( ,  )... 

这种行为对用户来说非常不便,而且当我们快速更新时,很明显的决定就是从缓存中删除旧数据。 这个过程称为残疾。 对于"post_%id%"类的简单键, "post_%id%"不是很困难。


雄辩的事件会有所帮助,或者如果您的应用程序生成特殊事件(如PostPublishedUserBanned它甚至会更加简单。 口才事件示例。 首先,您需要创建事件类。 为了方便起见,我将为它们使用一个抽象类:


 abstract class PostEvent { /** @var Post */ private $post; public function __construct(Post $post) { $this->post = $post; } public function getPost(): Post { return $this->post; } } final class PostSaved extends PostEvent{} final class PostDeleted extends PostEvent{} 

当然,根据PSR-4,每个类都必须位于自己的文件中。 设置Post Eloquent类(使用文档 ):


 class Post extends Model { protected $dispatchesEvents = [ 'saved' => PostSaved::class, 'deleted' => PostDeleted::class, ]; } 

为这些事件创建一个侦听器:


 class EventServiceProvider extends ServiceProvider { protected $listen = [ PostSaved::class => [ ClearPostCache::class, ], PostDeleted::class => [ ClearPostCache::class, ], ]; } class ClearPostCache { public function handle(PostEvent $event) { \Cache::forget('post_' . $event->getPost()->id); } } 

每次更新或删除Post实体后,此代码都会删除缓存的值。 无效的实体列表(例如,排名前5位的文章或突发新闻)将更加复杂。 我看到了三种策略:


不要禁用策略


只是不要触摸这些值。 通常,这不会带来任何问题。 没关系,新新闻稍后会出现在后者的列表中(当然,如果这不是一个大新闻门户)。 但是对于某些项目,在这些列表中拥有新数据确实很重要。


查找和分散策略


每次更新发布时,都可以尝试在缓存列表中找到它,如果存在,请删除此缓存值。


 public function getTopPosts() { return \Cache::remember('top_posts', 900, function() { return Post::/*   top-5*/()->get(); }); } class CheckAndClearTopPostsCache { public function handle(PostEvent $event) { $updatedPost = $event->getPost(); $posts = \Cache::get('top_posts', []); foreach($posts as $post) { if($updatedPost->id == $post->id) { \Cache::forget('top_posts'); return; } } } } 

看起来很丑,但是行得通。


策略“商店ID”


如果列表中项目的顺序不重要,则仅条目的ID可以存储在缓存中。 接收到ID之后,您可以创建形式为'post_'.$id的键列表,并使用Cache::many方法获取所有值,该方法在一个请求中从缓存中获取很多值(也称为多获取)。


缓存无效并不是徒劳的, 这是编程中的两个困难之一,在某些情况下非常困难。


关系缓存


缓存具有关系的实体需要更多的关注。


 $post = Post::findOrFail($id); foreach($post->comments...) 

此代码执行两个SELECT查询。 通过id获取实体,通过post_id获取评论。 我们实现缓存:


 public function getPost($id): Post { return \Cache::remember('post_' . $id, 900, function() use ($id) { return Post::findOrFail($id); }); } $post = getPost($id); foreach($post->comments...) 

第一个请求已缓存,第二个未缓存。 当缓存驱动程序将“发布”写入缓存时, comments尚未加载。 如果我们也想缓存它们,那么我们必须手动加载它们:


 public function getPost($id): Post { return \Cache::remember('post_' . $id, 900, function() use ($id) { $post = Post::findOrFail($id); $post->load('comments'); return $post; }); } 

现在都缓存了这两个请求,但是每次添加注释时,我们都必须使'post_'.$id的值无效。 它不是很有效,因此最好单独存储注释缓存:


 public function getPostComments(Post $post) { return \Cache::remember('post_comments_' . $post->id, 900, function() use ($post) { return $post->comments; }); } $post = getPost($id); $comments = getPostComments($post); foreach($comments...) 

有时,本质和态度彼此紧密联系在一起,并始终在一起使用(按细节顺序排列,将出版物翻译成所需的语言)。 在这种情况下,将它们存储在一个缓存中是很正常的。


缓存键的唯一事实来源


如果项目实现了无效,则至少在两个位置生成高速缓存密钥:用于调用Cache::get / Cache::remember / Cache::remember以及用于调用Cache::forget 。 我已经遇到过这样的情况,即在一个地方而不是在另一个地方更改了此密钥,并且残障中断了。 在这种情况下,通常的建议是使用常量,但是缓存键是动态生成的,因此我使用特殊的类来生成键:


 final class CacheKeys { public static function postById($postId): string { return 'post_' . $postId; } public static function postComments($postId): string { return 'post_comments' . $postId; } } \Cache::remember(CacheKeys::postById($id), 900, function() use ($id) { $post = Post::findOrFail($id); }); // .... \Cache::forget(CacheKeys::postById($id)); 

为了更好的可读性,还可以将键生存期呈现为常量。 900或15 * 60会增加阅读代码时的认知负担。


不要在写操作中使用缓存


在实现写操作(例如更改发布的标题或文本)时,很容易使用getPost编写的getPost方法:


 $post = getPost($id); $post->title = $newTitle; $post->save(); 

请不要这样做。 即使正确完成了无效,缓存中的值也可能已过时。 竞态条件较小且发布会丢失其他用户所做的更改。 乐观锁将至少有助于避免丢失更改,但是错误请求的数量会大大增加。


最好的解决方案是对读写操作使用完全不同的实体选择逻辑(Hello,CQRS)。 在写操作中,您始终需要从数据库中选择最新值。 并且不要忘记重要数据的锁定(乐观或悲观)。


我认为这对于介绍性文章就足够了。 缓存是一个非常复杂且冗长的主题,为开发人员带来了陷阱,但是有时性能的提高会胜过所有的困难。

Source: https://habr.com/ru/post/zh-CN463495/


All Articles