Redis事務和分散式鎖
Redis事務
Redis中的事務(transaction)是一組命令的集合。事務同命令一樣都是Redis最小的執行單位,一個事務中的命令要麼都執行,要麼都不執行。Redis事務的實現需要用到 MULTI 和 EXEC 兩個命令,事務開始的時候先向Redis伺服器傳送 MULTI 命令,然後依次傳送需要在本次事務中處理的命令,最後再發送 EXEC 命令表示事務命令結束。
舉個例子,使用redis-cli連線redis,然後在命令列工具中輸入如下命令:
從輸出中可以看到,當輸入MULTI命令後,伺服器返回OK表示事務開始成功,然後依次輸入需要在本次事務中執行的所有命令,每次輸入一個命令伺服器並不會馬上執行,而是返回”QUEUED”,這表示命令已經被伺服器接受並且暫時儲存起來,最後輸入EXEC命令後,本次事務中的所有命令才會被依次執行,可以看到最後伺服器一次性返回了三個OK,這裡返回的結果與傳送的命令是按順序一一對應的,這說明這次事務中的命令全都執行成功了。
再舉個例子,在命令列工具中輸入如下命令:
和前面的例子一樣,先輸入MULTI最後輸入EXEC表示中間的命令屬於一個事務,不同的是中間輸入的命令有一個錯誤(set寫成了sett),這樣因為有一個錯誤的命令導致事務中的其他命令都不執行了(通過後續的get命令可以驗證),可見事務中的所有命令是同呼吸共命運的。
如果客戶端在傳送EXEC命令之前斷線了,則伺服器會清空事務佇列,事務中的所有命令都不會被執行。而一旦客戶端傳送了EXEC命令之後,事務中的所有命令都會被執行,即使此後客戶端斷線也沒關係,因為伺服器已經儲存了事務中的所有命令。
除了保證事務中的所有命令要麼全執行要麼全不執行外,Redis的事務還能保證一個事務中的命令依次執行而不會被其他命令插入。試想一個客戶端A需要執行幾條命令,同時客戶端B傳送了幾條命令,如果不使用事務,則客戶端B的命令有可能會插入到客戶端A的幾條命令中,如果想避免這種情況發生,也可以使用事務。
Redis事務錯誤處理
如果一個事務中的某個命令執行出錯,Redis會怎樣處理呢?要回答這個問題,首先要搞清楚是什麼原因導致命令執行出錯:
1.語法錯誤:就像上面的例子一樣,語法錯誤表示命令不存在或者引數錯誤,這種情況需要區分Redis的版本,Redis 2.6.5之前的版本會忽略錯誤的命令,執行其他正確的命令,2.6.5之後的版本會忽略這個事務中的所有命令,都不執行,就比如上面的例子(使用的Redis版本是2.8的)
2.執行錯誤 執行錯誤表示命令在執行過程中出現錯誤,比如用GET命令獲取一個散列表型別的鍵值。這種錯誤在命令執行之前Redis是無法發現的,所以在事務裡這樣的命令會被Redis接受並執行。如果食物裡有一條命令執行錯誤,其他命令依舊會執行(包括出錯之後的命令)。比如下例:
Redis中的事務並沒有關係型資料庫中的事務回滾(rollback)功能,因此使用者必須自己收拾剩下的爛攤子。不過由於Redis不支援事務回滾功能,這也使得Redis的事務簡潔快速。
回顧上面兩種型別的錯誤,語法錯誤完全可以在開發的時候發現並作出處理,另外如果能很好地規劃Redis資料的鍵的使用,也是不會出現命令和鍵不匹配的問題的。
WATCH、UNWATCH、DISCARD命令
從上面的例子我們可以看到,事務中的命令要全部執行完之後才能獲取每個命令的結果,但是如果一個事務中的命令B依賴於他上一個命令A的結果的話該怎麼辦呢?就比如說實現類似Java中的i++的功能,先要獲取當前值,才能在當前值的基礎上做加一操作。這種場合僅僅使用上面介紹的MULTI和EXEC是不能實現的,因為MULTI和EXEC中的命令是一起執行的,並不能將其中一條命令的執行結果作為另一條命令的執行引數,所以這個時候就需要引進Redis事務家族中的另一成員:WATCH命令
換個角度思考上面說到的實現i++的方法,可以這樣實現:
- 監控i的值,保證i的值不被修改
- 獲取i的原值
- 如果過程中i的值沒有被修改,則將當前的i值+1,否則不執行
這樣就能夠避免競態條件,保證i++能夠正確執行。
WATCH命令可以監控一個或多個鍵,一旦其中有一個鍵被修改(或刪除),之後的事務就不會執行,監控一直持續到EXEC命令(事務中的命令是在EXEC之後才執行的,EXEC命令執行完之後被監控的鍵會自動被UNWATCH)。舉個例子:
上面的例子中,首先設定mykey的鍵值為1,然後使用WATCH命令監控mykey,隨後更改mykey的值為2,然後進入事務,事務中設定mykey的值為3,然後執行EXEC執行事務中的命令,最後使用get命令檢視mykey的值,發現mykey的值還是2,也就是說事務中的命令根本沒有執行(因為WATCH監控mykey的過程中,mykey被修改了,所以隨後的事務便會被取消)。
UNWATCH命令可以在WATCH命令執行之後、MULTI命令執行之前取消對某個鍵的監控。舉個例子:
上面的例子中,首先設定mykey的鍵值為1,然後使用WATCH命令監控mykey,隨後更改mykey的值為2,然後取消對mykey的監控,再進入事務,事務中設定mykey的值為3,然後執行EXEC執行事務中的命令,最後使用get命令檢視mykey的值,發現mykey的值還是3,也就是說事務中的命令執行成功。
DISCARD命令則可以在MULTI命令執行之後,EXEC命令執行之前取消WATCH命令並清空事務佇列,然後從事務狀態中退出。舉個例子:
上面的例子中,首先設定mykey的鍵值為1,然後使用WATCH命令監控mykey,隨後更改mykey的值為2,然後進入事務,事務中設定mykey的值為3,然後執行DISCARD命令,再執行EXEC執行事務中的命令,發現報錯“ERR EXEC without MULTI”,說明DISCARD命令成功執行——取消WATCH命令並清空事務佇列,然後從事務狀態中退出。
Redis分散式鎖
上面介紹的Redis的WATCH、MULTI和EXEC命令,只會在資料被其他客戶端搶先修改的情況下,通知執行這些命令的客戶端,讓它撤銷對資料的修改操作,並不能阻止其他客戶端對資料進行修改,所以只能稱之為樂觀鎖(optimistic locking)。
而這種樂觀鎖並不具備可擴充套件性——當客戶端嘗試完成一個事務的時候,可能會因為事務執行失敗而進行反覆的重試。保證資料準確性非常重要,但是當負載變大的時候,使用樂觀鎖的做法並不完美。這時就需要使用Redis實現分散式鎖。
分散式鎖:是控制分散式系統之間同步訪問共享資源的一種方式。在分散式系統中,常常需要協調他們的動作。如果不同的系統或是同一個系統的不同主機之間共享了一個或一組資源,那麼訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分散式鎖。
Redis命令介紹:
Redis實現分散式鎖主要用到命令是SETNX命令(SET if Not eXists)。
語法:SETNX key value
功能:當且僅當 key 不存在,將 key 的值設為 value ,並返回1;若給定的 key 已經存在,則 SETNX 不做任何動作,並返回0。
使用Redis構建鎖:
思路:將“lock:”+引數名設定為鎖的鍵,使用SETNX命令嘗試將一個隨機的uuid設定為鎖的值,併為鎖設定過期時間,使用SETNX設定鎖的值可以防止鎖被其他程序獲取。如果嘗試獲取鎖的時候失敗,那麼程式將不斷重試,直到成功獲取鎖或者超過給定是時限為止。
程式碼:
public String acquireLockWithTimeout( Jedis conn, String lockName, long acquireTimeout, long lockTimeout) { String identifier = UUID.randomUUID().toString(); //鎖的值 String lockKey = "lock:" + lockName; //鎖的鍵 int lockExpire = (int)(lockTimeout / 1000); //鎖的過期時間 long end = System.currentTimeMillis() + acquireTimeout; //嘗試獲取鎖的時限 while (System.currentTimeMillis() < end) { //判斷是否超過獲取鎖的時限 if (conn.setnx(lockKey, identifier) == 1){ //判斷設定鎖的值是否成功 conn.expire(lockKey, lockExpire); //設定鎖的過期時間 return identifier; //返回鎖的值 } if (conn.ttl(lockKey) == -1) { //判斷鎖是否超時 conn.expire(lockKey, lockExpire); } try { Thread.sleep(1000); //等待1秒後重新嘗試設定鎖的值 }catch(InterruptedException ie){ Thread.currentThread().interrupt(); } } // 獲取鎖失敗時返回null return null; }
鎖的釋放:
思路:使用WATCH命令監視代表鎖的鍵,然後檢查鍵的值是否和加鎖時設定的值相同,並在確認值沒有變化後刪除該鍵。
程式碼:
public boolean releaseLock(Jedis conn, String lockName, String identifier) { String lockKey = "lock:" + lockName; //鎖的鍵 while (true){ conn.watch(lockKey); //監視鎖的鍵 if (identifier.equals(conn.get(lockKey))){ //判斷鎖的值是否和加鎖時設定的一致,即檢查程序是否仍然持有鎖 Transaction trans = conn.multi(); trans.del(lockKey); //在Redis事務中釋放鎖 List<Object> results = trans.exec(); if (results == null){ continue; //事務執行失敗後重試(監視的鍵被修改導致事務失敗,重新監視並釋放鎖) } return true; } conn.unwatch(); //解除監視 break; } return false; }
通過在客戶端上面實現一個真正的鎖(非樂觀鎖),將會為程式帶來更好的效能,更簡單易用的API,但是與此同時,請記住Redis並不會主動使用這個自制的分散式鎖,我們必須自己使用這個鎖來代替WATCH命令,或者協同WATCH命令一起工作,從而保證資料的準確性與一致性。
參考:
http://qifuguang.me/2015/09/30/Redis%E4%BA%8B%E5%8A%A1%E4%BB%8B%E7%BB%8D/
http://blog.csdn.net/ugg/article/details/41894947
轉載請註明出處。