Redis事務和分佈式鎖

Redis事務

  Redis中的事務(transaction)是一組命令的集合。事務同命令同樣都是Redis最小的執行單位,一個事務中的命令要麼都執行,要麼都不執行。Redis事務的實現須要用到 MULTI  EXEC 兩個命令,事務開始的時候先向Redis服務器發送 MULTI 命令,而後依次發送須要在本次事務中處理的命令,最後再發送 EXEC 命令表示事務命令結束。redis

  舉個例子,使用redis-cli鏈接redis,而後在命令行工具中輸入以下命令:  數據庫

  從輸出中能夠看到,當輸入MULTI命令後,服務器返回OK表示事務開始成功,而後依次輸入須要在本次事務中執行的全部命令,每次輸入一個命令服務器並不會立刻執行,而是返回」QUEUED」,這表示命令已經被服務器接受而且暫時保存起來,最後輸入EXEC命令後,本次事務中的全部命令纔會被依次執行,能夠看到最後服務器一次性返回了三個OK,這裏返回的結果與發送的命令是按順序一一對應的,這說明此次事務中的命令全都執行成功了。服務器

  再舉個例子,在命令行工具中輸入以下命令:  dom

  和前面的例子同樣,先輸入MULTI最後輸入EXEC表示中間的命令屬於一個事務,不一樣的是中間輸入的命令有一個錯誤(set寫成了sett),這樣由於有一個錯誤的命令致使事務中的其餘命令都不執行了(經過後續的get命令能夠驗證),可見事務中的全部命令是同呼吸共命運的。分佈式

  若是客戶端在發送EXEC命令以前斷線了,則服務器會清空事務隊列,事務中的全部命令都不會被執行。而一旦客戶端發送了EXEC命令以後,事務中的全部命令都會被執行,即便此後客戶端斷線也不要緊,由於服務器已經保存了事務中的全部命令。ide

  除了保證事務中的全部命令要麼全執行要麼全不執行外,Redis的事務還能保證一個事務中的命令依次執行而不會被其餘命令插入。試想一個客戶端A須要執行幾條命令,同時客戶端B發送了幾條命令,若是不使用事務,則客戶端B的命令有可能會插入到客戶端A的幾條命令中,若是想避免這種狀況發生,也可使用事務。 工具

 

Redis事務錯誤處理

  若是一個事務中的某個命令執行出錯,Redis會怎樣處理呢?要回答這個問題,首先要搞清楚是什麼緣由致使命令執行出錯:性能

  1.語法錯誤:就像上面的例子同樣,語法錯誤表示命令不存在或者參數錯誤,這種狀況須要區分Redis的版本,Redis 2.6.5以前的版本會忽略錯誤的命令,執行其餘正確的命令,2.6.5以後的版本會忽略這個事務中的全部命令,都不執行,就好比上面的例子(使用的Redis版本是2.8的)ui

  2.運行錯誤 運行錯誤表示命令在執行過程當中出現錯誤,好比用GET命令獲取一個散列表類型的鍵值。這種錯誤在命令執行以前Redis是沒法發現的,因此在事務裏這樣的命令會被Redis接受並執行。若是食物裏有一條命令執行錯誤,其餘命令依舊會執行(包括出錯以後的命令)。好比下例:  spa

  Redis中的事務並無關係型數據庫中的事務回滾(rollback)功能,所以使用者必須本身收拾剩下的爛攤子。不過因爲Redis不支持事務回滾功能,這也使得Redis的事務簡潔快速。

  回顧上面兩種類型的錯誤,語法錯誤徹底能夠在開發的時候發現並做出處理,另外若是能很好地規劃Redis數據的鍵的使用,也是不會出現命令和鍵不匹配的問題的。 

 

WATCH、UNWATCH、DISCARD命令

  從上面的例子咱們能夠看到,事務中的命令要所有執行完以後才能獲取每一個命令的結果,可是若是一個事務中的命令B依賴於他上一個命令A的結果的話該怎麼辦呢?就好比說實現相似Java中的i++的功能,先要獲取當前值,才能在當前值的基礎上作加一操做。這種場合僅僅使用上面介紹的MULTI和EXEC是不能實現的,由於MULTI和EXEC中的命令是一塊兒執行的,並不能將其中一條命令的執行結果做爲另外一條命令的執行參數,因此這個時候就須要引進Redis事務家族中的另外一成員:WATCH命令

  換個角度思考上面說到的實現i++的方法,能夠這樣實現:

  1. 監控i的值,保證i的值不被修改
  2. 獲取i的原值
  3. 若是過程當中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

相關文章
相關標籤/搜索