一個事務從開始到結束通常會經歷以下三個階段:
1. 事務開始。
2. 命令入隊。
3. 事務執行。
本節接下來的內容將對這三個階段進行介紹, 說明一個事務從開始到結束的整個過程。
## 事務開始
MULTI?命令的執行標志著事務的開始:
~~~
redis> MULTI
OK
~~~
MULTI?命令可以將執行該命令的客戶端從非事務狀態切換至事務狀態, 這一切換是通過在客戶端狀態的?`flags`?屬性中打開?`REDIS_MULTI`?標識來完成的,?MULTI?命令的實現可以用以下偽代碼來表示:
~~~
def MULTI():
# 打開事務標識
client.flags |= REDIS_MULTI
# 返回 OK 回復
replyOK()
~~~
## 命令入隊
當一個客戶端處于非事務狀態時, 這個客戶端發送的命令會立即被服務器執行:
~~~
redis> SET "name" "Practical Common Lisp"
OK
redis> GET "name"
"Practical Common Lisp"
redis> SET "author" "Peter Seibel"
OK
redis> GET "author"
"Peter Seibel"
~~~
與此不同的是, 當一個客戶端切換到事務狀態之后, 服務器會根據這個客戶端發來的不同命令執行不同的操作:
* 如果客戶端發送的命令為?EXEC?、?DISCARD?、?WATCH?、?MULTI?四個命令的其中一個, 那么服務器立即執行這個命令。
* 與此相反, 如果客戶端發送的命令是?EXEC?、?DISCARD?、?WATCH?、?MULTI?四個命令以外的其他命令, 那么服務器并不立即執行這個命令, 而是將這個命令放入一個事務隊列里面, 然后向客戶端返回?`QUEUED`?回復。
服務器判斷命令是該入隊還是該立即執行的過程可以用流程圖 IMAGE_ENQUEUE_OR_EXEC 來描述。

## 事務隊列
每個 Redis 客戶端都有自己的事務狀態, 這個事務狀態保存在客戶端狀態的?`mstate`?屬性里面:
~~~
typedef struct redisClient {
// ...
// 事務狀態
multiState mstate; /* MULTI/EXEC state */
// ...
} redisClient;
~~~
事務狀態包含一個事務隊列, 以及一個已入隊命令的計數器 (也可以說是事務隊列的長度):
~~~
typedef struct multiState {
// 事務隊列,FIFO 順序
multiCmd *commands;
// 已入隊命令計數
int count;
} multiState;
~~~
事務隊列是一個?`multiCmd`?類型的數組, 數組中的每個?`multiCmd`?結構都保存了一個已入隊命令的相關信息, 包括指向命令實現函數的指針, 命令的參數, 以及參數的數量:
~~~
typedef struct multiCmd {
// 參數
robj **argv;
// 參數數量
int argc;
// 命令指針
struct redisCommand *cmd;
} multiCmd;
~~~
事務隊列以先進先出(FIFO)的方式保存入隊的命令: 較先入隊的命令會被放到數組的前面, 而較后入隊的命令則會被放到數組的后面。
舉個例子, 如果客戶端執行以下命令:
~~~
redis> MULTI
OK
redis> SET "name" "Practical Common Lisp"
QUEUED
redis> GET "name"
QUEUED
redis> SET "author" "Peter Seibel"
QUEUED
redis> GET "author"
QUEUED
~~~
那么服務器將為客戶端創建圖 IMAGE_TRANSACTION_STATE 所示的事務狀態:
* 最先入隊的?SET?命令被放在了事務隊列的索引?`0`?位置上。
* 第二入隊的?GET?命令被放在了事務隊列的索引?`1`?位置上。
* 第三入隊的另一個?SET?命令被放在了事務隊列的索引?`2`?位置上。
* 最后入隊的另一個?GET?命令被放在了事務隊列的索引?`3`?位置上。

