LabHub
学习 学习路径 课程

Redis 与缓存

三种写策略与击穿

在 LabHub 中继续学习

一句话总结

阅读策略和写作策略是相互不同的轴线。而且越受欢迎的键盘,到期时刻就越危险,这一点是Stampede的核心。

概念图: 决定什么时候扔掉 · 用时间泻掉(TTL)。 · 变更时删除。 · 必须知道所有这些键

为什么需要这个?

在谈论缓存模式时,人们会混淆使用三种名称:cache-aside、write-through和write-behind。这些不是相互排斥的选择,而是对不同问题的答案。前者是对谁填充读取的答案,而后两者是对如何传播写入的答案。

cache-aside 是应用程序直接管理缓存。读取时查看缓存,如果没有的话从原件读取并放入缓存。写入时更新原件并删除缓存。重要的是不是更新而是删除——如果两个写入发生冲突,缓存可能会留下旧值,但删除就没有那个风险。

write-through写入时同时更新缓存和原件。因为缓存总是最新的,所以读取速度总是很快。相反,写入速度变慢,甚至填充缓存了一段时间内不会被读取的数据,浪费空间。

write-behind只使用缓存,将原始反映放在以后的放置中。写入速度非常快,可以收集多个写入,但反映前缓存死亡时数据就会消失。只有在可以承受损失或有单独的耐用性装置时才写入。

怎么行动

把三个策略用一张票整理起来的话是这样的。

战略 写作速度 阅读最新性 丢失风险 代表情况
cache-aside 普通 未失效后最新 通用,以读为主
write-through 总是最新 最新性很重要
write-behind 非常快 总是最新(以缓存为基准) 写入狂潮,允许丢失

而且在缓存中最臭名昭著的陷阱是Stampede。每秒钟被读取数千次的人气键到期的那一刻,数千个请求同时出现错误,全部涌向原始数据重新计算相同的值。原本足够的一个查询暴增成数千个,原始数据被压制。最坏的情况是原始数据死亡,出现更多错误,并连锁崩溃。

讽刺的是,越是受欢迎的项目越危险。因为越是经常被阅读的值,到期时的同时错误就越大。

缓解措施有四个。第一,锁定或单航班——在失效时只查询第一个请求的原始数据,其余的等待。第二,TTL波动——分散到期时间。第三,stale-while-revalidate——即使过期也立即返回旧值,并在后台重新更新一次。第四,概率性提前过期——临近过期时,有更高的概率提前重新更新。

在实际工作中,我们组合了:用jitter分散到期,用stale-while-revalidate消除等待,用锁将更新合并为一个。

在现场相遇的样子

使用锁时必须一起携带的两件事是:设置TTL在锁上,即使所有者死亡也能解锁,以及设置所有权代币,防止别人不小心解锁别人的锁。如果遗漏后者的话,A的锁会因TTL到期后被B抓住,但醒来的A会解锁B的锁。

如何设计无效化

在现金中最困难的问题不是填充,而是决定什么时候扔掉。方法有三个,各自承担的复杂度不同。

用时间泻掉(TTL)。 最简单,在大多数情况下足够。只要确定了“可以显示到多少旧值”就可以了,这就是TTL。但是这个问题不是技术问题,而是业务决定,所以如果开发者一个人决定后就过去了,以后就会说“为什么刚才改的看不到”。

变更时删除。 删除与修改原件相关的缓存键。虽然更新了最新性,但必须知道所有这些键才是问题所在。如果一个商品变更,商品详情、列表、搜索结果、推荐列表都会变旧,很难不遗漏散落在各个处的代码之间的关系并保持。

将整个版本全部失效。在键上输入版本号,如果有什么变化,就更新那个号码。旧的键没有人找到,自然消失为TTL。不需要跟踪单独的键,所以更结实,但是一次失效太多,所以紧接着就会出现错误。前面看到的史泰普迪正是这个时候发生的。

在实际工作中,通常会把**TTL放在底层,只选择真正需要立即反映的内容进行删除。**如果试图把所有内容都删除的话,就无法维持,如果全部只用TTL的话,重要的变更就会显得迟钝。哪一个数据是哪一个,是和业务负责人一起决定的,把这个决定写在表格上,以后一半的缓存相关事故就会消失。

最后,即使没有缓存,服务也必须恢复。如果完全清空缓存时,原件无法承受该负载,那么这不是缓存的问题,而是原件的一部分问题。在这种状态下,缓存故障很快就会变成服务故障,所以至少应该知道原件能承受多少。

下次实习要做的事情

针对慢速的原始文件,实现cache-aside,以库存版本键重新激活整个无效化,然后在单独的练习中实际引发快取速度,依次添加四个防御,用数字比较原始文件调用次数。