缓存一致性策略个人实践心得

164 阅读4分钟

程序员最大的两大难题:变量命名和缓存一致。现在我们来讨论两大难题之一的缓存一致性。

无论什么策略最终都要归到对缓存的三项基本操作上来:读取、写入、删除,即老生常谈的CRUD操作。

读取:包括两部分:用户如何从缓存中读数据,缓存如何从数据源中读数据。

写入:源头的数据更新时缓存的数据如何确保一致性,

删除:缓存的淘汰与过期策略。

技术选型需要在功能实现和成本上进行权衡,因此一个成熟的缓存方案一定要这些方面都能适应业务要求。

最简单粗暴的策略

全量加载+定期全量更新+无淘汰! 不要笑,当你的场景对缓存的实时性要求不是很高,数据总量又不算大时,这个方案几乎就是最佳的。

快速粗暴实现需求,把时间留下来给业务迭代,这在项目初期是绝对合算的。

需要注意几个细节: 定时任务的时间间隔建议使用质数,最好再带一些随机数,以尽可能将数据库的负载进行均摊。如果你的缓存组件不是并发安全的,注意加载数据时的并发问题。

如果全量更新行不通

启动时可以进行一次全量加载,但启动后只能增量更新或者定时更新的缓存实时性满足不了要求,怎么办?

参考设计模式中的观察者模式,即增加一个监听器来监听数据更新,当数据源的数据有更新时,将消息异步发送到缓存中,供缓存进行数据更新。比如监听MySQL binlog。

如果能确保数据库内每一条数据都有一个updated_at字段来维护上次更新的时间,就更方便了,定时使用类似于 "SELECT a,b,c FROM table WHERE updated_at > xxx" 的语句进行增量查询。 (个人建议把每一张数据表都加上created_atupdated_at作为团队的硬性规定)

最八股的策略

如果你有看过任何关于后端面经的文章,一定对这一套不陌生:LRU + Cache-Aside。

而它烂大街也是有它的道理,因为在多数场景下,这就是能用到公司倒闭的“最终方案”了。

通过LRU避免缓存无限增长,自动淘汰数据;通过Cache-Aside实现了当数据更新时缓存中的数据一致性。如果你对缓存的数据一致性有要求,缓存的数据量又需要控制,它就能满足需求。

对这套策略还可以做一些针对性的优化,以适合自己的特殊场景或达到更好的效果。 比如把LRU换成LFU/ARC/TinyLFU 等。

对Cache-aside的优化

与上面的LRU类似,Cache-Aside在绝大多数场景下工作得很好,但也有小的缺陷。原始cache-aside的问题是当数据有Miss时它会立刻去读数据库并补数据。这既直观又实用,为什么是问题呢?

1.如果有用户恶意访问大量不在缓存中的数据,就会造成大量的缓存未命中,因为缓存的读取性能往往比其数据源高得多,这对数据源来说可能就是巨大的负载了。

2.另外如果数据库短时间大量更新,也会导致Cache-Aside策略产生较多的回源。

3.还有一些极端场景是,哪怕有少量正常的回源,导致的时延上升在业务场景内也无法接受。

4.超高频的热点数据。比如有明星离婚,访问这条微博消息的请求量突然暴增几千倍。

怎么办?

问题1:关键是过滤重复的大量请求。可以使用去重buffer的方法,比如所有的更新请求并不是立刻执行,而是先推到一个任务buffer(queue)中,buffer每秒会对其中的任务进行汇总去重,再交给数据库执行。

问题2:八股文中另一个经典的策略 Read/Write through就有用了。这个策略是让所有的更新也由缓存组件来接管,就避免了更新带来的回源

问题3:可能只有全量加载到内存中这一条路。

问题4:这种热点数据会造成两个问题:

一是在分布式缓存下,大量请求会集中到一个分片中,这时需要识别到这个热点key, 通过加后缀等方式把它打散到多个分片下。识别热点key的方式可以通过监听监控数据的方式实现,也看到一些大厂在缓存中间件里实现了热点自动识别+打散的功能。

二是整体缓存集群/机器的负载都扛不住时,需要熔断+扩容。很多人一讨论到热点问题就只说打散热点,但如果这个热点key的负载超过整个集群的能力,打散也是把整个集群打崩。这时流量治理是更重要的。