缓存一致性

谈谈一致性

一致性就是数据保持一致,在分布式系统中,可以理解为多个节点中数据的值是一致的。

  • 强一致性:这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大

  • 弱一致性:这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态

  • 最终一致性:最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型

场景:秒杀

  • 跳过分布式复制,直接就在单节点完成读写,这是保证强一致性最好的办法。

  • 性能上需要垂直切分数据,分散各节点压力。也就是每个 Redis 都是作为主节点存在,将需要秒杀的商品,做数据上的切分,由每个 Redis 承担一部分商品的读写访问,那么这个过程就是分布式计算中无共享的并行化模式了,适合使用 Redis cluster 架构

场景:数据库 + 缓存

理论上讲,给缓存设置过期时间,就能保证最终一致性。

1. 先更缓存,再更数据库

  1. 如果更数据库失败,需要自行实现缓存的回滚逻辑,很麻烦

  2. 和下面一样的并发问题

2. 先更数据库,再更缓存

2.1. 更新失败后的回滚

如果更缓存失败,会导致缓存和数据库数据不一致。需要业务增加重试逻辑

2.2. 并发场景下的顺序问题

如果同时有请求A和请求B进行更新操作,那么以下场景会出现脏数据:

  1. 线程A更新了数据库

  2. 线程B更新了数据库

  3. 线程B更新了缓存

  4. 线程A更新了缓存

2.3 业务场景角度

如果读较集中于部分热点数据,则大量缓存不会被读到,更新这些缓存浪费资源

2.4 解决

要解决 2.2 并发下的顺序问题,想办法保证操作的顺序就可以了。像上面的场景里,起个线程 C 通过阿里的 canal 中间件从 DB binlog 同步数据到缓存,就可以保证顺序。

另一种思路是在更新数据时从缓冲中删除数据,这种方案叫【Cache-Aside】。设计如下:

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。

  • 命中:应用程序从cache中取数据,取到后返回。

  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

但先删缓存,再更新数据库。还是先更新数据库,再删缓存?

3. 先删缓存,再更数据库

该方案会导致不一致的原因是:同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:

  1. A删缓存

  2. B读不到缓存,从 DB 加载旧数据到缓存

  3. A更新 DB

由于读比写的速度快,所以步骤2一般比步骤3快

  • 可以采用延时双删策略,即第一步删完缓存后,睡眠过一会儿再删一次

采用这种同步淘汰策略,吞吐量降低怎么办?

  • 睡眠和第二次删除新起线程来做

4. 先更数据库,再删缓存

facebook 在论文《Scaling Memcache at Facebook》中提出,他们用的也是先更新数据库,再删缓存的策略。

该方案也有不一致问题:

  1. A查缓存,缓存刚好失效;A从DB查出旧数据

  2. B更数据库并删除缓存

  3. A加载旧数据进缓存

不过一般A读的速度原高于B写的速度,即步骤3通常早于步骤2,所以这种情况很难出现。

假设,有人非要抬杠,有强迫症,一定要解决怎么办?

首先,给缓存设有效时间是一种方案。其次,采用异步延时删除策略,保证读请求完成以后,再进行删除操作。

这个方案还有一种做法:单起一个线程C,通过阿里 canal 中间件消费 DB binlog,消费后不是更新缓存,而是删除缓存。B站的评论就是这么做的。

也有问题:1. A读取旧数据(但还没写到缓存)、2. C从缓存删除数据、3. B读取新数据到缓存、4. A写旧数据到缓存。不过同样几率很小,步骤4 一般都早于3

删缓存失败的解决方案

这是3和4都存在的一个问题。要解决,提供一个保障的重试机制即可。

  1. 写压力不大的场景,将删除失败的数据直接放入内存里,单起一个线程不断重试即可

  2. 删除失败的数据,放入消息队列里进行重试

    • 删除失败属于很少见的 case,消息队列里不会有太多数据

    • 为此引入消息队列,会对业务线代码造成大量的侵入

  3. 接数据库 binlog

    • mysql 有个阿里中间件叫 canal,可以完成订阅 binlog 日志的功能

    • 另起一段非业务代码,同步 binlogredis

参考

读字节 - 对于秒杀、抢购等场景,主从结构,多个redis之间同时读写,怎么确保数据一致性?

Last updated