redis之事務(wù)

Redis通過(guò)MULTI、EXEC、WATCH等命令來(lái)實(shí)現(xiàn)事務(wù)(transaction)功能。事務(wù)提供了一種將多個(gè)命令請(qǐng)求打包,然后一次性、按順序地執(zhí)行多個(gè)命令的機(jī)制,并且在事務(wù)執(zhí)行期間,服務(wù)器不會(huì)中斷事務(wù)而改去執(zhí)行其他客戶(hù)端的命令請(qǐng)求,它會(huì)將事務(wù)中的所有命令都執(zhí)行完畢,然后才去處理其他客戶(hù)端的命令請(qǐng)求。

以下是一個(gè)事務(wù)執(zhí)行的過(guò)程,該事務(wù)首先以一個(gè)MULTI命令為開(kāi)始,接著將多個(gè)命令放入事務(wù)當(dāng)中,最后由EXEC命令將這個(gè)事務(wù)提交(commit)給服務(wù)器執(zhí)行:

redis> MULTI
OK

redis> SET "name" "Practical Common Lisp"
QUEUED

redis> GET "name"
QUEUED

redis> SET "author" "Peter Seibel"
QUEUED

redis> GET "author"
QUEUED

redis> EXEC
1) OK
2) "Practical Common Lisp"
3) OK
4) "Peter Seibel"

在接下來(lái)的內(nèi)容中,我們首先會(huì)介紹Redis如何使用MULTI和EXEC命令來(lái)實(shí)現(xiàn)事務(wù)功能,說(shuō)明事務(wù)中的多個(gè)命令是如何被保存到事務(wù)里面的,而這些命令又是如何被執(zhí)行的。

在介紹了事務(wù)的實(shí)現(xiàn)原理之后,我們將對(duì)WATCH命令的作用進(jìn)行介紹,并說(shuō)明WATCH命令的實(shí)現(xiàn)原理。

19.1 事務(wù)的實(shí)現(xiàn)

一個(gè)事務(wù)從開(kāi)始到結(jié)束通常會(huì)經(jīng)歷以下三個(gè)階段:
1)事務(wù)開(kāi)始。
2)命令入隊(duì)。
3)事務(wù)執(zhí)行。

本節(jié)接下來(lái)的內(nèi)容將對(duì)這三個(gè)階段進(jìn)行介紹,說(shuō)明一個(gè)事務(wù)從開(kāi)始到結(jié)束的整個(gè)過(guò)程。

19.1.1 事務(wù)開(kāi)始

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

redis> MULTI
OK

MULTI命令可以將執(zhí)行該命令的客戶(hù)端從非事務(wù)狀態(tài)切換至事務(wù)狀態(tài),這一切換是通過(guò)在客戶(hù)端狀態(tài)的flags屬性中打開(kāi)REDIS_MULTI標(biāo)識(shí)來(lái)完成的,MULTI命令的實(shí)現(xiàn)可以用以下偽代碼來(lái)表示:

def MULTI():

  # 打開(kāi)事務(wù)標(biāo)識(shí)
  client.flags |= REDIS_MULTI

  # 返回OK回復(fù)
  replyOK()
19.1.2 命令入隊(duì)

當(dāng)一個(gè)客戶(hù)端處于非事務(wù)狀態(tài)時(shí),這個(gè)客戶(hù)端發(fā)送的命令會(huì)立即被服務(wù)器執(zhí)行:

redis> SET "name" "Practical Common Lisp"
OK

redis> GET "name"
"Practical Common Lisp"

redis> SET "author" "Peter Seibel"
OK

redis> GET "author"
"Peter Seibel"

與此不同的是,當(dāng)一個(gè)客戶(hù)端切換到事務(wù)狀態(tài)之后,服務(wù)器會(huì)根據(jù)這個(gè)客戶(hù)端發(fā)來(lái)的不同命令執(zhí)行不同的操作:
■ 如果客戶(hù)端發(fā)送的命令為EXEC、DISCARD、WATCH、MULTI四個(gè)命令的其中一個(gè),那么服務(wù)器立即執(zhí)行這個(gè)命令。
■ 與此相反,如果客戶(hù)端發(fā)送的命令是EXEC、DISCARD、WATCH、MULTI四個(gè)命令以外的其他命令,那么服務(wù)器并不立即執(zhí)行這個(gè)命令,而是將這個(gè)命令放入一個(gè)事務(wù)隊(duì)列里面,然后向客戶(hù)端返回QUEUED 回復(fù)。

