三种写策略与击穿
一句话总结
阅读策略和写作策略是相互不同的轴线。而且越受欢迎的键盘,到期时刻就越危险,这一点是Stampede的核心。
为什么需要这个?
在谈论缓存模式时,人们会混淆使用三种名称: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,以库存版本键重新激活整个无效化,然后在单独的练习中实际引发快取速度,依次添加四个防御,用数字比较原始文件调用次数。