本文始發於個人公眾號:TechFlow
在計算機系統的領域,一致性可以說是一個高頻詞,可能出現的場景很多。從分布式系統到數據庫的事務,都有它的身影。
之前我們在介紹數據庫事務的時候,談到過事務的一致性。在數據庫當中,一致性是一種目的,不是一種手段。數據庫希望控制事務的原子性、隔離性和持久性來保證數據的一致性。這里的一致性更多的指的是實際和我們觀念的一致。也就是說結果都在我們預期之內。而在分布式系統當中,一致性有另外的含義,一個是多份數據副本之間的一致性問題,另一個是多階段提交的一致性問題。我們今天先來聊聊副本一致性問題。
這個問題出現的原因很簡單,因為分布式系統當中,數據往往會有多個副本。如果是一台數據庫處理所有的數據請求,那么通過ACID四原則,基本可以保證數據的一致性。而多個副本就需要保證數據會有多份拷貝。這就帶來了同步的問題,因為我們幾乎沒有辦法保證可以同時更新所有機器當中的包括備份所有數據。尤其是當這些機器分布在全國各地甚至是世界各地的時候,由於網絡延遲,即使我在同一時間給所有機器發送了更新數據的請求,也不能保證這些請求被響應的時間保持一致。只要存在時間差,就會存在某些機器之間的數據不一致的情況。也就是說,在分布式系統當中的一致性,指的是數據一致性。
這其實是一個兩難問題,為了解決流量過大的壓力問題,我們設計了分布式系統。但是分布式系統又會帶來數據多份拷貝不同步違反一致性的問題,我們既不能容忍數據出錯,也不能放棄分布式系統,唯一的辦法就是采取一些措施,來最大可能地降低這個問題的影響力。
多種多樣的一致性模型,就是這些措施的體現。讓我們從最簡單的嚴格一致性說起。
嚴格一致性
嚴格一致性是最理想的情況,如果我們每次請求一個數據,不管什么情況下,我們都能獲得它的最后一次改動的結果。很遺憾的是,嚴格一致性是不可能實現的。
不可能實現的原因很簡單,因為多台機器之間的數據同步需要時間,無論這個時間多小,它都是確定存在的。只要存在,就不可能實現嚴格一致性。舉個簡單的例子,我們有A和B兩台機器。在t時刻,A機器修改了某條數據,在1毫秒之前,B機器收到了一條查詢該數據的請求。當B執行這個查詢的時候,A機器已經修改完成,那么究竟B查詢到的值應該是什么呢?是A修改之前的還是修改之后的呢?在A機器看來,B的查詢發生在它修改之后,可是B機器看來卻恰恰相反,A修改值發生了在它收到請求之后。如果我們要保證嚴格一致性,那么究竟什么結果才是對的呢?
當然上面這個例子只是最極端的情況,一般只會在理論上發生,但是通過對極端情況的分析,我們也可以看得出來,嚴格一致性是不可能實現的。
強一致性與弱一致性
數據不一致出現的根本原因在於多台機器更新數據的時間差,我們更新多台機器,總有先有后,很難保證完全同步。根據同步數據時采用同步還是異步策略,又可以將一致性分為強一致性與弱一致性。
使用同步策略更新數據時,我們每次請求發給主節點,主節點收到數據之后使用同步更新的策略將數據發送給從節點。當所有的從節點更新成功之后,主節點會更新數據的狀態,使它生效,之后返回response給用戶,告知更新成功。
顯然,通過同步更新數據的策略下,一致性的保障更強。如果我們在主機上做好隔離措施,比如在更新結束之前,用戶不能發起下一次更新,那么盡可能地保證數據一致性不出問題。但是這種做法也有很大的弊端,最大的弊端就在於使用同步更新的操作,而且要所有從庫都更新成功才能返回,這樣的時間開銷非常大。最關鍵的一個缺點在於,如果一個從庫宕機,那么主庫就不可能更新所有的從庫,那么新來的請求永遠不會更新,這顯然是不能接受的。
和強一致性對應的是弱一致性,我們不采用同步策略來更新數據,而采用異步更新的方式。好處也很明顯,同步改成了異步,時間消耗大大縮減。但是問題也很明顯,除了異步本身帶來的問題之外,由於多個副本之間的數據更新發生不同時,如果連續多次訪問落到了不同的副本上,就會出現多次訪問的結果不一致的情況。
本質上來說分布式系統的一致性模型,只有強弱兩種。只不過在這兩種基礎的模型上,針對許多可能出現的問題還會進行相應的優化。總體上而言,分布式系統對於性能的要求要高於一致性,所以大多分布式系統的一致性模型,還是基於弱一致性設計的。下面就來列舉幾種,比較經典的弱一致性模型的優化方案。
讀寫一致性
讀寫一致性在日常中經常遇見,比如在某論壇當中,用戶回復了某個帖子。但是當用戶刷新的時候,可能會出現這個回復消失的情況。用戶會陷入困擾,不知道這個回復究竟有沒有成功。
會發生這種情況的原因也很簡單,因為用戶刷新的時候,訪問的從庫可能還沒有獲取到用戶回復的數據,所以顯示的結果當中自然就沒有用戶剛剛回復的內容。在這種情況下,我們需要保證讀寫一致性。也就是說用戶讀取自己寫入結果的一致性,保證用戶永遠能夠第一時間看到自己更新的內容。比如我們發一條朋友圈,朋友圈的內容是不是第一時間被朋友看見不重要,但是一定要顯示在自己的列表上。
那如何實現呢?
方案有好幾種,一種方案是對於一些特定的內容我們每次都去主庫讀取。比如我們讀取帖子當中回復信息的時候,永遠都去主庫讀取。但是這樣的問題也很明顯,可能會導致主庫的壓力過大。另一種方案是我們設置一個更新時間窗口,在剛剛更新的一段時間內,我們默認都從主庫讀取,過了這個窗口之后,我們會挑選最近有過更新的從庫進行讀取。
還有一種更好的方案是我們直接記錄用戶更新的時間戳,在請求的時候把這個時間戳帶上,凡是最后更新時間小於這個時間戳的從庫都不予以響應。也就是說只有包含用戶寫入這個更新的庫可以響應這個請求,就可以保證實現用戶端的讀寫性一致了。
單調讀
單調讀解決的是最經典的弱一致性的不一致問題,出現的場景也很簡單。由於主從節點更新數據的時間不一致,導致用戶在不停地刷新的時候,有時候能刷出來,再次刷新之后會發現數據不見了,再刷新又可能再刷出來,就好像遇見靈異事件一樣。我記得以前微博或者人人就存在這個問題,單調讀就是針對的這個場景,可以保證不會出現這種情況。
解決的方法其實很簡單,就是根據用戶ID計算一個hash值,再通過hash值映射到機器。同一個用戶不管怎么刷新,都只會被映射到同一台機器上。這樣就保證了不會讀到其他從庫的內容,帶來用戶體驗不好的影響。當然,這只是一種解決方案,其他的解決方法還有很多,這里不一一討論。
因果一致性
因果一致性針對的數據之間邏輯上的因果問題,舉個例子,比如說用戶在知乎里提問題和回答問題。想要回答問題,必須要保證有對應的問題。也就是說一定是先有問題,再有的回答。可是問題和回答並不一定存儲在同一個節點上,很有可能出現問題存入節點A,回答存入節點B的情況。因為存在同步延遲,所以就可能出現查詢的用戶只看得到回答,卻找不到對應問題的情況,違反了事物之間的因果性。
為了解決這個問題,一種方案是在寫入的時候遵循某種邏輯順序,那么在讀取的時候,就可以保證不會出現因果錯亂的情況。但問題是,很多因果性並不想問題和回答這么明顯,一些隱藏的因果性可能很難被輕易判斷,就需要引入更高深的技術,感興趣的同學可以去搜索一下向量時鍾深入了解。
最終一致性
聽名字這種方案似乎很厲害,其實最終一致性是所有分布式一致性模型當中最弱的。可以認為是沒有任何優化的“最”弱一致性,它的意思是說,我不考慮所有的中間狀態的影響,只保證當沒有新的更新之后,經過一段時間之后,最終系統內所有副本的數據是正確的。
有大神打了這么一個比方,就好像你去點星巴克。你並不知道星巴克什么時候做好,你在做好之前去拿,拿到的結果是錯的。但是你知道,經過一段時間之后,你一定可以拿到你想要的。至於星巴克做好需要多久,往往沒有定論,可以是幾百分之一秒,也可以是幾個小時。
聽起來這個方案很不靠譜,在確定它可行之前,我們還想問幾個問題,首先,系統能不能保證一段時間是多久?如果天荒地老怎么辦?其次,因為最終才能收斂,那么在收斂之前,多個副本之間的值可能都不同,究竟又該以哪個為准?
好在,這兩個問題都能回答。對於第一個問題,答案是系統沒辦法確定究竟需要多久收斂,但是可以確定最大的收斂時間。有點像是物理學上的半衰期,我們不知道一個粒子究竟需要多久衰變,但是可以確定足夠多的粒子當中的一半衰變所需要的時間。第二個問題更好回答,當有多份數據出現的時候,通常的做法是選擇其中時間戳較大的,也就是說出現較晚的值。
雖然最終一致性看起來很不靠譜,但是它最大程度上保證了系統的並發能力,也因此,在高並發的場景下,它也是使用最廣的一致性模型。
到這里為止,分布式系統當中常見的一致性模型就介紹完了。分布式系統有一個很大的特點,就是我們看專業名詞的時候往往雲里霧里,不知所雲。但是當我們去了解它背后的設計理念與出現的原因,就能發現它的有趣。衷心希望大家都能從中發現自己的樂趣,都有學有收獲。
如果喜歡本文,請順手點個關注吧,你們的支持是我最大的動力。