分佈式數據一致性解決方案推理過程

使用 redis 很容易實現分佈式鎖:setnx,同一個 key,誰設置成功了,誰就搶到了鎖,所以就產生了多鎖問題。

假設客戶端 1 搶到了鎖,redis 掛了,redis 重啓了之後,客戶端 2 又搶到了鎖。

redis持久化能否解決多鎖問題?

持久化有 RDB 和 AOF。

RDB 是窗口機制的,會丟失將近一個窗口的數據,比如丟失半小時的數據。

AOF 有三個緩存級別:每操作成功就記日誌、一秒記一次、內核的緩衝區滿了才記。

redis 要開啓 AOF 最高的緩存級別來保證鎖的一致性。

用戶發了一個 setnx 請求,立刻持久化到 AOF 裏面,這樣才能保證重啓之後,鎖被帶到內存,另外一個客戶端搶不到該鎖,因爲它沒有被釋放。

當開啓 redis 持久化方案的時候,性能會下降,redis 本來就是基於內存的,它的特徵就是快,開啓 AOF 必然降低性能。

如果不想開啓 AOF,開啓哨兵模式:主從複製,加一個哨兵,把鎖同步到另外一個節點。

一個 redis 掛了之後,由哨兵快速的把另外一個 redis 切換成主。

redis數據同步

一個 a 程序、一個 b 程序、一個客戶端,

客戶端將 a=3 寫入 a 程序,爲了保證 a 和 b 2 個 redis 數據的一致性,會有 2 種同步方式:

第一種:

客戶端將 a=3 寫入 a 程序,寫成功了,但不返回成功,還在這裏阻塞着,a 同步給 b,把 a=3 寫給 b, b 返回成功之後,a 再給客戶端返回成功。

如果 a 掛了,還可以從 b 取到 a=3,這是強一致性的。

有了redis a,爲什麼還要有redis b?

爲了解決可用性問題,本來不加 b,a 可以一直存活,加了 b,b 掛了,a 沒掛,給 a 寫,還必須要同步給 b,b 還不在,只能阻塞等 b 恢復,即強一致性行爲間接會破壞可用性。如果再有一個 redis c,則出現問題的幾率更高。

redis 主從複製的確可以設置爲強一致性,可以配置主收到幾個從節點返回的 ok,才允許客戶端返回。

還有另外一種一致性行爲,非同步阻塞,

只要給 a 寫入成功,就可以給客戶端返回成功,

這時候 a 需要把數據發給 b 或發給 c,但是 a 不等 b 或 c 返回 ok,

若 a 剛給客戶端返回 ok,a 掛了,但 b 和 c 還沒有處理,此時隨便選擇一個當主,客戶端就獲取不到 a=3 了,這叫弱一致性,弱一致性不能保證數據的一致性。

a 掛掉了,把 a=3 帶走了,a 把 a=3 給 b 和 c 的時候,這是一個不可靠的給法,因爲它不知道對方有沒有收到 a=3 或 a=3 有沒有處理成功等一些列的未知情況。

假設在 a 和 b、c 中間加一個可靠的技術 d,d 和 a、b、c 之間不會出現網絡斷開的情況。

a 把 a=3 給到了 d,a 掛了,d 還沒有給到 b 和 c,d 中有 a=3,未來只需要 b 能夠連接到 d 這個區域,把 a=3 取回來,最終 b 和 c 可以還原到 a 掛機的狀態。

數據同步可能會有延遲,但可以達到最終一致性。

d 區域一般不是一臺服務器,推薦 3、5、7.. 臺。

客戶端把 a=3 寫入中間集羣中的每一個節點,如果 3 個都返回 ok,才返回給客戶端,這是強一致性,但又破壞了可用性。

有一部分返回 ok 就可以的話就規避了掛機的風險。

那一部分是多少個呢?

如果把 a=3 請求其中一臺,再請求另外兩臺的時候,就請求不到 a=3 了或者另外一個客戶端從別的節點上取不到 a=3。

如果 1 個節點的話,那更多的節點是取回數據不一致性的情況,但只要有一個節點活着,就可以對外提供服務。

如果是 2 個節點返回 ok 呢?

a 給這個集羣寫 a=3,返回 2 個 ok。

同時不要只關注 a,還要關注中間的這個集羣,集羣裏面必須要兩兩通信。

只要兩個節點能夠互相通信,勢力範圍才能滿足 2,只要有一個節點不能和別人通信了,勢力範圍就是 1 了,不能滿足 2,這臺就會 shutdown 自己,不會對外提供讀寫服務。

客戶端從集羣中節點 3 取數據的時候,因爲它已經 shutdown 了(此時進程還活着,只是跟別的節點連不上 ,必須要找到另外一個小夥伴,2 個節點的時候,才能對外提供服務), b 和 c 只能從另外兩臺(節點 1、2)取,這兩臺曾經又登記過 a=3,所以對方一定能夠取到正確的數據,這樣就可以保證數據最終一致性了。

節點 3 也會從另外 2 個節點同步數據,最終整個集羣也是數據一致的。

如果集羣中有 5 個節點,

1,2 組成一個小集羣 A,4,5 組成一個小集羣 B,3 誰也不理誰,2 個小集羣 A,B 都可以對外提供服務,這樣出現了腦裂情況。

所以如果 5 個節點,3 個組成一個小集羣才能對外提供服務的話,上面的 2 個小集羣均不能對外提供服務,即 redis b 和 c 從小集羣 A 和 B 上是獲取不到 a=3 的,因爲不滿足 3 個節點,不能對外提供服務,

集羣中,5 個節點,同時給這 3(節點 1-2-3)個寫入成功,這 3 個組成的小集羣才能對外提供服務。

