Redis 通過 MULTI 、 DISCARD 、 EXEC 和 WATCH 四個命令來實現(xiàn)事務功能,本章首先討論使用 MULTI 、 DISCARD 和 EXEC 三個命令實現(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)歷以下三個階段:
下文將分別介紹事務的這三個階段。
MULTI 命令的執(zhí)行標記著事務的開始:
redis> MULTI
OK
這個命令唯一做的就是,將客戶端的 REDIS_MULTI
選項打開,讓客戶端從非事務狀態(tài)切換到事務狀態(tài)。
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
以下流程圖展示了這一行為:
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ù)組項是都包含三個屬性:
舉個例子,如果客戶端執(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 |
前面說到,當客戶端進入事務狀態(tài)之后,客戶端發(fā)送的命令就會被放進事務隊列里。
但其實并不是所有的命令都會被放進事務隊列,其中的例外就是 EXEC 、 DISCARD 、 MULTI 和 WATCH 這四個命令 ——當這四個命令從客戶端發(fā)送到服務器時,它們會像客戶端處于非事務狀態(tài)一樣,直接被服務器執(zhí)行:
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)下,還是在非事務狀態(tài)下,Redis 命令都由同一個函數(shù)執(zhí)行,所以它們共享很多服務器的一般設置,比如 AOF 的配置、RDB 的配置,以及內(nèi)存限制,等等。
不過事務中的命令和普通命令在執(zhí)行上還是有一點區(qū)別的,其中最重要的兩點是:
非事務狀態(tài)下的命令以單個命令為單位執(zhí)行,前一個命令和后一個命令的客戶端不一定是同一個;
而事務狀態(tài)則是以一個事務為單位,執(zhí)行事務隊列中的所有命令:除非當前事務執(zhí)行完畢,否則服務器不會中斷事務,也不會執(zhí)行其他客戶端的其他命令。
在非事務狀態(tài)下,執(zhí)行命令所得的結果會立即被返回給客戶端;
而事務則是將所有命令的結果集合到回復隊列,再作為 EXEC 命令的結果返回給客戶端。
除了 EXEC 之外,服務器在客戶端處于事務狀態(tài)時,不加入到事務隊列而直接執(zhí)行的另外三個命令是 DISCARD 、 MULTI 和 WATCH 。
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 [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)視的鍵是否被修改,從而保證事務的安全性的。
在每個代表數(shù)據(jù)庫的 redis.h/redisDb
結構類型中,都保存了一個 watched_keys
字典,字典的鍵是這個數(shù)據(jù)庫被監(jiān)視的鍵,而字典的值則是一個鏈表,鏈表中保存了所有監(jiān)視這個鍵的客戶端。
比如說,以下字典就展示了一個 watched_keys
字典的例子:
更多建議: