Redis缓存与数据库数据一致性

作者: 925781609 | 来源:发表于2020-04-06 11:48 被阅读0次

    Redis由于将数据保存在内存中,访问速度远远大于基于磁盘的数据库(如MySQL)。但是由于其容量有限,不开启持久化时断电数据容易丢失(开启持久化影响性能)等特点, 因此常常作为数据库的缓存来使用。Redis也主要适合于读多写少,且对一致性要求不是特别高的场景,这是使用Redis的前提。

    缓存更新策略

    由于引入缓存,数据就会分散在缓存和数据库两处不同的数据源,当数据更新时,事实上很难做到数据一致,除非采用强一致性方案,这里不在进行讨论。关于数据的更新,主要有以下4种模式, 其中Read Through和Write Through放在一起讨论:

    • Cache Aside
    • Read Through
    • Write Through
    • Write Back

    这四种模式的主要区别在于最新数据在缓存中还是数据库中,由谁进行更新

    模式 最新数据在哪里 由谁更新
    Cache Aside 数据库 应用程序更新缓存
    Read Through/Write Through 缓存服务更新数据库
    Write Back 缓存 缓存服务更新数据库

    下面分别对这四种模式进行阐述,为保证行文连贯,先假设更新数据库以及缓存都会事务成功,由于某一种更新导致的不一致性在后续章节讨论

    1. Cache Aside

    Cache Aside顾名思义,就是缓存“靠边站”,只是在访问数据库的主流程上帮个忙,最新的数据还是以数据库为主

    Cache Aside主要有三点:

    1. 命中:应用程序从cache中取数据,取到后返回。
    2. 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
    3. 更新:先把数据存到数据库中,成功后,应用程序再让缓存失效。

    至于为什么是先更新数据库,再让缓存失效, 而不是直接更新缓存,主要是为了保证在并发的情况下,尽可能降低数据不一致出现的概率,具体参考附录,在大概率的情况下先更新数据库再失效缓存能够保证数据一致,也是业界推荐的处理方式,包括Facebook的论文《Scaling Memcache at Facebook》也使用了这个策略。


    图片来源于https://coolshell.cn/articles/17416.html

    2. Read Through与Write Through

    Cache Aside 对缓存以及数据库的更新逻辑是由应用程序去控制的,很显然这是一个很复杂的过程。Write/Read Through对调用方而言,缓存是作为整个的数据存储,而不用关心缓存后面的数据库,数据库的更新则是由缓存统一进行管理,对调用方而言只需要和缓存进行交互,整体过程是透明的。可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的Cache。如下图,图片来源于Cache (computing)

    • Read Through
      查询操作中,当缓存失效的时(过期或LRU换出),由缓存服务负责从数据库中加载数据到缓存中,对应用方是透明的。
    • Write Through
      更新数据时,如果没有命中缓存,直接更新数据库,然后返回。
      如果命中了缓存,则更新缓存,然后再由缓存服务自己更新数据库(这是一个同步操作)

    3. Write Back

    这种模式是当数据更新的时候直接更新缓存数据,然后建立异步任务去更新数据库。这种异步方式请求响应会很快,系统的吞吐量会明显提升。但是,因为是异步更新数据库,数据一致性的保障就会变弱,如果更新数据库失败则会永远的造成系统脏数据,需要很精细设计系统重试的策略,另外如果异步服务宕机的话,还要考虑更新的数据如何持久化,服务重启后能够迅速恢复。在更新数据库时,由于并发多任务的存在,还需要考虑并发写是否会造成脏数据的问题,就需要追溯每次更新数据的时序。使用这种模式需要考虑的细节会有很多,设计出一套好的方案是件很不容易的事情。

    图片来源于https://en.wikipedia.org/wiki/Cache_(computing)

    数据不一致的原因

    1. 数据不一致的原因

    1. 逻辑失败造成的数据不一致
      因为异步读写请求在并发情况下的操作时序导致的数据不一致,称之为”逻辑失败“。解决这种因为并发时序导致的问题,核心的解决思想是将异步操作进行串行化。
    2. 物理失败造成的数据不一致
      在Cache Aside 模式中先更新数据库再删除缓存以及异步双删策略等等,如果删除缓存失败时都出现数据不一致的情况。出于性能的考虑,数据库及缓存的操作不会放在一个事务中,因为缓存操作失败,导致的数据不一致称之为“物理失败”。大多数情况物理失败的情况会重用重试的方式进行解决。

    2. 数据最终一致性解决方案:

    在绝大部分业务场景中,追求的是最终一致性,针对物理失败造成的数据不一致常用的方案有:消费消息异步删除缓存以及订阅Binlog的方式,针对逻辑失败造成的数据不一致常用的方案有:队列异步操作同步化。
    消费消息异步删除缓存
    流程如下图所示,主要包括:

    1. 应用程序更新数据库数据;
    2. 删除缓存
    3. 如果缓存删除失败,将删除失败的key 发送到消息队列
    4. 应用程序自己消费消息,获得需要删除的key
    5. 继续重试删除操作,直到成功

    订阅binlog
    消费消息异步删除缓存有一个缺点,对业务线代码有大量的侵入,所以引出了本方案, 主要流程如下:

    1. 应用程序更新数据库
    2. 通过canal 订阅数据库的binlog
    3. 数据更新服务解析binlog
    4. 根据解析的binlog更新缓存
    5. 对于更新失败的,将失败的key发送到消息队列
    6. 缓存服务订阅消息队列, 重试

    总结:

    无论是4种更新策略,还是缓存的最终一致性方案,Redis缓存与数据库都会有短时间或者小概率的不一致的风险, 这又回到了开篇Redis适合使用的场景,读多写少, 对一致性要求不高,如果是一致性要求特别高的情况,比如交易场景,则只能使用数据库了。

    附录

    1. 先更新数据库再失效缓存

    • 数据库读请求比写请求快,出现数据不一致概率极低
      1. 读请求时,缓存刚好失效(图中1、2)
      2. 读请求从数据库中读取数据并更新缓存期间(图中3、4、9、10),正好有写请求更新数据库并使缓存失效(图中5、6、7、8), 且读数据库比写数据库慢
    • 可以通过异步双删的策略以及过期失效的方式来避免这种不一致
    先更新数据库再失效缓存出错情况(概率小)

    2. 先失效缓存再更新数据库

    • 数据库读请求比写请求快,容易出现数据不一致
      写请求失效缓存并更新数据库期间(图中1、2、7、8),读请求读取数据库旧值并更新缓存(图中3、4、5、6)
    • 通过延时双删(影响吞吐率),异步双删降低不一致的影响
    先失效缓存再更新数据库数据不一致情况(概率大)

    3. 先更新缓存再更新数据库与先更新数据库再更新缓存

    • 并发写容易写覆盖造成脏数据问题
      具体如如下两图所示
    • 双写不同数据源容易造成数据不一致
      同时写数据库以及缓存数据,任何一个更新失败都会造成数据不一致,即物理失败。另外事务都成功,无论是先更新缓存还是再更新数据库,还是先更新数据库再更新缓存,这两种情况在并发的情况下也很容易出现双写不成功。
    • 违背数据懒加载,避免不必要的计算消耗(这点还好)
      如果有些缓存值是需要经过复杂的计算才能得出,所以如果每次更新数据的时候都更新缓存,但是后续在一段时间内并没有读取该缓存数据,这样就白白浪费了大量的计算性能,完全可以后续由读请求的时候,再去计算即可,这样更符合数据懒加载,降低计算开销。


      先更新数据库再更新缓存数据不一致情况(概率大)
    先更新缓存再更新数据库数据不一致情况(概率大)

    参考:

    1. 缓存更新的套路
    2. 响应速度不给力?解锁正确缓存姿势

    相关文章

      网友评论

        本文标题:Redis缓存与数据库数据一致性

        本文链接:https://www.haomeiwen.com/subject/tsquphtx.html