Redis

洞悉Redis技术内幕:缓存,数据结构,并发,集群与算法
帅旋
关注
充电
IT宅站长,技术博主,共享单车手,全网id:arthinking。

如何同步Redis缓存和数据库的数据?

发布于 2021-06-16 | 更新于 2024-03-03

技术的出现都是有特定的背景的,我们摸清了技术的发展脉络,也就能更好的掌握这门技术,也能理解未来的发展趋势。所以我在Java架构杂谈公众号以及IT宅(itzhai.com)中写的一些技术文章有可能会顺便梳理一下发展脉络。如:架构演变之路:为何要搞微服务架构?, 三万长文50+趣图带你领悟web编程的内功心法

为了同步缓存和数据库的数据,我们也先来看看传统的缓存策略。常见的有以下几种更新策略:

1、Cache-Aside策略

我们学过操作系统的缓存之后,知道无论是LLC还是page cache,我们都不会显示的去维护它,而是在操作系统内部直接集成了这些缓存。

而Redis的缓存是独立于应用程序的,我们要使用Redis缓存,必须手动在程序中添加缓存操作代码,所以我们把这种使用缓存的方式叫旁路缓存(Cache-Aside)[^20] (把Redis和数据库数据同步逻辑封装好下沉到持久层的此类场景不算是Cache-Aside)。

以下是Cache-Aside的查询数据的图示:

image-20211010135036507

这也是最常用的缓存方法,缓存位于侧面(Aside),应用程序直接与缓存和数据库通信。具体交互:

  1. 应用程序首先检查缓存
  2. 如果缓存命中,直接返回数据给客户端
  3. 如果缓存没有命中,则查询数据库以读取数据,并将其返回给客户端以及存储在缓存中。

一般写数据流程如下图:

image-20211010135102057

  1. 更新数据库中的缓存
  2. 让缓存失效

1.1、优缺点

优点:

  • Cache-Aside很适合读取繁重的场景,使用Cache-Aside使系统对缓存故障具有弹性,如果缓存集群宕机了,系统仍然可以通过直接访问数据库来运行。不过响应时间可能变得很慢,在最坏的情况下,数据库可能会停止工作;
  • 这种方式,缓存的数据模型和数据库中的数据模型可以不同,比如,把数据库中多张表的数据组合加工之后,再放入缓存。

缺点:

  • 使用Cache-Aside的时候,写数据之后,很容易导致数据不一致。可以给缓存设置一个TTL,让其自动过期,如果要保证数据的实时性,那么必须手动清除缓存。

1.2、数据不一致问题

并发写导致的脏数据

有些写缓存的代码会按如下逻辑编写:

  1. 更新数据
  2. 更新缓存

这种方案,可能会因为并发写导致脏数据,如下图:

image-20211010135134197

线程1设置a为12,线程2设置a为14,由于执行顺序问题。最终,数据库中的值是14,而缓存中的值是12,导致缓存数据和数据库中的数据不一致。

为此,我们一般不写完数据库之后立刻更新缓存。

读写并发导致的脏数据

即使是写完数据之后,我们直接删掉缓存,也是有可能导致缓存中出现脏数据的,如下图:

image-20211010135159330

数据库被线程2更新为了12,但是缓存最终又加载到了老的值10。

不过这个场景概率很低,因为一方面一般读操作比写操作只需快得多,并且另一方面还需要读操作必须在写操作之前就进入数据库查询,才能导致这种场景的出现。

**我们最常用的兜底策略是设置一个缓存过期时间,好让这种极端场景产生的脏数据能够定时被淘汰。**当然,用2PC或者Paxos协议来保证一致性也可以,不过实现起来太复杂了。

2、Read/Write Through策略

前面的Cache-Aside需要应用程序参与整个缓存和数据库的同步过程,而Read/Write Throught策略则不需要应用程序参与,而是让缓存自己来代理缓存和数据的同步,在应用层看来,后端就是一个单一的存储介质,至于存储内部的缓存机制,应用层无需关注

就像操作系统的缓存,无论是LLC还是page cache,我们都不会显示的去维护它,而是在操作系统内部直接集成了这些缓存。

接下来看看这个策略。

Read-Through

Read-Through,缓存与数据库保持一致,当缓存未命中的时候,从数据库中加载丢失的缓存,填充缓存,并将其返回给应用程序。加载缓存过程对应用透明:

image-20211010135233568

这种策略要保证数据库和缓存中的数据模型必须相同。

Write-Through

Write-Through,当更新数据的时候,如果没有命中缓存,则直接更新数据库,然后返回。如果命中了缓存,则直接更新缓存,缓存内部触发自动更新数据库,都更新完成之后,再返回。

缓存和数据库保持一致,写入总是通过缓存到达数据库。如下图:

image-20211010135257718

Write-Back

Write-Back,更新数据的时候,只更新缓存,不立刻更新数据库,而是异步的批量更新数据到数据库中。

这个策略与Write-Through相比,写入的时候,避免了要同步写数据库,让写入的速度有了很大的提高,但是确定也很明显:缓存和数据库中的数据不是强一致性的,还有可能会导致数据丢失

image-20211010135325835

Write-Back策略适用于写入繁重的场景,通过与Read-Through配合使用,可以很好的适用于读写都和频繁的场景。

