天天看點

百度二面:談談你對Redis事務的了解

本篇内容為大家介紹下Redis的事務,我們知道Redis的事務是通過MULTI、EXEC、WATCH等指令來實作事務(transaction)功能。事務提供了一種将多個指令請求打包,然後一次性、按順序地執行多個指令的機制,并且在事務執行期間,伺服器不會中斷事務而改去執行其他用戶端的指令請求,它會将事務中的所有指令都執行完畢,然後才去處理其他用戶端的指令請求。

一、事務執行過程簡單說明

我們這裡先使用簡單的指令來示範一下事務執行的過程。該事務首先以一個MULTI指令為開始,接着将多個指令放入事務當中,最後由EXEC指令将這個事務送出(commit)給伺服器執行。

[root@localhost bin]# ./redis-cli

127.0.0.1:6379> multi

OK

127.0.0.1:6379> set "name" "Shiwk Common Lisp"

QUEUED

127.0.0.1:6379> get "name"

QUEUED

127.0.0.1:6379> set "author" "shiwk"

QUEUED

127.0.0.1:6379> get "author"

QUEUED

127.0.0.1:6379> exec

1) OK

2) "Shiwk Common Lisp"

3) OK

4) "shiwk"

127.0.0.1:6379>

下面的内容中,小編首先會介紹Redis如何使用MULTI和EXEC指令來實作事務功能,說明事務中的多個指令是如何被儲存到事務裡面的,而這些指令又是如何被執行的。

在介紹了事務的實作原理之後,我們将對WATCH指令的作用進行介紹,并說明WATCH指令的實作原理。

因為事務的安全性和可靠性也是大家關注的焦點,是以最後小編将以常見的ACID性質對Redis事務的原子性、一緻性、隔離性和耐久性進行說明。

1.1事務的實作

一個事務從開始到結束通常會進過以下三個階段:

(1)事務開始 (2)指令入隊 (3)事務執行

接下來我們就對這個過程進行介紹,說明一個事務從開始到結束的整個過程。

(1)事務開始

MULTI指令的執行标志着事務的開始。

127.0.0.1:6379> multi

OK

MULTI指令可以将執行該指令的用戶端從非事務狀态切換至事務狀态,這一切換是通過在用戶端狀态的flags屬性中打開REDIS_MULTI辨別來完成的,MULTI指令的實作可以用下面的僞代碼來表示:

def MULTI();

#打開事務辨別

client.flags |= REDIS_MULTI

#傳回ok回複

replyOK()

(2)指令入隊

當一個用戶端處于非事務狀态時,這個用戶端發送的指令會立即被伺服器執行:

[root@localhost bin]# ./redis-cli

127.0.0.1:6379> set "name" "Shiwk Common Lisp"

OK

127.0.0.1:6379> get "name"

Shiwk Common Lisp

127.0.0.1:6379> set "author" "shiwk"

OK

127.0.0.1:6379> get "author"

shiwk

而當一個用戶端處于事務狀态時,伺服器會根據用戶端發來的不同指令執行不同的操作:

如果用戶端發送的指令為EXEC、DISCARD、WATCH、MULTI四個指令的其中一個,那麼伺服器立即執行這個指令。

與此相反,如果用戶端發送的指令是EXEC、DISCARD、WATCH、MULTI四個指令以外的其他指令,那麼伺服器并不立即執行這個指令,而是将這個指令放入一個事務隊列裡面,然後向用戶端傳回QUEUED回複。

伺服器判斷指令是該入隊還是該立即執行的過程流程圖描述:

百度二面:談談你對Redis事務的了解

上述圖中事務隊列解釋:

每個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)的方式儲存入隊的指令,較先入隊的指令會被放到數組的前面,而較後入隊的指令則會被放到數組的後面。

以下列操作為例,我們分析一下伺服器執行不同指令時,事務的狀态。

127.0.0.1:6379> multi

OK

127.0.0.1:6379> set "name" "Shiwk Common Lisp"

QUEUED

127.0.0.1:6379> get "name"

QUEUED

127.0.0.1:6379> set "author" "shiwk"

QUEUED

127.0.0.1:6379> get "author"

QUEUED

