缓存与数据库双写一致性

介绍

  缓存由于其高并发和高性能的特性,已经在项目中被广泛使用。在缓存的使用中,通常会面临一个更新的问题,当数据源产生变化,如何去更新到数据库与缓存之中,并且尽量保证安全与性能。

  读缓存的情况一般包括下面几种情况:

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

  读的地方没有争议,都是先读缓存,没有则取数据库数据,并写入缓存,但是写的地方就有了争议。


先更新缓存,再写数据库

  这种情况一般不考虑使用,在多线程情况下,会有线程安全问题。如:

  • 线程A更新了数据库
  • 线程B更新了数据库
  • 线程B更新了缓存
  • 线程A更新了缓存

  正常情况下,A线程先更新的数据库,也应该先更新缓存,但是因为网络等原因,导致A更新缓存晚于B更新缓存,这种会导致数据库与缓存不一致,存在脏数据。


先删除缓存,在更新数据库

  这种模式在一个写线程和一个读线程到来时也会发生线程安全问题,造成脏数据。

  • 线程A到来,删除缓存,需要进行写操作
  • 线程B到来,查询发现缓存为空,就去查询数据库
  • 线程B在数据库查到未更新前的旧值
  • 线程B将查到的旧值写入缓存
  • 线程A写入数据库,更新数据

  这种情况下,缓存的就是旧数据,而数据库则是新数据,造成脏数据,如果不设置缓存过期策略,这些数据就会成永久脏数据。
  对于这种,一般采用延时双删的办法,就是在更新数据库后,过一段时间后在删除一次数据库。流程基本就是:

  • 先删除缓存
  • 在更新数据库
  • 休眠一段时间,在删除缓存,将休眠时间内的脏数据删除

    这个一段时间,一般需要指定为,读数据业务逻辑加一点点,就是稍大于读业务逻辑,在读写分离架构中,这个时间还需要加上主从同步时间。

  这种主要会是有线程,在删除缓存后,和操作数据库的间隙中,进行读操作,而写数据库时,会有写锁无法读取,所以,等待一次读的时间,足够将间隙读操作造成的脏数据清理掉。
  这种同步双删的策略,会导致吞吐量降低,毕竟改了库,一定会等待一段时间,可以重启一个线程,异步进行第二次删除。


先更新数据库,在删除缓存

  这种模式一般比较常用,也是facebook的策略。
  这种模式在模拟情况下,一样会有并发问题,但实际上可以忽略不记。

  • 缓存刚好失效
  • A线程查询数据库得到一个旧值
  • B线程更新数据库
  • B线程删除缓存
  • A线程将查到的旧值,写入缓存

  这种情况只有在数据的读速度慢于写速度的情况下才会发生,这种一般不会发生。即使发生也可以用延时双删策略进行保证一致性。

删除失败导致脏数据

  上面两种策略,删除缓存失败了,或者延时双删,第二次失败,都会产生脏数据。
  通过一个保障重试的机制来重试删除缓存。可以将需要删除的key放入消息队列中,利用消息队列的失败机制进行重试。


参考文献 & 鸣谢