redis 事務 事務機制詳解 MULTI、EXEC、DISCARD、WATCH


1. Redis服務端是個單線程的架構,不同的Client雖然看似可以同時保持連接,但發出去的命令是序列化執行的,這在通常的數據庫理論下是最高級別的隔離
2. 用MULTI/EXEC 來把多個命令組裝成一次發送,達到原子性( 有點像命令具有打包功能
3. 用WATCH提供的樂觀鎖功能,在你EXEC的那一刻,如果被WATCH的鍵發生過改動,則MULTI到EXEC之間的指令全部不執行,不需要rollback

Discard 命令

 Discard 命令用於取消事務,放棄執行事務塊內的所有命令。

可用版本

  >= 2.0.0

返回值

  總是返回 OK 。

 

事務回滾情況一: 事務回滾,執行事務過程中,命令格式正確,數據類型錯誤

 我們將 key1 設置為字符串,而使用命令 incr 對其自增,但是命令只會進入事務隊列,而沒有被執行,所以它不會有任何的錯誤發生,而是等待 exec 命令的執行。

 當 exec 命令執行后,之前進入隊列的命令就依次執行,當遇到 incr 時發生命令操作的數據類型錯誤,所以顯示出了錯誤,而其之前和之后的命令都會被正常執行.

事務回滾情況二: 事務回滾,執行事務過程中,命令格式錯誤

 可以看到我們使用的 incr 命令格式是錯誤的,這個時候 Redis 會立即檢測出來並產生錯誤,而在此之前我們設置了 keyl , 在此之后我們設置了 key2 a 當事務執行的時候,我們發現 keyl 和 key2 的值都為空,說明被 Redis 事務回滾了。

 

  通過上面兩個例子,可以看出Redis在執行事務命令的時候,在命令入隊的時候, Redis 就會檢測事務的命令是否正確,如果不正確則會產生錯誤。無論之前和之后的命令都會被事務所回滾,就變為什么都沒有執行。

  命令格式正確,而因為操作數據結構引起的錯誤 ,則該命令執行出現錯誤,而其之前和之后的命令都會被正常執行。這點和數據庫很不一樣,這是需注意的地方。

  對於一些重要的操作,我們必須通過程序去檢測數據的正確性,以保證 Redis 事務的正確執行,避免出現數據不一致的情況。 Redis 之所以保持這樣簡易的事務,完全是為了保證移動互聯網的核心問題一----性能

 

情況類型:watch -> multi -> exec (修改key之前,先watch,在exec之前,開啟另一個客戶端修改key值觀察事務是否執行)

客戶端一提前設置key1、key2的值為1、2

 客戶端一執行watch之后修改key1、key2的值,為2、3,在提交這兩個命令的事務之前,開啟另一個客戶端二修改key1的值為10,之后提交客戶端一的事務

 客戶端二

 這時候發現客戶端一中提交的兩個命令都沒有生效。

 

情況類型:會出現樂觀鎖的ABA問題嗎

客戶端一

 客戶端二

在客戶端執行exec事務提交命令之前,通過客戶端二兩次修改key1的值,最終使得key1的值回復原樣,之后執行客戶端一的exec事務命令,查看事務的執行情況。

結論: redis不存在ABA問題,只有事務執行過程中值被其他線程改變過,事務就會回滾,原理是什么呢? 

 

redis事務實現的底層原理

開啟事務

MULTI 命令的執行標記着事務的開始:

redis> MULTI
OK

這個命令唯一做的就是, 將客戶端的 REDIS_MULTI 選項打開, 讓客戶端從非事務狀態切換到事務狀態。

 命令入隊

當客戶端處於非事務狀態下時, 所有發送給服務器端的命令都會立即被服務器執行:

redis> SET msg "hello moto"
OK

redis> GET msg
"hello moto"

但是, 當客戶端進入事務狀態之后, 服務器在收到來自客戶端的命令時, 不會立即執行命令, 而是將這些命令全部放進一個事務隊列里, 然后返回 QUEUED , 表示命令已入隊:

redis> MULTI
OK

redis> SET msg "hello moto"
QUEUED

redis> GET msg
QUEUED

以下流程圖展示了這一行為:

     

事務隊列是一個數組, 每個數組項是都包含三個屬性:

  1. 要執行的命令(cmd)。
  2. 命令的參數(argv)。
  3. 參數的個數(argc)。

舉個例子, 如果客戶端執行以下命令:

redis> MULTI
OK

redis> SET book-name "Mastering C++ in 21 days"
QUEUED

redis> GET book-name
QUEUED

redis> SADD tag "C++" "Programming" "Mastering Series"
QUEUED

redis> SMEMBERS tag
QUEUED

那么程序將為客戶端創建以下事務隊列:

 

執行事務 

 前面說到, 當客戶端進入事務狀態之后, 客戶端發送的命令就會被放進事務隊列里。

 但其實並不是所有的命令都會被放進事務隊列, 其中的例外就是 EXEC 、 DISCARD 、 MULTI 和 WATCH 這四個命令 —— 當這四個命令從客戶端發送到服務器時, 它們會像客戶端處於非事務狀態一樣, 直接被服務器執行:

 如果客戶端正處於事務狀態, 那么當 EXEC 命令執行時, 服務器根據客戶端所保存的事務隊列, 以先進先出(FIFO)的方式執行事務隊列中的命令: 最先入隊的命令最先執行, 而最后入隊的命令最后執行。

比如說,對於以下事務隊列:

  

程序會首先執行 SET 命令, 然后執行 GET 命令, 再然后執行 SADD 命令, 最后執行 SMEMBERS 命令。

執行事務中的命令所得的結果會以 FIFO 的順序保存到一個回復隊列中。

比如說,對於上面給出的事務隊列,程序將為隊列中的命令創建如下回復隊列:

 當事務隊列里的所有命令被執行完之后, EXEC 命令會將回復隊列作為自己的執行結果返回給客戶端, 客戶端從事務狀態返回到非事務狀態, 至此, 事務執行完畢。

 事務的整個執行過程可以用以下偽代碼表示:

def execute_transaction():

    # 創建空白的回復隊列
    reply_queue = []

    # 取出事務隊列里的所有命令、參數和參數數量
    for cmd, argv, argc in client.transaction_queue:

        # 執行命令,並取得命令的返回值
        reply = execute_redis_command(cmd, argv, argc)

        # 將返回值追加到回復隊列末尾
        reply_queue.append(reply)

    # 清除客戶端的事務狀態
    clear_transaction_state(client)

    # 清空事務隊列
    clear_transaction_queue(client)

    # 將事務的執行結果返回給客戶端
    send_reply_to_client(client, reply_queue) 

在事務和非事務狀態下執行命令

 無論在事務狀態下, 還是在非事務狀態下, Redis 命令都由同一個函數執行, 所以它們共享很多服務器的一般設置, 比如 AOF 的配置、RDB 的配置,以及內存限制,等等。

 不過事務中的命令和普通命令在執行上還是有一點區別的,其中最重要的兩點是:

  (1)非事務狀態下的命令以單個命令為單位執行,前一個命令和后一個命令的客戶端不一定是同一個;

      而事務狀態則是以一個事務為單位,執行事務隊列中的所有命令:除非當前事務執行完畢,否則服務器不會中斷事務,也不會執行其他客戶端的其他命令。

  (2)在非事務狀態下,執行命令所得的結果會立即被返回給客戶端;

      而事務則是將所有命令的結果集合到回復隊列,再作為 EXEC 命令的結果返回給客戶端。

事務狀態下的 DISCARD 、 MULTI 和 WATCH 命令

 除了 EXEC 之外, 服務器在客戶端處於事務狀態時, 不加入到事務隊列而直接執行的另外三個命令是 DISCARD 、 MULTI 和 WATCH 

 DISCARD 命令用於取消一個事務, 它清空客戶端的整個事務隊列, 然后將客戶端從事務狀態調整回非事務狀態, 最后返回字符串 OK 給客戶端, 說明事務已被取消。

 Redis 的事務是不可嵌套的, 當客戶端已經處於事務狀態, 而客戶端又再向服務器發送 MULTI 時, 服務器只是簡單地向客戶端發送一個錯誤, 然后繼續等待其他命令的入隊。 MULTI 命令的發送不會造成整個事務失敗, 也不會修改事務隊列中已有的數據。

 WATCH 只能在客戶端進入事務狀態之前執行, 在事務狀態下發送 WATCH 命令會引發一個錯誤, 但它不會造成整個事務失敗, 也不會修改事務隊列中已有的數據(和前面處理 MULTI 的情況一樣)。

 

帶 WATCH 的事務

 WATCH 命令用於在事務開始之前監視任意數量的鍵: 當調用 EXEC 命令執行事務時, 如果任意一個被監視的鍵已經被其他客戶端修改了, 那么整個事務不再執行, 直接返回失敗。

 以下示例展示了一個執行失敗的事務例子:

redis> WATCH name
OK

redis> MULTI
OK

redis> SET name peter
QUEUED

redis> EXEC
(nil)

以下執行序列展示了上面的例子是如何失敗的:

 在時間 T4 ,客戶端 B 修改了 name 鍵的值, 當客戶端 A 在 T5 執行 EXEC 時,Redis 會發現 name 這個被監視的鍵已經被修改, 因此客戶端 A 的事務不會被執行,而是直接返回失敗。

 下文就來介紹 WATCH 的實現機制,並且看看事務系統是如何檢查某個被監視的鍵是否被修改,從而保證事務的安全性的。

 

WATCH 命令的實現

 在每個代表數據庫的 redis.h/redisDb 結構類型中, 都保存了一個 watched_keys 字典, 字典的鍵是這個數據庫被監視的鍵, 而字典的值則是一個鏈表, 鏈表中保存了所有監視這個鍵的客戶端。

 比如說,以下字典就展示了一個 watched_keys 字典的例子:

 其中, 鍵 key1 正在被 client2 、 client5 和 client1 三個客戶端監視, 其他一些鍵也分別被其他別的客戶端監視着。

 WATCH 命令的作用, 就是將當前客戶端和要監視的鍵在 watched_keys 中進行關聯。

 舉個例子, 如果當前客戶端為 client10086 , 那么當客戶端執行 WATCH key1 key2 時, 前面展示的 watched_keys 將被修改成這個樣子:
 

 通過 watched_keys 字典, 如果程序想檢查某個鍵是否被監視, 那么它只要檢查字典中是否存在這個鍵即可; 如果程序要獲取監視某個鍵的所有客戶端, 那么只要取出鍵的值(一個鏈表), 然后對鏈表進行遍歷即可。

 

WATCH 的觸發

 在任何對數據庫鍵空間(key space)進行修改的命令成功執行之后 (比如 FLUSHDB 、 SET 、 DEL 、 LPUSH 、 SADD 、 ZREM ,諸如此類), multi.c/touchWatchedKey 函數都會被調用 —— 它檢查數據庫的 watched_keys 字典, 看是否有客戶端在監視已經被命令修改的鍵, 如果有的話, 程序將所有監視這個/這些被修改鍵的客戶端的 REDIS_DIRTY_CAS 選項打開:

 當客戶端發送 EXEC 命令、觸發事務執行時, 服務器會對客戶端的狀態進行檢查:

 如果客戶端的 REDIS_DIRTY_CAS 選項已經被打開,那么說明被客戶端監視的鍵至少有一個已經被修改了,事務的安全性已經被破壞。服務器會放棄執行這個事務,直接向客戶端返回空回復,表示事務執行失敗。
 如果 REDIS_DIRTY_CAS 選項沒有被打開,那么說明所有監視鍵都安全,服務器正式執行事務。
可以用一段偽代碼來表示這個檢查:

def check_safety_before_execute_trasaction():

    if client.state & REDIS_DIRTY_CAS:
        # 安全性已破壞,清除事務狀態
        clear_transaction_state(client)
        # 清空事務隊列
        clear_transaction_queue(client)
        # 返回空回復給客戶端
        send_empty_reply(client)
    else:
        # 安全性完好,執行事務
        execute_transaction()

舉個例子,假設數據庫的 watched_keys 字典如下圖所示:

  

 如果某個客戶端對 key1 進行了修改(比如執行 DEL key1 ), 那么所有監視 key1 的客戶端, 包括 client2 、 client5 和 client1 的 REDIS_DIRTY_CAS 選項都會被打開, 當客戶端 client2 、 client5 和 client1 執行 EXEC 的時候, 它們的事務都會以失敗告終。

 最后,當一個客戶端結束它的事務時,無論事務是成功執行,還是失敗, watched_keys 字典中和這個客戶端相關的資料都會被清除。

事務的 ACID 性質

 在傳統的關系式數據庫中,常常用 ACID 性質來檢驗事務功能的安全性。

 Redis 事務保證了其中的一致性(C)和隔離性(I),但並不保證原子性(A)和持久性(D)。

 以下四小節是關於這四個性質的詳細討論。

 

原子性(Atomicity) 

單個 Redis 命令的執行是原子性的,但 Redis 沒有在事務上增加任何維持原子性的機制,所以 Redis 事務的執行並不是原子性的。

如果一個事務隊列中的所有命令都被成功地執行,那么稱這個事務執行成功。

另一方面,如果 Redis 服務器進程在執行事務的過程中被停止 —— 比如接到 KILL 信號、宿主機器停機,等等,那么事務執行失敗。

當事務失敗時,Redis 也不會進行任何的重試或者回滾動作。

一致性(Consistency)

  Redis 的一致性問題可以分為三部分來討論:入隊錯誤、執行錯誤、Redis 進程被終結。

入隊錯誤

 在命令入隊的過程中,如果客戶端向服務器發送了錯誤的命令,比如命令的參數數量不對,等等, 那么服務器將向客戶端返回一個出錯信息, 並且將客戶端的事務狀態設為 REDIS_DIRTY_EXEC 。

 當客戶端執行 EXEC 命令時, Redis 會拒絕執行狀態為 REDIS_DIRTY_EXEC 的事務, 並返回失敗信息。

redis 127.0.0.1:6379> MULTI
OK

redis 127.0.0.1:6379> set key
(error) ERR wrong number of arguments for 'set' command

redis 127.0.0.1:6379> EXISTS key
QUEUED

redis 127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

 因此,帶有不正確入隊命令的事務不會被執行,也不會影響數據庫的一致性。

 

執行錯誤

 如果命令在事務執行的過程中發生錯誤,比如說,對一個不同類型的 key 執行了錯誤的操作, 那么 Redis 只會將錯誤包含在事務的結果中, 這不會引起事務中斷或整個失敗,不會影響已執行事務命令的結果,也不會影響后面要執行的事務命令, 所以它對事務的一致性也沒有影響。

 

Redis 進程被終結

 如果 Redis 服務器進程在執行事務的過程中被其他進程終結,或者被管理員強制殺死,那么根據 Redis 所使用的持久化模式,可能有以下情況出現:

 內存模式:如果 Redis 沒有采取任何持久化機制,那么重啟之后的數據庫總是空白的,所以數據總是一致的。

 RDB 模式:在執行事務時,Redis 不會中斷事務去執行保存 RDB 的工作,只有在事務執行之后,保存 RDB 的工作才有可能開始。所以當 RDB 模式下的 Redis 服務器進程在事務中途被殺死時,事務內執行的命令,不管成功了多少,都不會被保存到 RDB 文件里。恢復數據庫需要使用現有的 RDB 文件,而這個 RDB 文件的數據保存的是最近一次的數據庫快照(snapshot),所以它的數據可能不是最新的,但只要 RDB 文件本身沒有因為其他問題而出錯,那么還原后的數據庫就是一致的。

 AOF 模式:因為保存 AOF 文件的工作在后台線程進行,所以即使是在事務執行的中途,保存 AOF 文件的工作也可以繼續進行,因此,根據事務語句是否被寫入並保存到 AOF 文件,有以下兩種情況發生:

  1)如果事務語句未寫入到 AOF 文件,或 AOF 未被 SYNC 調用保存到磁盤,那么當進程被殺死之后,Redis 可以根據最近一次成功保存到磁盤的 AOF 文件來還原數據庫,只要 AOF 文件本身沒有因為其他問題而出錯,那么還原后的數據庫總是一致的,但其中的數據不一定是最新的。

  2)如果事務的部分語句被寫入到 AOF 文件,並且 AOF 文件被成功保存,那么不完整的事務執行信息就會遺留在 AOF 文件里,當重啟 Redis 時,程序會檢測到 AOF 文件並不完整,Redis 會退出,並報告錯誤。需要使用 redis-check-aof 工具將部分成功的事務命令移除之后,才能再次啟動服務器。還原之后的數據總是一致的,而且數據也是最新的(直到事務執行之前為止)。

 

