如何在Redis中實現事務

sandylove 8年前發布 | 13K 次閱讀 Redis NoSQL數據庫

事務介紹

事務(Transaction) ,是指作為單個邏輯工作單元執行的一系列操作。事務必須滿足ACID原則(原子性、一致性、隔離性和持久性)。

簡單來說,事務可能包括1~N條命令,當這些命令被作為事務處理時,將會順序執行這些命令直到完成,并返回結果,如果中途有命令失敗,則會回滾所有操作。

舉個例子:

  1. 我們到銀行ATM機取一筆錢,我們的操作可能是如下:

  2. 插卡(輸入密碼)

  3. 輸入要取的金額

  4. ATM吐鈔

    后臺在你的戶頭上扣掉相應金額

整個操作是一個順序,不可分割的整體。上一步完成后才會執行下一步,如果ATM沒吐鈔卻扣了用戶的錢,銀行可是要關門了。

Redis中的事務

先來看一下事務相關的命令

命令原型 命令描述
MULTI 用于標記事務的開始,其后執行的命令都將被存入命令隊列,直到執行EXEC時,這些命令才會被原子的執行。
EXEC 執行在一個事務內命令隊列中的所有命令,同時將當前連接的狀態恢復為正常狀態,即非事務狀態。如果在事務中執行了WATCH命令,那么只有當WATCH所監控的Keys沒有被修改的前提下,EXEC命令才能執行事務隊列中的所有命令,否則EXEC將放棄當前事務中的所有命令。
DISCARD 回滾事務隊列中的所有命令,同時再將當前連接的狀態恢復為正常狀態,即非事務狀態。如果WATCH命令被使用,該命令將UNWATCH所有的Keys。
WATCH key [key ...] 在MULTI命令執行之前,可以指定待監控的Keys,然而在執行EXEC之前,如果被監控的Keys發生修改,EXEC將放棄執行該事務隊列中的所有命令。
UNWATCH 取消當前事務中指定監控的Keys,如果執行了EXEC或DISCARD命令,則無需再手工執行該命令了,因為在此之后,事務中所有被監控的Keys都將自動取消。

和關系型數據庫中的事務相比,在Redis事務中如果有某一條命令執行失敗,其后的命令仍然會被繼續執行。

我們可以通過MULTI命令開啟一個事務,有關系型數據庫開發經驗的人可以將其理解為 BEGIN TRANSACTION 語句。在該語句之后執行的命令都將被視為事務之內的操作,最后我們可以通過執行 EXEC/DISCARD 命令來提交/回滾該事務內的所有操作。這兩個Redis命令可被視為等同于關系型數據庫中的 COMMIT/ROLLBACK 語句。

在事務開啟之前,如果客戶端與服務器之間出現通訊故障并導致網絡斷開,其后所有待執行的語句都將不會被服務器執行。然而如果網絡中斷事件是發生在客戶端執行 EXEC 命令之后,那么該事務中的所有命令都會被服務器執行。

當使用Append-Only模式時,Redis會通過調用系統函數write將該事務內的所有寫操作在本次調用中全部寫入磁盤。然而如果在寫入的過程中出現系統崩潰,如電源故障導致的宕機,那么此時也許只有部分數據被寫入到磁盤,而另外一部分數據卻已經丟失。Redis服務器會在重新啟動時執行一系列必要的一致性檢測,一旦發現類似問題,就會立即退出并給出相應的錯誤提示。此時,我們就要充分利用Redis工具包中提供的redis-check-aof工具,該工具可以幫助我們定位到數據不一致的錯誤,并將已經寫入的部分數據進行回滾。修復之后我們就可以再次重新啟動Redis服務器了。

樣例