服務(wù)器判斷命令是該入隊(duì)還是該立即執(zhí)行的過(guò)程可以用流程圖19-1來(lái)描述。

image.png
19.1.3 事務(wù)隊(duì)列

每個(gè)Redis客戶(hù)端都有自己的事務(wù)狀態(tài),這個(gè)事務(wù)狀態(tài)保存在客戶(hù)端狀態(tài)的mstate屬性里面:

typedef struct redisClient {

  // ...

  // 事務(wù)狀態(tài)
  multiState mstate;  /* MULTI/EXEC state */

  // ...

} redisClient;

事務(wù)狀態(tài)包含一個(gè)事務(wù)隊(duì)列,以及一個(gè)已入隊(duì)命令的計(jì)數(shù)器(也可以說(shuō)是事務(wù)隊(duì)列的長(zhǎng)度):

typedef struct multiState {

  // 事務(wù)隊(duì)列,F(xiàn)IFO順序
  multiCmd *commands;

  // 已入隊(duì)命令計(jì)數(shù)
  int count; 

} multiState;

事務(wù)隊(duì)列是一個(gè)multiCmd類(lèi)型的數(shù)組,數(shù)組中的每個(gè)multiCmd結(jié)構(gòu)都保存了一個(gè)已入隊(duì)命令的相關(guān)信息,包括指向命令實(shí)現(xiàn)函數(shù)的指針、命令的參數(shù),以及參數(shù)的數(shù)量:

typedef struct multiCmd { 

  // 參數(shù)
  robj **argv;

  // 參數(shù)數(shù)量
  int argc;

  // 命令指針
  struct redisCommand *cmd;

} multiCmd;

事務(wù)隊(duì)列以先進(jìn)先出(FIFO)的方式保存入隊(duì)的命令,較先入隊(duì)的命令會(huì)被放到數(shù)組的前面,而較后入隊(duì)的命令則會(huì)被放到數(shù)組的后面。

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

redis> MULTI
OK

redis> SET "name" "Practical Common Lisp"
QUEUED

redis> GET "name"
QUEUED

redis> SET "author" "Peter Seibel"
QUEUED

redis> GET "author"
QUEUED

那么服務(wù)器將為客戶(hù)端創(chuàng)建圖19-2所示的事務(wù)狀態(tài):


image.png

■ 最先入隊(duì)的SET命令被放在了事務(wù)隊(duì)列的索引0位置上。
■ 第二入隊(duì)的GET命令被放在了事務(wù)隊(duì)列的索引1位置上。
■ 第三入隊(duì)的另一個(gè)SET命令被放在了事務(wù)隊(duì)列的索引2位置上。
■ 最后入隊(duì)的另一個(gè)GET命令被放在了事務(wù)隊(duì)列的索引3位置上。

19.1.4 執(zhí)行事務(wù)

當(dāng)一個(gè)處于事務(wù)狀態(tài)的客戶(hù)端向服務(wù)器發(fā)送EXEC命令時(shí),這個(gè)EXEC命令將立即被服務(wù)器執(zhí)行。服務(wù)器會(huì)遍歷這個(gè)客戶(hù)端的事務(wù)隊(duì)列,執(zhí)行隊(duì)列中保存的所有命令,最后將執(zhí)行命令所得的結(jié)果全部返回給客戶(hù)端。

舉個(gè)例子,對(duì)于圖19-2所示的事務(wù)隊(duì)列來(lái)說(shuō),服務(wù)器首先會(huì)執(zhí)行命令:

SET "name" "Practical Common Lisp"

接著執(zhí)行命令:

GET "name"

之后執(zhí)行命令:

SET "author" "Peter Seibel"

再之后執(zhí)行命令:

GET "author"

最后,服務(wù)器會(huì)將執(zhí)行這四個(gè)命令所得的回復(fù)返回給客戶(hù)端:

redis> EXEC
1) OK
2) "Practical Common Lisp"
3) OK
4) "Peter Seibel"

EXEC命令的實(shí)現(xiàn)原理可以用以下偽代碼來(lái)描述:

def EXEC():
  # 創(chuàng)建空白的回復(fù)隊(duì)列
  reply_queue = []
 
  # 遍歷事務(wù)隊(duì)列中的每個(gè)項(xiàng)
  # 讀取命令的參數(shù),參數(shù)的個(gè)數(shù),以及要執(zhí)行的命令
  for argv, argc, cmd in client.mstate.commands:

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

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

  # 移除REDIS_MULTI標(biāo)識(shí),讓客戶(hù)端回到非事務(wù)狀態(tài)
  client.flags & = ~REDIS_MULTI

  # 清空客戶(hù)端的事務(wù)狀態(tài),包括:
  #1 )清零入隊(duì)命令計(jì)數(shù)器
  #2 )釋放事務(wù)隊(duì)列

  client.mstate.count = 0
  release_transaction_queue(client.mstate.commands)

  # 將事務(wù)的執(zhí)行結(jié)果返回給客戶(hù)端
  send_reply_to_client(client, reply_queue)
