Redis与Mysql数据一致性

type
status
date
slug
summary
tags
category
difficulty
icon
password
在正式开始之前,需要先取得以下两点的共识:
  1. 缓存必须要有过期时间;
  1. 保证数据库跟缓存的最终一致性即可,不必追求强一致性。
目录:
  • 1. 什么是数据库与缓存一致性
  • 2. 缓存的使用策略
    • 2.1 Cache-Aside (旁路缓存)
    • 2.2 Read-Through(直读)
    • 2.3 Write-Through 同步直写
    • 2.4 Write-Behind
  • 3. 旁路缓存下的一致性问题分析
    • 3.1 先更新缓存,再更新数据库
    • 3.2 先更新数据库,再更新缓存
    • 3.3 先删缓存,再更新数据库
    • 3.4 先更新数据库,再删缓存
  • 4. 一致性解决方案有哪些?
    • 4.1 删除缓存重试机制
    • 4.2 读取 binlog 异步删除
  • 总结

1. 什么是数据库与缓存一致性

数据一致性指的是:
  • 缓存中存有数据,缓存的数据值 = 数据库中的值;
  • 缓存中没有该数据,数据库中的值 = 最新值。
反推缓存与数据库不一致:
  • 缓存的数据值 ≠ 数据库中的值;
  • 缓存或者数据库存在旧的数据,导致线程读取到旧数据。
为何会出现数据一致性问题呢?
把 Redis 作为缓存的时候,当数据发生改变我们需要双写来保证缓存与数据库的数据一致。
数据库跟缓存,毕竟是两套系统,如果要保证强一致性,势必要引入 2PCPaxos 等分布式一致性协议,或者分布式锁等等,这个在实现上是有难度的,而且一定会对性能有影响。
如果真的对数据的一致性要求这么高,那引入缓存是否真的有必要呢?

2. 缓存的使用策略

在使用缓存时,通常有以下几种缓存使用策略用于提升系统性能:
  • Cache-Aside Pattern(旁路缓存,业务系统常用)
  • Read-Through Pattern
  • Write-Through Pattern
  • Write-Behind Pattern
2.1 Cache-Aside (旁路缓存)
所谓「旁路缓存」,就是读取缓存、读取数据库和更新缓存的操作都在应用系统来完成业务系统最常用的缓存策略
2.1.1 读取数据
notion image
时序图
notion image
优点
  • 缓存中仅包含应用程序实际请求的数据,有助于保持缓存大小的成本效益。
  • 实现简单,并且能获得性能提升。
缺点
由于数据仅在缓存未命中后才加载到缓存中,因此初次调用的数据请求响应时间会增加一些开销,因为需要额外的缓存填充和数据库查询耗时。
2.1.2 更新数据
使用 cache-aside 模式写数据时,如下流程。
notion image
  1. 写数据到数据库;
  1. 将缓存中的数据失效或者更新缓存数据;
使用 cache-aside 时,最常见的写入策略是直接将数据写入数据库,但是缓存可能会与数据库不一致。
我们应该给缓存设置一个过期时间,这个是保证最终一致性的解决方案。
如果过期时间太短,应用程序会不断地从数据库中查询数据。同样,如果过期时间过长,并且更新时没有使缓存失效,缓存的数据很可能是脏数据。
最常用的方式是删除缓存使缓存数据失效
为啥不是更新缓存呢?
性能问题
当缓存的更新成本很高,需要访问多张表联合计算,建议直接删除缓存,而不是更新缓存数据来保证一致性。
安全问题
在高并发场景下,可能会造成查询查到的数据是旧值
2.2 Read-Through(直读)
当缓存未命中,也是从数据库加载数据,同时写到缓存中并返回给应用系统。
虽然 read-throughcache-aside 非常相似,在 cache-aside应用系统负责从数据库获取数据和填充缓存。
而 Read-Through 将获取数据存储中的值的责任转移到了缓存提供者身上。
notion image
Read-Through 实现了关注点分离原则。代码只与缓存交互,由缓存组件来管理自身与数据库之间的数据同步。
2.3 Write-Through 同步直写
与 Read-Through 类似,发生写请求时,Write-Through 将写入责任转移到缓存系统,由缓存抽象层来完成缓存数据和数据库数据的更新,时序流程图如下:
notion image
Write-Through 的主要好处是应用系统的不需要考虑故障处理和重试逻辑,交给缓存抽象层来管理实现。
优缺点
单独直接使用该策略是没啥意义的,因为该策略要先写缓存,再写数据库,对写入操作带来了额外延迟。
Write-ThroughRead-Through 配合使用,就能成分发挥 Read-Through 的优势,同时还能保证数据一致性,不需要考虑如何将缓存设置失效
notion image
这个策略颠倒了 Cache-Aside 填充缓存的顺序,并不是在缓存未命中后延迟加载到缓存,而是在数据先写缓存,接着由缓存组件将数据写到数据库
优点
  • 缓存与数据库数据总是最新的;
  • 查询性能最佳,因为要查询的数据有可能已经被写到缓存中了。
