Code前端首页关于Code前端联系我们

Redis与MySQL数据一致性问题:探索缓存工作机制及缓存一致性解决方案

terry 2年前 (2023-09-26) 阅读数 45 #数据库

Redis拥有强大的数据读写能力,广泛应用于缓存场景。第一,可以提高业务系统的性能;其次,数据库能够承受大并发流量请求。

Redis作为缓存组件,必须防止出现以下问题,否则可能会造成生产事故。

  • Redis缓存满了怎么办?
  • 如何解决缓存入侵、缓存崩溃、缓存雪崩?
  • Redis数据过期后会立即删除吗?
  • Redis 突然变慢。如何排查和解决问题?
  • 如何处理Redis和MySQL之间的数据一致性问题?

今天“码哥”就和大家一起彻底考察缓存的工作机制以及缓存一致性的解决方案

在本文正式开始之前,我想我们需要就以下两点达成共识:

  1. 缓存必须有有效期;
  2. 保证数据库和缓存的最终一致性就足够了,不需要强一致性。 。

目录如下:

[toc]

1。什么是数据库和缓存一致性?

数据一致性是指:

  • 缓存中有数据,缓存中数据的值=数据库中的值;
  • 缓存中没有数据,数据库中的值=最新值。

反向缓存与数据库不一致:

  • 缓存的数据值≠数据库中的值;
  • 缓存或数据库中有旧数据,因此线程读取旧数据。

为什么会出现数据一致性问题?

使用Redis作为缓存时,当数据发生变化时,我们需要写入两次才能使缓存与数据库中的数据保持一致。

数据库和缓存毕竟是两个系统。如果要保证强一致性,就需要引入2PC或者Paxos等分布式一致性协议,而且实现起来肯定比较困难,或者分布式Walock等。它们会影响性能。

如果对数据一致性的要求真的这么高,真的有必要实现缓存吗?

2。缓存使用策略

在使用缓存时,通常有以下几种缓存使用策略来提高系统性能:

  • Cache-Aside Pattern(Bypass Cache,业务系统中常用)
  • Overwrite 模式
  • 覆盖模式示例

2.1 缓存旁路

所谓“旁路缓存”对数据库读缓存的缓存以及在应用系统中完成的读缓存操作用得最多业务系统中的缓存策略

2.1.1 读取数据

Redis 与 MySQL 数据一致性问题:探索缓存工作机制和缓存一致性应对方案

读取数据 逻辑如下:

  1. 如果应用需要从数据库读取数据,首先检查是否找到了缓存的数据。
  2. 如果缓存未命中,则查询数据库获取数据,并将数据写入缓存,这样后续读取的相同数据就会被缓存,最后将数据返回给调用者。
  3. 如果到达缓存,则直接返回。

时序图如下:

Redis 与 MySQL 数据一致性问题:探索缓存工作机制和缓存一致性应对方案

优点

  • 缓存仅包含应用程序实际请求的数据,有助于使缓存大小具有成本效益。
  • 实施简单并且可以提高性能。

实现的伪代码如下:

String cacheKey = "公众号:码哥字节";
String cacheValue = redisCache.get(cacheKey);
//缓存命中
if (cacheValue != null) {
  return cacheValue;
} else {
  //缓存缺失, 从数据库获取数据
  cacheValue = getDataFromDB();
  // 将数据写到缓存中
  redisCache.put(cacheValue)
}

缺点

由于只有在缓存未命中后才将数据加载到缓存中,因此初始调用数据请求的响应时间会产生一些开销。需要进一步填充缓存并且数据库查询需要时间。

2.1.2 更新数据

在模式下预留缓存,流程如下。

Redis 与 MySQL 数据一致性问题:探索缓存工作机制和缓存一致性应对方案

  1. 将数据写入数据库;
  2. 使缓存数据无效或更新缓存数据;

使用cache 时最常见的写入策略是直接将数据写入数据库,但缓存可能与数据库不兼容。

必须为缓存设置过期时间。这是确保最终一致性的解决方案。

如果过期时间太短,应用程序会不断从数据库中查询数据。同样,如果过期时间太长,在没有使缓存失效的情况下刷新缓存,则缓存中的数据很可能是脏的。

最常用的方法是擦除缓存,使缓存数据失效

为什么不刷新缓存?

性能问题

如果缓存更新成本很高,需要访问多个表进行共同计算,建议直接清缓存,而不是更新缓存数据以保持一致性。

安全问题

大并发场景下,查询到的数据可能是旧值。码哥稍后会分析细节,所以不用担心。

2.2 Read-through(直读)

当缓存未命中时,数据也会从数据库中加载,写入缓存,返回到应用系统。

