事務(Transaction) ,是指做爲單個邏輯工做單元執行的一系列操做。事務必須知足ACID原則(原子性、一致性、隔離性和持久性)。
簡單來講,事務可能包括1~N條命令,當這些命令被做爲事務處理時,將會順序執行這些命令直到完成,並返回結果,若是中途有命令失敗,則會回滾全部操做。
舉個例子:php
咱們到銀行ATM機取一筆錢,咱們的操做多是以下:html
插卡(輸入密碼)redis
輸入要取的金額數據庫
ATM吐鈔
後臺在你的戶頭上扣掉相應金額服務器
整個操做是一個順序,不可分割的總體。上一步完成後纔會執行下一步,若是ATM沒吐鈔卻扣了用戶的錢,銀行但是要關門了。網絡
先來看一下事務相關的命令併發
命令原型 | 命令描述 |
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
爲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 以可嵌入,輕量,高效著稱,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]\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); ...... }