Cache && DB 数据一致性

Redis、MySQL 如何保证数据一致性?

一般情况下,Redis 是用来实现应用和数据库之间的一个读操作的缓存层。主要目的是去减少数据库的 IO ,还可以提升数据库的 IO 性能。

整体架构:

当应用程序需要去读取某个数据的时候,首先会尝试 Redis 里面去加载,如果命中了就直接去加载,直接返回数据,如果没有命中,就查询数据库,然后再将查询到的数据写入到 redis 缓存中。

在这个架构里面呢,会出现数据一致性的问题

一份数据同时被保存在 redis 和 mysql 里面,当某个数据需要被更新的时候,由于更新数据是具有前后顺序的,它并不像 mysql 中的多表事务操作,可以满足 ACID 的特性。

常规解决办法只有两种:

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

    该种方案,若出现缓存失败的话,会出现数据不一致的问题。

  2. 先删除缓存,再更新数据库。

    该种方案,是借助再次访问该数据的时候,发现 redis 里的数据为空,然后查询数据库再次加载。但是这两个操作并不是原子操作,所以在这个过程中,如果出现其他线程来访问,还是会存在数据不一致的问题。

那么,需要在极端情况下仍要保证 redis 和 mysql 数据一致性,就需要采用 最终一致性的方案。

例如 基于 MQ 的可靠性消息通信来实现数据最终的一致性。

或者,直接通过 Canel 组件,监控 mysql 中的 bingo 的日志,将更新后的数据同步到 Redis 里面。

接下来,就聊一下各种缓存方案…

一致性(consistency)

一致性就是缓存和数据库存储数据的两份数据保持一致性

  • 强一致性要求:

    所谓强一致性,就是对于 app 来说,缓存和数据库存储的数据读写是符合原子性的,要求读写的一致性,实现起来时对系统的影响大。

  • 弱一致性:

    这部分对于缓存层和数据库层数据一致性要求较低,不要求在更新数据时,缓存和数据库立即同步更新的情况,也不会要求缓存和数据库多久达到一致性,但是会尽可能保证
    到某个时间级别后,数据能够达到一致性状态。

  • 最终一致性:

    最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。

三种经典的缓存模式

  • Cache-Aside Pattern
  • Read-Through/Write through
  • Write behind

Cache-Aside 读流程

旁路缓存模式,它的提出是为了尽可能地解决缓存与数据库的数据不一致问题

Cache-Aside 读流程

  1. 读取数据的时候,先读缓存,缓存命中时,直接返回数据。
  2. 缓存未命中时,读取数据库,取数据的时候同时更新数据到缓存中,返回响应

Cache-Aside 写流程

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

Read-Through

Read/Write Through 模式中,服务端把缓存作为主要数据存储。应用程序跟数据库缓存交互,都是通过抽象缓存层完成的

Read-Through 的思想是采用的将缓存作为主要的存储结构,是从性能的角度出发。

Write-Through

这种模式下,当发生请求时,是由缓存抽象层完成数据源和缓存数据的更新

Write behind (异步缓存写入)

Write behind 和 Read-Through/Write-Through 相似,都是由 Cache Provider来负责缓存和数据库看的读写。
但是 前者是同步更新缓存和数据的,Write Behind 则只是更新缓存,不直接更新数据库,通过批量异步的方式来更新数据库。

三种模式的比较

Cache Aside 更新模式实现起来比较简单,但是需要维护两个数据存储:

  • 缓存(Cache)
  • 一个是数据库(Repository)

Read/Write Through 的写模式需要维护一个数据存储(Cache Provider),实现起来较为复杂一些。

Write Behind Caching 更新模式和 Read/Write Through 更新模式类似,区别是 Write Behind Caching 更新模式的数据持久化操作是异步的,但是`Read/Write Through 更新模式的数据持久化操作是同步的

Write Behind Caching 的优点是直接操作内存速度快,多次操作可以合并持久化到数据库。缺点是数据可能会丢失,例如系统断电等等。

Cache-Aside 问题

我们在更新数据的时候,Cache-Aside 是删除缓存呢,还是应该更新缓存?

我们在操作缓存的时候,到底是应该删除缓存还是说更新缓存呢?我们先来看个例子:
多线程情况:

以上情况就可以看出来问题:

线程 A 先发起一个写操作,先更新数据库。
线程 B 再发起写操作,更新数据库。

现在由于网络原因,线程 B 的更新缓存的操作却在 A 之后发生。
此时缓存中的数据就是与数据库中不一致了。

更新缓存相对于删除缓存还有两点劣势

  1. 若写入缓存的值,是经过复杂计算才得到,更新频率较高的情况下,十分浪费性能。
  2. 在写多读少的情况下,也会十分浪费性能。

双写的情况下,先操作数据库还是先操作缓存?

如果遇到多线程情况,也会有类似情况,出现缓存中存储的是脏数据。因此 Cache-Aside 缓存模式,选择了先操作数据库而不是先操作缓存

两种方案保证数据库与缓存的一致性

  • 删除缓存重试机制
  • 读取 binlog 异步删除缓存

删除缓存重试机制

多次删除确保 cache 中存储的是正确的数据

删除缓存重试机制的大致步骤:

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

同步 binlog 异步删除缓存

重试删除缓存机制还可以,就是会造成好多业务代码入侵。
其实,还可以通过数据库的 binlog 来异步淘汰 key

以 mysql 为例,可以使用阿里的 canal 将 binlog 日志采集发送到 MQ 队列里面,然后编写一个简单的缓存删除
消息者订阅 binlog 日志,根据更新 log 删除缓存,并且通过 ACK 机制确认处理这条更新 log,保证数据缓存一致性。

总结:
综上所述,在分布式系统中,缓存和数据库同时存在时,如果有写操作的时候,「 先操作数据库,再操作缓存 」

参考:「 https://www.cnblogs.com/crazymakercircle/p/14853622.html

-------------THANKS FOR READING-------------