@Test
public void test2Trans() { 
  Jedis jedis = new Jedis("localhost"); 
  long start = System.currentTimeMillis(); 
  Transaction tx = jedis.multi(); 
  for (int i = 0; i < 100000; i++) { 
    tx.set("t" + i, "t" + i); 
  } 
  List<Object> results = tx.exec(); 
  long end = System.currentTimeMillis(); 
  System.out.println("Transaction SET: " + ((end - start)/1000.0) + " seconds"); 
  jedis.disconnect(); 
}

得到事務結果result之后,可以檢查當中是否有非OK的返回值,如果存在則說明中間執行錯誤,可以使用 DISCARD 來回滾執行結果。

WATCH命令

WATCH 為 MULTI 執行之前的某個Key提供監控(樂觀鎖)的功能,如果Key的值變化了,就會放棄事務的執行。

當事務 EXEC 執行完成之后,就會自動 UNWATCH 。

</tr>
  • Session 1 Session 2
    (1)第1步
    redis 127.0.0.1:6379> get age
    "10"
    redis 127.0.0.1:6379> watch age
    OK
    redis 127.0.0.1:6379> multi
    OK
    redis 127.0.0.1:6379>
    </td>
     
      </tbody> </table> </td> </tr>
    (2)第2步
    redis 127.0.0.1:6379> set age 30
    OK
    redis 127.0.0.1:6379> get age
    "30"
    redis 127.0.0.1:6379>
    </tbody> </table> </td> </tr> </tbody> </table>

    樣例

    <?php
    header("content-type:text/html;charset=utf-8");
    $redis = new redis();
    $result = $redis->connect('localhost', 6379);
    $mywatchkey = $redis->get("mywatchkey");
    $rob_total = 100; //搶購數量
    if($mywatchkey<$rob_total){
    $redis->watch("mywatchkey");
    $redis->multi();

    //設置延遲,方便測試效果。  
    sleep(5);  
    //插入搶購數據  
    $redis->hSet("mywatchlist","user_id_".mt_rand(1, 9999),time());  
    $redis->set("mywatchkey",$mywatchkey+1);  
    $rob_result = $redis->exec();  
    if($rob_result){  
        $mywatchlist = $redis->hGetAll("mywatchlist");  
        echo "搶購成功!<br/>";  
        echo "剩余數量:".($rob_total-$mywatchkey-1)."<br/>";  
        echo "用戶列表:<pre>";  
        var_dump($mywatchlist);  
    }else{  
        echo "手氣不好,再搶購!";exit;  
    }  
    

    }
    ?></code></pre>

    在上例是一個秒殺的場景,該部分搶購的功能會被并行執行。

    通過已銷售數量(mywatchkey)的監控,達到了控制庫存,避免超賣的作用。

    WATCH是一個樂觀鎖,有利于減少并發中的沖突, 提高吞吐量。

    樂觀鎖與悲觀鎖

    樂觀鎖(Optimistic Lock)又叫共享鎖(S鎖),每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。樂觀鎖適用于多讀的應用類型,這樣可以提高吞吐量。

    悲觀鎖(Pessimistic Lock)又叫排他鎖(X鎖),每次去拿數據的時候都認為別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。傳統的關系型數據庫里邊就用到了很多這種鎖機制,比如行鎖,表鎖等,都是在做操作之前先上鎖。

    Lua腳本與事務

    Lua 以可嵌入,輕量,高效著稱,Redis于2.6版本之后,增加了Lua語言解析模塊,可以用于一些簡單的事務與邏輯運算。

    (3)第3步
    redis 127.0.0.1:6379> set age 20
    QUEUED
    redis 127.0.0.1:6379> exec
    (nil)
    redis 127.0.0.1:6379> get age
    "30"
    redis 127.0.0.1:6379>
     
    命令原型 命令描述
    EVAL script numkeys key[key ...] arg [arg...] 傳入并執行一段Lua腳本,script為腳本內容,numkeys表示傳入參數數量,key表示腳本要訪問的key,arg為傳入參數
    EVALSHA sha1 通過SHA1序列調用lua_scripts字典預存的腳本
    SCRIPT FLUSH 用于清除服務器中lua有關的腳本,釋放lua_scripts字典,關閉現有的lua環境,并重新創建
    SCRIPT EXISTS sha1 輸入SHA1校驗和,判斷是否存在
    SCRIPT LOAD script 與EVAL相同,創建對應的lua函數,存放到字典中
    SCRIPT KILL 殺掉正在執行的腳本。正在執行的腳本會中斷并返回錯誤,腳本中的寫操作已被執行則不能殺死,因為違反原子性原則。此時只有手動回滾或shutdown nosave來還原數據

    應用原理

    客戶端將Lua腳本作為命令傳給服務端,服務端讀取并解析后,執行并返回結果

    127.0.0.1:6379> eval 'return redis.call("zrange", "name2", 0 , -1);' 0
    1) "1"

    Redis啟動時會創建一個內建的lua_script哈希表,客戶端可以將腳本上傳到該表,并得到一個SHA1序列。之后可以通過該序列來調用腳本。(類似存儲過程)

    redis> SCRIPT LOAD "return 'dlrow olleh'"
    "d569c48906b1f4fca0469ba4eee89149b5148092"

    redis> EVALSHA d569c48906b1f4fca0469ba4eee89149b5148092 0 "dlrow olleh"</code></pre>

    約束

    Redis會把Lua腳本作為一個整體執行,由于Redis是單線程,因此在腳本執行期間,其他腳本或命令是無法插入執行,這個特性符合事務的原子性。

    TIP

    1. 表是Lua中的表達式,與很多流行語言不同。KEYS中的第一個元素是KEYS[1],第二個是KEYS[2](譯注:不是0開始)

    2. nil是表的結束符,[1,2,nil,3]將自動變為[1,2],因此在表中不要使用nil。

    3. redis.call會觸發Lua中的異常,redis.pcall將自動捕獲所有能檢測到的錯誤并以表的形式返回錯誤內容。

    4. Lua數字都將被轉換為整數,發給Redis的小數點會丟失,返回前把它們轉換成字符串類型。

    5. 確保在Lua中使用的所有KEY都在KEY表中,否則在將來的Redis版中你的腳本都有不能被很好支持的危險。

    6. 腳本要保持精簡,以免阻塞其他客戶端操作

    一致性

    為了保證腳本執行結果的一致性,重復執行同一段腳本,應該得到相同的結果。Redis做了如下約束:

    • Lua沒有訪問系統時間或者其他內部狀態的命令。

    • Lua腳本在解析階段,如果發現 RANDOMKEY 、 SRANDMEMBER 、 TIME 這類返回隨機性結果的命令,且腳本中有寫指令(SET)類,則會返回錯誤,不允許執行。

    • Lua腳本中調用返回無序元素的命令時,如 SMEMBERS ,Redis會在后臺將命令的結果排序后傳回腳本

    • Lua中的偽隨機數生成函數 math.random 和 math.randomseed 會被替換為Redis內置的函數來執行,以保證腳本執行時的seed值不變。

    樣例

    private static String getSCRIPT() {
            return "local key = KEYS[1]\n" +
                    "local localIp = ARGV[1]\n" +
                    "\n" +
                    "local gateIp = redis.call(\"HGET\", key, \"gateIp\")\n" +
                    "if gateIp == localIp then\n" +
                    "    redis.call(\"HSET\", key, \"userStatus\", \"false\")\n" +
                    "    return 1\n" +
                    "else\n" +
                    "    return 0\n" +
                    "end";
        }

    @Test public void testTrans() { ...... Jedis jedis = new Jedis("localhost"); result = jedis.evalsha(getSCRIPT, keys, args); ...... }</code></pre>

     

    來自:https://segmentfault.com/a/1190000007429197

     

     本文由用戶 sandylove 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
     轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
     本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!
  • sesese色