虽然 通读 和 缓存预留

非常相似 至 应用系统负责从 获取数据数据库和缓存,上传。

而 Read-Through 将获取数据存储中的值的责任转移给了缓存提供者。

Redis 与 MySQL 数据一致性问题:探索缓存工作机制和缓存一致性应对方案

Read-Through 实现了关注点分离的原则。代码只与缓存交互,缓存组件处理自身与数据库之间的数据同步。

2.3 重写同步直写

与read-through类似,当写请求发生时,Write-Through将写职责转移给缓存系统,由缓存抽象层完成缓存数据和数据库的更新。 data,时序流程图如下:

Redis 与 MySQL 数据一致性问题:探索缓存工作机制和缓存一致性应对方案

Rewrite 主要优点是应用系统不需要考虑错误处理和重试逻辑,而是交给缓存抽象层处理。控制实施。

优缺点

直接使用该策略本身没有任何意义,因为该策略需要先写入缓存,再写入数据库,这会给写入操作增加额外的延迟。

Rewrite可读组件一起使用时,♹♹通读,同时还确保数据一致性。无需考虑如何使缓存设置失效。

Redis 与 MySQL 数据一致性问题:探索缓存工作机制和缓存一致性应对方案

此策略颠倒了Cache-Aside 的填充顺序。缓存未命中后,不会延迟加载到缓存,而是先将数据写入缓存,然后缓存组件将数据写入写入数据库。

优点

  • 缓存和数据库数据始终是最新的;
  • 查询性能是最好的,因为你要查询的数据可能会被缓存。

缺点

不经常请求的数据也会写入缓存,这会导致缓存更大且更昂贵。

2.4 书写背后

乍一看,这张图片似乎与抄本相同,但其实不然。 区别在于最后一个箭头的箭头:它从实线变成了线。

这意味着缓存系统异步更新数据库数据,应用系统只与缓存系统通信。

应用程序不必等待数据库更新完成,这提高了应用程序性能,因为更新数据库是最慢的操作。

Redis 与 MySQL 数据一致性问题:探索缓存工作机制和缓存一致性应对方案

该策略,缓存与数据库的一致性不强,不建议用于高一致性系统。 ?读取过程是先读取缓存,命中则返回;如果省略,则从数据库读取数据并写入缓存,因此读操作不会导致缓存和数据库不一致。

最底线是,对于写操作,数据库和缓存都要修改,而且两者之间会有顺序,可能会导致数据不一致。写起来,我们需要考虑两个问题:

  • 我们应该先更新缓存还是数据库?
  • 当数据发生变化时,你决定更改缓存(更新)还是清除缓存(删除)?

将这两个问题排列组合起来,提供了四种解决方案:

  1. 先更新缓存,再更新数据库;
  2. 先更新数据库,再更新缓存;
  3. 先清除缓存,再刷新数据库;
  4. 先更新数据库,再清除缓存。

下面的分析不需要背。关键是在减法过程中,只需要考虑以下两种情况是否会导致严重问题:

  • 第一个操作成功,第二个成功。该错误会导致什么问题?
  • 高并发情况下读取的数据会不一致吗?

为什么不想想第一个失败第二个成功的情况呢?

你觉得怎么样?

由于第一个失败,所以第二个不需要执行。只需在第一步返回 50x 等异常信息,就不会出现任何不一致的问题。

只有第一次成功,第二次失败就让人头疼了。如果要保证它们的原子性,那么这就包括分布式事务的范畴。

3.1 先更新缓存,再更新数据库数据。

之后,其他查询者会立即收到此数据,但数据库中不存在此数据。

对于数据库中不存在的数据,将其放入缓存并发送回客户端是没有意义的。

程序直接通过

3.2 先更新数据库,再更新缓存

一切都是这样的:

  • 先写数据库,成功;
  • 然后刷新缓存,成功。

更新缓存失败

目前我们总结一下,如果这两个操作的原子性被破坏了:第一步成功,第二步失败

数据库是最新的数据,而缓存的是旧数据,这就造成了一致性问题。

我不会画这张图。与上图类似,只是Redis和MySQL的位置需要改变。

高并发脚本

谢八哥经常上996,背脖子都疼,写错了越写越多,想要按摩一下提高编程能力。

受疫情影响订单难以到达。高端俱乐部的技术人员试图接下这个订单。很好的协议,兄弟们。

进店后,接待将客户资料录入系统,并进行xx=待定的设定服务技术人员。初始值表示当前无人可用,保存在数据库和缓存中,稍后会安排技术人员进行按摩。供应。

如下图所示:

Redis 与 MySQL 数据一致性问题:探索缓存工作机制和缓存一致性应对方案

  1. 技术员98先出击,发送命令设置谢巴格服务技术员=98到系统。在此期间,系统网络出现波动。 ,卡住了,数据没有时间写入缓存
  2. 之后,技术员520也发送设置谢霸哥维修技师=520到系统中,写入数据库,同时也将这个数据写入缓存。
  3. 此时,技术员98之前的写缓存请求开始,数据设置谢巴格的服务技术员=98成功写入缓存。

最终发现数据库值 = 设置谢八哥维修技师 = 520 缓存值 = 八哥维修技师 = 520 。

技术员 520 的最新数据在缓存中被技术员 98 的旧数据覆盖。

所以,在高并发场景下,如果多个线程同时写入数据,然后写入缓存,就会出现缓存是旧值,数据库是最新值的不一致。 。

此计划也可以直接转账。

如果第一步失败,系统会直接返回50次异常,不会出现数据冲突。

3.3 先清除缓存,再刷新数据库。

按照“码哥”之前提到的套路,如果第一次行动成功,第二次行动失败,会发生什么?高并发场景下会发生什么?

第二步写入数据库失败

假设有两个请求:写请求A和读请求B。

写下请求。第一步是成功清除缓存。如果我们不向数据库写入数据,写入的数据将会丢失,数据库会保存旧值

然后又一个读请求B到来,发现缓存不存在。旧数据从数据库中读取并写入缓存。

高并发问题

Redis 与 MySQL 数据一致性问题:探索缓存工作机制和缓存一致性应对方案

  1. 技术员98先攻击。系统收到删除缓存数据的请求。当系统准备写入的时候,给数据库设置=98到数据库就卡住了,来不及写入。
  2. 此时,大堂经理向系统发出读取请求,查看小彩姬是否有技术人员接待,从而方便技术人员服务。系统发现缓存中没有数据,于是从数据库中读取旧数据设置小菜鸡维修技师=被发现并写入缓存。
  3. 此时原本卡住的98号技术员写入数据设置小菜姬的服务技术员=98数据库操作完成。

这种情况下,缓存的数据将是旧数据,在缓存过期之前无法读取到最新的数据。 小菜鸡已经被98号技术员订购了,但大堂经理以为没人收到。

这个方案是成功的,因为第一步成功了,第二步不成功,就会导致数据库中出现旧数据。缓存中没有数据,从数据库读取旧值写入缓存,造成数据冲突和另一个缓存。

无论是异常情况还是大型同步场景,都可能出现数据不一致的情况。丢失的

3.4 先更新数据库,再清除缓存

经过前面三个解决方案,都通过了。让我们看看最终的解决方案是否可行。

按照“套路”,判断异常和高并发导致了哪些问题。

这个策略可以知道,如果数据库的写阶段失败,直接返回客户端异常,不需要进行缓存操作。

这样如果第一步失败就不会出现数据一致性。

删除缓存失败

最重要的是第一步要把最新的数据成功录入数据库。清除缓存失败怎么办?

您可以将这两个操作放在一个事务中。如果缓存刷新失败,则重置写入数据库。

不适合大并发场景,因为大事务容易发生死锁问题。

如果不向后滚动,会显示数据库是新的、缓存的还是旧的数据以及数据不一致。我应该怎么办?

所以我们需要想办法成功清除缓存,否则就只能等到有效期到期了。

使用重试机制。

比如重试3次,失败3次,日志会记录在数据库、xxl-job分布式调度组件等中。用它来实现后续的处理。

对于高并发最好使用异步方式进行重试,比如发送消息到mq中间件,实现异步解耦。

也可以使用Canal框架订阅MySQL binlog,监听相应的更新请求,并执行相应的flush缓存操作。

高并发场景

分析高并发读写问题...

Redis 与 MySQL 数据一致性问题:探索缓存工作机制和缓存一致性应对方案

  1. 技术员98首次攻击,接管小菜鸡的业务,小菜鸡的♻服务集❝ianiiaocian是菜鸟= 98;网络仍然卡住,没有时间清除缓存
  2. 主管Candy向系统发出读取请求,查看小菜姬是否有技术人员接收,发现缓存中有数据小菜姬的服务技术人员=直接发回了顾客,他想是主管。没有接待处。
  3. 98号技术员原本接单,但由于延迟,缓存操作并未取消。现在删除成功了。

读请求读取少量旧数据,但旧数据很快就会被删除,后续请求可以获得最新数据,所以没有太大问题。

还有一种更极端的情况。当缓存自动过期时,就会出现高并发读写的情况。假设会有两个请求,一个线程A进行查询操作,一个线程B进行更新操作,那么会出现以下情况:

Redis 与 MySQL 数据一致性问题:探索缓存工作机制和缓存一致性应对方案

  1. 缓存过期,缓存失效。
  2. 线程A的读请求读取缓存,如果缺失,则查询数据库获取旧值(因为B写的是新值,相对旧值),并准备好数据缓存。发送网络卡住
  3. 线程B执行写操作并将新值写入数据库。
  4. 线程 B 执行缓存刷新。
  5. 线程A恢复,从延迟中醒来,并将查询到的旧值写入缓存。

码哥,这游戏还是有矛盾的地方。

不要惊慌,这种情况的概率很低。上述情况发生的必要条件:

  1. 步骤(3)中,数据库的写操作比步骤(2)中的读操作更短且更快,因此可以避免步骤(4)。步骤(5)。
  2. 缓存刚刚到期。

通常MySQL单机的QPS约为5K,TPS约为1k(ps:Tomcat的QPS约为4K,TPS = 1k左右)。

数据库读操作比写操作快得多(这就是读写分离的原因),所以步骤(3)很难比步骤(2)快。同时,缓存必须协调起来才失败。

因此,在使用绕过缓存策略时,建议进行写操作: 先更新数据库,再清除缓存。

4。有哪些一致的解决方案?

最后对于Cache-Aside(绕过缓存)策略,当写操作首先使用更新数据库然后清除缓存时,我们来分析一下数据一致性的解决方案有哪些?

4.1 双缓存延迟删除

先清除缓存再刷新数据库如何避免脏数据?

采用延迟双删除策略。

  1. 先清除缓存。
  2. 写入数据库。
  3. 睡眠 500 毫秒,然后清除缓存。

这种情况下,最多会出现500毫秒的脏数据读取时间。关键是我们如何确定这个睡眠时间呢?

延迟时间的目的是保证读请求结束,写请求清除读请求造成的缓存脏数据。

所以我们需要自己估算一下读取项目的业务逻辑需要多长时间,只要在读取时间的基础上加上几百毫秒的延迟时间即可

4.2 缓存清除重试机制

缓存清除失败怎么办?例如,如果延迟双重擦除的第二次擦除失败,则无法擦除脏数据。

采用重试机制,确保缓存清除成功。

例如重试3次,失败3次,日志会记录在数据库中,并发出警告,需要人工干预。

对于高并发最好使用异步方式进行重试,比如发送消息到mq中间件,实现异步解耦。

Redis 与 MySQL 数据一致性问题:探索缓存工作机制和缓存一致性应对方案

(步骤5)如果删除不成功且未达到最大重试次数,消息将重新排队,直到删除成功,否则将记录在数据库中,需要人工干预。

这种方案有个缺点,会侵入业务代码,所以接下来的方案是启动一个专门订阅数据库binlog的服务来读取需要删除的数据,进行缓存删除操作。 。?通道数据,分析目标键并尝试清除缓存。

  • 如果删除不成功,消息会被放入消息队列;
  • 缓存擦除系统会重新从消息队列中取出数据并再次执行擦除操作。
  • 总结

    缓存策略的最佳实践是缓存旁路模式。 分为读缓存和写缓存最佳实践。

    读缓存最佳实践:先读缓存,命中则返回;如果丢失,它会查询数据库,然后写入数据库。

    写入缓存最佳实践:

    • 先写入数据库,再管理缓存;
    • 直接清除缓存而不是修改它,因为当缓存更新成本较高时,需要访问更多的页面来计算表摘要,建议直接清除缓存而不是更新。此外,清除缓存很容易,副作用只是增加了一个缓存未命中的情况。建议大家都使用这个策略。

    根据上面的最佳实践,我们可以使用延迟双删除的方式来尽可能保证缓存和数据库的一致性。

    为了避免删除错误,我们采用异步重试机制来保证正确删除。我们可以使用异步机制向mq消息中间件发送删除消息,也可以使用Canal订阅MySQL binlog来监听写请求来删除相应的缓存。

    那么如果我需要确保绝对一致性怎么办?首先,我总结一下:

    没有办法达到绝对的一致性。这是由CAP理论决定的。缓存系统的一个适用场景是非强一致性。这是一个性爱场景,所以它属于CAP中的AP。

    所以我们必须做出妥协,达到BASE理论中提到的最终一致性

    事实上,在解决方案中使用缓存往往意味着放弃强大的数据一致性,但这也意味着我们的系统可以获得性能提升。

    这就是所谓的妥协。

    版权声明

    本文仅代表作者观点,不代表Code前端网立场。
    本文系作者Code前端网发表,如需转载,请注明页面地址。

    发表评论:

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

    热门