也许你会觉得缓存读写很简单:
先读缓存,缓存不命中就查DB,查到了就回种缓存
先删缓存,再更新DB,而后续操作会把数据再装载到缓存
这是错误的。最简单的两个并发操作:更新&查询。
更新操作删除缓存后,查询操作没有命中缓存,先把老数据读出来后放到缓存,然后更新操作更新了数据库。于是,缓存中的数据还是老数据,导致缓存中的数据是脏的,而且还一直这样脏下去。
针对不同的业务场景,实际选用的缓存的读写策略也不同。为方便讨论,这里假定更新数据库、缓存都成功。
1 Cache Aside(旁路缓存)
最常用的模式:
- 失效
应用先从cache取数据,没有得到,则从DB取数据,成功后,放入cache - 命中
应用程序从cache中取数据,取到后返回 - 更新
先把数据存到DB,成功后,再让缓存失效
注意,是先更新DB,成功后,让缓存失效。
一个查询操作,一个更新操作的并发
首先,没有了删除cache数据的操作,而是先更新数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新的数据,但是,更新操作马上让缓存的失效了,后续的查询操作再把数据从数据库中拉出来。而不会像文章开头的那个逻辑产生的问题,后续的查询操作一直都在取老的数据。
这是标准的design pattern,包括Facebook的论文《Scaling Memcache at Facebook》也使用了这个策略。为什么不是写DB后更新缓存?可以看一下Quora上的这个问答《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》,主要是怕两个并发的写操作导致脏数据。
那Cache Aside有并发问题吗?
有。比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。
这个情形理论上会出现,不过,实际上出现的概率可能非常低,因为需要发生在读缓存时缓存失效,而且并发着有一个写操作。
而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大
这也就是Quora上的那个答案里说的,要么通过2PC或是Paxos协议保证一致性,要么就是拼命的降低并发时脏数据的概率,而Facebook使用了这个降低概率的玩法,因为2PC太慢,而Paxos太复杂。当然,最好还是为缓存设置上过期时间。