今天談談分佈式事務的時序問題。在說這個問題以前首先說說這爲何是個問題。html
對於數據庫來講,讀到已經commit的數據是最基本的要求。通常來講,爲了性能,讀寫不互相阻塞,如今的數據庫系統(Oracle,MySQL,OceanBase,Spanner,CockRoachDB,HBase)幾乎無一例外的使用MVCC技術來達到這個目的。說白了,就是數據有多個版本,每次寫產生新的更大的版本。讀事務能夠指定某個版本讀,即快照讀,數據庫返回比指定的版本小的最大的版本的數據。固然也能夠不指定,即讀最新的已經commit的版本的數據。從時序上來看,越後寫的數據,版本號越大,很顯然,這個版本號能夠經過實現一個單機內單調遞增的counter來解決,counter從0開始以1遞增。可是這樣作,快照讀搞不定:查找2015年3月29日1點的最新數據。這是由於這個counter和時間沒有任何關係。那麼顯然,時間戳做爲版本號再適合不過了。在單機上,即便出現clock skew(即單機上前後兩次調gettimeofday取到的wall time,後面一次取到的wall time反而更小),維護一個單機內單調遞增的時間戳很容易辦到。能夠看出,在單機狀況下,知足了Linearizability: T2在T1 commit成功後start,T2的commit timestamp必定大於T1的commit timestamp。下面看看多機的狀況。git
在多機狀況下,如何知足Linearizability。github
仍是以寫事務T1(修改x),T2(修改y)爲例,時序上T2在T1 commit以後start,因爲不一樣的服務器的時鐘不同,有些快有些慢,致使T2可能拿到比T1更小的時間戳。算法
舉個例子:數據庫
假設機器M1的時鐘比M2的時鐘快30,T1事務在M1上提交,得到commit timestamp 200,隨後T2事務在M2上開始並提交,因爲M2時鐘更慢30,T2的commit timestamp多是180。隨後來了一個讀事務T3,讀x和y,分配的讀版本號多是190,結果他只能都到T2的值,不能讀到T1 !數組
問題的根源在於機器之間的時鐘不一樣,沒有全局時鐘。服務器
Google的Spanner(看這 和 這)和Percolator(看這和這)都是搞了一個全局時鐘來解決,區別在於Percolator的全局時鐘就是基於固定的一臺服務器產生,全部的事務獲取commit時間戳都問這個全局時鐘服務器要,天然保證了單調遞增。問題,顯而易見,單點,性能,擴展性。Spanner利用原子鐘和GPS接收器,實現了一個較爲精確的時鐘,這個時鐘叫作TrueTime,每次調用TrueTime API返回的是一個時間區間,而不是一個具體的值,這個TrueTime保證的是真實時間(absolute time/real time)必定在這個區間內,這個區間範圍一般大約14ms,甚至更小。app
下面說說Spanner是如何保證Linearizability(external consistency)。分佈式
事務的執行過程當中,Spanner保證每一個事務最後獲得的commit timestamp介於這個事務的start和commit之間。基於這個條件,若是T2在T1 commit完成後才start,那麼顯然,T2的commit timestamp確定大於T1的timestamp。性能
Spanner是如何保證每一個事務最後獲得的commit timestamp介於這個事務的start和commit之間?
在事務開始階段調用一次TrueTime,返回[t-ε1,t1+ε1],在事務commit階段時再調用一次TrueTime,返回[t2-ε2,t2+ε2],根據TrueTime的定義,顯然,只要t1+ε1<t2-ε2,那麼commit timestamp確定位於start和commit之間。等待的時間大概爲2ε,大約14ms左右。能夠說,這個延時基本上還能夠接受。
至於讀請求,直接調用TrueTime API,拿着右界去讀便可。
CockRoachDB是一個前Google員工創業的開源項目,基本上能夠認爲就是Spanner的開源實現。機器時鐘經過NTP同步,基本能夠保證機器間偏差在150ms左右。
若是按照Spanner的作法,寫事務提交時每次都須要等待150ms,性能基本不可接受,固然CockRoachDB可讓客戶端選擇是否使用這種方案,這種方法實現了Linearizability,能夠性能太差,由於時鐘偏差太大,和Spanner的高精度時鐘無法比。
CockRoachDB作了一點work around,同時實現了一種比Linearizability更relax一點的一致性模型,能夠保證下面兩種狀況的Linearizability。
CockRoachDB 實現了單個客戶端的Linearizability,保證同一個客戶端前後發出去的兩個事務T1和T2,T2的commit timestamp比T1的commit timestamp更大。方法就是T1事務執行完成會將commit timestamp返回給客戶端,客戶端執行T2時提供一個更大的時間戳給server,告訴server,T2的commit timestamp必須比這個時間戳更大。這樣就保證了單個客戶端的Linearizability。
假設有兩個客戶端C1和C2,C1先執行寫事務T1,請求發送給了機器M1,其中須要修改x,T1 commit後,C2寫事務T2 start,請求發給了機器M2,事務也須要修改x,CockRoachDB能夠保證T2分配到的commit timestamp比T1更大。
說這個以前,先看看如何界定兩個事件的前後順序。
經過捕捉兩個事件的因果關係能夠給兩個事件定序,主要基於以下兩條規則:
Vector Clock能夠用來維護這種因果關係,基本原理就是在一個有N個節點的集羣中,每一個機器都維護一個大小爲N的數組(Vector),數組記做VC,機器i上的VC[k]表明機器i對機器k的clock的認知。每一個機器i在發消息m時都會將本地的VC[i]加1(更新本地的clock),而後用它標記消息m,最後把消息發出。每一個接收到消息的機器都會取本身的clock和消息中的clock的最大值來更新本身的clock(更新本地的clock)。這個clock實際上就是Logical Clock。Logical Clock越大說明這臺機器的"時間"越靠後。在這個思想中,實際上,假設的是機器和機器之間的物理時鐘差是無窮大的,只要兩臺機器之間沒有進行過消息交互,那麼這兩臺機器互相之間對對方沒有任何知識。那麼,顯然,因爲這種logical clock的實現和物理時間沒有任何關係,在真實的系統中,沒法知足快照讀:讀2015年3月29日以前1點的最新數據。
而Spanner的TrueTime API和上述方法是兩個極端,徹底不捕捉事務之間的因果關係,純粹的根據TrueTime來對事件進行定序。
而CockRoachDB使用了Hybrid Logical Clock(HLC),它是另一種Logical Clock的實現,它將Logical Clock和物理時鐘(wall time)聯繫起來,而且他們之間的偏差在一個固定的值以內。這個值是由NTP決定的。每臺機器更新HLC的算法和上面描述VC的過程大同小異。這種Logical Clock的實現很是簡單,這裏就不展開,具體看這篇論文。實際上,HLC帶來了兩點好處:
回到這一小節最開始的例子:
假設有兩個客戶端C1和C2,C1先執行寫事務T1,請求發送給了機器M1,其中須要修改x,T1 commit後,C2寫事務T2 start,請求發給了機器M2,事務也須要修改x,CockRoachDB能夠保證T2分配到的commit timestamp比T1更大。
那麼只要分佈式事務的coordinator在肯定事務的commit timestamp的過程當中詢問各個參與者participants的本地HLC,選取其中最大的HLC做爲事務的commit timestamp,便可知足Linearizability的要求。
能夠看出,CockRoachDB實際上實現了兩種一致性級別,第一種就是Linearizability,實現方式和Spanner同樣,commit的時候都須要等(實際上,Spanner不是每次都要等,而CockRoachDB每次都須要等),可是因爲其時鐘偏差很大,實際性能不好。第二種就是比Linearizability更寬鬆一點的一致性,這種一致性級別能夠保證同一個客戶端的Linearizability和相關事務的Linearizability。
Spanner: Google’s Globally-Distributed Database
Logical Physical Clocks
and Consistent Snapshots in Globally Distributed Databases
Beyond TrueTime: Using AugmentedTime for Improving Spanner
Spencer Kimball on CockroachDB, talk given at Yelp, 9/5/2014