最近思路有些枯竭,找些務虛的話題來湊。本文內容徹底來自於Martin Kelppmann在2019年Kafka倫敦峯會上的演講。順便提一句,Kelppmann是《Designing Data-Intensive Applications》的做者。提起DDIA的大名想必各位應該是有所耳聞的。mysql
Apache Kafka是數據庫嗎?你可能會以爲奇怪,Kafka怎麼多是數據庫呢?它沒有schema,也沒有表,更沒有索引。它僅僅是生產消息流、消費消息流而已。從這個角度來講Kafka的確不像數據庫,至少不像咱們熟知的關係型數據庫。那麼到底什麼是數據庫呢?或者說什麼特性使得一個系統能夠被稱爲數據庫?經典的教科書是這麼說的:數據庫是提供 ACID 特性的,即atomicity、consistency、isolation和durability。好了,如今問題演變成了Apache Kafka支持ACID嗎?若是它支持,Kafka又是怎麼支持的呢?要回答這些問題,咱們依次討論下ACID。redis
一、持久性(durability)sql
咱們先從最容易的持久性開始提及,由於持久性最容易理解。在80年代持久性指的是把數據寫入到磁帶中,這是一種很古老的存儲設備,如今應該已經絕跡了。目前實現持久性更常見的作法是將數據寫入到物理磁盤上,而這也只能實現單機的持久性。當演進到分佈式系統時代後,持久性指的是將數據經過備份機制拷貝到多臺機器的磁盤上。不少數據庫廠商都有本身的分佈式系統解決方案,如GreenPlum和Oracle RAC。它們都提供了這種多機備份的持久性。和它們相似,Apache Kafka自然也是支持這種持久性的,它提供的副本機制在實現原理上幾乎和數據庫廠商的方案是同樣的。數據庫
二、原子性(atomicity)緩存
數據庫中的原子性和多線程領域內的原子性不是一回事。咱們知道在Java中有AtomicInteger這樣的類可以提供線程安全的整數操做服務,這裏的atomicity關心的是在多個線程併發的狀況下如何保證正確性的問題。而在數據庫領域,原子性關心的是如何應對錯誤或異常狀況,特別是對於事務的處理。若是服務發生故障,以前提交的事務要保證已經持久化,而當前運行的事務要終止(abort),它執行的全部操做都要回滾,最終的狀態就好像該事務從未運行過那樣。舉個實際的例子,好比下面這張圖:安全
在異質分佈式系統中一個比較經典的問題就是如何確保不一樣系統之間的數據同步。好比這個圖中如何確保數據庫、緩存和搜索索引之間的數據一致性就是一個關於原子性的問題:app寫入數據庫的寫更新如何同步到cache和Index中,更關鍵的是如何確保這些寫更新與以前寫數據庫是原子性的,要麼它們所有寫入成功,要麼所有寫入失敗。我以前在知乎上也回答過一個相似的帖子,是關於「如何保持mysql和redis中數據一致性」的。使人意外地收穫了近100個贊,感受比我回答10個Kafka問題獲得的贊還要多,這也足見這種一致性問題是多麼地受歡迎。網絡
顯然,要實現這種分佈式場景下的數據一致性並不容易。一個典型的異常場景就是當發生cache寫入成功,而Index寫入失敗時,應用程序應該如何處理?以下圖所示:多線程
讓app重試彷佛是一個可行的選擇,但重試的頻率該怎麼設定呢?更要命的是,若是由於網絡的問題使得Index其實寫入成功,但response返回失敗,此時app重試有可能發生重複生產數據的問題,這還須要Index端有數據去重的能力。若是是撤銷數據庫和cache以前的寫入呢? 以下圖所示:架構
彷佛這個方法也是可行的,但這就有了linearizability的問題了:即用戶在某個時刻T看到了這個寫入帶來的新值,但在以後的某個時刻T1該值又變回了以前的老值,這必然形成用戶的困擾,所以也不是一個好辦法。併發
實際上,解決這個問題的常見作法是採用兩階段提交(2PC)這樣分佈式事務。不過2PC是出了名的慢,並且存在單點故障的隱患(coordinator),更重要的是它要求全部系統都要支持XA,但像Redis和ElasticSearch這樣的系統本質上是不支持XA的,所以也就不能使用2PC來保證原子性。
第三個方法是採用基於日誌結構的消息隊列來實現,好比使用Kafka來作,以下圖所示:
在這個架構中app僅僅是向Kafka寫入消息,而下面的數據庫、cache和index做爲獨立的consumer消費這個日誌——Kafka分區的順序性保證了app端更新操做的順序性。若是某個consumer消費速度慢於其餘consumer也不要緊,畢竟消息依然在Kafka中保存着。總而言之,有了Kafka全部的異質系統都能以相同的順序應用app端的更新操做,從而實現了數據的最終一致性。這種方法有個專屬的名字,叫capture data change,也稱CDC。
三、隔離性(isolation)
在傳統的關係型數據庫中最強的隔離級別一般是指serializability,國內通常翻譯成可串行化或串行化。表達的思想就是鏈接數據庫的每一個客戶端在執行各自的事務時數據庫會給它們一個假象:彷彿每一個客戶端的事務都順序執行的,即執行完一個事務以後再開始執行下一個事務。其實數據庫端同時會處理多個事務,但serializability保證了它們就像單獨執行同樣。舉個例子,在一個論壇系統中,每一個新用戶都須要註冊一個惟一的用戶名。一個簡單的app實現邏輯大概是這樣的:
1) 首先,發起SQL查詢:select count(*) from user_accounts where username = 'jane',查看是否存在名爲jane的用戶;
2. 若是返回0, 則執行 insert into user_accounts(username, ...) values("janes", ...) 註冊用戶
顯然存在某個特殊的時刻,使得兩個新用戶同時發現某個用戶名可用,從而最終註冊了相同的用戶名,以下圖所示:
這種就不是serializability級別的隔離,若是要實現這種惟一性,你就須要提升數據庫的隔離級別到serializability。針對這個需求,咱們可使用Kafka來幫助實現嗎?固然是能夠的!以下圖所示:
若是把用戶名做爲key,那麼顯然請求同一個用戶名的用戶必然訪問Kafka主題的同一個分區上,此時根據Kafka分區消息寫入先後順序來肯定誰先誰後就是一個天然的選擇。數據庫讀取Kafka分區中的註冊消息,發現紅色標識的用戶最早寫入了key=jane的消息,那麼當它再次讀到key=jane的消息時就能明確拒絕綠色用戶發起的請求,由於jane用戶名已經被註冊了。固然要實現這一整套的流程,你須要的不只是Kafka,更要是一套相應的流處理管道,好比使用Kafka Streams。但不管如何,Kafka能夠被用來實現這種事務的隔離性。依託Kafka的好處在於它不只實現了serializability,並且依靠Kafka的分區機制,它能處理多個不一樣的用戶名註冊,於是也實現了scalability。
四、一致性(consistency)
最後說說一致性。按照Kelppmann大神的原話,這是一個很奇怪的屬性:在全部ACID特性中,其餘三項特性的確屬於數據庫層面須要實現或保證的,但只有一致性是由用戶來保證的。嚴格來講,它不屬於數據庫的特性,而應該屬於使用數據庫的一種方式。坦率說第一次聽到這句話時我本人仍是有點震驚的,由於從沒有往這個方面考慮過,但仔細想一想還真是這麼回事。好比剛纔的註冊用戶名的例子中咱們要求每一個用戶名是惟一的。這種一致性約束是由咱們用戶作出的,而不是數據庫自己。數據庫自己並不關心或並不知道用戶名是否應該是惟一的。針對Kafka而言,這種一致性又意味着什麼呢?Kelppmann沒有具體展開,但我我的認爲他應該指的是linearizability、消息順序之間的一致性以及分佈式事務。幸運的是,Kafka的備份機制實現了linearizability和total order broadcast,並且在Kafka 0.11開始也支持分佈式事務了。
至此,咱們說完了經典數據庫中的ACID特性以及在Kafka中是如何支持它們的。如今你以爲Kafka是數據庫了嗎:) 這是個開放的問題,咱們能夠一塊兒討論下~~