# 事務
[TOC=2,3]
Redis 通過 [MULTI](http://redis.readthedocs.org/en/latest/transaction/multi.html#multi "(in Redis 命令參考 v2.8)") 、 [DISCARD](http://redis.readthedocs.org/en/latest/transaction/discard.html#discard "(in Redis 命令參考 v2.8)") 、 [EXEC](http://redis.readthedocs.org/en/latest/transaction/exec.html#exec "(in Redis 命令參考 v2.8)") 和 [WATCH](http://redis.readthedocs.org/en/latest/transaction/watch.html#watch "(in Redis 命令參考 v2.8)") 四個命令來實現事務功能,本章首先討論使用 [MULTI](http://redis.readthedocs.org/en/latest/transaction/multi.html#multi "(in Redis 命令參考 v2.8)") 、 [DISCARD](http://redis.readthedocs.org/en/latest/transaction/discard.html#discard "(in Redis 命令參考 v2.8)") 和 [EXEC](http://redis.readthedocs.org/en/latest/transaction/exec.html#exec "(in Redis 命令參考 v2.8)") 三個命令實現的一般事務,然后再來討論帶有 [WATCH](http://redis.readthedocs.org/en/latest/transaction/watch.html#watch "(in Redis 命令參考 v2.8)") 的事務的實現。
因為事務的安全性也非常重要,所以本章最后通過常見的 ACID 性質對 Redis 事務的安全性進行了說明。
### 事務
事務提供了一種“將多個命令打包,然后一次性、按順序地執行”的機制,并且事務在執行的期間不會主動中斷 ——服務器在執行完事務中的所有命令之后,才會繼續處理其他客戶端的其他命令。
以下是一個事務的例子,它先以 [MULTI](http://redis.readthedocs.org/en/latest/transaction/multi.html#multi "(in Redis 命令參考 v2.8)") 開始一個事務,然后將多個命令入隊到事務中,最后由 [EXEC](http://redis.readthedocs.org/en/latest/transaction/exec.html#exec "(in Redis 命令參考 v2.8)") 命令觸發事務,一并執行事務中的所有命令:
~~~
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"
~~~
一個事務從開始到執行會經歷以下三個階段:
1. 開始事務。
1. 命令入隊。
1. 執行事務。
下文將分別介紹事務的這三個階段。
### 開始事務
[MULTI](http://redis.readthedocs.org/en/latest/transaction/multi.html#multi "(in Redis 命令參考 v2.8)") 命令的執行標記著事務的開始:
~~~
redis> MULTI
OK
~~~
這個命令唯一做的就是,將客戶端的 `REDIS_MULTI` 選項打開,讓客戶端從非事務狀態切換到事務狀態。
![digraph normal_to_transaction { rankdir = LR; node [shape = circle, style = filled]; edge [style = bold]; label = "客戶端狀態的切換"; normal [label = "非事務狀態", fillcolor = "#FADCAD"]; transaction [label = "事務狀態", fillcolor = "#A8E270"]; normal -> transaction [label = "打開選項\nREDIS_MULTI"];}](https://box.kancloud.cn/2015-09-13_55f4effd1373a.svg)
### 命令入隊
當客戶端處于非事務狀態下時,所有發送給服務器端的命令都會立即被服務器執行:
~~~
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
~~~
以下流程圖展示了這一行為:
![digraph enqueue { node [shape = plaintext, style = filled]; edge [style = bold]; command_in [label = "服務器接到來自客戶端的命令"]; in_transaction_or_not [label = "客戶端是否正處于事務狀態?", shape = diamond, fillcolor = "#95BBE3"]; enqueu_command [label = "將命令放進事務隊列里", fillcolor = "#A8E270"]; return_enqueued [label = "向客戶端返回 QUEUED 字符串\n表示命令已入隊", fillcolor = "#A8E270"]; exec_command [label = "執行命令", fillcolor = "#FADCAD"]; return_command_result [label = "向客戶端返回命令的執行結果", fillcolor = "#FADCAD"]; // command_in -> 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;}](https://box.kancloud.cn/2015-09-13_55f4effd23181.svg)
事務隊列是一個數組,每個數組項是都包含三個屬性:
1. 要執行的命令(cmd)。
1. 命令的參數(argv)。
1. 參數的個數(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
~~~
那么程序將為客戶端創建以下事務隊列:
| 數組索引 | 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` |
### 執行事務
前面說到,當客戶端進入事務狀態之后,客戶端發送的命令就會被放進事務隊列里。
但其實并不是所有的命令都會被放進事務隊列,其中的例外就是 [EXEC](http://redis.readthedocs.org/en/latest/transaction/exec.html#exec "(in Redis 命令參考 v2.8)") 、 [DISCARD](http://redis.readthedocs.org/en/latest/transaction/discard.html#discard "(in Redis 命令參考 v2.8)") 、 [MULTI](http://redis.readthedocs.org/en/latest/transaction/multi.html#multi "(in Redis 命令參考 v2.8)") 和 [WATCH](http://redis.readthedocs.org/en/latest/transaction/watch.html#watch "(in Redis 命令參考 v2.8)") 這四個命令 ——當這四個命令從客戶端發送到服務器時,它們會像客戶端處于非事務狀態一樣,直接被服務器執行:
![digraph not_enque_command { node [shape = plaintext, style = filled]; edge [style = bold]; command_in [label = "服務器接到來自客戶端的命令"]; in_transaction_or_not [label = "客戶端是否正處于事務狀態?", shape = diamond, fillcolor = "#95BBE3"]; not_exec_and_discard [label = "命令是否\nEXEC 、 DISCARD 、\nMULTI 或 WATCH ?", shape = diamond, fillcolor = "#FFC1C1"]; enqueu_command [label = "將命令放進事務隊列里", fillcolor = "#A8E270"]; return_enqueued [label = "向客戶端返回 QUEUED 字符串\n表示命令已入隊", fillcolor = "#A8E270"]; exec_command [label = "執行命令", fillcolor = "#FADCAD"]; return_command_result [label = "向客戶端返回命令的執行結果", fillcolor = "#FADCAD"]; // command_in -> 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;}](https://box.kancloud.cn/2015-09-13_55f4effd2ccb1.svg)
如果客戶端正處于事務狀態,那么當 [EXEC](http://redis.readthedocs.org/en/latest/transaction/exec.html#exec "(in Redis 命令參考 v2.8)") 命令執行時,服務器根據客戶端所保存的事務隊列,以先進先出(FIFO)的方式執行事務隊列中的命令:最先入隊的命令最先執行,而最后入隊的命令最后執行。
比如說,對于以下事務隊列:
| 數組索引 | 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` |
程序會首先執行 [SET](http://redis.readthedocs.org/en/latest/string/set.html#set "(in Redis 命令參考 v2.8)") 命令,然后執行 [GET](http://redis.readthedocs.org/en/latest/string/get.html#get "(in Redis 命令參考 v2.8)") 命令,再然后執行 [SADD](http://redis.readthedocs.org/en/latest/set/sadd.html#sadd "(in Redis 命令參考 v2.8)") 命令,最后執行 [SMEMBERS](http://redis.readthedocs.org/en/latest/set/smembers.html#smembers "(in Redis 命令參考 v2.8)") 命令。
執行事務中的命令所得的結果會以 FIFO 的順序保存到一個回復隊列中。
比如說,對于上面給出的事務隊列,程序將為隊列中的命令創建如下回復隊列:
| 數組索引 | 回復類型 | 回復內容 |
|-----|-----|-----|
| `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"]` |
當事務隊列里的所有命令被執行完之后,[EXEC](http://redis.readthedocs.org/en/latest/transaction/exec.html#exec "(in Redis 命令參考 v2.8)") 命令會將回復隊列作為自己的執行結果返回給客戶端,客戶端從事務狀態返回到非事務狀態,至此,事務執行完畢。
事務的整個執行過程可以用以下偽代碼表示:
~~~
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.
非事務狀態下的命令以單個命令為單位執行,前一個命令和后一個命令的客戶端不一定是同一個;
而事務狀態則是以一個事務為單位,執行事務隊列中的所有命令:除非當前事務執行完畢,否則服務器不會中斷事務,也不會執行其他客戶端的其他命令。
1.
在非事務狀態下,執行命令所得的結果會立即被返回給客戶端;
而事務則是將所有命令的結果集合到回復隊列,再作為 [EXEC](http://redis.readthedocs.org/en/latest/transaction/exec.html#exec "(in Redis 命令參考 v2.8)") 命令的結果返回給客戶端。
### 事務狀態下的 DISCARD 、 MULTI 和 WATCH 命令
除了 [EXEC](http://redis.readthedocs.org/en/latest/transaction/exec.html#exec "(in Redis 命令參考 v2.8)") 之外,服務器在客戶端處于事務狀態時,不加入到事務隊列而直接執行的另外三個命令是 [DISCARD](http://redis.readthedocs.org/en/latest/transaction/discard.html#discard "(in Redis 命令參考 v2.8)") 、 [MULTI](http://redis.readthedocs.org/en/latest/transaction/multi.html#multi "(in Redis 命令參考 v2.8)") 和 [WATCH](http://redis.readthedocs.org/en/latest/transaction/watch.html#watch "(in Redis 命令參考 v2.8)") 。
[DISCARD](http://redis.readthedocs.org/en/latest/transaction/discard.html#discard "(in Redis 命令參考 v2.8)") 命令用于取消一個事務,它清空客戶端的整個事務隊列,然后將客戶端從事務狀態調整回非事務狀態,最后返回字符串 `OK` 給客戶端,說明事務已被取消。
Redis 的事務是不可嵌套的,當客戶端已經處于事務狀態,而客戶端又再向服務器發送 [MULTI](http://redis.readthedocs.org/en/latest/transaction/multi.html#multi "(in Redis 命令參考 v2.8)") 時,服務器只是簡單地向客戶端發送一個錯誤,然后繼續等待其他命令的入隊。[MULTI](http://redis.readthedocs.org/en/latest/transaction/multi.html#multi "(in Redis 命令參考 v2.8)") 命令的發送不會造成整個事務失敗,也不會修改事務隊列中已有的數據。
[WATCH](http://redis.readthedocs.org/en/latest/transaction/watch.html#watch "(in Redis 命令參考 v2.8)") 只能在客戶端進入事務狀態之前執行,在事務狀態下發送 [WATCH](http://redis.readthedocs.org/en/latest/transaction/watch.html#watch "(in Redis 命令參考 v2.8)") 命令會引發一個錯誤,但它不會造成整個事務失敗,也不會修改事務隊列中已有的數據(和前面處理 [MULTI](http://redis.readthedocs.org/en/latest/transaction/multi.html#multi "(in Redis 命令參考 v2.8)") 的情況一樣)。
### 帶 WATCH 的事務
[WATCH](http://redis.readthedocs.org/en/latest/transaction/watch.html#watch "(in Redis 命令參考 v2.8)") [http://redis.readthedocs.org/en/latest/transaction/watch.html#watch] 命令用于在事務開始之前監視任意數量的鍵:當調用 [EXEC](http://redis.readthedocs.org/en/latest/transaction/exec.html#exec "(in Redis 命令參考 v2.8)") [http://redis.readthedocs.org/en/latest/transaction/exec.html#exec] 命令執行事務時,如果任意一個被監視的鍵已經被其他客戶端修改了,那么整個事務不再執行,直接返回失敗。
以下示例展示了一個執行失敗的事務例子:
~~~
redis> WATCH name
OK
redis> MULTI
OK
redis> SET name peter
QUEUED
redis> EXEC
(nil)
~~~
以下執行序列展示了上面的例子是如何失敗的:
| 時間 | 客戶端 A | 客戶端 B |
|-----|-----|-----|
| T1 | `WATCH name` | ? |
| T2 | `MULTI` | ? |
| T3 | `SET name peter` | ? |
| T4 | ? | `SET name john` |
| T5 | `EXEC` | ? |
在時間 T4 ,客戶端 B 修改了 `name` 鍵的值,當客戶端 A 在 T5 執行 [EXEC](http://redis.readthedocs.org/en/latest/transaction/exec.html#exec "(in Redis 命令參考 v2.8)") 時,Redis 會發現 `name` 這個被監視的鍵已經被修改,因此客戶端 A 的事務不會被執行,而是直接返回失敗。
下文就來介紹 [WATCH](http://redis.readthedocs.org/en/latest/transaction/watch.html#watch "(in Redis 命令參考 v2.8)") 的實現機制,并且看看事務系統是如何檢查某個被監視的鍵是否被修改,從而保證事務的安全性的。
### WATCH 命令的實現
在每個代表數據庫的 `redis.h/redisDb` 結構類型中,都保存了一個 `watched_keys` 字典,字典的鍵是這個數據庫被監視的鍵,而字典的值則是一個鏈表,鏈表中保存了所有監視這個鍵的客戶端。
比如說,以下字典就展示了一個 `watched_keys` 字典的例子:
![digraph watched_keys { rankdir = LR; node [shape = record, style = filled]; edge [style = bold]; // keys watched_keys [label = "watched_keys |<key1> key1 |<key2> key2 |<key3> key3 | ... |<keyN> keyN", fillcolor = "#A8E270"]; // clients blocking for key1 client1 [label = "client1", fillcolor = "#95BBE3"]; client5 [label = "client5", fillcolor = "#95BBE3"]; client2 [label = "client2", fillcolor = "#95BBE3"]; null_1 [label = "NULL", shape = plaintext]; watched_keys:key1 -> client2; client2 -> client5; client5 -> client1; client1 -> null_1; // clients blocking for key2 client7 [label = "client7", fillcolor = "#95BBE3"]; null_2 [label = "NULL", shape = plaintext]; watched_keys:key2 -> client7; client7 -> null_2; // key3 client3 [label = "client3", fillcolor = "#95BBE3"]; client4 [label = "client4", fillcolor = "#95BBE3"]; client6 [label = "client6", fillcolor = "#95BBE3"]; null_3 [label = "NULL", shape = plaintext]; watched_keys:key3 -> client3; client3 -> client4; client4 -> client6; client6 -> null_3;}](https://box.kancloud.cn/2015-09-13_55f4effd35981.svg)
其中, 鍵 `key1` 正在被 `client2` 、 `client5` 和 `client1` 三個客戶端監視,其他一些鍵也分別被其他別的客戶端監視著。
[WATCH](http://redis.readthedocs.org/en/latest/transaction/watch.html#watch "(in Redis 命令參考 v2.8)") 命令的作用,就是將當前客戶端和要監視的鍵在 `watched_keys` 中進行關聯。
舉個例子,如果當前客戶端為 `client10086` ,那么當客戶端執行 `WATCH key1 key2` 時,前面展示的 `watched_keys` 將被修改成這個樣子:
![digraph new_watched_keys { rankdir = LR; node [shape = record, style = filled]; edge [style = bold]; // keys watched_keys [label = "watched_keys |<key1> key1 |<key2> key2 |<key3> key3 | ... |<keyN> keyN", fillcolor = "#A8E270"]; // clients blocking for key1 client1 [label = "client1", fillcolor = "#95BBE3"]; client5 [label = "client5", fillcolor = "#95BBE3"]; client2 [label = "client2", fillcolor = "#95BBE3"]; client10086 [label = "client10086", fillcolor = "#FFC1C1"]; null_1 [label = "NULL", shape = plaintext]; watched_keys:key1 -> client2; client2 -> client5; client5 -> client1; client1 -> client10086; client10086 -> null_1; // clients blocking for key2 client7 [label = "client7", fillcolor = "#95BBE3"]; client10086_2 [label = "client10086", fillcolor = "#FFC1C1"]; null_2 [label = "NULL", shape = plaintext]; watched_keys:key2 -> client7; client7 -> client10086_2; client10086_2 -> null_2; // key3 client3 [label = "client3", fillcolor = "#95BBE3"]; client4 [label = "client4", fillcolor = "#95BBE3"]; client6 [label = "client6", fillcolor = "#95BBE3"]; null_3 [label = "NULL", shape = plaintext]; watched_keys:key3 -> client3; client3 -> client4; client4 -> client6; client6 -> null_3;}](https://box.kancloud.cn/2015-09-13_55f4effd40188.svg)
通過 `watched_keys` 字典,如果程序想檢查某個鍵是否被監視,那么它只要檢查字典中是否存在這個鍵即可;如果程序要獲取監視某個鍵的所有客戶端,那么只要取出鍵的值(一個鏈表),然后對鏈表進行遍歷即可。
### WATCH 的觸發
在任何對數據庫鍵空間(key space)進行修改的命令成功執行之后(比如 [FLUSHDB](http://redis.readthedocs.org/en/latest/server/flushdb.html#flushdb "(in Redis 命令參考 v2.8)") 、 [SET](http://redis.readthedocs.org/en/latest/string/set.html#set "(in Redis 命令參考 v2.8)") 、 [DEL](http://redis.readthedocs.org/en/latest/key/del.html#del "(in Redis 命令參考 v2.8)") 、 [LPUSH](http://redis.readthedocs.org/en/latest/list/lpush.html#lpush "(in Redis 命令參考 v2.8)") 、 [SADD](http://redis.readthedocs.org/en/latest/set/sadd.html#sadd "(in Redis 命令參考 v2.8)") 、 [ZREM](http://redis.readthedocs.org/en/latest/sorted_set/zrem.html#zrem "(in Redis 命令參考 v2.8)") [http://redis.readthedocs.org/en/latest/sorted_set/zrem.html#zrem] ,諸如此類),`multi.c/touchWatchedKey` 函數都會被調用 ——它檢查數據庫的 `watched_keys` 字典,看是否有客戶端在監視已經被命令修改的鍵,如果有的話,程序將所有監視這個/這些被修改鍵的客戶端的 `REDIS_DIRTY_CAS` 選項打開:
![digraph dirty_cas { rankdir = LR; node [shape = circle, style = filled]; edge [style = bold]; label = "客戶端狀態的切換"; normal [label = "非事務狀態", fillcolor = "#FADCAD"]; transaction [label = "事務狀態", fillcolor = "#A8E270"]; dirty_cas [label = "事務安全性\n已被破壞", fillcolor = "#B22222"]; normal -> transaction [label = "打開選項\nREDIS_MULTI"]; transaction -> dirty_cas [label = "打開選項\nREDIS_DIRTY_CAS"];}](https://box.kancloud.cn/2015-09-13_55f4effd4aae6.svg)
當客戶端發送 [EXEC](http://redis.readthedocs.org/en/latest/transaction/exec.html#exec "(in Redis 命令參考 v2.8)") 命令、觸發事務執行時,服務器會對客戶端的狀態進行檢查:
- 如果客戶端的 `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` 字典如下圖所示:
![digraph watched_keys { rankdir = LR; node [shape = record, style = filled]; edge [style = bold]; // keys watched_keys [label = "watched_keys |<key1> key1 |<key2> key2 |<key3> key3 | ... |<keyN> keyN", fillcolor = "#A8E270"]; // clients blocking for key1 client1 [label = "client1", fillcolor = "#95BBE3"]; client5 [label = "client5", fillcolor = "#95BBE3"]; client2 [label = "client2", fillcolor = "#95BBE3"]; null_1 [label = "NULL", shape = plaintext]; watched_keys:key1 -> client2; client2 -> client5; client5 -> client1; client1 -> null_1; // clients blocking for key2 client7 [label = "client7", fillcolor = "#95BBE3"]; null_2 [label = "NULL", shape = plaintext]; watched_keys:key2 -> client7; client7 -> null_2; // key3 client3 [label = "client3", fillcolor = "#95BBE3"]; client4 [label = "client4", fillcolor = "#95BBE3"]; client6 [label = "client6", fillcolor = "#95BBE3"]; null_3 [label = "NULL", shape = plaintext]; watched_keys:key3 -> client3; client3 -> client4; client4 -> client6; client6 -> null_3;}](https://box.kancloud.cn/2015-09-13_55f4effd51be3.svg)
如果某個客戶端對 `key1` 進行了修改(比如執行 `DEL key1` ),那么所有監視 `key1` 的客戶端,包括 `client2` 、 `client5` 和 `client1` 的 `REDIS_DIRTY_CAS` 選項都會被打開,當客戶端 `client2` 、 `client5` 和 `client1` 執行 [EXEC](http://redis.readthedocs.org/en/latest/transaction/exec.html#exec "(in Redis 命令參考 v2.8)") 的時候,它們的事務都會以失敗告終。
最后,當一個客戶端結束它的事務時,無論事務是成功執行,還是失敗, `watched_keys` 字典中和這個客戶端相關的資料都會被清除。
### 事務的 ACID 性質
Warning
勘誤:Redis 的事務是保證原子性的,本節的內容將原子性和回滾功能混淆了,等待修復中。 —— 2013.6.23
在傳統的關系式數據庫中,常常用 [ACID 性質](http://en.wikipedia.org/wiki/ACID) [http://en.wikipedia.org/wiki/ACID]來檢驗事務功能的安全性。
Redis 事務保證了其中的一致性(C)和隔離性(I),但并不保證原子性(A)和持久性(D)。
以下四小節是關于這四個性質的詳細討論。
### 原子性(Atomicity)
單個 Redis 命令的執行是原子性的,但 Redis 沒有在事務上增加任何維持原子性的機制,所以 Redis 事務的執行并不是原子性的。
如果一個事務隊列中的所有命令都被成功地執行,那么稱這個事務執行成功。
另一方面,如果 Redis 服務器進程在執行事務的過程中被停止 —— 比如接到 KILL 信號、宿主機器停機,等等,那么事務執行失敗。
當事務失敗時,Redis 也不會進行任何的重試或者回滾動作。
### 一致性(Consistency)
Redis 的一致性問題可以分為三部分來討論:入隊錯誤、執行錯誤、Redis 進程被終結。
#### 入隊錯誤
在命令入隊的過程中,如果客戶端向服務器發送了錯誤的命令,比如命令的參數數量不對,等等,那么服務器將向客戶端返回一個出錯信息,并且將客戶端的事務狀態設為 `REDIS_DIRTY_EXEC` 。
當客戶端執行 [EXEC](http://redis.readthedocs.org/en/latest/transaction/exec.html#exec "(in Redis 命令參考 v2.8)") 命令時,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 ”模式類似,所以它們都是不持久的。
### 小結
- 事務提供了一種將多個命令打包,然后一次性、有序地執行的機制。
- 事務在執行過程中不會被中斷,所有事務命令執行完之后,事務才能結束。
- 多個命令會被入隊到事務隊列中,然后按先進先出(FIFO)的順序執行。
- 帶 `WATCH` 命令的事務會將客戶端和被監視的鍵在數據庫的 `watched_keys` 字典中進行關聯,當鍵被修改時,程序會將所有監視被修改鍵的客戶端的 `REDIS_DIRTY_CAS` 選項打開。
- 只有在客戶端的 `REDIS_DIRTY_CAS` 選項未被打開時,才能執行事務,否則事務直接返回失敗。
- Redis 的事務保證了 ACID 中的一致性(C)和隔離性(I),但并不保證原子性(A)和持久性(D)。