标记更新 缓存一致性终极版?

2,806 阅读3分钟

说正事之前先扯谈一下。cache怎么读? cake? catch? key-ch(teach里的ch)? No no. cash!!! 不信翻词典:dictionary.cambridge.org/dictionary/…

如何实现缓存一致性?

  1. 先写数据库,再删除缓存 -- 先写后删
  2. 先删除缓存,再写数据库 -- 先删后写
  3. 先删除缓存,再写数据库,再延时删除数据库 -- 延迟双删

第一二条方案肯定不太行,第三条。。。。

这个第三条,延迟双删,延时设置多长? 设置一秒! 系统压力上来卡一下,一秒就过了,然后。。。。

设置10秒,有问题的时候,十秒内数据肯定不一致。

另外,延迟删除这个操作,如果要保证一定成功,工作量大性能又不好。

隆重推出标记更新方案:(此方案有线程安全BUG,请看后面的补丁:V1.0.1)

  1. 设置数据库事务过期时间
  2. 将缓存的值替换成系统保留关键字,例如:“##SYS_UPDATING##”。同时设置其TTL为事务过期时间的两倍。
  3. 写数据库。
  4. 删除缓存Key。
  5. 读数据的线程,如果读缓存得到保留字“##SYS_UPDATING##”,直接读数据库。此时不更新缓存。

允许第4步失败。如果第4步失败,这个key没有删除,那么在这个Key过期之前,所有访问将一直打到数据库上,性能有没有问题?我认为没有大问题,因为这只是偶发现象,假设这种现象发生率为1%,那么缓存的作用率是99%,缓存还是基本上抗住了大多数流量。

此方案,缓存和数据库一致性可以认为是100%


小黑人:100%?如果数据库读写分离,主从同步延迟。。。

小白人: 滚,主从同步延迟这锅不背,我都已经将TTL设置为事务过期时间的两倍了。你还想咋地?

小白人: 。。。。要不,第4步“删除缓存Key”不由程序发起,由数据库从库发起?

小黑人: 如何发起?多个从库怎么办?

小白人: 滚!


其它思考:此方案需要设置数据库事务过期时间,如果系统不能设置数据库事务过期时间咋办? 考虑采用分布式锁给锁的TTL续期的模式,给这里的Key的TTL续期。但强烈建议设置数据库事务过期时间,这也是数据库死锁的兜底方案。

原创,首发掘金,版权保留,转载请注明出处。


V1.0.1 修复线程安全问题

感谢watermelonX指出方案中的巨大bug:线程安全。 下面是v1.0.1:

参考分布式锁的实现,加锁来协调各线程操作。

  • 获取数据中:##SYS_FETCHINNG##<ownerUUID> (<ownerUUID>用于记录身份,不能解锁别人的锁)
  • 更新数据中:##SYS_UPDATING##<ownerUUID>
  • 更新数据完毕:##SYS_UPDATED##<ownerUUID>

更新数据库线程如下操作

  1. 设置数据库事务过期时间。 包括只读事务。
  2. 将缓存的值替换成系统保留关键字,例如:“##SYS_UPDATING##<ownerUUID>”。同时设置其TTL为事务过期时间的两倍。 设置之前,需要检查key是否存在,如果值为##SYS_UPDATING##<otherOwnerUUID>,自旋重试。 其它值一律覆盖。 (lua脚本同一个事务完成这些操作,下面略过)
  3. 写数据库。
  4. 将缓存的值替换成保留关键字,"##SYS_UPDATED##<ownerUUID>". TTL两倍事务过期时间。 替换前检查是不是自己加锁,使用事务

读取数据线程如下操作

  • 读取缓存,如果Key不存在,或者为"##SYS_UPDATED##<ownerUUID>, 设置标志:##SYS_FETCHINNG##<ownerUUID>, 然后去读数据库,更新缓存。更新缓存前需要检查标志是不是自己设置的。如果不是自己设置的标志,直接返回数据,不能更新数据库。
  • 读取缓存, 如果缓存值为##SYS_FETCHINNG##<ownerUUID> 自旋重试。
  • 读取缓存, 如果缓存值为##SYS_UPDATING##<ownerUUID> ,根据业务需求,可自旋重试。或者直接读数据库然后返回,不动缓存。