Redis 事務(wù)

2018-08-02 11:49 更新

事務(wù)

Redis 通過(guò) MULTI 、 DISCARD 、 EXECWATCH 四個(gè)命令來(lái)實(shí)現(xiàn)事務(wù)功能,本章首先討論使用 MULTI 、 DISCARDEXEC 三個(gè)命令實(shí)現(xiàn)的一般事務(wù),然后再來(lái)討論帶有 WATCH 的事務(wù)的實(shí)現(xiàn)。

因?yàn)槭聞?wù)的安全性也非常重要,所以本章最后通過(guò)常見的 ACID 性質(zhì)對(duì) Redis 事務(wù)的安全性進(jìn)行了說(shuō)明。

事務(wù)

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

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

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"

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

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

下文將分別介紹事務(wù)的這三個(gè)階段。

開始事務(wù)

MULTI 命令的執(zhí)行標(biāo)記著事務(wù)的開始:

redis> MULTI
OK

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

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

命令入隊(duì)

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

redis> SET msg "hello moto"
OK

redis> GET msg
"hello moto"

但是,當(dāng)客戶端進(jìn)入事務(wù)狀態(tài)之后,服務(wù)器在收到來(lái)自客戶端的命令時(shí),不會(huì)立即執(zhí)行命令,而是將這些命令全部放進(jìn)一個(gè)事務(wù)隊(duì)列里,然后返回 QUEUED ,表示命令已入隊(duì):

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;}" />

事務(wù)隊(duì)列是一個(gè)數(shù)組,每個(gè)數(shù)組項(xiàng)是都包含三個(gè)屬性:

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

舉個(gè)例子,如果客戶端執(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)建以下事務(wù)隊(duì)列:

數(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í)行事務(wù)

前面說(shuō)到,當(dāng)客戶端進(jìn)入事務(wù)狀態(tài)之后,客戶端發(fā)送的命令就會(huì)被放進(jìn)事務(wù)隊(duì)列里。

但其實(shí)并不是所有的命令都會(huì)被放進(jìn)事務(wù)隊(duì)列,其中的例外就是 EXEC 、 DISCARD 、 MULTIWATCH 這四個(gè)命令 ——當(dāng)這四個(gè)命令從客戶端發(fā)送到服務(wù)器時(shí),它們會(huì)像客戶端處于非事務(wù)狀態(tài)一樣,直接被服務(wù)器執(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;}" />

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

比如說(shuō),對(duì)于以下事務(wù)隊(duì)列:

數(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

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

執(zhí)行事務(wù)中的命令所得的結(jié)果會(huì)以 FIFO 的順序保存到一個(gè)回復(fù)隊(duì)列中。

比如說(shuō),對(duì)于上面給出的事務(wù)隊(duì)列,程序?qū)殛?duì)列中的命令創(chuàng)建如下回復(fù)隊(duì)列:

數(shù)組索引 回復(fù)類型 回復(fù)內(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"]

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

事務(wù)的整個(gè)執(zhí)行過(guò)程可以用以下偽代碼表示:

def execute_transaction():

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

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

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

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

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

    # 清空事務(wù)隊(duì)列
    clear_transaction_queue(client)

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

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

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

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

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

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

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

而事務(wù)則是將所有命令的結(jié)果集合到回復(fù)隊(duì)列,再作為 EXEC 命令的結(jié)果返回給客戶端。

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

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

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

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

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

帶 WATCH 的事務(wù)

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

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

redis> WATCH name
OK

redis> MULTI
OK

redis> SET name peter
QUEUED

redis> EXEC
(nil)

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

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

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

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

WATCH 命令的實(shí)現(xiàn)

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

比如說(shuō),以下字典就展示了一個(gè) watched_keys 字典的例子:

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)