如果不過半,就通過,那麼就會產生腦裂問題。

可不可以偶數個節點?

3 臺過半是 2 臺,允許一臺掛機,集羣還能繼續運行。

四臺過半數是 3,承擔風險還是 1 個。

奇數 3 和偶數 4 承擔的風險是一樣的,4 臺還更容易掛一臺,所以偶數臺沒有必要,風險反而更大,所以從成本和風險考慮,選擇奇數臺。

中間這個集羣是什麼?

zk 是分佈式協調服務,自帶高可靠光環。

redis 組建一個主從複製分佈式集羣,需要解決分佈式數據同步或選主等分佈式行爲。

zk 給別人提供服務的時候,如果它自己都不可靠的話,那別人更不可靠。

zk 必須是集羣多機的,所以自帶了高可靠的光環,掛了一臺無所謂,依然對外提供服務。

zk 對外提供服務的時候,還需要考慮成本,

a 和集羣中的 5 個節點都建立連接,然後過半的節點都給它返回 ok,這樣增加了客戶端連接和通信的成本。

將中間區域做一個整體對外提供服務

a 向這個中間件發送數據,中間件返回給 a 有沒有寫入成功即可。

redis 集羣部署的時候,一般是主從集羣,在三個裏面,選出一個 leader,這個 leader 負責增刪改查和向其他從節點同步數據,只要 leader 活着,其他從節點有一個活着,在滿足勢力範圍是 2 的條件下,就可以組成一個小集羣對外提供服務。

a 只要發給中間件集羣中的 leader 節點就可以了。

zk 爲了完成分佈式協調做了一些事情,redis 卻沒有做這些事情。

zk 在 leader 節點活着的時候叫主從集羣,和主節點通信就可以了,主在同步給從節點,主一旦掛掉了,就會進入無主集羣,只有主才能對外提供服務,此時就會立刻選主,從剩下 2 臺中選擇一個主,只要選主夠快,對外影響就不大。

選的要夠快,而且能夠選出來,官方在集羣壓測的情況下,恢復 leader 時間是 200ms,

裏面也有 observer 觀察者,即便 zk 集羣很大,最終選主投票的節點可以限定的很少,所以恢復 leader 的時間是很快的,

這麼快是怎麼實現的?

比如班級 100 個人中,投票選舉誰最帥,每個人都發自己最帥,這樣會陷入惡性循環,第一輪投票失效,可能會投出 30 秒或 30 個小時。加一個條件,誰可以說話,誰就最帥,這樣只能選擇老師,只有老師纔可以說話。

zk 哨兵模式就是這樣做的,zk 配置文件中有一個 myid,server.1 server.2 ... 會給每一個 zk server 一個數值,而且全局唯一不能重複。

主從集羣有一個好處 ,主是單點的,一臺很容易實現串行化。

還有一個事務 id,客戶端給主發了一個操作,給一個事務 id 加 1,第二個操作再增加 1,這就是 zxid。

當有一個節點掛了之後,剩餘存活的節點要亮出自己的 2 個 id,優先判斷誰的事務 id 最大,因爲事務 id 最大代表數據最全,所以誰的事務 id 最大,誰立刻就是 leader 了。

如果兩個人的事務 id 都一樣,就亮出自己的全局唯一的 id,誰最大,誰就是 leader,所以選舉的過程,根本不需要消耗太多的時間,這是謙讓機制。

從無主投票到過半機制,其實是符合 paxos 協議的,zk 單獨實現了 paxos 協議,叫 zab 協議。

如果想使用redis做分佈式鎖怎麼做

redis 開啓磁盤持久化後性能會下降,開啓主從複製,強一致性數據同步容易破壞可用性,redis 中間還沒有加數據緩衝的過程即上面所說的中間件區域,redis 本身沒有實現,但 zk 實現了。

redis 自己同步數據的時候只能實現弱一致性,可能會出現數據不一致,就會造成雙鎖的現象。

c1 從 redis A 獲取到了分佈式鎖,redisA 還沒有來得及同步給 redisB,redisA 就掛了。

哨兵把 redisB 設置成主,c2 從新主搶到了鎖,結果 2 個客戶端都拿到一把鎖。

使用 redis 做分佈式鎖一直就存在爭議。

替代性 redis 分佈式鎖的是 zk,zk 做分佈式協調,本身就是主從的。

redis 胖客戶端

應用程序中引入 redis jar 包,客戶端自己每次要寫三個,

這是一個間接的僞 cp,客戶端儘量向所有 redis 去寫數據,

客戶端 1 和客戶端 2 都去搶鎖,如何確定鎖的唯一性?

不是 redis 自身實現的(類似於 zk 實現的),而是由客戶端來代替了 leader 角色。

倆客戶端去搶鎖,要是 zk 的話 ,客戶端給 zk 的 leader 就可以了,zk 自己去同步。

redis 胖客戶端寫數據給 redis 集羣中的每個節點,寫之後,另外一個客戶端如果想判斷有沒有這把鎖,有的話,就說明被其他客戶端是使用了,儘量查詢每個節點數據,客戶端再判斷到底有沒有這把鎖。

這種方式也可以承擔一部分掛掉的風險,3 個 redis 節點允許掛掉一個。

解決分佈式數據一致性的話,要麼是基於 paxos 協議,由中間件自己去設計實現,要麼就是由客戶端自己完成對過半機制的判定。

不推薦 redis 實現分佈式鎖,zk 肯定要優於 redis,還有另外一種 k8s etcd 速度高於 zk,實現較複雜。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/0YjpUhSjXWNs3T9MtiBCRw