https://blog.csdn.net/HalfImmortal/article/details/106962943
https://coolshell.cn/articles/17416.html
https://www.quora.com/Why-does-Facebook-use-delete-to-remove-the-key-value-pair-in-Memcached-instead-of-updating-the-Memcached-during-write-request-to-the-backend
https://zhuanlan.zhihu.com/p/86396877
引言:一个错误的例子(先删cache后update DB)
更新缓存数据代码时,先删除缓存,然后再更新数据库,而后续的操作会把数据再装载的缓存中。然而,这个是逻辑是错误的。试想,两个并发操作,一个是更新操作,另一个是查询操作,更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存中,然后更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
更新缓存的的Design Pattern有四种:Cache aside, Read through, Write through, Write behind caching
1. Cache aside(先数据库后缓存)
最常用的pattern,其具体逻辑如下:
(1)失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
(2)命中:应用程序从cache中取数据,取到后返回。
(3)更新:先把数据存到数据库中,成功后,再让缓存失效。
- 为什么使用失效delete而不是替换update?
担心两个并发的写操作导致不可控的后果,反正最终以数据库为标准数据,并发delete的结果是相对可控的。
- 是不是Cache Aside这个就不会有并发问题了?
不是的,比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。
这个case理论上虽然会出现,不过,实际上出现的概率可能非常低(需要满足四个条件),因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。但是出现了也没关系,我们还有依照:延时双删策略(如果是分布式的主从模式,睡眠时间修改为在主从同步的延时时间基础上,加几百ms)
// 通过设置延迟删除,保障单机模式下在该时延内无脏数据的情况
public void write(String key,Object data){
redis.delKey(key);
db.updateData(data);
Thread.sleep(1000);
redis.delKey(key);
}
对于删除cache失败的情况,可以结合binlog和消息队列进行重试:
对于删除数据库的数据失败的情况:如果你需要强一致性,你需要使用“两阶段提交协议”(2PC)——prepare,commit/rollback,比如Java 7 的XAResource,还有MySQL 5.7的 XA Transaction,有些cache也支持XA,比如EhCache。当然,强一致性会导致性能下降,也就是慢。
正如Quora上的那个答案里说的,要么通过2PC(两阶段提交)或是Paxos协议保证一致性,要么就是拼命的降低并发时脏数据的概率,而Facebook使用了这个降低概率的玩法,因为2PC太慢,而Paxos太复杂。当然,最好还是为缓存设置上过期时间。
2. Read/Write Through Pattern(缓存隔离了用户的更新操作,交给缓存服务来代理)
我们的应用代码不再需要维护两个数据存储,缓存(Cache)和数据库(Repository)。Read/Write Through套路是把更新数据库(Repository)的操作由缓存自己代理了,所以,对于应用层来说,就简单很多了。可以理解为,应用认为后端就是一个单一的存储,而存储自己维护自己的Cache。
Read Through
Read Through 套路就是在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出),Cache Aside是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载,从而对应用方是透明的。
Write Through
Write Through 套路和Read Through相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作)
3. Write Behind Caching Pattern
在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作加速(因为直接操作内存嘛 ),因为异步,write backg还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。
但是,其带来的问题是,数据不是强一致性的,而且可能会丢失。因为一般只会在内存接近溢出或者Cache即将失效的情况下才会把缓存放到硬盘上。