缓存的读写策略

Cache Aside(旁路缓存)策略

这个策略数据以数据库中的数据为准,缓存中的数据是按需加载的。它可以分为读策略和写策。

读策略步骤:

  • 从缓存中读取数据;
  • 如果缓存命中,则直接返回数据;
  • 如果缓存不命中,则从数据库中查询数据;
  • 查询到数据后,将数据写入到缓存中,并且返回给用户。

写策略步骤:

  • 更新数据库中的记录;
  • 删除缓存记录(必须在更新数据库后面)。

这样的读写策略能解决大部分并发更新时缓存数据不一致的问题也正说明他不适用于写入频繁的场景,但是还是会出现一种问题

例如:用户数据在缓存中不存在,请求 A 读取数据时从数据库中查询到年龄为 20,在未写入缓存中时另一个请求 B 更新数据。它更新数据库中的年龄为 21,并且清空缓存。这时请求 A 把从数据库中读到的年龄为 20 的数据写入到缓存中,造成缓存和数据库数据不一致。

如果你的业务对缓存命中率有严格的要求,那么可以考虑两种解决方案:

  1. 一种做法是在更新数据时也更新缓存,只是在更新缓存前先加一个分布式锁解决并发问题。
  2. 另一种做法同样也是在更新数据时更新缓存,只是给缓存加一个较短的过期时间。

Read/Write Through(读穿 / 写穿)策略

这个策略的核心原则是用户只与缓存打交道,由缓存和数据库通信,写入或者读取数据

读穿策略:

  • 查询缓存中数据是否存在

    • 如果存在则直接返回。

    • 如果不存在,则由缓存组件负责从数据库中同步加载数据。

      亮点方案可以使用异步更新

写穿策略:

  • 查询要写入的数据在缓存中是否已经存在
    • 如果已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中
    • 如果缓存中数据不存在,则Write Miss(写失效)
      • 按写分配:写入缓存相应位置,再由缓存组件同步更新到数据库中
      • 不按写分配(常用):不写入缓存中,而是直接更新到数据库中

这种策略相对来说比较少见,因为主流的缓存中间件很少有提供直接写入数据库/自动加载数据库中的数据的功能。

Write Back(写回)策略

这个策略的核心思想是在写入数据时只写入缓存,并且把缓存块儿标记为“脏”的。而脏块儿只有被再次使用时才会将其中的数据写入到后端存储中

写策略:

image-20230427220119946

读策略:

  • 判断是否命中缓存
    • 发现缓存命中则直接返回缓存数据
    • 如果缓存不命中则寻找一个可用的缓存块儿判断是否是脏的(使用setnx)
      • 如果是脏的把缓存块儿中之前的数据写入到后端存储中,并且从后端存储加载数据到缓存块儿
      • 如果不是脏的,则由缓存组件将后端存储中的数据加载到缓存中,最后我们将缓存设置为不是脏的,返回数据就好了

这种策略不能被应用到我们常用的数据库和缓存的场景中,但是无论是操作系统层面的 Page Cache,还是日志的异步刷盘都是采用的这种策略。

Refresh Ahead

Refresh Ahead 是指利用 CDC(Capture Data Change)接口异步刷新缓存的模式。这种模式在实践中也很常见,比如说利用 Canal 来监听数据库的 binlog,然后 Canal 刷新 Redis。这种模式也有缓存一致性的问题,也是出在缓存未命中的读请求和写请求上。

image-20231228111349786

一致性同样使用setnx解决

Singleflight

Singleflight 主要是为了控制住加载数据的并发量。Singleflight 模式是指当缓存未命中的时候,访问同一个 key 的线程或者协程中只有一个会去真的加载数据,其他都在原地等待。

image-20231228111556561

一致性解决办法

延迟双删

延迟双删类似于删除缓存的做法,它在第一次删除操作之后设定一个定时器,在一段时间之后再次执行删除。这样能避开删除缓存中的读写导致数据不一致的场景。

缺点:延迟双删因为存在两次删除,所以实际上缓存命中率下降的问题更加严重。

image-20231228112002990