策略1 - 先更新数据库,再更新缓存
分析一(线程安全角度)
同时有请求A和请求B进行更新操作,那么会出现
- 线程A更新了数据库
- 线程B更新了数据库
- 线程B更新了缓存
- 线程A更新了缓存
一般来说,请求A更新缓存应该比请求B更新缓存早才对,但理论上,也可能因为网络等原因(虽然概率相对较低),B却比A更早更新了缓存。这就导致了脏数据。
Potential Solution
- 在更新缓存时,加锁
分析二(业务场景角度)
如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求。而且,如果你写入数据库的值,并不等于要接写入缓存的值,而是后者是根据前者,经过一系列复杂的计算才得到的。那么,每次写入数据库后,为了写入缓存而进行复杂的运算,无疑是浪费性能的。显然,删除缓存更为适合。
这一点在 https://swsmile.info/post/cache-invalidation-vs-cache-update/ 有讨论到。
策略2 - 先更新数据库,再删缓存
这其实就是 Cache Aside pattern,refer to https://swsmile.info/post/cache-aside/ 。
是不是Cache Aside就一定不会有并发问题了?不是的,比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让 cache 失效。
然后,由于某些原因(比如网络拥塞了),读操作读到的旧数据被写入到了 cache。
这时,仍然出现了脏数据。
但,这个case理论上会出现。不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时,出现缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还可能锁表,而读操作必需在写操作前进行数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。
当然,还有其他的可能也会导致脏数据,比如
- 如果使用了MySQL Master-Slave,当出现DB delay的时候,当完成写操作且触发 invalidate cache 操作后,从Slave DB读取数据,且读取到的数据是更新前的旧数据(由于 DB delay),从而使得重新写入 cache 中的数据仍然是旧数据。
Solution
Solution 1
如果把 invalidate/purge cache 改成 update cache,则可以避免这个问题
Solution 2 - 延时双删策略(缓存双淘汰法)
采用延时双删策略(缓存双淘汰法),可以将前面所造成的缓存脏数据,再次删除,即:
-
先写数据库
-
删除缓存
-
休眠若干时间,再次删除缓存
-
这一步可以这样实现:
- Solution 1:在第一次删除缓存后,开启一个线程,并让这个线程在1s后,执行再次删除
- Solution 2:通过读取DB的binlog和一个消息队列来实现再次删除
- 这样做的好处是对于业务的侵入性更小
-
Analysis
- single source of truth 为 DB
- 这里具体休眠多久要结合业务情况考虑
- 如果考虑到删除可能失败,再增加删除失败时的重试机制
策略3 - 先删缓存,再更新数据库
先写数据库 vs 先删缓存(Purger Cache)
OK,当写操作发生时,假设删缓存(purge)作为对缓存通用的处理方式,又面临两种抉择:
- 先写数据库,再删缓存
- 先删缓存,再写数据库
究竟采用哪种时序呢?
对于一个不能保证事务性的操作,一定涉及“哪个任务先做,哪个任务后做”的问题,解决这个问题的方向是:
如果出现不一致,谁先做对业务的影响较小,就谁先执行。
由于写数据库与删缓存不能保证原子性,谁先谁后同样要遵循上述原则。
假设先写数据库,再删缓存:第一步写数据库操作成功,第二步删缓存失败,则会出现DB中是新数据,Cache中是旧数据,数据不一致。
假设先删缓存,再写数据库:第一步删缓存成功,第二步写数据库失败,则只会引发一次Cache miss。
但是需要注意的是,可能存在这样的race condition(特别是当读QPS很高的时候)
- App1 invalidates cache
- App2 reads cache, cache miss
- Thus App2 reads from DB, and gets the old value
- App2 saves into cache with the old value
- App1 writes DB with the new value
As a result, stale data occurs.
Possible Solution
- 用 distristibuted lock, 当然,就可能suffer performance了
Better Solution
“Servilise” Storage Layer
上述缓存架构有一个缺点:业务方需要同时关注缓存与DB,有没有进一步的优化空间呢?有两种常见的方案,一种主流方案,一种非主流方案(一家之言,勿拍)。
主流优化方案是服务化:加入一个服务层,向上游提供帅气的数据访问接口,向上游屏蔽底层数据存储的细节,这样业务线不需要关注数据是来自于cache还是DB。
异步缓存更新
非主流方案是异步缓存更新:业务线所有的写操作都走数据库,所有的读操作都走总缓存,由一个异步的工具来做数据库与缓存之间数据的同步,具体细节是:
- 要有一个init cache的过程,将需要缓存的数据全量写入cache
- 如果DB有写操作,异步更新程序读取binlog,更新cache
在(1)和(2)的合作下,cache中有全部的数据,这样:
- 业务线读cache,一定能够hit(很短的时间内,可能有脏数据),无需关注数据库
- 业务线写DB,cache中能得到异步更新,无需关注缓存
这样将大大简化业务线的调用逻辑,存在的缺点是,如果缓存的数据业务逻辑比较复杂,async-update(异步更新)的逻辑可能也会比较复杂。
Reference
- https://www.usenix.org/system/files/conference/nsdi13/nsdi13-final170_update.pdf
- https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=404087915&idx=1&sn=075664193f334874a3fc87fd4f712ebc&scene=21#wechat_redirect
- https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=404202261&idx=1&sn=1b8254ba5013952923bdc21e0579108e&scene=21
- https://www.cnblogs.com/rjzheng/p/9041659.html
- https://juejin.cn/post/6964531365643550751#heading-10