缺点
不经常请求的数据也会写入缓存,从而导致缓存更大、成本更高。
2.4 Write-Behind
这个图一眼看去似乎与 Write-Through 一样,其实不是的,区别在于最后一个箭头的箭头:它从实心变为线。
这意味着缓存系统将异步更新数据库数据,应用系统只与缓存系统交互
应用程序不必等待数据库更新完成,从而提高应用程序性能,因为对数据库的更新是最慢的操作
notion image
这种策略下,缓存与数据库的一致性不强,对一致性高的系统不建议使用。

3. 旁路缓存下的一致性问题分析

业务场景用的最多的就是 Cache-Aside (旁路缓存) 策略,在该策略下,客户端对数据的读取流程是先读取缓存,如果命中则返回;未命中,则从数据库读取并把数据写到缓存中,所以读操作不会导致缓存与数据库的不一致。
重点是写操作,数据库和缓存都需要修改,而两者就会存在一个先后顺序,可能会导致数据不再一致。针对写,我们需要考虑两个问题:
  • 先更新缓存还是更新数据库?
  • 当数据发生变化时,选择修改缓存(update),还是删除缓存(delete)?
将这两个问题排列组合,会出现四种方案:
  1. 先更新缓存,再更新数据库;
  1. 先更新数据库,再更新缓存;
  1. 先删除缓存,再更新数据库;
  1. 先更新数据库,再删除缓存。
3.1 先更新缓存,再更新数据库
notion image
3.2 先更新数据库,再更新缓存
在高并发的场景中,多线程同时写数据再写缓存,就会出现缓存是旧值,数据库是最新值的不一致情况。
3.3 先删缓存,再更新数据库
第二步写数据库失败
假设现在有两个请求:写请求 A,读请求 B。
写请求 A 第一步先删除缓存成功,写数据到数据库失败,就会导致该次写数据丢失,数据库保存的是旧值
接着另一个读请 B 求进来,发现缓存不存在,从数据库读取旧数据并写到缓存中。
该方案 pass,因为第一步成功,第二步失败,会造成数据库是旧数据,缓存中没数据继续从数据库读取旧值写入缓存,造成数据不一致,还会多一次 cache。
不论是异常情况还是高并发场景,会导致数据不一致
3.4 先更新数据库,再删缓存
在写数据库阶段失败的话就直返返回客户端异常,不需要执行缓存操作了。
所以第一步失败不会出现数据不一致的情况。
删缓存失败
重点在于第一步写最新数据到数据库成功,删除缓存失败怎么办?
可以把这两个操作放在一个事务中,当缓存删除失败,那就把写数据库回滚。
如果不回滚,那就出现数据库是新数据,缓存还是旧数据,数据不一致了,咋办?
所以,我们要想办法让缓存删除成功,不然只能等到有效期失效那可不行。
使用重试机制。
比如重试三次,三次都失败则记录日志到数据库,使用分布式调度组件 xxl-job 等实现后续的处理。
在高并发的场景下,重试最好使用异步方式,比如发送消息到 mq 中间件,实现异步解耦。
亦或是利用 Canal 框架订阅 MySQL binlog 日志,监听对应的更新请求,执行删除对应缓存操作。
极端场景
notion image
  1. 缓存的过期时间到期,缓存失效。
  1. 线程 A 读请求读取缓存,没命中,则查询数据库得到一个旧的值(因为 B 会写新值,相对而言就是旧的值了),准备把数据写到缓存时发送网络问题卡顿了
  1. 线程 B 执行写操作,将新值写数据库。
  1. 线程 B 执行删除缓存。
  1. 线程 A 继续,从卡顿中醒来,把查询到的旧值写到入缓存。
不要慌,发生这个情况的概率微乎其微,发生上述情况的必要条件是:
步骤 (3)的写数据库操作要比步骤(2)读操作耗时短速度快,才可能使得步骤(4)先于步骤(5)。
缓存刚好到达过期时限。
所以,在用旁路缓存策略的时候,对于写操作推荐使用:先更新数据库,再删除缓存。

4. 一致性解决方案有哪些?

4.1删除缓存重试机制
使用重试机制,保证删除缓存成功。
比如重试三次,三次都失败则记录日志到数据库并发送警告让人工介入。
在高并发的场景下,重试最好使用异步方式,比如发送消息到 mq 中间件,实现异步解耦。
notion image
该方案有个缺点,就是对业务代码中造成侵入,于是就有了下一个方案,启动一个专门订阅 数据库 binlog 的服务读取需要删除的数据进行缓存删除操作。
4.2 读取 binlog 异步删除
notion image
  1. 更新数据库;
  1. 数据库会把操作信息记录在 binlog 日志中;
  1. 使用 canal 订阅 binlog 日志获取目标数据和 key;
  1. 缓存删除系统获取 canal 的数据,解析目标 key,尝试删除缓存。
  1. 如果删除失败则将消息发送到消息队列;
  1. 缓存删除系统重新从消息队列获取数据,再次执行删除操作。

总结

缓存策略的最佳实践是 Cache Aside Pattern。分别分为读缓存最佳实践和写缓存最佳实践。
读缓存最佳实践:先读缓存,命中则返回;未命中则查询数据库,再写到缓存中。
写缓存最佳实践:
先写数据库,再删除缓存;
上一篇
Kafka
下一篇
Kafka原理与实践

评论
Loading...