事務介紹
事務(Transaction) ,是指作為單個邏輯工作單元執行的一系列操作。事務必須滿足ACID原則(原子性、一致性、隔離性和永續性)。
簡單來說,事務可能包括1~N條命令,當這些命令被作為事務處理時,將會順序執行這些命令直到完成,並返回結果,如果中途有命令失敗,則會回滾所有操作。
舉個例子:
-
我們到銀行ATM機取一筆錢,我們的操作可能是如下:
-
插卡(輸入密碼)
-
輸入要取的金額
-
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
。
Session 1 | Session 2 | ||||||||
|
|||||||||
|
|||||||||
|
樣例
<?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
表是Lua中的表示式,與很多流行語言不同。KEYS中的第一個元素是KEYS[1],第二個是KEYS[2](譯註:不是0開始)
nil是表的結束符,[1,2,nil,3]將自動變為[1,2],因此在表中不要使用nil。
redis.call會觸發Lua中的異常,redis.pcall將自動捕獲所有能檢測到的錯誤並以表的形式返回錯誤內容。
Lua數字都將被轉換為整數,發給Redis的小數點會丟失,返回前把它們轉換成字串型別。
確保在Lua中使用的所有KEY都在KEY表中,否則在將來的Redis版中你的指令碼都有不能被很好支援的危險。
指令碼要保持精簡,以免阻塞其他客戶端操作
一致性
為了保證指令碼執行結果的一致性,重複執行同一段指令碼,應該得到相同的結果。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]
" +
"local localIp = ARGV[1]
" +
"
" +
"local gateIp = redis.call("HGET", key, "gateIp")
" +
"if gateIp == localIp then
" +
" redis.call("HSET", key, "userStatus", "false")
" +
" return 1
" +
"else
" +
" return 0
" +
"end";
}
@Test
public void testTrans() {
......
Jedis jedis = new Jedis("localhost");
result = jedis.evalsha(getSCRIPT, keys, args);
......
}