Redis事務機制

1.概述

Redis 事務能夠一次執行多個命令, 而且帶有如下兩個重要的保證:html

  • 批量操做在發送 EXEC 命令前被放入隊列緩存。
  • 收到 EXEC 命令後進入事務執行,事務中任意命令執行失敗,其他的命令依然被執行。
  • 在事務執行過程,其餘客戶端提交的命令請求不會插入到事務執行命令序列中。

一個事務從開始到執行會經歷如下三個階段:java

  • 開始事務。
  • 命令入隊。
  • 執行事務。

2.Redis事務命令

命令

命令及描述

MULTI redis

標記一個事務塊的開始。其後執行的命令都將被存入命令隊列,直到執行EXEC時,這些命令纔會被原子執行。數據庫

EXEC 緩存

執行全部事務塊內的命令,同時將當前鏈接的狀態恢復爲正常狀態,即非事務狀態。若是在事務中執行了WATCH命令,那麼只有當WATCH所監控的keys沒有被修改的前提下,EXEC命令才能執行事務隊列中的全部命令,不然EXEC將放棄當前事務中的全部命令。數據結構

DISCARD 多線程

取消事務,放棄執行事務塊內的全部命令。同時再將當前鏈接的狀態恢復爲正常狀態,即非事務狀態。如 果WATCH命令被使用,該命令將UNWATCH全部的keys.app

WATCH key [key ...] 分佈式

監視一個(或多個) key ,若是在事務執行以前這個(或這些) key 被其餘命令所改動,那麼事務將被打斷。學習

UNWATCH 

取消 WATCH 命令對全部 key 的監視。若是執行了EXEC或DISCARD命令,則無需再手工執行  該命令了,由於在此以後,事務中全部的keys都將自動取消,

 

3.事務的使用

Redis提供了一個 multi 命令開啓事務,exec 命令提交事務,在它們之間的命令是在一個事務內的,能保證原子性。

127.0.0.1:6379>multi
"OK"
127.0.0.1:6379>set tran1 hello
"QUEUED"
127.0.0.1:6379>set tran2 world
"QUEUED"
127.0.0.1:6379>exec
 1)  "OK"
 2)  "OK"

 經過上面的命令能夠看到使用 multi 命令開啓事務以後,執行的Redis命令返回結果 QUEUED,表示命令並無執行,而是暫時保存在Redis事務中,直到執行 exec 命令後纔會執行上面的命令而且返回結果。

此外咱們可使用DISCARD取消事務。當執行 DISCARD 命令時, 事務會被放棄, 事務隊列會被清空, 而且客戶端會從事務狀態中退出。

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name2 "lisi"
QUEUED
127.0.0.1:6379> set age 22
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI
127.0.0.1:6379>

 下面這個是Jedis客戶端執行事務的代碼:

public static void testTran() {
    // 開啓事務
    Transaction transaction = jedis.multi();
    // 執行事務內的Redis命令
    transaction.set("tran1", "hello");
    transaction.set("tran2", "world");
    // 提交事務
    List<Object> exec = transaction.exec();
    // 事務中每條命令的執行結果
    System.out.println(exec);
}

須要注意的是開啓事務以後,執行命令的對象不是Jedis對象,而是Transaction對象,不然會拋出下面的異常:

Exception in thread "main" redis.clients.jedis.exceptions.JedisDataException: 
Cannot use Jedis when in Multi. 
Please use Transation or reset jedis state.

4.事務對異常的處理機制

 Redis執行命令的錯誤主要分爲兩種: 

- 1.命令錯誤:執行命令語法錯誤,好比說將 set 命令寫成 sett 
- 2.運行時錯誤:命令語法正確,可是執行錯誤,好比說對 List 集合執行 sadd 命令

 Redis事務中若是發生上面兩種錯誤,處理機制也是不一樣的。

(1)命令錯誤處理機制

這種狀況須要區別Redis版本,Redis2.65以前的版本會忽略錯誤的命令,執行其餘正確的命令,2.65以後的版本會忽略這個事務中的全部命令,都不執行,就好比上面的例子(使用的Redis版本是2.8的);

127.0.0.1:6379>multi
"OK"
127.0.0.1:6379>set a1 a
"QUEUED"
127.0.0.1:6379>sett a2 b
"ERR unknown command 'sett'"
127.0.0.1:6379>exec
"EXECABORT Transaction discarded because of previous errors."
127.0.0.1:6379>get a1
null

 上面案例中,開啓事務後第一條命令添加返回QUEUED,第二條命令語法錯誤,最後提交事務。

能夠看到,事務提交後 get a1 返回值是null,因此第二條命令的語法錯誤致使整個事務中的命令都不會執行。

(2)運行時錯誤處理機制

運行錯誤表示命令執行過程當中出現錯誤,就好比用GET命令去獲取一個散列表類型的鍵值。

這種錯誤在命令執行以前Redis是沒法發現的,因此在事務裏這樣的命令都會被Redis接受並執行.若是事務裏有一條命令執行錯誤,Redis不只不會回滾事務,還會跳過這個運行時錯誤,其餘命令依舊會執行(包括出錯後的命令)。

