阅读 398

数据库和缓存强一致性设计

一般来说,一个业务会经历一下几个阶段

单库

image.png
刚开始流量较小,读写请求都落到同一个库无压力

多库

随着业务发展,单库QPS逐渐变高,甚至出现读瓶颈。这时候可以将数据库读写流量分发到多个实例

分库分表

image.png

 基于路由策略(比如对主键求hash),将读写请求落到指定库中。业务可以通过集成比较成熟的中间件降低复杂度:如ShardJdbc(推荐),Mycat等

但是这并不是一颗完美的银弹,首先需要考虑如何设计分库,避免将热点数据落到同一个实例。另外DML逻辑变得复杂,比如排序,分布式事务等

读写分离

image.png

如果业务存在读多写少的情况,可以采用该方式: 数据库采用一主多从部署模式,写流量落到主库,读流量落到从库。

随之引入的是主从延迟问题。若需要追求强一致性,可参考地址

数据库+缓存

首先将读写请求分开看

读请求

先读缓存,缓存读不到就读数据库,并设置缓存

写请求

缓存的写操作分为两种: 删除缓存和更新缓存。
由于操作数据库和操作缓存不具备原子性,还需要考虑两者的先后操作顺序。
所以需要分四种情况考虑:

  • 更新缓存

- 1. 先写数据库再更新缓存

反例: 数据库写成功但缓存更新失败

- 2. 先更新缓存再写数据库

反例: 缓存更新成功但数据库写失败,读请求会查询到脏数据。
即使在数据库写失败后追加缓存还原操作,也会存在缓存更新失败的问题。

  • 删除缓存

- 3. 先写数据库再删除缓存

反例: 数据库写成功缓存删除失败

- 4. 先删除缓存再写数据库

避免了方案(2)由于数据库写失败但缓存更新成功引入的脏数据问题。不过由于删除缓存和写数据库操作不具备原子性,可能会出现读写请求并行引起的不一致问题


示例: 原始数据key=X,现在需要将数据改为key=Y

image.png

  1. A删除缓存
  2. B读取缓存,由于缓存已经被A删除,读取数据库并设置缓存key=X
  3. A写数据库key=Y

为了解决这个问题,可以引入分布式锁

  • 写请求

image.png 先获取锁,获取成功再删除缓存并更新数据库,最后释放锁。

  • 读请求

image.png

读到缓存数据直接返回; 读不到缓存数据,先尝试获取锁

如果获取锁失败,说明存在并发写,读完数据库直接返回数据。
如果获取锁成功,说明不存在并发写,先读数据库再设置缓存,最后释放锁。

当然这时候可能有人会说,如果标识过期但数据库还没更新完怎么办,所以锁需要支持自动续期(可参考redisson的实现)

让数据库操作和缓存操作具备原子性

基于数据库binlog并更新或删除缓存,即使缓存操作失败,可以一直重放。常见的消费如Kafka,阿里Canal等。这里不做细究。

但如果消费端存在多实例部署,在修改一张表的多行记录或多张表的记录时会出现乱序情况。

文章分类
后端
文章标签