## 執行事務
當一個處于事務狀態的客戶端向服務器發送?EXEC?命令時, 這個?EXEC?命令將立即被服務器執行: 服務器會遍歷這個客戶端的事務隊列, 執行隊列中保存的所有命令, 最后將執行命令所得的結果全部返回給客戶端。
舉個例子, 對于圖 IMAGE_TRANSACTION_STATE 所示的事務隊列來說, 服務器首先會執行命令:
~~~
SET "name" "Practical Common Lisp"
~~~
接著執行命令:
~~~
GET "name"
~~~
之后執行命令:
~~~
SET "author" "Peter Seibel"
~~~
再之后執行命令:
~~~
GET "author"
~~~
最后, 服務器會將執行這四個命令所得的回復返回給客戶端:
~~~
redis> EXEC
1) OK
2) "Practical Common Lisp"
3) OK
4) "Peter Seibel"
~~~
EXEC?命令的實現原理可以用以下偽代碼來描述:
~~~
def EXEC():
# 創建空白的回復隊列
reply_queue = []
# 遍歷事務隊列中的每個項
# 讀取命令的參數,參數的個數,以及要執行的命令
for argv, argc, cmd in client.mstate.commands:
# 執行命令,并取得命令的返回值
reply = execute_command(cmd, argv, argc)
# 將返回值追加到回復隊列末尾
reply_queue.append(reply)
# 移除 REDIS_MULTI 標識,讓客戶端回到非事務狀態
client.flags &= ~REDIS_MULTI
# 清空客戶端的事務狀態,包括:
# 1)清零入隊命令計數器
# 2)釋放事務隊列
client.mstate.count = 0
release_transaction_queue(client.mstate.commands)
# 將事務的執行結果返回給客戶端
send_reply_to_client(client, reply_queue)
~~~
- 介紹
- 前言
- 致謝
- 簡介
- 第一部分:數據結構與對象
- 簡單動態字符串
- SDS 的定義
- SDS 與 C 字符串的區別
- SDS API
- 重點回顧
- 參考資料
- 鏈表
- 鏈表和鏈表節點的實現
- 鏈表和鏈表節點的 API
- 重點回顧
- 字典
- 字典的實現
- 哈希算法
- 解決鍵沖突
- rehash
- 漸進式 rehash
- 字典 API
- 重點回顧
- 跳躍表
- 跳躍表的實現
- 跳躍表 API
- 重點回顧
- 整數集合
- 整數集合的實現
- 升級
- 升級的好處
- 降級
- 整數集合 API
- 重點回顧
- 壓縮列表
- 壓縮列表的構成
- 壓縮列表節點的構成
- 連鎖更新
- 壓縮列表 API
- 重點回顧
- 對象
- 對象的類型與編碼
- 字符串對象
- 列表對象
- 哈希對象
- 集合對象
- 有序集合對象
- 類型檢查與命令多態
- 內存回收
- 對象共享
- 對象的空轉時長
- 重點回顧
- 第二部分:單機數據庫的實現
- 數據庫
- 數據庫鍵空間
- 重點回顧
- RDB 持久化
- RDB 文件結構
- 重點回顧
- AOF 持久化
- AOF 持久化的實現
- 重點回顧
- 事件
- 文件事件
- 重點回顧
- 參考資料
- 客戶端
- 客戶端屬性
- 重點回顧
- 服務器
- 命令請求的執行過程
- 重點回顧
- 第三部分:多機數據庫的實現
- 復制
- 舊版復制功能的實現
- 重點回顧
- Sentinel
- 啟動并初始化 Sentinel
- 重點回顧
- 參考資料
- 集群
- 節點
- 重點回顧
- 第四部分:獨立功能的實現
- 發布與訂閱
- 頻道的訂閱與退訂
- 重點回顧
- 參考資料
- 事務
- 事務的實現
- 重點回顧
- Lua 腳本
- 創建并修改 Lua 環境
- 重點回顧
- 排序
- SORT <key> 命令的實現
- 重點回顧
- 二進制位數組
- GETBIT 命令的實現
- 重點回顧
- 慢查詢日志
- 慢查詢記錄的保存
- 慢查詢日志的閱覽和刪除
- 添加新日志
- 重點回顧
- 監視器
- 成為監視器
- 向監視器發送命令信息
- 重點回顧
- 源碼、相關資源和勘誤