我们可以稍微来总结一下缓存策略的选用:

缓存策略 优点 缺点 使用场景
Read-Through 读效率高 缓存和数据库的数据可能不一致 适合读取繁重的场景
Write-Through 保证数据一致性,避免缓存失效,保证缓存数据是最新的 写效率低 写入不是很频繁,对数据一致性要求比较高的场景,如资讯网站,博客
Write-Back 写效率高,保证缓存数据是最新的 数据库可能丢数据 适合写入繁重但是数据可靠性要求不是很严格的场景,如评论系统,消息通知系统
Cache-Aside 缓存和数据库的数据模型不要求一致,可根据业务灵活组织,应用不强依赖缓存,缓存实时性高 编码复杂,缓存与数据库可能不一致 数据模型复杂的业务系统

Read-Through的缓存和数据库数据不一致解决方案在于写入策略,只要我们配合合理的写入策略,就更好的保证缓存和数据库数据的一致性。

在实际的使用场景中,我们会关注使用的缓存要不要求实时更新。根据实时性,Redis缓存更新可以分为两种策略:实时策略,异步策略。这两种策略都是离不开以上介绍的几种的缓存策略的思想。

实时策略

实时策略,是最常用的缓存策略。

类似Cache-Aside策略的实现就是实时策略,

**读取:**先从缓存读取数据,如果缓存没有命中,则从数据库中读取,读取到了则放到缓存中。如果缓存命中,则直接从缓存中读取数据。

**写入:**写入的时候,先把数据写入到数据库中,然后在让缓存失效,缓存下次读取的时候,从数据库中加载数据。

这种方案会存在数据不一致问题,在Cache-Aside策略小节已经有讲过了。

对于缓存与数据库的数据一致性要求高的场景,建议使用实时策略;如果对缓存与数据库一致性要求不高,则可以使用异步策略。接下来讲讲异步策略。

异步策略

所谓的异步,我们可以做成读写都是异步的:

  • **读取:**当从缓存中读取不到数据的时候,不直接从数据库加载,而是返回一个fallback数据,然后往消息队列中放入一个加载数据的消息,通过异步消费消息去加载数据。可以避免因为缓存失效,导致高并发大流量一起请求到数据库,从而对数据库造成压力。

  • **写入:**总是先写入数据库或者缓存,然后异步更新另一个:

    • 先更新数据库:生成消息异步更新缓存,优点:数据持久性可以得到保证,缺点:缓存实时性差
    • 先更新缓存:异步刷新缓存到数据库,相当于把缓存当成了数据库在用,优点:完全使用缓存,IO效率高,缺点:可能丢数据

定时策略

针对读写并发量过大的场景,我们可以进一步降级,定时的把缓存中的数据刷到数据库,可以在缓存中对数据进行整合,然后在刷新到数据库中。

优点:IO效率高,比异步策略更高,缺点:可能丢数据。

其实MySQL的Change Buffer就是采用异步策略,同时又使用Redo Log来实现数据的不丢失。进一步阅读:洞悉MySQL底层架构:游走在缓冲与磁盘之间[1]

来总结对比下这几种方案:

策略 优点 缺点 适用场景
实时策略 实时性高 缓存更新困难,容易导致数据不一致 金融,交易系统等业务数据实时性要求高,数据可靠性要求高的场景
异步策略-先更新数据库 数据持久性可以得到保证 缓存实时性差 产品详情,文章详情等不要求实时展现,但要求不丢数据
异步策略-先更新缓存 完全使用缓存,IO效率高 可能丢数据 评论系统,消息通知
定时策略 IO效率高,比异步策略更高 比异步策略-先更新缓存更容易丢数据 评论系统,消息通知

其实,在使用实时策略的时候,我们关注的是如何进一步降低丢数据的风险,有两种处理方式

  • 用2PC或者Paxos协议来保证一致性也可以,不过实现起来太复杂了;
  • 通过各种其他五花八门的骚操作,来进一步降低实时策略丢数据的概率。

降低丢数据的概率的常用措施有哪些?

  • 实时策略场景:

    • 缓存双删,可以在第二次删除之前休眠一小段时间。进一步降低了数据不一致的概率,但是也不能避免数据不一致;
    • 增加组件订阅binglog,完成缓存的更新,适合缓存结构和数据库结构一致的场景。如果缓存结构复杂,也不好写成通用的组件;
    • 通过分布式事务,把缓存操作和数据库操作通过一个事务完成,但由于Redis并不支持类似MySQL的事务,所以在分布式处理过程中,还是可能导致其他客户端读取到中间数据,导致脏读问题。
  • 异步策略场景:

    • 优先修改缓存数据,通过队列异步写到请求到数据库中;(异步直写),如果消息队列可靠,则可以保证数据最终写入数据库。

References


  1. 洞悉MySQL底层架构:游走在缓冲与磁盘之间. Retrieved from https://www.itzhai.com/columns/mysql/ ↩︎

本文作者: 帅旋

本文链接: https://www.itzhai.com/columns/redis/cache-database-data-synchronization.html

版权声明: 版权归作者所有,未经许可不得转载,侵权必究!联系作者请加公众号。

×
IT宅

关注公众号及时获取网站内容更新。