zoukankan      html  css  js  c++  java
  • 数据库和缓存一致性

    缓存

    缓存构建的基本思想是利用时间局限性原理,通过空间换时间来达到加速数据获取的目的,同时由于缓存空间的成本较高,在实际设计架构中还要考虑访问延迟和成本的权衡问题。

    业务系统读写缓存有 3 种模式:

    • Cache Aside(旁路缓存),先更新db,后删除缓存
    • Read/Write Through(读写穿透),cache服务更新缓存,并更新db。
    • Write Behind Caching(异步缓存写入),cache服务甘心缓存,异步更新dn。

    Cache Aside模式(旁路缓存)

    1.Write: 更新 DB 后,直接将 key 从 cache 中删除,然后由 DB 驱动缓存数据的更新;

    2.Read: 是先读 cache,如果 cache 没有,则读 DB,同时将从 DB 中读取的数据回写到 cache。

    特点:

    确保数据以DB 结果为准

    适用场景:

    对数据一致性要求比较高的业务,或者是缓存数据更新比较复杂的业务,比如需要通过多个原始数据进行计算后设置的缓存数据

     

    Read/Write Through模式(读写穿透)

    1. Write: 存储服务收到业务应用的写请求时,会首先查 cache,如果数据在 cache 中不存在,则只更新 DB,如果数据在 cache 中存在,则先更新 cache,然后更新 DB。

    2. Read: 存储服务收到读请求时,如果命中 cache 直接返回,否则先从 DB 加载,回写到 cache 后返回响应。

    特点:

    • 存储服务封装了所有的数据处理细节,业务应用端代码只用关注业务逻辑本身,系统的隔离性更佳。
    • 进行写操作时,如果 cache 中没有数据则不更新,有缓存数据才更新,内存效率更高。

    适用场景:

    用户最新Feed列表

     

    Write Behind Caching模式(异步缓存写入)

    1.Write: 只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB

    2.Read: 如果命中 cache 直接返回,否则先从 DB 加载,回写到 cache 后返回响应。

    特点:

    写性能最高,定期异步刷新,存在数据丢失概率

    适用场景:

    适合变更频率特别高,但对一致性要求不太高的业务,特别是可以合并写请求的业务,比如对一些计数业务

     

    这里用的最多的是旁路模式

     

    缓存与数据库的一致性问题

    1、先删缓存,再更新数据库

    如果有 2 个线程要并发「读写」数据,可能会发生以下场景:

    1. 线程 A 要更新 X = 2(原值 X = 1)
    2. 线程 A 先删除缓存
    3. 线程 B 读缓存,发现不存在,从数据库中读取到旧值(X = 1)
    4. 线程 A 将新值写入数据库(X = 2)
    5. 线程 B 将旧值写入缓存(X = 1)

    最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),发生不一致。

    可见,先删除缓存,后更新数据库,当发生「读+写」并发时,还是存在数据不一致的情况。

     

    解决方案:延时双删(没法完全保证)

    2、先更新数据库,再删缓存

    依旧是 2 个线程并发「读写」数据:

    1. 缓存中 X 不存在(数据库 X = 1)
    2. 线程 A 读取数据库,得到旧值(X = 1)
    3. 线程 B 更新数据库(X = 2)
    4. 线程 B 删除缓存
    5. 线程 A 将旧值写入缓存(X = 1)

    最终 X 的值在缓存中是 1(旧值),在数据库中是 2(新值),也发生不一致。

    这种情况「理论」来说是可能发生的,但实际真的有可能发生吗?

    其实概率「很低」,这是因为它必须满足 3 个条件:

    1. 缓存刚好已失效
    2. 读请求 + 写请求并发
    3. 更新数据库 + 删除缓存的时间(步骤 3-4),要比读数据库 + 写缓存时间长(步骤 2 和 5)

    也就是步骤5通常会在步骤四的前面。

     

    这种方案并发条件下数据一致性的可能性很小,但第二步执行失败会导致数据一致性的问题。

    解决方案

    (1)、消息队列

    异步重试,确保步骤二成功 

    消息可靠性投递,确保消费者成功消费消息。

     

    (2)binlog日志 + MQ

    订阅数据库变更日志,再操作缓存。具体来讲就是,我们的业务应用在修改数据时,「只需」修改数据库,无需操作缓存。

    可以做到强一致吗?

       性能和一致性不能同时满足,为了性能考虑,通常会采用「最终一致性」的方案。

     

      要想做到强一致,最常见的方案是 2PC、3PC、Paxos、Raft 这类一致性协议,但它们的性能往往比较差,而且这些方案也比较复杂,还要考虑各种容错问题。

    相反,这时我们换个角度思考一下,我们引入缓存的目的是什么?

    没错,性能

      一旦我们决定使用缓存,那必然要面临一致性问题。性能和一致性就像天平的两端,无法做到都满足要求。

    而且,就拿我们前面讲到的方案来说,当操作数据库和缓存完成之前,只要有其它请求可以进来,都有可能查到「中间状态」的数据。

      所以如果非要追求强一致,那必须要求所有更新操作完成之前期间,不能有「任何请求」进来。

      虽然我们可以通过加「分布锁」的方式来实现,但我们要付出的代价,很可能会超过引入缓存带来的性能提升。

      所以,既然决定使用缓存,就必须容忍「一致性」问题,我们只能尽可能地去降低问题出现的概率。

      同时我们也要知道,缓存都是有「失效时间」的,就算在这期间存在短期不一致,我们依旧有失效时间来兜底,这样也能达到最终一致。

     

     

     

    参考:

    https://mp.weixin.qq.com/s/D4Ik6lTA_ySBOyD3waNj1w

    https://mp.weixin.qq.com/s/dYvM8_6SQnYRB6KjPsprbw

     

  • 相关阅读:
    max_element( )
    dp
    dfs
    dp
    区间dp
    树形dp
    dp-最长回文串
    go 结构体函数
    go 结构体初始化
    Golang数组和切片的区别
  • 原文地址:https://www.cnblogs.com/chenfx/p/15376576.html
Copyright © 2011-2022 走看看