缓存技术使您可以创建更多可扩展的应用程序,将某些查询的结果存储在快速的内存中。 但是,实施不正确的缓存会大大降低用户对应用程序的印象。 本文包含一些有关缓存的基本概念,从过去的几个项目中学到的各种规则和禁忌。
不要使用缓存。
您的项目速度很快,没有性能问题吗?
不用缓存了。 认真地:)
这将极大地复杂化从数据库进行的读取操作,而没有任何好处。
没错,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%"
不是很困难。
雄辩的事件会有所帮助,或者如果您的应用程序生成特殊事件(如PostPublished
或UserBanned
它甚至会更加简单。 口才事件示例。 首先,您需要创建事件类。 为了方便起见,我将为它们使用一个抽象类:
abstract class PostEvent { 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::()->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); });
为了更好的可读性,还可以将键生存期呈现为常量。 900或15 * 60会增加阅读代码时的认知负担。
不要在写操作中使用缓存
在实现写操作(例如更改发布的标题或文本)时,很容易使用getPost
编写的getPost
方法:
$post = getPost($id); $post->title = $newTitle; $post->save();
请不要这样做。 即使正确完成了无效,缓存中的值也可能已过时。 竞态条件较小且发布会丢失其他用户所做的更改。 乐观锁将至少有助于避免丢失更改,但是错误请求的数量会大大增加。
最好的解决方案是对读写操作使用完全不同的实体选择逻辑(Hello,CQRS)。 在写操作中,您始终需要从数据库中选择最新值。 并且不要忘记重要数据的锁定(乐观或悲观)。
我认为这对于介绍性文章就足够了。 缓存是一个非常复杂且冗长的主题,为开发人员带来了陷阱,但是有时性能的提高会胜过所有的困难。