分析:

最先入隊的SET指令被放在了事務隊列的索引0位置上。

第二入隊的GET指令被放在了事務隊列的索引1位置上。

第三入隊的另一個SET指令被放在了事務隊列的索引2位置上。最後入隊的另一個GET指令被放在了事務隊列的索引3位置上。

百度二面:談談你對Redis事務的了解

(3)事務執行

當一個處于事務狀态的用戶端向伺服器發送EXEC指令時,這個EXEC指令将立即被伺服器執行。伺服器會周遊這個用戶端的事務隊列,執行隊列中儲存的所有指令,最後将執行指令所得的結果全部傳回給用戶端。

舉例說明:

set "name""Shiwk Common Lisp"

接着執行指令:

get "name"

之後執行指令:

set "author" "shiwk"

再之後執行指令:

get "author"

最後,伺服器會将執行這四個指令所得的回複傳回給用戶端:

127.0.0.1> exec

1)OK

2)"Shiwk Common Lisp"

3)OK

4)"shiwk"

exec指令的實作原理可以用以下僞代碼來描述:

def EXEC();

//建立空白的回複隊列

reply_queue =[ ]

//周遊事務隊列中的每個項

//讀取指令的參數,參數的個數,以及要執行的指令

for argv,argc, cmd in client.mstate.commands:

#執行指令,并取得指令的傳回值

reply = execute_command (cmd, argv, argc)

#将傳回值追加到回複隊列末尾

reply_queue.append (reply)

//移除REDIS_MULT工辨別,讓用戶端回到非事務狀态client.flags &=~REDIS_MULTI

//清空用戶端的事務狀态,包括:#1)清零入隊指令計數器

//2)釋放事務隊列

client.mstate.count = o

release_transaction_queue (client.mstate.commands)

//将事務的執行結果傳回給用戶端

send_reply_to_client (client, reply_queue)

1.2watch指令的實作

WATCH指令是一個樂觀鎖(optimistic locking),它可以在EXEC指令執行之前,監視任意數量的資料庫鍵,并在EXEC指令執行時,檢查被監視的鍵是否至少有一個已經被修改過了,如果是的話,伺服器将拒絕執行事務,并向用戶端傳回代表事務執行失敗的空回複。

我們通過舉例,來分析事務執行失敗的原因。

c10086:6379> watch "name"

OK

c10086:6379> multi

OK

c10086:6379> set "name" "guigu"

QUEUED

c10086:6379> exec

(nil)

錯誤原因分析:

時間 用戶端A 用戶端B
T1 watch "name"
T2 multi
T3 set "name" "guigu"
T4 set "name" "ss"
T5 exec

在時間T4,用戶端B修改了"name"鍵的值,當用戶端A在T5執行EXEC指令時,伺服器會發現WATCH監視的鍵"name"已經被修改,是以伺服器拒絕執行用戶端A的事務,并向用戶端A傳回空回複。

1.3watch指令監控資料庫鍵

每個Redis資料庫都儲存着一個watched keys字典,這個字典的鍵是某個被WATCH指令監視的資料庫鍵,而字典的值則是一個連結清單,連結清單中記錄了所有監視相應資料庫鍵的用戶端:

typedef struct redisdb{

...

// 正在被Watch指令監控的鍵

dict *watched_keys;

// ...

} redisdb;

通過watched_keys字典,伺服器可以清楚地知道哪些資料庫鍵正在被監視,以及哪些用戶端正在監視這些資料庫鍵。

watched_keys字典示例:

從這個watched_keys字典中可以看出

用戶端c1和c2正在監視鍵"name"。

用戶端c3正在監視鍵"age"。

用戶端c2和c4正在監視鍵"address"。

通過執行WATCH指令,用戶端可以在watched_keys字典中與被監視的鍵進行關聯。

舉個例子,如果目前用戶端為c10086,那麼用戶端執行以下WATCH指令之後:

redis> WATCH "name" "age"

OK

watched_keys字典中資料将執行更新操作,其中用虛線包圍的兩個c10086節點就是由剛剛執行的WATCH指令添加到字典中的。

圖解分析:

百度二面:談談你對Redis事務的了解

1.4watch監控機制的觸發

所有對資料庫進行修改的指令,比如SET、LPUSH、SADD、ZREM、DEL、FLUSHDB等等,在執行之後都會調用multi.c/touchwatchKey函數對watched_keys字典進行檢查,檢視是否有用戶端正在監視剛剛被指令修改過的資料庫鍵,如果有的話,那麼touchwatchKey()函數會将監視被修改鍵的用戶端的REDIS_DIRTY_CAS辨別打開,表示該用戶端的事務安全性已經被破壞。

touchwatchKey函數的定義可以用以下僞代碼來描述:

def touchwatchKey(db,key) :

#如果鍵key存在于資料庫的watched_keys字典中

#那麼說明至少有一個用戶端在監視這個key

if key in db . watched_keys :

#周遊所有監視鍵key的用戶端

for client in db . watched_keys [ key] :

#打開辨別

client.flags l=REDIS_DIRTY_CAS

舉例說明:

百度二面:談談你對Redis事務的了解

如果鍵"name”被修改,那麼c1、c2、c10086三個用戶端的REDIS_DIRTY_CAS辨別将被打開。

如果鍵"age"被修改,那麼c3和c10086兩個用戶端的REDIS_DIRTY_CAS辨別将被打開。

如果鍵"address"被修改,那麼c2和c4兩個用戶端的REDIS_DIRTY_CAS辨別将被打開。

1.5判斷事務是否安全

當伺服器接收到一個用戶端發來的EXEC指令時,伺服器會根據這個用戶端是否打開了REDIS_DIRTY_CAS辨別來決定是否執行事務:

百度二面:談談你對Redis事務的了解

如果用戶端的REDIS_DIRTY_CAS辨別已經被打開,那麼說明用戶端所監視的鍵當中,至少有一個鍵已經被修改過了,在這種情況下,用戶端送出的事務已經不再安全,是以伺服器會拒絕執行用戶端送出的事務。

如果用戶端的REDIS_DIRTY_CAS辨別沒有被打開,那麼說明用戶端監視的所有鍵都沒有被修改過(或者用戶端沒有監視任何鍵),事務仍然是安全的,伺服器将執行用戶端送出的這個事務。

1.6一個完整的Watch事務執行過程

假設目前用戶端為c10086,而資料庫watched_keys字典的目前狀态如下圖所示:

百度二面:談談你對Redis事務的了解

當c10086用戶端執行一下watch指令後:

c10086>watch "name"

OK

watched_keys字典狀态更新如下:

百度二面:談談你對Redis事務的了解

接下來,用戶端c10086繼續向伺服器發送MULTI指令,并将一個SET指令放入事務隊列:

c10086>multi

OK

c10086>set "name" "guigu"

QUEUED

就在這時,另一個用戶端c999向伺服器發送了一條SET指令,将"name"鍵的值設定成了"john" :

c999>set "name" "john"

OK

c999執行的這個SET指令會導緻正在監視"name"鍵的所有用戶端的REDIS.DIRTY_CAS辨別被打開,其中包括用戶端c10086。

之後,當c10086向伺服器發送EXEC指令時候,因為c10086的REDIS_DIRTY_CAS标志已經被打開,是以伺服器将拒絕執行它送出的事務:

c10086>exec

(nil)

二、事務的ACID性質

在傳統的關系式資料庫中,常常用ACID性質來檢驗事務功能的可靠性和安全性。在Redis中,事務總是具有原子性(Atomicity )、一緻性(Consistency)和隔離性( Isolation ),并且當Redis運作在某種特定的持久化模式下時,事務也具有耐久性(Durability )。

2.1原子性

事務具有原子性指的是,資料庫将事務中的多個操作當作一個整體來執行,伺服器要麼就執行事務中的所有操作,要麼就一個操作也不執行。

對于Redis 的事務功能來說,事務隊列中的指令要麼就全部都執行,要麼就一個都不執行,是以,Redis 的事務是具有原子性的。

舉例說明,一個成功執行的事務,事務中的所有指令都會被執行:

127.0.0.1:6379> multi

OK

127.0.0.1:6379> set msg "hello"

QUEUED

