Redis 事務

2018-08-02 11:49 更新

事務

Redis 通過 MULTI 、 DISCARD 、 EXECWATCH 四個命令來實現(xiàn)事務功能,本章首先討論使用 MULTIDISCARDEXEC 三個命令實現(xiàn)的一般事務,然后再來討論帶有 WATCH 的事務的實現(xiàn)。

因為事務的安全性也非常重要,所以本章最后通過常見的 ACID 性質(zhì)對 Redis 事務的安全性進行了說明。

事務

事務提供了一種“將多個命令打包,然后一次性、按順序地執(zhí)行”的機制,并且事務在執(zhí)行的期間不會主動中斷 ——服務器在執(zhí)行完事務中的所有命令之后,才會繼續(xù)處理其他客戶端的其他命令。

以下是一個事務的例子,它先以 MULTI 開始一個事務,然后將多個命令入隊到事務中,最后由 EXEC 命令觸發(fā)事務,一并執(zhí)行事務中的所有命令:

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

redis> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"
   2) "C++"
   3) "Programming"

一個事務從開始到執(zhí)行會經(jīng)歷以下三個階段:

  1. 開始事務。
  2. 命令入隊。
  3. 執(zhí)行事務。

下文將分別介紹事務的這三個階段。

開始事務

MULTI 命令的執(zhí)行標記著事務的開始:

redis> MULTI
OK

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

digraph normal_to_trans {    rankdir = LR;    node [shape = circle, style = filled];    edge [style = bold];    label = transaction [label = "打開選項\nREDIS_MULTI"];}" />

命令入隊

當客戶端處于非事務狀態(tài)下時,所有發(fā)送給服務器端的命令都會立即被服務器執(zhí)行:

redis> SET msg "hello moto"
OK

redis> GET msg
"hello moto"

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

redis> MULTI
OK

redis> SET msg "hello moto"
QUEUED

redis> GET msg
QUEUED

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

digraph enqueue {    node [shape = plaintext, style = filled];    edge [style = bold];    command_in [label = in_transaction_or_not; in_transaction_or_not -> enqueu_command [label = "是"]; in_transaction_or_not -> exec_command [label = "否"]; exec_command -> return_command_result; enqueu_command -> return_enqueued;}" />

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

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

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

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

那么程序?qū)榭蛻舳藙?chuàng)建以下事務隊列:

數(shù)組索引 cmd argv argc
0 SET ["book-name", "Mastering C++ in 21 days"] 2
1 GET ["book-name"] 1
2 SADD ["tag", "C++", "Programming", "Mastering Series"] 4
3 SMEMBERS ["tag"] 1

執(zhí)行事務

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

但其實并不是所有的命令都會被放進事務隊列,其中的例外就是 EXEC 、 DISCARD 、 MULTIWATCH 這四個命令 ——當這四個命令從客戶端發(fā)送到服務器時,它們會像客戶端處于非事務狀態(tài)一樣,直接被服務器執(zhí)行:

digraph not_enque_command {    node [shape = plaintext, style = filled];    edge [style = bold];    command_in [label = in_transaction_or_not; in_transaction_or_not -> not_exec_and_discard [label = "是"]; not_exec_and_discard -> enqueu_command [label = "否"]; not_exec_and_discard -> exec_command [label = "是"]; in_transaction_or_not -> exec_command [label = "否"]; exec_command -> return_command_result; enqueu_command -> return_enqueued;}" />

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

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

數(shù)組索引 cmd argv argc
0 SET ["book-name", "Mastering C++ in 21 days"] 2
1 GET ["book-name"] 1
2 SADD ["tag", "C++", "Programming", "Mastering Series"] 4
3 SMEMBERS ["tag"] 1

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

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

比如說,對于上面給出的事務隊列,程序?qū)殛犃兄械拿顒?chuàng)建如下回復隊列:

數(shù)組索引 回復類型 回復內(nèi)容
0 status code reply OK
1 bulk reply "Mastering C++ in 21 days"
2 integer reply 3
3 multi-bulk reply ["Mastering Series", "C++", "Programming"]

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

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

def execute_transaction():

    # 創(chuàng)建空白的回復隊列
    reply_queue = []

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

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

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

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

    # 清空事務隊列
    clear_transaction_queue(client)

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

在事務和非事務狀態(tài)下執(zhí)行命令

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

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

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

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

  1. 在非事務狀態(tài)下,執(zhí)行命令所得的結果會立即被返回給客戶端;

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

事務狀態(tài)下的 DISCARD 、 MULTI 和 WATCH 命令

除了 EXEC 之外,服務器在客戶端處于事務狀態(tài)時,不加入到事務隊列而直接執(zhí)行的另外三個命令是 DISCARD 、 MULTIWATCH 。

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

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

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

帶 WATCH 的事務

WATCH [http://redis.readthedocs.org/en/latest/transaction/watch.html#watch] 命令用于在事務開始之前監(jiān)視任意數(shù)量的鍵:當調(diào)用 EXEC [http://redis.readthedocs.org/en/latest/transaction/exec.html#exec] 命令執(zhí)行事務時,如果任意一個被監(jiān)視的鍵已經(jīng)被其他客戶端修改了,那么整個事務不再執(zhí)行,直接返回失敗。

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

redis> WATCH name
OK

redis> MULTI
OK

redis> SET name peter
QUEUED

redis> EXEC
(nil)

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

時間 客戶端 A 客戶端 B
T1 WATCH name  
T2 MULTI  
T3 SET name peter  
T4   SET name john
T5 EXEC  

在時間 T4 ,客戶端 B 修改了 name 鍵的值,當客戶端 A 在 T5 執(zhí)行 EXEC 時,Redis 會發(fā)現(xiàn) name 這個被監(jiān)視的鍵已經(jīng)被修改,因此客戶端 A 的事務不會被執(zhí)行,而是直接返回失敗。

下文就來介紹 WATCH 的實現(xiàn)機制,并且看看事務系統(tǒng)是如何檢查某個被監(jiān)視的鍵是否被修改,從而保證事務的安全性的。

WATCH 命令的實現(xiàn)

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

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

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號