127.0.0.1:6379>lpush l1 a
"1"
127.0.0.1:6379>lpush l2 b
"1"
127.0.0.1:6379>lpush l3 c
"1"
127.0.0.1:6379>multi 
"OK"
127.0.0.1:6379>lpush l1 aa
"QUEUED"
127.0.0.1:6379>sadd l2 bb
"QUEUED"
127.0.0.1:6379>lpush l3 cc
"QUEUED"
127.0.0.1:6379>exec
1) "2"
2) "WRONGTYPE Operation against a key holding the wrong kind of value"
3) "2"

上面這個案例中,先建立了三個List類型 l一、l二、l3,而後開啓事務,第一條命令往l1中插入元素,第二條命令使用 sadd 命令往List類型的l2中添加元素,第三天命令往l2中插入元素,最後提交事務。

能夠看到最後事務的執行結果是第一條和第三條命令執行成功,第二條命令執行失敗,因此第二條命令的執行失敗不只沒有回滾事務並且還不會影響後續第三條命令的執行。

 5.Watch命令(樂觀鎖的實現)

 WATCH 對key值進行鎖操做。 在 WATCH 執行以後, EXEC 執行以前, 有其餘客戶端修改了 key 的值, 那麼當前客戶端的事務就會失敗。以下:

 Client1開啓watch name並在事務中修改name,可是沒有執行exec

127.0.0.1:6379> get name

"huangliu"

127.0.0.1:6379> watch name

OK

127.0.0.1:6379> multi

OK

127.0.0.1:6379> set name lisi

QUEUED

Client2 修改name

127.0.0.1:6379> get name

"huangliu"

127.0.0.1:6379> set name "wangwu"

OK

127.0.0.1:6379> get name

"wangwu"

127.0.0.1:6379>

Client1執行exec

127.0.0.1:6379> exec

(nil)

127.0.0.1:6379>

 可見,因爲被watch的name已經被Client2 修改,因此Client1的事務執行失敗,程序須要作的, 就是不斷重試這個操做, 直到沒有發生碰撞(Crash)爲止。對key進行加鎖監視的機制相似Java多線程中的鎖(synchronized中的監視器對象),被稱做樂觀鎖。樂觀是一種很是強大的鎖機制,後面咱們會進一步學習redis的分佈式鎖。下面咱們來看一下watch在java的操做:

@Test
public void testWatch() {
    JedisPool jedisPool = new JedisPool("192.168.1.4");
    // 設定 nowatch 的初始值爲 hello
    Jedis jedis = jedisPool.getResource();
    jedis.set("watchtest", "hello");
    // 開啓事務
    Transaction multi = jedis.multi();
    // 另外一個jedis客戶端對 watchtest進行append操做
    jedisPool.getResource().append("watchtest", " xxx");
    // 事務內部對watchtest進行append操做
    multi.append("watchtest", " world");
    // 提交事務
    multi.exec();
    // 打印watchtest對應的value
    System.out.println(jedis.get("watchtest"));
}

 上面這個案例,watchtest的初始值是」hello」,開啓了一個事務,而且往watchtest中append 」 world」,咱們預期的結果是」hello world」,可是在事務執行過程當中有另外一個jedis客戶端往watchtest中append 」 xxx」,因此上面這段代碼會在控制檯打印:

hello xxx world

 

咱們每每但願當前事務的執行不會受到其餘事務的影響,因此這個結果明顯不是咱們所預期的。

Redis提供了一個 watch 命令來幫咱們解決上面描述的這個問題,在 multi 命令以前咱們可使用 watch 命令來」觀察」一個或多個key,在事務提交以前Redis會確保被」觀察」的key有沒有被修改過,沒有被修改過纔會執行事務中的命令,若是存在key被修改過,那麼整個事務中的命令都不會執行,有點相似於樂觀鎖的機制。

仍是上面的案例,若是在開啓事務那一行上面添加 watch 命令:

// 使用 watch 命令watch "watchtest"
jedis.watch("watchtest");
// 開啓事務
Transaction multi = jedis.multi();

 最終控制檯打印結果會變成:

hello xxx

 能夠看出,使用 watch 命令以後,因爲watchtest被其餘客戶端修改過,因此事務中append " world" 的命令就不會執行,因此最終會打印 "hello xxx"。

通常樂觀鎖都須要配合重試機制來實現,因此這裏 watch 命令也能夠配合重試機制來實現:

public void incr(String key) {
    jedis.watch(key);
    Integer num = Integer.valueOf(jedis.get(key));
    Transaction multi = jedis.multi();
    multi.set(key, String.valueOf(num + 1));
    List<Object> exec = multi.exec();
    // exec爲空表示事務沒有執行,在這裏添加劇試機制
    if (exec.isEmpty()) {
        incr(key);
    }
}

 上面這段代碼是使用 watch 命令實現了Redis中的incr命令,這裏爲了演示 watch 命令配合重試的機制,就不去校驗key對應的數據結構是不是int類型。

綜上所述,在這裏引出樂觀鎖,針對樂觀鎖和悲觀鎖作一解釋:

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

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

6.參考資料

http://www.runoob.com/redis/redis-transactions.html

http://www.sohu.com/a/282419876_179850

https://blog.csdn.net/Hqs_1020417504/article/details/79908264

https://www.cnblogs.com/laojiao/p/9580653.html

https://blog.csdn.net/Leon_cx/article/details/82345054

https://www.cnblogs.com/hjwublog/p/5660578.html

相關文章
相關標籤/搜索