127.0.0.1:6379> get msg

QUEUED

127.0.0.1:6379> exec

1) OK

2) "hello"

與此相反,在展示一個執行失敗的事務,這個事務因為指令入隊出錯而被伺服器拒絕執行,事務中的所有指令都不會被執行:

127.0.0.1:6379> multi

OK

127.0.0.1:6379> set msg "hello"

QUEUED

127.0.0.1:6379> get

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

127.0.0.1:6379> get msg

QUEUED

127.0.0.1:6379>exec

(error) EXECABORT Transaction discarded because of previous errors.

Redis 的事務和傳統的關系型資料庫事務的最大差別在于,Redis不支援事務復原機制(rollback),即使事務隊列中的某個指令在執行期間出現了錯誤,整個事務也會繼續執行下去.直到将事務隊列中的所有指令都執行完畢為止。

2.2一緻性

事務具有一緻性指的是,如果資料庫在執行事務之前是一緻的,那麼在事務執行之後,無論事務是否執行成功,資料庫也應該仍然是一緻的。

“一緻”指的是資料符合資料庫本身的定義和要求,沒有包含非法或者無效的錯誤資料。Redis通過謹慎的錯誤檢測和簡單的設計來保證事務的一緻性。

以下三個小節将分别介紹三個Redis事務可能出錯的地方,并說明Redis是如何妥善地處理這些錯誤,進而確定事務的一緻性的。

(1)入隊錯誤

如果一個事務在入隊指令的過程中,出現了指令不存在,或者指令的格式不正确等情況,那麼Redis将拒絕執行這個事務。

在下面示例中,因為用戶端嘗試向事務入隊一個不存在的指令YAH0000,是以用戶端送出的事務會被伺服器拒絕執行:

127.0.0.1:6379> multi

OK

127.0.0.1:6379>SET msg "hello"

QUEUED

127.0.0.1:6379> YAHOOOO

(error) ERR unknown command 'YAHOOOO'

127.0.0.1:6379>get msg

QUEUED

127.0.0.1:6379>exec

(error)EXECABORT Transaction discarded because of previous errors.

因為伺服器會拒絕執行人隊過程中出現錯誤的事務,是以Redis事務的一緻性不會被帶有入隊錯誤的事務影響。

(2)執行錯誤

除了入隊時可能發生錯誤以外,事務還可能在執行的過程中發生錯誤。關于這種錯誤有兩個需要說明的地方:

執行過程中發生的錯誤都是一些不能在入隊時被伺服器發現的錯誤,這些錯誤隻會在指令實際執行時被觸發。即使在事務的執行過程中發生了錯誤,伺服器也不會中斷事務的執行,它會繼續執行事務中餘下的其他指令,并且已執行的指令(包括執行指令所産生的結果)不會被出錯的指令影響。

對資料庫鍵執行了錯誤類型的操作是事務執行期間最常見的錯誤之一。

在下面例子中,我們首先用set指令将鍵"msg"設定成了一個字元串鍵,然後在事務裡面嘗試對"msg"鍵執行隻能用于清單鍵的RPUSH指令,這将引發一個錯誤,并且這種錯誤隻能在事務執行(也即是指令執行)期間被發現:

127.0.0.1:6379> set msg "hello"

OK

127.0.0.1:6379> multi

OK

127.0.0.1:6379>sadd fruit "apple" "banana" "cherry"

QUEUED

127.0.0.1:6379>rpush msg "good bye" "bye bye"

QUEUED

127.0.0.1:6379>sadd alphabet "a" "b""c"

QUEUED

127.0.0.1:6379>EXEC

1)(integer) 3

2) (error) WRONGTYPE 0peration against a key holding the wrong kind of value

3)(integer) 3

因為在事務執行的過程中,出錯的指令會被伺服器識别出來,并進行相應的錯誤處理,是以這些出錯指令不會對資料庫做任何修改,也不會對事務的一緻性産生任何影響。

(3)伺服器停機

如果Redis伺服器在執行事務的過程中停機,那麼根據伺服器所使用的持久化模式,可能有以下情況出現:

如果伺服器運作在無持久化的記憶體模式下,那麼重新開機之後的資料庫将是空白的,是以資料總是一緻的。