19.2 WATCH 命令的實(shí)現(xiàn)

WATCH命令是一個(gè)樂(lè)觀(guān)鎖(optimistic locking),它可以在EXEC命令執(zhí)行之前,監(jiān)視任意數(shù)量的數(shù)據(jù)庫(kù)鍵,并在EXEC命令執(zhí)行時(shí),檢查被監(jiān)視的鍵是否至少有一個(gè)已經(jīng)被修改過(guò)了,如果是的話(huà),服務(wù)器將拒絕執(zhí)行事務(wù),并向客戶(hù)端返回代表事務(wù)執(zhí)行失敗的空回復(fù)。

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

redis> WATCH "name"
OK

redis> MULTI
OK

redis> SET "name" "peter"
QUEUED

redis> EXEC
(nil)

表19-1展示了上面的例子是如何失敗的

image.png

在時(shí)間T4,客戶(hù)端B修改了"name"鍵的值,當(dāng)客戶(hù)端A在T5執(zhí)行EXEC命令時(shí),服務(wù)器會(huì)發(fā)現(xiàn)WATCH監(jiān)視的鍵"name"已經(jīng)被修改,因此服務(wù)器拒絕執(zhí)行客戶(hù)端A的事務(wù),并向客戶(hù)端A返回空回復(fù)。

本節(jié)接下來(lái)的內(nèi)容將介紹WATCH命令的實(shí)現(xiàn)原理,說(shuō)明事務(wù)系統(tǒng)是如何監(jiān)視某個(gè)鍵,并在鍵被修改的情況下,確保事務(wù)的安全性的。

19.2.1 使用WATCH 命令監(jiān)視數(shù)據(jù)庫(kù)鍵

每個(gè)Redis數(shù)據(jù)庫(kù)都保存著一個(gè)watched_keys字典,這個(gè)字典的鍵是某個(gè)被WATCH命令監(jiān)視的數(shù)據(jù)庫(kù)鍵,而字典的值則是一個(gè)鏈表,鏈表中記錄了所有監(jiān)視相應(yīng)數(shù)據(jù)庫(kù)鍵的客戶(hù)端:

typedef struct redisDb {

  // ...

  // 正在被WATCH命令監(jiān)視的鍵
  dict *watched_keys;

  // ...

} redisDb;

通過(guò)watched_keys字典,服務(wù)器可以清楚地知道哪些數(shù)據(jù)庫(kù)鍵正在被監(jiān)視,以及哪些客戶(hù)端正在監(jiān)視這些數(shù)據(jù)庫(kù)鍵。

圖19-3是一個(gè)watched_keys字典的示例,從這個(gè)watched_keys字典中可以看出:
■ 客戶(hù)端c1和c2正在監(jiān)視鍵"name"。
■ 客戶(hù)端c3正在監(jiān)視鍵"age"。
■ 客戶(hù)端c2和c4正在監(jiān)視鍵"address"。

通過(guò)執(zhí)行WATCH命令,客戶(hù)端可以在watched_keys字典中與被監(jiān)視的鍵進(jìn)行關(guān)聯(lián)。舉個(gè)例子,如果當(dāng)前客戶(hù)端為c10086,那么客戶(hù)端執(zhí)行以下WATCH命令之后:

redis> WATCH "name" "age"
OK

圖19-3展示的watched_keys字典將被更新至圖19-4所示的狀態(tài),其中用虛線(xiàn)包圍的兩個(gè)c10086節(jié)點(diǎn)就是由剛剛執(zhí)行的WATCH命令添加到字典中的。

image.png
19.2.2 監(jiān)視機(jī)制的觸發(fā)

所有對(duì)數(shù)據(jù)庫(kù)進(jìn)行修改的命令,比如SET、LPUSH、SADD、ZREM、DEL、FLUSHDB等等,在執(zhí)行之后都會(huì)調(diào)用multi.c/touchWatchKey函數(shù)對(duì)watched_keys字典進(jìn)行檢查,查看是否有客戶(hù)端正在監(jiān)視剛剛被命令修改過(guò)的數(shù)據(jù)庫(kù)鍵,如果有的話(huà),那么touchWatchKey函數(shù)會(huì)將監(jiān)視被修改鍵的客戶(hù)端的REDIS_DIRTY_CAS標(biāo)識(shí)打開(kāi),表示該客戶(hù)端的事務(wù)安全性已經(jīng)被破壞。

