缓存更新的几种模式

635 阅读4分钟

在高并发场景中,缓存能抵挡大量数据库查询,减少数据库压力,对于缓存更新通常有以下几种模式可以选择:

  • cache aside
  • read/write through
  • write behind caching

cache aside模式

Cache-aside模式是一种常用的用于管理缓存的模式。它用于确保缓存与底层数据源之间的数据一致性。以下是cache-aside模式的工作原理:

  1. 从缓存读取:当有读取操作请求时,应用程序首先检查缓存中是否存在数据。如果在缓存中找到了数据,则将其返回给调用者,避免了访问底层数据源的需要。
  2. 缓存未命中:如果在缓存中未找到数据,则表示缓存未命中。在这种情况下,应用程序从底层数据源检索数据,并将检索到的数据填充到缓存中。
  3. 更新数据:当对数据执行写入或更新操作时,应用程序首先更新底层数据源中的数据。然后,清除缓存中的数据,以确保下一次读取从数据源中检索到更新后的数据。

通常写缓存和写数据库是两个独立的事务,选择先更新缓存还是先更新数据库都有可能产生数据不一致的情况。

先删缓存,再更新数据库的问题

假设有两个请求A、B。

  • 请求A先删除缓存,此时还未更新数据库
  • 请求B查询缓存未命中,然后查询数据库,查出旧数据写入缓存
  • 请求A继续将数据写入数据库
  • 此时缓存与数据库中的数据出现了不一致的情况
sequenceDiagram
actor 请求A
actor 请求B
请求A->>Cache: 1.失效缓存
Cache-->>请求A:成功
请求B->>Cache: 2.查询缓存
Cache-->>请求B:缓存未命中
请求B->>DB: 3.查询db
DB-->>请求B:查询成功
请求B->>Cache: 4.写入缓存
Cache-->>请求B:写入缓存成功
请求A->>DB: 5.更新db
DB-->>请求A:成功

将缓存更新不做删除的问题

  • 请求A先更新了数据库
  • 请求B更新了数据库,并更新了缓存
  • 请求A最后更新缓存,此时请求A的数据是脏数据。
sequenceDiagram
actor 请求A
actor 请求B
participant Cache
请求A->>DB: 1.更新db
DB-->>请求A:成功
请求B->>DB: 2.更新db
DB-->>请求B:成功
请求B->>Cache: 3.更新缓存
Cache-->>请求B:更新缓存成功
请求A->>Cache: 4.更新缓存
Cache-->>请求A:更新缓存成功

先更新db再失效缓存问题

先更新DB,再失效缓存也会出现问题

  • 请求A读取缓存未命中,查询数据库成功查到数据
  • 请求B进来更新数据库成功,并删除缓存数据
  • 请求A将查询的数据写入到缓存中,此时请求A写入缓存的数据已经是脏数据
sequenceDiagram
actor 请求A
actor 请求B
请求A->>Cache: 1.查询缓存
Cache-->>请求A:缓存未命中
请求A->>DB: 2.查询db
DB-->>请求A:查询成功

请求B->>Cache: 3.写入缓存
Cache-->>请求B:写入缓存成功

请求B->>Cache: 4.失效缓存
Cache-->>请求B:成功

请求A->>Cache: 5.写入缓存
Cache-->>请求A:写入缓存成功

read/write through模式

cache aside模式需要应用方维护缓存的读写,对数据和缓存的维护设计侵入代码,代码复杂性增加。read/write through模式弥补了这一问题,调用方无需管理缓存和数据库调用,通过抽象缓存管理组件维护缓存和数据库的读写,解耦业务代码。

read through模式

sequenceDiagram
actor User
participant App as 缓存组件
participant Cache as 缓存
participant DataSource as DB

User ->> App: 读取数据
alt 缓存命中
    App ->> Cache: 读缓存
    Cache -->> App: 返回数据
    
else 数据未命中
    App ->> DataSource: 缓存读取请求
    DataSource -->> App: 返回数据
    App ->> Cache: 写入缓存
    Cache -->> App: 返回数据
end
App -->> User:返回

write through模式

sequenceDiagram
actor User
participant App as 缓存组件
participant Cache as 缓存
participant DataSource as DB

User ->> App: 更新数据
App ->> DataSource: 更新数据库
DataSource -->> App: 成功
App ->> Cache: 写入缓存
Cache -->> App: 成功
App -->> User:返回

write behind caching模式

Write Behind模式和Write Through模式整个架构是一样的,核心在于write through在缓存数据库中的更新是同步的,而Write Behind是异步的。

每次的请求写都是直接更新缓存然后就成功返回,并没有同步把数据更新到数据库。而把更新到数据库的过程称为flush,触发flush的条件可自定义,如定时或达到一定容量阈值时进行flush操作。并且可以实现批量写,合并写等策略,也有效减少了更新数据的频率,这种模式最大的好处就是读写响应非常快,吞吐量也会明显提升。这种模式也有其他的问题,比如数据不是强一致性的,因为把最新的数据放在缓存里,如果缓存在flush到数据库之前宕机了就会丢失数据,另外实现也比较复杂。

sequenceDiagram
actor User
participant Application as 缓存组件
participant Cache as 缓存
participant Database as DB

User ->> Application: 读取数据
Application->>Cache: get(key) 
Cache-->>Application: Success
Application-->>User:success

User ->> Application: 写数据
Application->>Cache: Write(key, value) 
Cache-->>Application: Success
Application-->>User:success

alt flush
Application->>Database: save
Database-->>Application: Success
end

几种模式对比

模式优点缺点
Cache Aside实现比较简单需要应用程序负责缓存的读取和写入操作,代码侵入较大
Read/Write Through引入缓存管理组件,缓存和数据库的维护对应用程序透明;应用代码入侵小,逻辑更清晰引入缓存管理组件,实现更复杂
Write Behind Caching读写直接和缓存交互,异步批量更新数据库,性能最好实现最复杂,数据一致性最弱