缓存与数据库一致性系列-02

Posted by Kido on 2018-12-07

《缓存与数据库一致性系列-01》文章,我们提到,“它的一个比较大的缺陷在于刷新缓存有可能会失败,而失败之后缓存中数据就一直会处于错误状态,所以它并不能保证数据的最终一致性”

为了保证“数据最终一致性”,我们引入binlog,通过解析binlog来刷新缓存,这样即使刷新失败,依然可以进行日志回放,再次刷新缓存

写流程:

第一步先删除缓存,删除之后再更新DB,我们监听从库(资源少的话主库也ok)的binlog,通过分析binlog我们解析出需要需要刷新的数据,然后读主库把最新的数据写入缓存。

这里需要提一下:最后刷新前的读主库或者读从库,甚至不读库直接通过binlog解析出需要的数据都是ok的,这由业务决定,比如刷新的数据只是表的一行,那直接通过binlog就完全能解析出来;然而如果需要刷新的数据来自多行,多张表,甚至多个库的话,那就需要读主库或是从库才行

读流程:

第一步先读缓存,如果缓存没读到,则去读DB,之后再异步将数据刷回缓存

方案分析

优点剖析

1. 容灾

写步骤1.4或1.5 如果失败,可以进行日志回放,再次重试。
无论步骤1.1是否删除成功,后续的刷新操作是有保证的

妈耶,怎么就一个优点,讲道理这个其实很常用的,那我们再来看看缺点

缺点剖析

分析缺点之前,我们先来看一下知识点

  1. 对于同一张表的同一条记录的更新,Databus会以串行形式的通知下游服务,也就是说,只有当我们正确返回后,它才会推送该记录的下一次更新。

  2. 对于同一张表的不同记录的更新, Databus会以事件时间为顺序的通知下游服务,但并不会等待我们返回后才推送下一条,也就是说它是非串行的。

  3. 对于不同表,根据其下游的消费速度,不同表之间没有明确的时间顺序。

1. 只适合简单业务,复杂业务容易发生并发问题

这里先来解释一下这里说的“简单业务”是啥意思?

简单业务:每次需要刷新的数据,都来自单表单行

为什么复杂业务就不行呢?我举个例子
我们假设 一个订单 = A表信息 + B表信息

由于A表先变化,经过1,2,3步后,线程1获取了A’B (A表是新数据,B表的老数据),当线程1还没来得及刷新缓存时,并发发生了:

此时,B表发生了更新,经过4,5,6,7将最新的数据A’B’写入缓存,此时此刻缓存数据是符合要求的。

但是,后来线程1进行了第8步,将A’B写入数据,使得缓存最终结果 与 DB 不一致。

缺点1的改进
  • 针对单库多表单次更新的改进:利用事务

当AB表的更新发生在一个事务内时,不管线程1、线程2如何读取,他们都能获取两张表的最新数据,所以刷新缓存的数据都是符合要求的。

但是这种方案具有局限性:那就是一个订单的信息会存储在很多表中,并不是每一次更新都会刷新所有的表,比如再次单独更新C表的操作,并发问题依然会发生。

所以这种方案只针对多表单次更新的情况

  • 针对多表多次更新的改进:增量更新

每张表的更新,在同步缓存时,只获取该表的字段覆盖缓存。

这样,线程1,线程2总能获取对应表最新的字段,而且Databus对于同表同行会以串行的形式通知下游,所以能保证缓存的最终一致性。

这里有一点需要提一下:更新“某张表多行记录“时,这个操作要在一个事务内,不然并发问题依然存在,正如前面分析的

2. 依然是并发问题

即使对于缺点1我们提出了改进方案,虽然它解决了部分问题,但在极端场景下依然存在并发问题。
这个场景,就是缓存中没有数据的情况:

  • 读的时候,缓存中的数据已失效,此时又发生了更新
  • 数据更新的时候,缓存中的数据已失效,此时又发生了更新

这个时候,我们在上面提到的“增量更新”就不起作用了,我们需要读取所有的表来拼凑出初始数据,那这个时候又涉及到读所有表的操作了,那我们在缺点1中提到的并发问题会再次发生

方案总结

适合使用的场景:业务简单,读写QPS比较低的情况
今天这个方案呢,优缺点都比较明显,binlog用来刷新缓存是一个很棒的选择,它天然的顺序性用来做同步操作很具有优势;其实它的并发问题来自于Canal 或 Databus。拿Databus来说,由于不同行、表、库的binlog的消费并不是时间串行的,那怎么解决这个问题呢,篇幅有限,我们后续文章再继续分享

参考文献

1. Canal
2. Databus
3. Databus & Canal 对比