touchWatchKey函數(shù)的定義可以用以下偽代碼來(lái)描述:

def touchWatchKey(db, key):

  # 如果鍵key存在于數(shù)據(jù)庫(kù)的watched_keys字典中
  # 那么說(shuō)明至少有一個(gè)客戶(hù)端在監(jiān)視這個(gè)key
  if key in db.watched_keys:

    # 遍歷所有監(jiān)視鍵key的客戶(hù)端
    for client in db.watched_keys[key]:

    # 打開(kāi)標(biāo)識(shí)
    client.flags |= REDIS_DIRTY_CAS

舉個(gè)例子,對(duì)于圖19-5所示的watched_keys字典來(lái)說(shuō):


image.png

■ 如果鍵"name"被修改,那么c1、c2、c10086三個(gè)客戶(hù)端的REDIS_DIRTY_CAS 標(biāo)識(shí)將被打開(kāi)。
■ 如果鍵"age"被修改,那么c3 和c10086 兩個(gè)客戶(hù)端的REDIS_DIRTY_CAS 標(biāo)識(shí)將被打開(kāi)。
■ 如果鍵"address"被修改,那么c2 和c4 兩個(gè)客戶(hù)端的REDIS_DIRTY_CAS 標(biāo)識(shí)將被打開(kāi)。

19.2.3 判斷事務(wù)是否安全

當(dāng)服務(wù)器接收到一個(gè)客戶(hù)端發(fā)來(lái)的EXEC命令時(shí),服務(wù)器會(huì)根據(jù)這個(gè)客戶(hù)端是否打開(kāi)了REDIS_DIRTY_CAS標(biāo)識(shí)來(lái)決定是否執(zhí)行事務(wù):

■ 如果客戶(hù)端的REDIS_DIRTY_CAS 標(biāo)識(shí)已經(jīng)被打開(kāi),那么說(shuō)明客戶(hù)端所監(jiān)視的鍵當(dāng)中,至少有一個(gè)鍵已經(jīng)被修改過(guò)了,在這種情況下,客戶(hù)端提交的事務(wù)已經(jīng)不再安全,所以服務(wù)器會(huì)拒絕執(zhí)行客戶(hù)端提交的事務(wù)。
■ 如果客戶(hù)端的REDIS_DIRTY_CAS 標(biāo)識(shí)沒(méi)有被打開(kāi),那么說(shuō)明客戶(hù)端監(jiān)視的所有鍵都沒(méi)有被修改過(guò)(或者客戶(hù)端沒(méi)有監(jiān)視任何鍵),事務(wù)仍然是安全的,服務(wù)器將執(zhí)行客戶(hù)端提交的這個(gè)事務(wù)。

這個(gè)判斷是否執(zhí)行事務(wù)的過(guò)程可以用流程圖19-6來(lái)描述。


image.png

舉個(gè)例子,對(duì)于圖19-5所示的watched_keys字典來(lái)說(shuō),如果某個(gè)客戶(hù)端對(duì)"name"鍵進(jìn)行了修改(比如執(zhí)行SET "name" "john"),那么c1、c2、c10086三個(gè)客戶(hù)端的REDIS_DIRTY_CAS標(biāo)識(shí)將被打開(kāi)。當(dāng)這三個(gè)客戶(hù)端向服務(wù)器發(fā)送EXEC命令的時(shí)候,服務(wù)器會(huì)拒絕執(zhí)行它們提交的事務(wù),以此來(lái)保證事務(wù)的安全性。

19.2.4 一個(gè)完整的WATCH 事務(wù)執(zhí)行過(guò)程

為了進(jìn)一步熟悉WATCH命令的運(yùn)作方式,讓我們來(lái)看一個(gè)帶有WATCH的事務(wù)從開(kāi)始到失敗的整個(gè)過(guò)程。

image.png

假設(shè)當(dāng)前客戶(hù)端為c10086,而數(shù)據(jù)庫(kù)watched_keys字典的當(dāng)前狀態(tài)如圖19-7所示,那么當(dāng)c10086執(zhí)行以下WATCH命令之后:

c10086> WATCH "name"
OK

watched_keys字典將更新至圖19-8所示的狀態(tài)。


image.png

接下來(lái),客戶(hù)端c10086繼續(xù)向服務(wù)器發(fā)送MULTI命令,并將一個(gè)SET命令放入事務(wù)隊(duì)列