如果伺服器運作在RDB模式下,那麼在事務中途停機不會導緻不一緻性,因為伺服器可以根據現有的RDB檔案來恢複資料,進而将資料庫還原到一個一緻的狀态。如果找不到可供使用的RDB檔案,那麼重新開機之後的資料庫将是空白的,而空白資料庫總是一緻的。

如果伺服器運作在AOF模式下,那麼在事務中途停機不會導緻不一緻性,因為伺服器可以根據現有的AOF檔案來恢複資料,進而将資料庫還原到一個一緻的狀态。如果找不到可供使用的AOF檔案,那麼重新開機之後的資料庫将是空白的,而空白資料庫總是一緻的。

綜上所述,無論Redis伺服器運作在哪種持久化模式下,事務執行中途發生的停機都不會影響資料庫的一緻性。

2.3隔離性

事務的隔離性指的是,即使資料庫中有多個事務并發地執行,各個事務之間也不會互相影響,并且在并發狀态下執行的事務和串行執行的事務産生的結果完全相同。

因為Redis使用單線程的方式來執行事務(以及事務隊列中的指令),并且伺服器保證,在執行事務期間不會對事務進行中斷,是以,Redis的事務總是以串行的方式運作的,并且事務也總是具有隔離性的。

2.4耐久性

事務的耐久性指的是,當一個事務執行完畢時,執行這個事務所得的結果已經被儲存到永久性存儲媒體(比如硬碟)裡面了,即使伺服器在事務執行完畢之後停機,執行事務所得的結果也不會丢失。

因為Redis的事務不過是簡單地用隊列包裹起了一組Redis指令,Redis并沒有為事務提供任何額外的持久化功能,是以Redis事務的耐久性由Redis所使用的持久化模式決定:

當伺服器在無持久化的記憶體模式下運作時,事務不具有耐久性:一旦伺服器停機,包括事務資料在内的所有伺服器資料都将丢失。

當伺服器在RDB持久化模式下運作時,伺服器隻會在特定的儲存條件被滿足時,才會執行BGSAVE指令,對資料庫進行儲存操作,并且異步執行的BGSAVE不能保證事務資料被第一時間儲存到硬碟裡面,是以RDB持久化模式下的事務也不具有耐久性。

當伺服器運作在AOF持久化模式下,并且appendfsync選項的值為always時,程式總會在執行指令之後調用同步(sync)函數,将指令資料真正地儲存到硬碟裡面,是以這種配置下的事務是具有耐久性的。

當伺服器運作在AOF持久化模式下,并且appendfsync選項的值為everysec時,程式會每秒同步一次指令資料到硬碟。因為停機可能會恰好發生在等待同步的那一秒鐘之内,這可能會造成事務資料丢失,是以這種配置下的事務不具有耐久性。

不論Redis在什麼模式下運作,在一個事務的最後加上SAVE指令總可以保證事務的耐久性:

127.0.0.1:6379>MULTI

OK

127.0.0.1:6379>SET msg "hello"

QUEUED

127.0.0.1:6379> SAVE

QUEUED

127.0.0.1:6379> EXEC

1)OK

2)OK

不過因為這種做法的效率太低,是以并不具有實用性。

總結

事務提供了一種将多個指令打包,然後一次性、有序地執行的機制。

多個指令會被入隊到事務隊列中,然後按先進先出(FIFO)的順序執行。

事務在執行過程中不會被中斷,當事務隊列中的所有指令都被執行完畢之後,事務才會結束。

帶有WATCH指令的事務會将用戶端和被監視的鍵在資料庫的watched_keys字典中進行關聯,當鍵被修改時,程式會将所有監視被修改鍵的用戶端的REDIS_DIRTY_CAS标志打開。

隻有在用戶端的REDIS_DIRTY_CAS标志未被打開時,伺服器才會執行用戶端送出的事務,否則的話,伺服器将拒絕執行用戶端送出的事務。

Redis 的事務總是具有ACID中的原子性、一緻性和隔離性,當伺服器運作在AOF持久化模式下,并且 appendfsync選項的值為always時,事務也具有耐久性。

繼續閱讀