如何在Redis中實現事務

事務介紹

事務(Transaction) ,是指做爲單個邏輯工做單元執行的一系列操做。事務必須知足ACID原則(原子性、一致性、隔離性和持久性)。
簡單來講,事務可能包括1~N條命令,當這些命令被做爲事務處理時,將會順序執行這些命令直到完成,並返回結果,若是中途有命令失敗,則會回滾全部操做。
舉個例子:php

  1. 咱們到銀行ATM機取一筆錢,咱們的操做多是以下:html

  2. 插卡(輸入密碼)redis

  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服務器了。dom

樣例

@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命令

WATCHMULTI執行以前的某個Key提供監控(樂觀鎖)的功能,若是Key的值變化了,就會放棄事務的執行。
當事務EXEC執行完成以後,就會自動UNWATCH工具

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>
(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>
(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>

樣例

<?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;  
    }  
}  
?>

在上例是一個秒殺的場景,該部分搶購的功能會被並行執行。
經過已銷售數量(mywatchkey)的監控,達到了控制庫存,避免超賣的做用。
WATCH是一個樂觀鎖,有利於減小併發中的衝突, 提升吞吐量。

樂觀鎖與悲觀鎖

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

悲觀鎖(Pessimistic Lock)又叫排他鎖(X鎖),每次去拿數據的時候都認爲別人會修改,因此每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。傳統的關係型數據庫裏邊就用到了不少這種鎖機制,好比行鎖,表鎖等,都是在作操做以前先上鎖。

Lua腳本與事務

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

命令原型 命令描述
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"

約束

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腳本在解析階段,若是發現RANDOMKEYSRANDMEMBERTIME這類返回隨機性結果的命令,且腳本中有寫指令(SET)類,則會返回錯誤,不容許執行。

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

  • Lua中的僞隨機數生成函數math.randommath.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);
  ......
}
相關文章
相關標籤/搜索