隔離性(Isolation)
 Redis 是單進程程序,並且它保證在執行事務時,不會對事務進行中斷,事務可以運行直到執行完所有事務隊列中的命令為止。因此,Redis 的事務是總是帶有隔離性的。

 

持久性(Durability)
 因為事務不過是用隊列包裹起了一組 Redis 命令,並沒有提供任何額外的持久性功能,所以事務的持久性由 Redis 所使用的持久化模式決定:

 在單純的內存模式下,事務肯定是不持久的。

 在 RDB 模式下,服務器可能在事務執行之后、RDB 文件更新之前的這段時間失敗,所以 RDB 模式下的 Redis 事務也是不持久的。

 在 AOF 的“總是 SYNC ”模式下,事務的每條命令在執行成功之后,都會立即調用 fsync 或 fdatasync 將事務數據寫入到 AOF 文件。但是,這種保存是由后台線程進行的,主線程不會阻塞直到保存成功,所以從命令執行成功到數據保存到硬盤之間,還是有一段非常小的間隔,所以這種模式下的事務也是不持久的。

 其他 AOF 模式也和“總是 SYNC ”模式類似,所以它們都是不持久的。

 

小結
  (1) 事務提供了一種將多個命令打包,然后一次性、有序地執行的機制。
  (2) 事務在執行過程中不會被中斷,所有事務命令執行完之后,事務才能結束。
  (3) 多個命令會被入隊到事務隊列中,然后按先進先出(FIFO)的順序執行。
  (4) 帶 WATCH 命令的事務會將客戶端和被監視的鍵在數據庫的 watched_keys 字典中進行關聯,當鍵被修改時,程序會將所有監視被修改鍵的客戶端的 REDIS_DIRTY_CAS 選項打開。
  (5) 只有在客戶端的 REDIS_DIRTY_CAS 選項未被打開時,才能執行事務,否則事務直接返回失敗。
  (6) Redis 的事務保證了 ACID 中的一致性(C)和隔離性(I),但並不保證原子性(A)和持久性(D)。

 

文章資料出處: 

  Redis之事務功能使用實現與分析

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM