System Design: Cache

 29th March 2021 at 12:57pm

缓存无从不在,CPU 就有寄存器、L1 L2 缓存,内存经常作为磁盘的缓存,Redis 作为 RDBMS 的缓存,等等。

什么时候用缓存

  1. 取数据慢时,比如 Redis 可以比 RDBMS 显著减少时延
  2. 取数据成本高时,比如 RDBMS 联表操作耗费性能大,且面对高 QPS 要求时不易满足;使用 Redis 做缓存则可以显著 提升吞吐量
  3. 请求集中在一部分热点数据上时(即 time-based locality)
  4. 请求集中在一小块区域的数据上时(即 space-based locality),比如数据库的相邻行是存储在一起的,此时 CPU 顺序读可以达到比较好的性能

缓存系统设计目标:优化延时及吞吐量。这要求 cahce hit 显著多于 cache miss。

读写策略

场景是 cache server 作为 database 的缓存。

Amazon ElastiCache 给的 建议 是,使用 lazy loading 配合 write through

Lazy loading

Lazy loading 指读数据时,如果 cache miss 则从数据库拿数据,并更新缓存。

好处:

  • 仅有被请求的数据被缓存,不需要缓存全部数据浪费空间
  • Cache server 崩溃、换新时没有影响,从数据库中再捞数据写进来就好

坏处:

  • Cache miss 的成本很高,需要 3 round trip
  • 数据陈旧:数据如果没有过期,就不会从数据库中刷新;这个可以结合下面的 write-through 策略解决

Write-through

Write-through 指每当更新数据库时,同时更新缓存。

好处:

  • Cache 中的数据永远不是陈旧的
  • 写的延时增加了,但是读的延时会减少(cache miss 出现的概率降低了)

坏处:

  • 缓存中丢失数据:如果一个缓存 server 重启或者新的 server 上线,此时缓存为空,无法起作用。这可以通过结合上述 lazy loading 机制解决
  • 没有被使用的数据也被写入了缓存,浪费了空间。这可以通过设置 TTL(time to live)来解决

小结一

使用 lazy loading 配合 write-through 是比较好的策略。同时应该给缓存内容设置 TTL。

异步更新

国内有一些实践采用异步更新缓存的方式。写完数据库后,可能有几种方式来更新缓存:

  • 通过往消息队列发送更新消息,再由 worker 将其更新入缓存
  • 编写代码作为数据库从机,通过 binlog 来感知数据库变化并更新缓存,比如 alibaba/canal 方案。京东有文章表示他们使用此方案

缓存作为代理层

使用这种方案时,读写均对缓存 server 进行(可以是自己实现的,而非使用 Redis 等通用组件),再由缓存对数据库做修改。这种方案很少见,实现复杂且丢失了 RDBMS 的优势。

缓存置换策略

指缓存容量有限时,采用怎样的策略来决定缓存什么内容、什么内容会被置换出缓存。

常用的有 LRU(least recently used)、LFU(lest frenquently used)以及它们的结合版 LFRU(least frequent recently used)。没有深究。

常见问题

这几种场景在中文技术文档中经常出现,但是我没有找到对应的英文内容。

缓存雪崩

缓存雪崩指的是大量缓存在同一时间全部失效,使得该时间 cache miss 增多,数据库流量突增。解决方法:

  • 设置缓存失效时间时,针对不同的 key 设置不同的时间,避免同时失效。比如在固定的时长(如 5min)基础上加上随机的时长(0 ~ 2.5min)
  • 程序实现上,使得有线程在更新某 key 的缓存时,其他要访问该 key 的线程 blocking wait

缓存击穿

缓存击穿跟缓存雪崩很像,雪崩是大量 key 同时失效,击穿是一个热点 key 失效,一样会造成大量请求到数据库。解决方法类似:

  • 程序实现上,使得有线程在更新某 key 的缓存时,其他要访问该 key 的线程 blocking wait

缓存穿透

指大量访问不存在的 key 造成数据库压力上升。解决方法:

  • 使用布隆过滤器,快速检测 key 有无存在数据库;或者
  • 即使该 key 不存在,也在缓存上放入该 key 并置其值为空,并设置一个短的 TTL;这样仍能挡住一小段时间的频繁访问
  • 应用层先判断该 key 是否合法(比如 key pattern 不符,应该是数字却传了个字符串),如果不合法就挡住,不再访问缓存 server 及数据库;也可应用类似防 DDOS 方法来挡住恶意攻击

参考