Redis 在 3.0 前通常有兩種集羣方案,一是 proxy(Twemproxy、Codis),二是使用 Sentinel(哨兵)。 經過 Sentinel 是一種使用哨兵來達到高可用的方案,而 proxy 是用於在前置上進行 sharding 用代理給後端的 redis 集羣的方案,達到負載均衡的方案,在單個分片的 redis 中做主從。 由於本文要重點講解的不是 3.0 前的方案,所以說的比較粗略。redis
Redis3.0 提供了官方的 Redis cluster 機制支持。主要經過內部無中心的多個節點來達到集羣、高可用的做用。下面是 Redis Cluster 的架構圖:算法
sharding 由 Redis cluster 根據 client 調用 redis 的 key 進行 hash 取模獲得一個 code,根據這個 code 放到 16384 個 slot 中。在以上的架構圖中 slot1 組、slot2 組、slot3 組服務器中分別是對應的差很少 1/3 的 slot。這樣就獲得了根據 key 的 shading。數據庫
說到 sharding 就會說到怎麼進行擴容和縮容,redis cluster 也提供了工具進行遷移。 不過因爲不是一致性 hash,因此涉及到遷移數據的節點數會多於一致性 hash,可是遷移的量仍是可控的,只會遷移部分以達到平均的效果。 (redis cluster 遷移詳細機制須要另外詳細研究)後端
在 redis cluster 的一個 slot 組中,採用的是主備的高可用模式,只有 master 對外提供服務,若是 master 掛掉,則 slave 會成爲新的 master,由新的 master 提供服務,在切換的過程當中會在短期的 redis 服務不可用。服務器
redis cluster 進入 fail 狀態的條件:網絡
redis cluster 具有高可用、sharding、負載均衡的功能。架構
若是多個 key 想要人爲控制落到一個 slot 組上,能夠經過對 key 進行改造實施。即若是 key 都以{key_pre}idxxxx 這樣,那麼全部的 key 將是以 key_pre 去肯定 slot 組,這樣就達到了以{key_pre}開頭的 key 都會是在一個 slot 組。併發
當向一個 master 中寫入數據時,數據是進行迅速返回的,返回後再進行主從同步的方式向 slave 進行同步,所以這裏是損失了 CAP 中的一致性的。 在未來的 redis 版本中可能會開放同步寫的方式寫入 slave,以維護一致性,固然這樣會損失必定的寫入速度。負載均衡
在上文中提到的在作主從切換的時候,會有短時的不可用狀態,所以會操做分佈式理論 CAP 中可用性。異步
單個 redis 服務器上的請求是順序執行的,由於 redis 服務器是單進程、單線程的。
分佈式鎖有不少的實現方案,一般有數據庫、文件系統、zookeeper、redis。下面講述基於 redis cluster 的分佈式鎖方案。
嚴格來講這並非分佈式鎖,只是經過改造能夠實現鎖的效果。這裏並非實現鎖定其餘的線程被阻塞的效果,而是若是數據被其餘客戶端修改了就返回失敗。原理是基於 reids 的 multi 和 watch 命令。 在事務開始前對要鎖到的數據進行 watch,進行業務操做後,若是發現鎖定的數據已經變了,就提交失敗,從新進行業務操做。 在這個方案中若是執行失敗就一直反覆執行直到成功,也是實現了多個 redis 客戶同時修改一個數據時的協調的鎖的功能。
僞代碼以下:
複製代碼
jedis.set("balance",String.valueOf(balance)); jedis.watch("balance"); Transaction transaction = jedis.multi(); transaction.decrBy("balance",amtToSubtract);/ transaction.incrBy("debt",amtToSubtract); List result = transaction.exec();// 執行事務 if(result==null){ // 從新執行事務或者其餘。 }else{ // 事務執行成功。 }
Redis set key 時的一個 NX 參數能夠保證在這個 key 不存在的狀況下寫入成功。而且再加上 EX 參數可讓該 key 在超時以後自動刪除。
jedis 僞代碼:
複製代碼
Stringset(Stringkey,Stringvalue,Stringnxxx,Stringexpx,longtime);
爲何要以一代代碼一個命令去實現呢,是由於要防止 NX 後忽然就宕機了會產生死鎖。
過時的時間的設置上要考慮幾個問題:
若是時間設置比較長,若是鎖定發起 server 發生宕機,那麼好久都解鎖不了。
若是時間設置比較短,可能會發生業務尚未作完,發生瞭解鎖,沒有起到鎖定的做用。
關於解鎖:在解鎖時須要判斷當前的鎖是否是本身所鎖定的,所以須要在加鎖時將 key 的 value 設置爲一個隨機數,在解鎖時進行判斷。 由於在解鎖時有 get 數據再判斷的多個操做,所以這裏也須要防止併發問題,所以使用 lua 腳本寫這個操做。如如下僞代碼:
複製代碼
$script ="if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; $result =$this->redis->eval(script,array($key,$val),1);
注意一個可能發生的問題:
redis 的主從異步複製機制可能丟失數據,會出現以下場景:A 線程得到了鎖,但鎖數據還未同步到 slave 上,master 掛了,slave 頂成主,線程 B 嘗試加鎖,仍然可以成功,形成 A、B 兩個線程併發訪問同一個資源。
因爲 redis 服務器的單進程單線程模型,所以產生這種被大量使用的分佈式鎖的方案。
lua 腳本經過 eval 或者 evalsha 方法進行執行,同時將腳本涉及到的 key 和參數傳遞給 redis 服務器執行。客戶端能夠經過 jedis 進行調用。 evalsha 是對腳本在 redis 服務器進行預編譯,這樣能夠減小網絡交互量,加速執行時的速度。
注意如下:
因爲是分佈式集羣環境,若是傳遞了多個 key,而 key 處於不一樣的 slot 組服務器,那麼執行將會報錯。
lua 腳本中因爲是單進程單線程執行,所以不要作消耗時間的操做。 在簡單操做的狀況下,在 CPU 6 核 Intel® Core™ i7-2720QM CPU @ 2.20GHz 內存 16GB 的狀況下能跑出 5 萬 TPS。
若是要進行 TPS 擴容,則須要經過 key 對應的 slot 組不一樣,將 lua 分發到不一樣的 slot 組中的 redis master 服務器去執行。
程序都建議使用 evalsha 的方法去執行,這樣能夠提升 TPS。
API 網關中針對一個 API、API 分組、接入應用 APP ID,IP 等進行限流。這些限流條件都將會產生一個限流使用的 key,在後續的限流中都是對這個 key 進行限流。
限流算法一般在 API 網關中能夠採用令牌桶算法實現。
必須說明一點的是分佈式限流因爲有網絡的開銷,TPS 的支持隔本地限流是有差距的,所以在對於 TPS 要求很高的場景,建議採用本地限流進行處理。
下面討論咱們應該採用 redis 的哪種分佈式鎖的方案:
因爲 redis 事務要獲得鎖的效果須要在高 TPS 時會產生大量的無效的訪問請求,因此不建議在這種場景下使用。
SET NX/EX 的鎖方案會產生在過時時間的問題,同時也有異步複製 master 數據到 slave 的問題。相比 lua 方案會產生更多的不穩定性。
我建議採用 lua 的方案來實施分佈式鎖,由於都是單進程單線程的執行,所以在 TPS 上和第二種方案沒有大的區別,並且因爲只是一個 lua 腳本在執行,甚至是可能純 lua 執行可能會有更高的 TPS。 固然是 lua 腳本中可能仍是會去設置過時時間,可是應用 server 宕機並不會影響到 redis 中的鎖。 固然 master 異步複製的問題仍是有, 可是並不會形成問題,由於數據只會有 1 個 lua 腳本執行問題,下一個執行就正常了。
在實現方案的時候使用了 Jedis 庫,有一些問題在方案的實現層面我已經去作過驗證了,可能也會是讀者的疑問。
答:配置全部節點,作自動轉發。
答:能自動處理。
答:能自動處理。
答:能自動處理,由 Jedis jar 進行維護節點的狀態,查詢最新的 master 節點的信息。