数据一致性

四种常用的解决方案

方案一:Cache Aside Pattern

  读请求

  1. 先读缓存再读库
  2. 如果缓存命中,返回数据
  3. 如果缓存未命中,读库并把数据写入缓存,然后再返回

  写请求

  1. 数据写库
  2. 删除缓存

 

  这里很重要的一点在写请求中,要删除缓存而不是更新缓存。缓存的更新会发生在下一次读请求时。这里为什么会选择删除缓存,而没有更新缓存呢。因为如果更新缓存的话,存在并发写操作时,无法保证多个进程的执行顺序,有可能旧数据会覆盖新数据。 Cache Aside Pattern方案虽然使用范围比较广,但它本身也存在一些问题。

  问题一

  如上图,进程A在T1时刻数据写入库中,T2时刻删除了缓存。在高并发场景下,T1和T2时刻之间的读请求从缓存中读到的数据和库中的数据会出现不一致。

  问题二

  如上图,进程A在T1时刻把数据写入库中,T2时刻删除缓存失败。失败的原因暂不详谈。这种情况下会导致库和缓存数据长时间不一致。

  问题三

  如上图,进程A是读请求,进程B是写请求。

  1. 进程A读缓存未命中,然后从库中读到值A;
  2. 此时进程A可能因为某种原因发生了进程切换。
  3. 进程B执行写库,把值B写入库中;
  4. 进程B删除缓存。
  5. 进程A排队完成继续执行,把值A写入缓存。

  此时库中数据是B,缓存中是A,出现了数据不一致。

方案二:Double delete方法

  方案二是在方案一的基础上发展而来的。  

  读请求

  同方案一

  写请求

  1. 删除缓存
  2. 数据写库,返回写入成功
  3. 睡眠一段时间(通常是几百ms、也可以是一定时间范围内的随机值,根据具体业务场景决定)
  4. 删除缓存 

  此方案的写请求,与Cache Aside Pattern方法相比,最前面多了一次删除缓存,这样就可以避免T1~T2时间差的数据不一致。在最后删除缓存前有一个短暂的等待,这样就避免了方案一中的问题三:防止因为上下文切换等原因导致的数据不一致问题。

方案三:基于分布式锁的方案

  读请求

  1. 先读缓存再读库
  2. 如果缓存命中,返回数据
  3. 如果缓存未命中,取锁(可重试多次)
  4. 取锁成功,读库并把数据写入缓存
  5. 释放锁

  写请求

  1. 取锁
  2. 取锁成功后,数据写库
  3. 删除缓存
  4. 释放锁

  加锁之后,数据一致性能得到很好的保证,但是数据的访问效率会受到较大的影响。所以,很多时候加锁未必是理想方案。不过在写少、读多的业务场景中也可以考虑。

方案四:基于Binlog订阅方式,删除缓存

  读请求

  1. 先读缓存再读库
  2. 如果缓存命中,返回数据
  3. 如果缓存未命中,读库并把数据写入缓存,然后再返回

  写请求

  只写数据库

 

  对于缓存的更新,我们采用订阅数据库日志的方式实现。比如采用阿里开源的Canal,订阅MySQL的Binlog,然后放入MQ,消费端消费数据,然后把数据和缓存中的数据进行比较,把不一致的数据从缓存中删除。删除失败可以尝试多次删除。这种方案,可以把缓存删除逻辑从业务代码中剥离,业务开发专注于业务;但是需要引入额外的组件,花费更高的维护成本。

总结

  以上是处理库和缓存数据一致性问题的常用方案。他们都有一个共同点删除缓存,而不是更新缓存。不管是哪一种方案,都很难做到库和缓存数据完全一致。所以在各方案中,都可以加异步的对账逻辑,定期检查库和缓存中的数据是否一致,出现不一致时,删除缓存数据即可。

 

链接:https://juejin.cn/post/6992576951470800932

posted @ 2022-02-27 12:08  林锅  阅读(310)  评论(0编辑  收藏  举报