c10086> MULTI
OK

c10086> SET "name" "peter"
QUEUED

就在這時(shí),另一個(gè)客戶(hù)端c999向服務(wù)器發(fā)送了一條SET命令,將"name"鍵的值設(shè)置成了 " john":

c999> SET "name" "john"
OK

c999執(zhí)行的這個(gè)SET命令會(huì)導(dǎo)致正在監(jiān)視"name"鍵的所有客戶(hù)端的REDIS_DIRTY_CAS標(biāo)識(shí)被打開(kāi),其中包括客戶(hù)端c10086。

之后,當(dāng)c10086向服務(wù)器發(fā)送EXEC命令時(shí)候,因?yàn)閏10086的REDIS_DIRTY_CAS標(biāo)志已經(jīng)被打開(kāi),所以服務(wù)器將拒絕執(zhí)行它提交的事務(wù):

c10086> EXEC
(nil)
19.3 事務(wù)的ACID 性質(zhì)

在傳統(tǒng)的關(guān)系式數(shù)據(jù)庫(kù)中,常常用ACID性質(zhì)來(lái)檢驗(yàn)事務(wù)功能的可靠性和安全性。

在Redis 中,事務(wù)總是具有原子性(Atomicity)、一致性(Consistency)和隔離性(Isolation),并且當(dāng)Redis 運(yùn)行在某種特定的持久化模式下時(shí),事務(wù)也具有耐久性(Durability)。

19.3.1 原子性

事務(wù)具有原子性指的是,數(shù)據(jù)庫(kù)將事務(wù)中的多個(gè)操作當(dāng)作一個(gè)整體來(lái)執(zhí)行,服務(wù)器要么就執(zhí)行事務(wù)中的所有操作,要么就一個(gè)操作也不執(zhí)行。
對(duì)于Redis的事務(wù)功能來(lái)說(shuō),事務(wù)隊(duì)列中的命令要么就全部都執(zhí)行,要么就一個(gè)都不執(zhí)行,因此,Redis的事務(wù)是具有原子性的。
舉個(gè)例子,以下展示的是一個(gè)成功執(zhí)行的事務(wù),事務(wù)中的所有命令都會(huì)被執(zhí)行:

redis> MULTI
OK

redis> SET msg "hello"
QUEUED

redis> GET msg
QUEUED

redis> EXEC
1) OK
2) "hello"

與此相反,以下展示了一個(gè)執(zhí)行失敗的事務(wù),這個(gè)事務(wù)因?yàn)槊钊腙?duì)出錯(cuò)而被服務(wù)器拒絕執(zhí)行,事務(wù)中的所有命令都不會(huì)被執(zhí)行:

redis> MULTI
OK

redis> SET msg "hello"
QUEUED

redis> GET
(error) ERR wrong number of arguments for 'get' command

redis> GET msg
QUEUED

redis> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

Redis的事務(wù)和傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)事務(wù)的最大區(qū)別在于,Redis不支持事務(wù)回滾機(jī)制(rollback),即使事務(wù)隊(duì)列中的某個(gè)命令在執(zhí)行期間出現(xiàn)了錯(cuò)誤,整個(gè)事務(wù)也會(huì)繼續(xù)執(zhí)行下去,直到將事務(wù)隊(duì)列中的所有命令都執(zhí)行完畢為止。

在下面的這個(gè)例子中,即使RPUSH命令在執(zhí)行期間出現(xiàn)了錯(cuò)誤,事務(wù)的后續(xù)命令也會(huì)繼續(xù)執(zhí)行下去,并且之前執(zhí)行的命令也不會(huì)有任何影響:

redis> SET msg "hello" # msg鍵是一個(gè)字符串
OK 

redis> MULTI
OK

redis> SADD fruit "apple" "banana" "cherry"
QUEUED

redis> RPUSH msg "good bye" "bye bye" # 錯(cuò)誤地對(duì)字符串鍵msg執(zhí)行列表鍵的命令
QUEUED 

redis> SADD alphabet "a" "b" "c"
QUEUED

redis> EXEC
1) (integer) 3
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) (integer) 3

Redis的作者在事務(wù)功能的文檔中解釋說(shuō),不支持事務(wù)回滾是因?yàn)檫@種復(fù)雜的功能和Redis追求簡(jiǎn)單高效的設(shè)計(jì)主旨不相符,并且他認(rèn)為,Redis事務(wù)的執(zhí)行時(shí)錯(cuò)誤通常都是編程錯(cuò)誤產(chǎn)生的,這種錯(cuò)誤通常只會(huì)出現(xiàn)在開(kāi)發(fā)環(huán)境中,而很少會(huì)在實(shí)際的生產(chǎn)環(huán)境中出現(xiàn),所以他認(rèn)為沒(méi)有必要為Redis開(kāi)發(fā)事務(wù)回滾功能。

19.3.2 一致性

事務(wù)具有一致性指的是,如果數(shù)據(jù)庫(kù)在執(zhí)行事務(wù)之前是一致的,那么在事務(wù)執(zhí)行之后,無(wú)論事務(wù)是否執(zhí)行成功,數(shù)據(jù)庫(kù)也應(yīng)該仍然是一致的。
“一致”指的是數(shù)據(jù)符合數(shù)據(jù)庫(kù)本身的定義和要求,沒(méi)有包含非法或者無(wú)效的錯(cuò)誤數(shù)據(jù)。
Redis通過(guò)謹(jǐn)慎的錯(cuò)誤檢測(cè)和簡(jiǎn)單的設(shè)計(jì)來(lái)保證事務(wù)的一致性,以下三個(gè)小節(jié)將分別介紹三個(gè)Redis事務(wù)可能出錯(cuò)的地方,并說(shuō)明Redis是如何妥善地處理這些錯(cuò)誤,從而確保事務(wù)的一致性的。

1.入隊(duì)錯(cuò)誤
如果一個(gè)事務(wù)在入隊(duì)命令的過(guò)程中,出現(xiàn)了命令不存在,或者命令的格式不正確等情況,那么Redis將拒絕執(zhí)行這個(gè)事務(wù)。
在以下展示的示例中,因?yàn)榭蛻?hù)端嘗試向事務(wù)入隊(duì)一個(gè)不存在的命令YAHOOOO,所以客戶(hù)端提交的事務(wù)會(huì)被服務(wù)器拒絕執(zhí)行:

redis> MULTI
OK

redis> SET msg "hello"
QUEUED

redis> YAHOOOO
(error) ERR unknown command 'YAHOOOO'

redis> GET msg
QUEUED

redis> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

因?yàn)榉?wù)器會(huì)拒絕執(zhí)行入隊(duì)過(guò)程中出現(xiàn)錯(cuò)誤的事務(wù),所以Redis事務(wù)的一致性不會(huì)被帶有入隊(duì)錯(cuò)誤的事務(wù)影響。

Redis 2.6.5 以前的入隊(duì)錯(cuò)誤處理
根據(jù)文檔記錄,在Redis 2.6.5 以前的版本,即使有命令在入隊(duì)過(guò)程中發(fā)生了錯(cuò)誤,事務(wù)一樣可以執(zhí)行,不過(guò)被執(zhí)行的命令只包括那些正確入隊(duì)的命令。以下這段代碼是在Redis 2.6.4 版本上測(cè)試的,可以看到,事務(wù)可以正常執(zhí)行,但只有成功入隊(duì)的SET命令和GET命令被執(zhí)行了,而錯(cuò)誤的YAHOOOO則被忽略了:

redis> MULTI
OK

redis> SET msg "hello"
QUEUED

redis> YAHOOOO
(error) ERR unknown command 'YAHOOOO'

redis> GET msg
QUEUED

redis> EXEC
1) OK
2) "hello"

因?yàn)殄e(cuò)誤的命令不會(huì)被入隊(duì),所以Redis 不會(huì)嘗試去執(zhí)行錯(cuò)誤的命令,因此,即使在2.6.5 以前的版本中,Redis 事務(wù)的一致性也不會(huì)被入隊(duì)錯(cuò)誤影響。

2.執(zhí)行錯(cuò)誤
除了入隊(duì)時(shí)可能發(fā)生錯(cuò)誤以外,事務(wù)還可能在執(zhí)行的過(guò)程中發(fā)生錯(cuò)誤。
關(guān)于這種錯(cuò)誤有兩個(gè)需要說(shuō)明的地方:
■ 執(zhí)行過(guò)程中發(fā)生的錯(cuò)誤都是一些不能在入隊(duì)時(shí)被服務(wù)器發(fā)現(xiàn)的錯(cuò)誤,這些錯(cuò)誤只會(huì)在命令實(shí)際執(zhí)行時(shí)被觸發(fā)。
■ 即使在事務(wù)的執(zhí)行過(guò)程中發(fā)生了錯(cuò)誤,服務(wù)器也不會(huì)中斷事務(wù)的執(zhí)行,它會(huì)繼續(xù)執(zhí)行事務(wù)中余下的其他命令,并且已執(zhí)行的命令(包括執(zhí)行命令所產(chǎn)生的結(jié)果)不會(huì)被出錯(cuò)的命令影響。

對(duì)數(shù)據(jù)庫(kù)鍵執(zhí)行了錯(cuò)誤類(lèi)型的操作是事務(wù)執(zhí)行期間最常見(jiàn)的錯(cuò)誤之一。
在下面展示的這個(gè)例子中,我們首先用SET命令將鍵"msg"設(shè)置成了一個(gè)字符串鍵,然后在事務(wù)里面嘗試對(duì)"msg"鍵執(zhí)行只能用于列表鍵的RPUSH命令,這將引發(fā)一個(gè)錯(cuò)誤,并且這種錯(cuò)誤只能在事務(wù)執(zhí)行(也即是命令執(zhí)行)期間被發(fā)現(xiàn):

redis> SET msg "hello"
OK

redis> MULTI
OK

redis> SADD fruit "apple" "banana" "cherry"
QUEUED

redis> RPUSH msg "good bye" "bye bye"
QUEUED

redis> SADD alphabet "a" "b" "c"
QUEUED

redis> EXEC
1) (integer) 3
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) (integer) 3

因?yàn)樵谑聞?wù)執(zhí)行的過(guò)程中,出錯(cuò)的命令會(huì)被服務(wù)器識(shí)別出來(lái),并進(jìn)行相應(yīng)的錯(cuò)誤處理,所以這些出錯(cuò)命令不會(huì)對(duì)數(shù)據(jù)庫(kù)做任何修改,也不會(huì)對(duì)事務(wù)的一致性產(chǎn)生任何影響

3.服務(wù)器停機(jī)
如果Redis服務(wù)器在執(zhí)行事務(wù)的過(guò)程中停機(jī),那么根據(jù)服務(wù)器所使用的持久化模式,可能有以下情況出現(xiàn):
■ 如果服務(wù)器運(yùn)行在無(wú)持久化的內(nèi)存模式下,那么重啟之后的數(shù)據(jù)庫(kù)將是空白的,因此數(shù)據(jù)總是一致的。
■ 如果服務(wù)器運(yùn)行在RDB 模式下,那么在事務(wù)中途停機(jī)不會(huì)導(dǎo)致不一致性,因?yàn)榉?wù)器可以根據(jù)現(xiàn)有的RDB 文件來(lái)恢復(fù)數(shù)據(jù),從而將數(shù)據(jù)庫(kù)還原到一個(gè)一致的狀態(tài)。如果找不到可供使用的RDB 文件,那么重啟之后的數(shù)據(jù)庫(kù)將是空白的,而空白數(shù)據(jù)庫(kù)總是一致的。
■ 如果服務(wù)器運(yùn)行在A(yíng)OF模式下,那么在事務(wù)中途停機(jī)不會(huì)導(dǎo)致不一致性,因?yàn)榉?wù)器可以根據(jù)現(xiàn)有的AOF文件來(lái)恢復(fù)數(shù)據(jù),從而將數(shù)據(jù)庫(kù)還原到一個(gè)一致的狀態(tài)。如果找不到可供使用的AOF文件,那么重啟之后的數(shù)據(jù)庫(kù)將是空白的,而空白數(shù)據(jù)庫(kù)總是一致的。

綜上所述,無(wú)論Redis服務(wù)器運(yùn)行在哪種持久化模式下,事務(wù)執(zhí)行中途發(fā)生的停機(jī)都不會(huì)影響數(shù)據(jù)庫(kù)的一致性。

19.3.3 隔離性
事務(wù)的隔離性指的是,即使數(shù)據(jù)庫(kù)中有多個(gè)事務(wù)并發(fā)地執(zhí)行,各個(gè)事務(wù)之間也不會(huì)互相影響,并且在并發(fā)狀態(tài)下執(zhí)行的事務(wù)和串行執(zhí)行的事務(wù)產(chǎn)生的結(jié)果完全相同。
因?yàn)镽edis使用單線(xiàn)程的方式來(lái)執(zhí)行事務(wù)(以及事務(wù)隊(duì)列中的命令),并且服務(wù)器保證,在執(zhí)行事務(wù)期間不會(huì)對(duì)事務(wù)進(jìn)行中斷,因此,Redis的事務(wù)總是以串行的方式運(yùn)行的,并且事務(wù)也總是具有隔離性的。

19.3.4 耐久性
事務(wù)的耐久性指的是,當(dāng)一個(gè)事務(wù)執(zhí)行完畢時(shí),執(zhí)行這個(gè)事務(wù)所得的結(jié)果已經(jīng)被保存到永久性存儲(chǔ)介質(zhì)(比如硬盤(pán))里面了,即使服務(wù)器在事務(wù)執(zhí)行完畢之后停機(jī),執(zhí)行事務(wù)所得的結(jié)果也不會(huì)丟失。
因?yàn)镽edis的事務(wù)不過(guò)是簡(jiǎn)單地用隊(duì)列包裹起了一組Redis命令,Redis并沒(méi)有為事務(wù)提供任何額外的持久化功能,所以Redis事務(wù)的耐久性由Redis所使用的持久化模式?jīng)Q定:
■ 當(dāng)服務(wù)器在無(wú)持久化的內(nèi)存模式下運(yùn)作時(shí),事務(wù)不具有耐久性:一旦服務(wù)器停機(jī),包括事務(wù)數(shù)據(jù)在內(nèi)的所有服務(wù)器數(shù)據(jù)都將丟失。
■ 當(dāng)服務(wù)器在RDB 持久化模式下運(yùn)作時(shí),服務(wù)器只會(huì)在特定的保存條件被滿(mǎn)足時(shí),才會(huì)執(zhí)行BGSAVE 命令,對(duì)數(shù)據(jù)庫(kù)進(jìn)行保存操作,并且異步執(zhí)行的BGSAVE 不能保證事務(wù)數(shù)據(jù)被第一時(shí)間保存到硬盤(pán)里面,因此RDB 持久化模式下的事務(wù)也不具有耐久性。
■ 當(dāng)服務(wù)器運(yùn)行在A(yíng)OF持久化模式下,并且appendfsync 選項(xiàng)的值為always 時(shí),程序總會(huì)在執(zhí)行命令之后調(diào)用同步(sync)函數(shù),將命令數(shù)據(jù)真正地保存到硬盤(pán)里面,因此這種配置下的事務(wù)是具有耐久性的。
■ 當(dāng)服務(wù)器運(yùn)行在A(yíng)OF持久化模式下,并且appendfsync選項(xiàng)的值為everysec 時(shí),程序會(huì)每秒同步一次命令數(shù)據(jù)到硬盤(pán)。因?yàn)橥C(jī)可能會(huì)恰好發(fā)生在等待同步的那一秒鐘之內(nèi),這可能會(huì)造成事務(wù)數(shù)據(jù)丟失,所以這種配置下的事務(wù)不具有耐久性。
■ 當(dāng)服務(wù)器運(yùn)行在A(yíng)OF持久化模式下,并且appendfsync 選項(xiàng)的值為no時(shí),程序會(huì)交由操作系統(tǒng)來(lái)決定何時(shí)將命令數(shù)據(jù)同步到硬盤(pán)。因?yàn)槭聞?wù)數(shù)據(jù)可能在等待同步的過(guò)程中丟失,所以這種配置下的事務(wù)不具有耐久性。

小結(jié)

■ 事務(wù)提供了一種將多個(gè)命令打包,然后一次性、有序地執(zhí)行的機(jī)制。
■ 多個(gè)命令會(huì)被入隊(duì)到事務(wù)隊(duì)列中,然后按先進(jìn)先出(FIFO)的順序執(zhí)行。
■ 事務(wù)在執(zhí)行過(guò)程中不會(huì)被中斷,當(dāng)事務(wù)隊(duì)列中的所有命令都被執(zhí)行完畢之后,事務(wù)才會(huì)結(jié)束。
■ 帶有WATCH命令的事務(wù)會(huì)將客戶(hù)端和被監(jiān)視的鍵在數(shù)據(jù)庫(kù)的watched_keys字典中進(jìn)行關(guān)聯(lián),當(dāng)鍵被修改時(shí),程序會(huì)將所有監(jiān)視被修改鍵的客戶(hù)端的REDIS_DIRTY_CAS標(biāo)志打開(kāi)。
■ 只有在客戶(hù)端的REDIS_DIRTY_CAS標(biāo)志未被打開(kāi)時(shí),服務(wù)器才會(huì)執(zhí)行客戶(hù)端提交的事務(wù),否則的話(huà),服務(wù)器將拒絕執(zhí)行客戶(hù)端提交的事務(wù)。
■ Redis 的事務(wù)總是具有ACID中的原子性、一致性和隔離性,當(dāng)服務(wù)器運(yùn)行在A(yíng)OF持久化模式下,并且appendfsync選項(xiàng)的值為always時(shí),事務(wù)也具有耐久性。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容