Redis實現原子操做的兩種方式與商品入庫出庫解決方案

在單機的Redis集羣下,想要實現針對多個key的複雜原子操做有兩種方法。一種是Watch+Multi,即監視器加事務方式,另外一種即是經過執行lua腳本實現。redis

這裏所說的複雜原子性操做好比,扣減某商品的5個庫存,須要先判斷當前商品的剩餘庫存是否足夠扣減。可是避免不了在判斷足夠的狀況下,再去執行扣減操做時,這個期間庫存沒有被別人修改的狀況。數據庫

Watch+Multi

Watch能夠監控多個key,被監聽的多個key,只要被監聽的key其中有一個key被修改,那麼全部操做都不會被執行,不只僅是對被監聽的key的操做。Watch並不能單獨使用,而是要結合事務使用。緩存

接着咱們看下,在Watch以後,事務執行以前,若是key被修改,會是什麼狀況。安全

這很好理解,即使事務中各命令的順序不同,由於單線程執行命令的緣由,exec執行時就已經知道被監聽的哪些key被修改過了,它只是不執行事務了。服務器

每一個Redis數據庫中都保存着一個watched_keys的字典,這個字典的鍵是某個被Watch命令監視的鍵,如上面例子中的s1s2,而字典的值則是一個鏈表,鏈表中記錄全部監視該鍵的客戶端,即一個鏈接。多線程

全部寫命令,在執行以後都會對watched_keys字典進行檢查,查看是否有客戶端正在監視剛剛寫的鍵,若是有,將監視該鍵的客戶端的REDIS_DIRTY_CAS標識打開,表示該客戶端的事務安全性已經被破壞。在客戶端(一個鏈接)提交(exec)事務執行時,先檢測該標識是否被打開,若是是,則會拒絕當前客戶端執行提交的事務。分佈式

假設在Cluster集羣下執行,會是什麼結果呢?我在個人服務器上部署了一個cluster集羣,這是很早以前就部署了的。來看下在Cluster集羣下執行Watch監聽多個key會怎樣。性能

能夠看到,在Watch的時候就出錯了,請求不容許key在不一樣的slot。即使slot不一樣但都在同一個節點也是不行的,必須是同一個slot。下圖中,s4s5這兩個key是在同一個節點上的。ui

Watch是真的嚴格,不一樣slot都不行。有沒有想過爲何監聽多個落在不一樣節點上的key,會不被容許?在單節點下,Redis單線程執行,可以保證原子性,但在不一樣節點下,就是多進程多線程的問題,Watch天然就不能用。lua

Lua腳本

Redis執行lua腳本是原子性操做,與執行Redis命令同樣對待。原子性操做得益於Redis執行命令是單線程的,lua腳本也會放在命令執行的等待隊列中排隊執行,所以也須要特別注意,lua腳本中不要執行太多代碼,最好不要寫for循環語句,不然會阻塞住,嚴重致使redis節點假死。

在主從Redis集羣與讀寫分離Redis集羣下,lua腳本的編寫不須要考慮太多問題,只需考慮腳本耗時問題。但在分槽位的cluster集羣下,咱們想要經過lua腳本實現原子性操做,就必需要確保腳本所要操做的key都在同一個Redis節點下,即全部key計算出來的槽位都落到同一個Redis節點(小集羣中的master)下,才能保證命令是原子性的。

事實上,若是要操做的key不在同一節點上,命令執行也會保錯。下圖是Lua腳本試圖訪問羣集中的非本地節點拋出的錯誤。

即使是在一個事務中執行多個lua腳本,只要有一個lua腳本操做的key落在不一樣的節點,結果都會執行失敗。

並不僅是lua腳本,由於事務也不支持multiexec之間的命令,操做的key落在不一樣節點的狀況。

不一樣slot也不行。

商品入庫出庫問題

關於商品庫存的問題,不少視頻教程都在講使用分佈式鎖,你可能也會想到使用分佈式鎖,可是用過度布式鎖的都知道性能是個什麼狀況。

若是商品庫存只使用redis緩存,在不須要修改數據庫中庫存的狀況下。對商品的庫存實現入庫和出庫,咱們能夠藉助lua腳本實現原子性修改庫存。在lua腳本中判斷庫存是否足夠減庫存,足夠扣減狀況下再更新庫存,這一系列操做是原子的。

lua腳本不難學,我看了一下條件選擇、分支語句以及判斷語句以後就能本身寫出腳本了。由於咱們也並不須要用lua作多複雜的事情。下面給出一個原子性修改庫存的lua腳本例子:

local kc=tonumber(redis.call('GET',KEYS[1])); 
if kc==nil 
then 
    return -1; 
end 
local newKc=kc-ARGV[1]; 
if newKc<0 
then 
    return 0; 
else 
    redis.call('SET',KEYS[1],newKc); 
    return 1; 
end
複製代碼
相關文章
相關標籤/搜索