数据库|如何保证缓存和数据库的一致性?( 二 )

  • 检查缓存中是否有需要的数据 , 如果命中缓存(Cache Hit) , 则直接返回数据 。
  • 如果没有命中缓存 , 即 Cache Miss , 那么就先去访问数据库 。
  • 将从数据库中读取到的数据设置到缓存中 。
  • 返回数据 。
  • 这是 Cache-Aside 的读缓存流程 。
    其实对于读缓存的流程而言 , 大家一般都没什么异议 , 有异议的主要是写流程 , 我们继续来看 。
    2.2 写缓存先来看一张流程图:

    这个写缓存的流程就比较简单 , 先更新数据库中的数据 , 然后删除旧的缓存即可 。
    流程虽然简单 , 但是却引伸出来两个问题:
    • 为什么是删除旧缓存而不是更新旧缓存?
    • 为什么不先删除旧的缓存 , 然后再更新数据库?
    我们来分别回答这两个问题 。
    为什么是删除旧缓存而不是更新旧缓存?
    • 更新缓存 , 说着容易做起来并不容易 。 很多时候我们更新缓存并不是简简单单更新一个 Bean 。 很多时候 , 我们缓存的都是一些复杂操作或者计算(例如大量联表操作、一些分组计算)的结果 , 如果不加缓存 , 不但无法满足高并发量 , 同时也会给 MySQL 数据库带来巨大的负担 。 那么对于这样的缓存 , 更新起来实际上并不容易 , 此时选择删除缓存效果会更好一些 。
    • 对于一些写频繁的应用 , 如果按照更新缓存->更新数据库的模式来 , 比较浪费性能 , 因为首先写缓存很麻烦 , 其次每次都要写缓存 , 但是可能写了十次 , 只读了一次 , 读的时候读到的缓存数据是第十次的 , 前面九次写缓存都是无效的 , 对于这种情况不如采取先写数据库再删除缓存的策略 。
    • 在多线程环境下 , 这样的更新策略还有可能会导致数据逻辑错误 , 来看如下一张流程图:

    可以看到 , 有两个并发的线程 A 和 B:
    • 首先 A 线程更新了数据库 。
    • 接下来 B 线程更新了数据库 。
    • 由于网络等原因 , B 线程先更新了缓存 。
    • A 线程更新了缓存 。
    那么此时 , 缓存中保存的数据就是不正确的 , 而如果采用了删除缓存的方式 , 就不会发生这种问题了 。
    为什么不先删除旧的缓存 , 然后再更新数据库?这个也是考虑到并发请求 , 假设我们先删除旧的缓存 , 然后再更新数据库 , 那么就有可能出现如下这种情况:

    这个操作是这样的 , 有两个线程 , A 和 B , 其中 A 写数据 , B 读数据 , 具体流程如下:
    1. A 线程首先删除缓存 。
    2. B 线程读取缓存 , 发现缓存中没有数据 。
    3. B 线程读取数据库 。
    4. B 线程将从数据库中读取到的数据写入缓存 。
    5. A 线程更新数据库 。
    一套操作下来 , 我们发现数据库和缓存中的数据不一致了!所以 , 在 Cache-Aside 中是先更新数据库 , 再删除缓存 。
    2.3 延迟双删其实无论是先更新数据库再删除缓存 , 还是先删除缓存再更新数据库 , 在并发环境下都有可能存在问题:
    假设有 A、B 两个并发请求: