高并发场景下,Redis与Mysql的数据一致性如何保证

2021/03/25

1、一致性

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

  • 强一致性:这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大
  • 弱一致性:这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态
  • 最终一致性:最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型

2、三个经典策略

在大型系统中,为了减少数据库压力通常会引入缓存机制,一旦引入缓存又很容易造成缓存和数据库数据不一致,导致用户看到的是旧数据。

为了减少数据不一致的情况,更新缓存和数据库的机制显得尤为重要,常用策略有3种:

Cache Aside 旁路缓存

读请求流程

1、应用首先会判断缓存是否有该数据,缓存命中直接返回数据

2、缓存没有命中,从数据库查询数据然后回写到缓存中,最后返回数据给客户端

写请求流程

更新的时候,先更新数据库,然后再删除缓存

有些同学可能要问了:为什么要删除缓存,直接更新不就行了?下面用3个场景说明一下问题

  • 1、先更新数据库,再更新缓存

    如果同时有两个写请求需要更新数据,每个写请求都先更新数据库再更新缓存,在并发场景可能会出现数据不一致的情况

    如上图的执行过程:

    (1)写请求1更新数据库,将 age 字段更新为18;

    (2)写请求2更新数据库,将 age 字段更新为20;

    (3)写请求2更新缓存,缓存 age 设置为20;

    (4)写请求1更新缓存,缓存 age 设置为18;

    执行完预期是数据库 age=20,缓存 age=20,但结果是缓存age=18,这就造成了缓存数据不是最新的,出现了脏数据。

    但是我有个问题:

    如果写请求是有本地事务控制的,db排斥锁(悲观锁)控制的,把更新数据库和更新缓存放在一个事务内,那么写请求2需要等待写请求1完成更新操作。这样可行吗?

    答:不可行,写mysql数据库与写redis缓存是两回事,本地事务控制不可能管控到写数据库与写redis缓存操作是一个原子性的操作,除非加同步锁或分布式锁,让多个写请求来排队处理。

  • 2、先删缓存,再更新数据库

    在一个读请求和一个写请求并发场景下可能会出现数据不一致情况。

    如上图的执行过程:

    (1)写请求删除缓存数据;

    (2)读请求查询缓存未击中(Hit Miss),紧接着查询数据库,将返回的数据回写到缓存中;

    (3)写请求更新数据库。

    整个流程下来发现数据库中age=20,缓存中age=18,缓存和数据库数据不一致,缓存出现了脏数据。

  • 3、先更新数据库,再删除缓存

    在实际的系统中针对写请求还是推荐先更新数据库再删除缓存,但是在理论上还是存在问题,以下面这个例子说明。

    上图的执行过程:

    (1)读请求先查询缓存,缓存未击中,查询数据库返回数据;

    (2)写请求更新数据库,删除缓存;

    (3)读请求回写缓存;

    整个流程操作下来发现数据库age=20,缓存 age=18,缓存和数据库数据不一致,缓存出现了脏数据。。

    但我们仔细想一下,上述问题发生的概率其实非常低,因为通常数据库更新操作比内存操作耗时多出几个数量级,上图中最后一步回写缓存(set age 18)速度非常快,通常会在更新数据库之前完成,也就是说set age 18 在 update age 20前完成,写请求会在最后del age,新的读请求会因为没有命中缓存,穿透到数据库。

    但我们还是要考虑这种极端的场景出现了怎么办?我们得想一个兜底的办法:

    缓存数据设置过期时间,通常在系统中是可以允许少量的数据短时间不一致的场景出现。

Read Through/Write Through读写穿透

在 Cache Aside 更新模式中,应用代码需要维护两个数据源头:一个是缓存,一个是数据库。而在 Read-Through 策略下,应用程序无需管理缓存和数据库,只需要将数据库的同步委托给缓存提供程序 Cache Provider 即可。所有数据交互都是通过抽象缓存层完成的。

Read Through读请求流程

如上图,应用程序只需要与Cache Provider交互,不用关心是从缓存取还是数据库。我们发现Read Through 与Cache Aside读流程是很相似的,就是多了一层Cache Provider, 架构上加了一层,它的优点是:在进行大量读取时,可以减少数据源上的负载,也对缓存服务的故障具备一定的弹性。如果缓存服务挂了,则缓存提供程序仍然可以通过直接转到数据源来进行操作。我在如果Cache是redis,那么Cache Provider会不会是Memcache 这样的本地缓存。

Read-Through 适用于多次请求相同数据的场景,这与 Cache Aside的读流程非常相似,但是二者还是存在一些差别,这里再次强调一下:

  • 在 Cache-Aside 中,应用程序负责从数据源中获取数据并更新到缓存。
  • 在 Read-Through 中,此逻辑通常是由独立的缓存提供程序(Cache Provider)支持。

Write Through 读请求流程

Write-Through 策略下,当发生写请求时,也是由 Cache Provider 负责更新数据源和缓存数据,流程如下:

Write Behind 异步缓存写入

Write behind在一些地方也称为Write back, 简单理解就是:应用程序更新数据时只更新缓存,由Cache Provider每隔一段时间将数据刷新到数据库中,说白了就是延迟写入,而Write Through则是同步更新缓存,流程如下图:

应用程序更新两个数据,Cache Provider 会立即写入缓存中,但是隔一段时间才会批量写入数据库中。

  • 优点:数据写入速度非常快,适用于频繁写的场景。
  • 缺点:是缓存和数据库不是强一致性,对一致性要求高的系统慎用。

总结

  • Cache Aside 通常先更新数据库然后再删除缓存,为了兜底通常还会将数据设置缓存时间TTL。
  • Read/Write Through 由一个 Cache Provider 对外提供读写操作,应用程序不用感知操作的是缓存还是数据库。
  • Write Behind 简单理解就是延迟写入,由Cache Provider每隔一段时间会批量写入数据库,优点是应用程序写入速度非常快。

3、数据库和缓存数据保持强一致,可以吗

实际上,没办法做到数据库与缓存绝对的一致性。这是由CAP理论决定的。缓存系统适用的场景就是非强一致性的场景,它属于CAP中的AP。个人觉得,追求绝对一致性的业务场景,不适合引入缓存

CAP理论,指的是在一个分布式系统中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。只能CP或者AP

4、3种方案保证数据库与缓存的一致性

缓存延时双删

如下图:

  1. 先删除缓存
  2. 再更新数据库
  3. 休眠一会(比如1秒),再次删除缓存。

这个休眠一会,一般多久呢?1秒?休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。

如果第二次删除缓存失败呢?缓存和数据库的数据还是可能不一致,对吧?给Key设置一个自然的expire过期时间,让它自动过期怎样?那业务要接受过期时间内,数据的不一致咯?还是有其他更佳方案呢?

删除缓存重试机制

不管是延时双删还是Cache-Aside的先操作数据库再删除缓存,都可能会存在第二步的删除缓存失败,导致的数据不一致问题,所以我们引入重试机制,流程图如下:

  1. 写请求更新数据库
  2. 缓存因为某些原因,删除失败
  3. 把删除失败的key放到消息队列
  4. 消费消息队列的消息,获取要删除的key
  5. 重试删除缓存操作

读取binlog异步删除缓存

以mysql为例吧

  • 可以使用阿里的canal将binlog日志采集发送到MQ队列里面
  • 然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致性

Post Directory