1. 概述
在上一篇文章《缓存读取术之防止缓存雪崩》里我们解决了引入缓存后读数据的问题,本文分析写数据要考虑的问题。数据变更时是更新缓存还是淘汰缓存?是先写DB再写Cache,还是先写Cache再写DB?如何考量?另外,如果写DB成功了但写Cache失败了,数据就会不一致,如何解决?下面逐一探讨。
本文讨论的缓存更倾向于分布式缓存,不过解决方案的思路对本地缓存而言也是大致适用的
2. 更新缓存还是淘汰缓存 ?
为方便讨论,我们先假设更新数据时,先操作数据库,再操作缓存。现在有2个并发的写操作,看下“更新”缓存会怎样,如下图
- 应用A先发起更新DB的操作A-1,应用B后发起操作B-1,都成功后DB的数据此时是B-1操作后的数据;
- 因为某种原因,应用B更新缓存(B-2操作)要快于应用A(A-2操作),从而后到的A-2操作会覆盖掉B-2的数据,导致缓存最终的数据是A-2更新的数据,这样DB和缓存的数据就不一致。
如果选择淘汰缓存的策略,就不存在这种并发下覆盖数据的问题。A-2和B-2都会把缓存删掉,谁先谁后都没关系,下次读的时候自然会重新加载。
结论:淘汰缓存。
3. 先操作DB还是先操作缓存 ?
因为我们采用了淘汰缓存的策略,所以问题变成先操作DB还是先淘汰缓存。来考虑下先淘汰缓存会怎样。假设一个写操作和一个读操作并发出现(这是很常见的情形),如下图
- 先发生写操作,应用A淘汰了缓存(W-1),在W-2没完成之前,应用B的读操作进来了。
- R-1操作读取缓存,发现没有就去读DB(R-2操作),然后把数据写入缓存。
- 应用A的W-2操作完成,把新的数据更新到DB。这样就导致DB和Cache数据不一致。
由于写操作往往比读操作更加耗时,所以上面的并发情况还是很有可能出现的。
那么先操作DB又如何呢?
首先类似上面的并发顺序下是没有问题的:应用A先更新DB,在淘汰缓存之前,有读操作发生,应用B拿到cache中的旧值并返回,然后应用A淘汰了缓存。那么下次的读操作就能获取到新值,缓存也会被更新。是没有问题的。
再考虑这种并发顺序:
- 应用B读操作先到(R-1操作),此时缓存为空,R-2操作读取DB。R-2成功执行。在把数据写到缓存以前(R-3操作),应用A的写操作进来了。
- 应用A更新DB(W-1操作),淘汰缓存(W-2操作)都已成功执行。接着应用B的R-3操作才执行。这样缓存的数据就是旧的值,跟DB的新值就不一致。
这种情况虽然有可能发生,但概率极低。因为要满足的条件非常苛刻:写操作(W-1,W-2)要刚好在R-2操作完成之后进来,并且要比一个写缓存操作(R-3)更快完成。写DB一般比写缓存要慢,所以上面是小概率事件。
结论:先操作DB后淘汰缓存。
4. 写DB成功但淘汰缓存失败怎么办?
如下图:
第一步更新DB成功,但第二步淘汰缓存却失败了。也会数据不一致。怎么办?可以对DB进行回滚操作,这在程序中是比较容易实现的,把写DB和写缓存放在一个事务内,捕获到写缓存的异常就回滚DB操作。也可以先重新若干次“淘汰缓存”操作后再考虑是否回滚。
5. 小结
经过上面的分析,为最大程度保证DB和缓存的数据一致性,显然我们可以优先考虑的策略是:
- 先更新DB后淘汰缓存
- 如果更新DB成功,但淘汰缓存失败,则回滚DB操作
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。