Redis 集群分布式锁与 API 网关分布式限流
https://www.infoq.cn/article/FoQGIk*BzdQWJJ0tKqrJ
Redis 集群的歷史
Redis 在 3.0 前一般有兩種集群方案,一是 proxy(Twemproxy、Codis),二是使用 Sentinel(哨兵)。 通過 Sentinel 是一種使用哨兵來達到高可用的方案,而 proxy 是用于在前置上進行 sharding 用代理給后端的 redis 集群的方案,達到負載均衡的方案,在單個分片的 redis 中作主從。 因為本文要重點講解的不是 3.0 前的方案,因此說的比較粗略。
Redis3.0 提供了官方的 Redis cluster 機制支持。主要通過內部無中心的多個節點來達到集群、高可用的作用。下面是 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 狀態的條件:
- 如果同時有大于 N(N 為總 slot 組數)*1/2 的 master 節點數同時掛掉,則 cluster 進入 fail 狀態,因為剩下的 master 已經無法選出新的 master 節點了。 分布式系統中必須要有大于 N(N 為總 slot 組數)*1/2 的節點存在才能判斷有效。 節如果有是 3 主 3 備的集群,如果同時有 2 個 master 節點掛掉了,那么集群就掛掉了。
- 如果一個 slot 組中的服務器已經沒有備用服務器了,這時 master 掛掉后,redis cluster 也會進入 fail 狀態。 因為部分 slot 的數據已經查詢不到了。
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 的分布式鎖方案。
基于 redis 事務實現
嚴格來說這并不是分布式鎖,只是通過改造可以實現鎖的效果。這里并不是實現鎖定其他的線程被阻塞的效果,而是如果數據被其他客戶端修改了就返回失敗。原理是基于 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{ |
| ? | ? |
| ? | // 事務執行成功。 |
| ? | ? |
| ? | } |
基于 setNX EX 實現
Redis set key 時的一個 NX 參數可以保證在這個 key 不存在的情況下寫入成功。并且再加上 EX 參數可以讓該 key 在超時之后自動刪除。
jedis 偽代碼:
復制代碼| ? | String set(String key, String value, String nxxx, String expx,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 兩個線程并發訪問同一個資源。
基于 lua 腳本實現
由于 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、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 節點的信息。
轉載于:https://www.cnblogs.com/davidwang456/articles/10191761.html
總結
以上是生活随笔為你收集整理的Redis 集群分布式锁与 API 网关分布式限流的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mongodb索引--1亿条记录的查询从
- 下一篇: Netty 学习和进阶策略