概括:緩存是通過犧牲強一致性來提高性能的。
這個是由CAP理論決定的。緩存系統適用的場景就是非強一致性的場景,它屬於CAP中的AP。
強一致性還是弱一致性?
CAP理論,指的是在一個分布式系統中,只能滿足其中兩項,三者不可兼得。
CAP理論作為分布式系統的基礎理論,它描述的是一個分布式系統在以下三個特性中:
- 一致性(Consistency)
- 可用性(Availability)
- 分區容錯性(Partition tolerance)
最多滿足其中的兩個特性。也就是下圖所描述的。分布式系統要么滿足CA,要么CP,要么AP。無法同時滿足CAP。
那么什么是一致性、分區容錯性、可用性?
分區容錯性:指的分布式系統中的某個節點或者網絡分區出現了故障的時候,整個系統仍然能對外提供滿足一致性和可用性的服務。也就是說部分故障不影響整體使用。
事實上我們在設計分布式系統是都會考慮到bug,硬件,網絡等各種原因造成的故障,所以即使部分節點或者網絡出現故障,我們要求整個系統還是要繼續使用的
(不繼續使用,相當於只有一個分區,那么也就沒有后續的一致性和可用性了)
可用性: 一直可以正常的做讀寫操作。簡單而言就是客戶端一直可以正常訪問並得到系統的正常響應。用戶角度來看就是不會出現系統操作失敗或者訪問超時等問題。
一致性:在分布式系統完成某寫操作后任何讀操作,都應該獲取到該寫操作寫入的那個最新的值。相當於要求分布式系統中的各節點時時刻刻保持數據的一致性。
所以,如果需要數據庫和緩存數據保持強一致,就不適合適用緩存。
所以使用緩存提升性能,就是會有數據更新的延遲。這需要我們在設計時結合業務仔細思考是否適合用緩存。然后緩存一定要設置過期時間,這個時間太短、或者太長都不好:
- 太短的話請求可能會比較多的落到數據庫上,這也意味着失去了緩存的優勢。
- 太長的話緩存中的臟數據會使系統長時間處於一個延遲的狀態,而且系統中長時間沒有人訪問的數據一直存在內存中不過期,浪費內存。
但是,通過一些方案優化處理,是可以保證弱一致性,最終一致性的。
保證數據庫與緩存的一致性的三種方案
緩存延遲雙刪
有些小伙伴可能會說,不一定要先操作數據庫呀,采用緩存延時雙刪策略就好啦?
那么什么是延時雙刪呢?
步驟一:
先刪除緩存
步驟二:
再更新數據庫
步驟 三:
休眠一會兒(比如1秒),再次刪除緩存。
為了確保讀請求結束,寫請求可以刪除讀請求可能帶來的緩存臟數據。
刪除緩存重試機制
不管是延時雙刪還是Cache-Aside的先操作數據庫再刪除緩存,如果第二步的刪除緩存失敗呢?
刪除失敗,就會導致臟數據。
刪除失敗就多刪幾次,保證刪除緩存成功。這里使用刪除重試機制
刪除緩存重試機制的大致步驟:
- 寫請求更新數據庫
- 緩存因為某些原因,刪除失敗
- 把刪除失敗的key放到消息隊列
- 消費消息隊列的消息,獲取要刪除的key
- 重試刪除緩存操作
同步biglog異步刪除緩存
重試刪除緩存機制可以解決刪除失敗的問題,但是會造成業務代碼入侵。
其實,還可以通過數據庫的binlog來異步淘汰key。
以MySQL為例,可以使用阿里的Canal將binlog日志采集發送到MQ隊列里面,然后編寫一個簡單的緩存刪除消息訂閱binlog日志,根據更新log刪除緩存,並且通過ACK機制確認處理這條更新log,保證數據緩存一致性。
同步binlog策略是如何確保消費成功的?
PushConsumer為了保證消息肯定消費成功,只有使用方明確表示消費成功,RocketMQ才會認為消息消費成功。中途斷電,拋出異常等都不會認為成功----即都會重新投遞。
首先,消費的時候,需要注入一個消息回調,具體樣例如下:
consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { System.out.println(Thread.currentThread().getName() + " message " + mgs ); delcache(key);// 執行真正刪除 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS } });
業務實現消費回調的時候,當且僅當回調函數返回SUCCESS,RocketMQ才會認為這批消息(默認是1條)是消費完成的。
如果這時候消息消費失敗,例如數據庫異常,余額不足扣款失敗等一切業務認為消息需要重試的場景,只要返回ConsumeConcurrentlyStatus.RECONSUME_LATER,RocketMQ就會認為這批消息消費失敗了。
為了保證消息是肯定被至少消費成功一次,RocketMQ會把這批消費失敗的消息重發回Broker(topic不是原topic而是這個消費的RETRY topic),在延遲的某個時間點(默認是10秒,業務可設置)后,再次投遞到這個ConsumerGroup。而如果一直這樣重復消費都持續失敗到一定次數(默認16次),就會投遞到DLQ死信隊列。應用可以監控死信隊列來做人工干預。
Redis的發布訂閱
Redis通過publish和subscribe命令實現訂閱和發布的功能。訂閱者可以通過subscribe向redis server訂閱自己感興趣的消息類型。redis將信息類型稱為通道(channel)。當發布者通過publish命令向redis server發送特定類型的信息時,訂閱該消息類型的全部訂閱者都會收到此消息。
主從數據庫通過binlog異步刪除
因為主從DB同步存在延時時間。如果刪除緩存之后,數據同步到備庫之前已經有請求過來時, 「會從備庫中讀到臟數據」,如何解決呢?解決方案如下流程圖:
緩存與數據的一致性的保障策略總結
綜上所述,在分布式系統中,緩存和數據庫同時存在時,如果有寫操作的時候,「先操作數據庫,再操作緩存」。如下:
1.讀取緩存中是否有相關數據
2.如果緩存中有相關數據value,則返回
3.如果緩存中沒有相關數據,則從數據庫讀取相關數據放入緩存中key->value,再返回
4.如果有更新數據,則先更新數據庫,再刪除緩存
5.為了保證第四步刪除緩存成功,使用binlog異步刪除
6.如果是主從數據庫,binglog取自於從庫
7.如果是一主多從,每個從庫都要采集binlog,然后消費端收到最后一台binlog數據才刪除緩存,或者為了簡單,收到一次更新log,刪除一次緩存