天天看點

Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

文章目錄

  • 一、釋出與訂閱
    • 1、頻道的訂閱與退訂
      • 1.1 訂閱頻道
      • 1.2 退訂頻道
    • 2、模式的訂閱與退訂
      • 2.1 訂閱模式
      • 2.2 退訂模式
    • 3、發送消息
      • 3.1 将消息發送給頻道訂閱者
      • 3.2 将消息發送給模式訂閱者
    • 4、檢視訂閱資訊
      • 4.1 PUBSUB CHANNELS
      • 4.2 PUBSUB NUMSUB
      • 4.3 PUBSUB NUMPAT
    • 5、重點回顧
  • 二、事務
    • 1、事務的實作
      • 1.1 事務開始
      • 1.2 指令入隊
      • 1.3 事務隊列
      • 1.4 執行事務
    • 2、WATCH指令的實作
      • 2.1 使用WATCH指令監視資料庫鍵
      • 2.2 監視機制的觸發
      • 2.3 判斷事務是否安全
    • 3、事務的ACID性質
      • 3.1 原子性
      • 3.2 一緻性
      • 3.3 隔離性
      • 3.4 耐久性
    • 4、重點回顧

一、釋出與訂閱

Redis的釋出與訂閱功能由PUBLISH、SUBSCRIBE、PSUBSCRIBE等指令組成。

通過執行SUBSCRIBE指令,用戶端可以訂閱一個或多個頻道,進而成為這些頻道的訂閱者(subscriber):每當有其他用戶端向被訂閱的頻道發送消息(message)時,頻道的所有訂閱者都會收到這條消息。

假設A、B、C三個用戶端都執行了指令:

SUBSCRIBE "news.it"
           

那麼這三個用戶端就是"news.it"頻道的訂閱者

Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

如果這時某個用戶端執行指令向"news.it"頻道發送消息"hello",那麼"news.it"的三個訂閱者都将收到這條消息

PUBLISH "news.it" "hello"
           
Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

除了訂閱頻道之外,用戶端還可以通過執行PSUBSCRIBE指令訂閱一個或多個模式,進而成為這些模式的訂閱者:每當有其他用戶端向某個頻道發送消息時,消息不僅會被發送給這個頻道的所有訂閱者,它還會被發送給所有與這個頻道相比對的模式的訂閱者。

Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

如果這時某個用戶端執行指令向"news.it"頻道發送消息"hello",那麼不僅正在訂閱"news.it"頻道的用戶端A會收到消息,用戶端C和用戶端D也同樣會收到消息,因為這兩個用戶端正在訂閱比對"news.it"頻道的"news.[ie]t"模式。

PUBLISH "news.it" "hello"
           
Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

1、頻道的訂閱與退訂

當一個用戶端執行SUBSCRIBE指令訂閱某個或某些頻道的時候,這個用戶端與被訂閱頻道之間就建立起了一種訂閱關系。

Redis将所有頻道的訂閱關系都儲存在伺服器狀态的pubsub_channels字典裡面,這個字典的鍵是某個被訂閱的頻道,而鍵的值則是一個連結清單,連結清單裡面記錄了所有訂閱這個頻道的用戶端:

struct redisServer {
  // ...
  // 儲存所有頻道的訂閱關系
  dict *pubsub_channels;
  // ...
};
           
Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

1.1 訂閱頻道

每當用戶端執行SUBSCRIBE指令訂閱某個或某些頻道的時候,伺服器都會将用戶端與被訂閱的頻道在pubsub_channels字典中進行關聯。

根據頻道是否已經有其他訂閱者,關聯操作分為兩種情況執行:

  • 如果頻道已經有其他訂閱者,那麼它在pubsub_channels字典中必然有相應的訂閱者連結清單,程式唯一要做的就是将用戶端添加到訂閱者連結清單的末尾。
  • 如果頻道還未有任何訂閱者,那麼它必然不存在于pubsub_channels字典,程式首先要在pubsub_channels字典中為頻道建立一個鍵,并将這個鍵的值設定為空連結清單,然後再将用戶端添加到連結清單,成為連結清單的第一個元素。

當用戶端client-10086執行指令:

SUBSCRIBE "news.sport" "news.movie"
           
Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

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

def subscribe(*all_input_channels):
  # 周遊輸入的所有頻道
  for channel in all_input_channels:
    # 如果channel不存在于pubsub_channels字典(沒有任何訂閱者)
    # 那麼在字典中添加channel鍵,并設定它的值為空連結清單
    if channel not in server.pubsub_channels:
    server.pubsub_channels[channel] = []
    # 将訂閱者添加到頻道所對應的連結清單的末尾
    server.pubsub_channels[channel].append(client)
           

1.2 退訂頻道

UNSUBSCRIBE指令的行為和SUBSCRIBE指令的行為正好相反,當一個用戶端退訂某個或某些頻道的時候,伺服器将從pubsub_channels中解除用戶端與被退訂頻道之間的關聯:

  • 程式會根據被退訂頻道的名字,在pubsub_channels字典中找到頻道對應的訂閱者連結清單,然後從訂閱者連結清單中删除退訂用戶端的資訊。
  • 如果删除退訂用戶端之後,頻道的訂閱者連結清單變成了空連結清單,那麼說明這個頻道已經沒有任何訂閱者了,程式将從pubsub_channels字典中删除頻道對應的鍵。

當用戶端client-10086執行指令:

UNSUBSCRIBE "news.sport" "news.movie"
           
Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

因為删除client-10086之後,頻道"news.movie"已經沒有任何訂閱者,是以鍵"news.movie"也從字典中被删除了。

2、模式的訂閱與退訂

伺服器将所有頻道的訂閱關系都儲存在伺服器狀态的pubsub_channels屬性裡面,與此類似,伺服器也将所有模式的訂閱關系都儲存在伺服器狀态的pubsub_patterns屬性裡面:

struct redisServer {
  // ...
  // 儲存所有模式訂閱關系
  list *pubsub_patterns;
  // ...
};
           

pubsub_patterns屬性是一個連結清單,連結清單中的每個節點都包含着一個pubsub Pattern結構,這個結構的pattern屬性記錄了被訂閱的模式,而client屬性則記錄了訂閱模式的用戶端:

typedef struct pubsubPattern {
  // 訂閱模式的用戶端
  redisClient *client;
  // 被訂閱的模式
  robj *pattern;
} pubsubPattern;
           
Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

2.1 訂閱模式

每當用戶端執行PSUBSCRIBE指令訂閱某個或某些模式的時候,伺服器會對每個被訂閱的模式執行以下兩個操作:

  • 1)建立一個pubsubPattern結構,将結構的pattern屬性設定為被訂閱的模式,client屬性設定為訂閱模式的用戶端。
  • 2)将pubsubPattern結構添加到pubsub_patterns連結清單的表尾。
Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

當用戶端client-9執行指令:

PSUBSCRIBE "news.*"
           
Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

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

def psubscribe(*all_input_patterns):
  # 周遊輸入的所有模式
  for pattern in all_input_patterns:
    # 建立新的pubsubPattern結構
    # 記錄被訂閱的模式,以及訂閱模式的用戶端
    pubsubPattern = create_new_pubsubPattern()
    pubsubPattern.client = client
    pubsubPattern.pattern = pattern
    # 将新的pubsubPattern追加到pubsub_patterns連結清單末尾
    server.pubsub_patterns.append(pubsubPattern)
           

2.2 退訂模式

模式的退訂指令PUNSUBSCRIBE是PSUBSCRIBE指令的反操作:當一個用戶端退訂某個或某些模式的時候,伺服器将在pubsub_patterns連結清單中查找并删除那些pattern屬性為被退訂模式,并且client屬性為執行退訂指令的用戶端的pubsubPattern結構。

3、發送消息

當一個Redis用戶端執行PUBLISH<channel><message>指令将消息message發送給頻道channel的時候,伺服器需要執行以下兩個動作:

  • 1)将消息message發送給channel頻道的所有訂閱者。
  • 2)如果有一個或多個模式pattern與頻道channel相比對,那麼将消息message發送給pattern模式的訂閱者。

3.1 将消息發送給頻道訂閱者

因為伺服器狀态中的pubsub_channels字典記錄了所有頻道的訂閱關系,是以為了将消息發送給channel頻道的所有訂閱者,PUBLISH指令要做的就是在pubsub_channels字典裡找到頻道channel的訂閱者名單(一個連結清單),然後将消息發送給名單上的所有用戶端。

Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務
PUBLISH "news.it" "hello"
           

那麼PUBLISH指令将在pubsub_channels字典中查找鍵"news.it"對應的連結清單值,并通過周遊連結清單将消息"hello"發送給"news.it"頻道的三個訂閱者:client-1、client-2和client-3。

3.2 将消息發送給模式訂閱者

因為伺服器狀态中的pubsub_patterns連結清單記錄了所有模式的訂閱關系,是以為了将消息發送給所有與channel頻道相比對的模式的訂閱者,PUBLISH指令要做的就是周遊整個pubsub_patterns連結清單,查找那些與channel頻道相比對的模式,并将消息發送給訂閱了這些模式的用戶端。

Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

如果這時某個用戶端執行指令:

PUBLISH "news.it" "hello"
           

那麼PUBLISH指令會首先将消息"hello"發送給"news.it"頻道的所有訂閱者,然後開始在pubsub_patterns連結清單中查找是否有被訂閱的模式與"news.it"頻道相比對,結果發現"news.it"頻道和用戶端client-9訂閱的"news.*"頻道比對,于是指令将消息"hello"發送給用戶端client-9。

def pattern_publish(channel, message):
  # 周遊所有模式訂閱消息
  for pubsubPattern in server.pubsub_patterns:
    # 如果頻道和模式相比對
    if match(channel, pubsubPattern.pattern):
          # 那麼将消息發送給訂閱該模式的用戶端
          send_message(pubsubPattern.client, message)
           

最後,PUBLISH指令的實作可以用以下僞代碼來描述:

def publish(channel, message):
  # 将消息發送給channel頻道的所有訂閱者
  channel_publish(channel, message)
  # 将消息發送給所有和channel頻道相比對的模式的訂閱者
  pattern_publish(channel, message)
           

4、檢視訂閱資訊

4.1 PUBSUB CHANNELS

PUBSUB CHANNELS[pattern]子指令用于傳回伺服器目前被訂閱的頻道,其中pattern參數是可選的:

  • 如果不給定pattern參數,那麼指令傳回伺服器目前被訂閱的所有頻道。
  • 如果給定pattern參數,那麼指令傳回伺服器目前被訂閱的頻道中那些與pattern模式相比對的頻道。

這個子指令是通過周遊伺服器pubsub_channels字典的所有鍵(每個鍵都是一個被訂閱的頻道),然後記錄并傳回所有符合條件的頻道來實作的,這個過程可以用以下僞代碼來描述:

def pubsub_channels(pattern=None):
  # 一個清單,用于記錄所有符合條件的頻道
  channel_list = []
  # 周遊伺服器中的所有頻道
  # (也即是pubsub_channels字典的所有鍵)
  for channel in server.pubsub_channels:
    # 當以下兩個條件的任意一個滿足時,将頻道添加到連結清單裡面:
    #1 )使用者沒有指定pattern參數
    #2 )使用者指定了pattern參數,并且channel和pattern比對
    if (pattern is None) or match(channel, pattern):
     channel_list.append(channel)
  # 向用戶端傳回頻道清單
  return channel_list
           
Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務
redis> PUBSUB CHANNELS
1) "news.it"
2) "news.sport"
3) "news.business"
4) "news.movie"
           

4.2 PUBSUB NUMSUB

PUBSUB NUMSUB[channel-1 channel-2…channel-n]子指令接受任意多個頻道作為輸入參數,并傳回這些頻道的訂閱者數量。

這個子指令是通過在pubsub_channels字典中找到頻道對應的訂閱者連結清單,然後傳回訂閱者連結清單的長度來實作的(訂閱者連結清單的長度就是頻道訂閱者的數量)。

Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務
redis> PUBSUB NUMSUB news.it news.sport news.business news.movie
1) "news.it"
2) "3"
3) "news.sport"
4) "2"
5) "news.business"
6) "2"
7) "news.movie"
8) "1"
           

4.3 PUBSUB NUMPAT

PUBSUB NUMPAT子指令用于傳回伺服器目前被訂閱模式的數量。

這個子指令是通過傳回pubsub_patterns連結清單的長度來實作的,因為這個連結清單的長度就是伺服器被訂閱模式的數量,這個過程可以用以下僞代碼來描述:

def pubsub_numpat():
  # pubsub_patterns連結清單的長度就是被訂閱模式的數量
  reply_pattern_count(len(server.pubsub_patterns))
           
Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務
redis> PUBSUB NUMPAT
(integer) 3
           

5、重點回顧

  • 伺服器狀态在pubsub_channels字典儲存了所有頻道的訂閱關系:SUBSCRIBE指令負責将用戶端和被訂閱的頻道關聯到這個字典裡面,而UNSUBSCRIBE指令則負責解除用戶端和被退訂頻道之間的關聯。
  • 伺服器狀态在pubsub_patterns連結清單儲存了所有模式的訂閱關系:PSUBSCRIBE指令負責将用戶端和被訂閱的模式記錄到這個連結清單中,而PUNSUBSCRIBE指令則負責移除用戶端和被退訂模式在連結清單中的記錄。
  • PUBLISH指令通過通路pubsub_channels字典來向頻道的所有訂閱者發送消息,通過通路pubsub_patterns連結清單來向所有比對頻道的模式的訂閱者發送消息。
  • PUBSUB指令的三個子指令都是通過讀取pubsub_channels字典和pubsub_patterns連結清單中的資訊來實作的。

二、事務

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

以下是一個事務執行的過程,該事務首先以一個MULTI指令為開始,接着将多個指令放入事務當中,最後由EXEC指令将這個事務送出(commit)給伺服器執行:

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"
           

1、事務的實作

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

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

1.1 事務開始

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

redis> MULTI
OK
           

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

def MULTI():
  # 打開事務辨別
  client.flags |= REDIS_MULTI
  # 傳回OK回複
  replyOK()
           

1.2 指令入隊

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

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回複。
Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

1.3 事務隊列

每個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
           
  • 最先入隊的SET指令被放在了事務隊列的索引0位置上。
  • 第二入隊的GET指令被放在了事務隊列的索引1位置上。
  • 第三入隊的另一個SET指令被放在了事務隊列的索引2位置上。
  • 最後入隊的另一個GET指令被放在了事務隊列的索引3位置上。
Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

1.4 執行事務

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

伺服器首先會執行指令:

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)
           

2、WATCH指令的實作

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

redis> WATCH "name"
OK
redis> MULTI
OK
redis> SET "name" "peter"
QUEUED
redis> EXEC
(nil)
           
Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

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

2.1 使用WATCH指令監視資料庫鍵

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

typedef struct redisDb {
  // ...
  // 正在被WATCH指令監視的鍵
  dict *watched_keys;
  // ...
} redisDb;
           

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

Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

這個watched_keys字典中可以看出:

  • 用戶端c1和c2正在監視鍵"name"。
  • 用戶端c3正在監視鍵"age"。
  • 用戶端c2和c4正在監視鍵"address"。

通過執行WATCH指令,用戶端可以在watched_keys字典中與被監視的鍵進行關聯。舉個例子,如果目前用戶端為c10086,那麼用戶端執行以下WATCH指令之後:

redis> WATCH "name" "age"
OK
           
Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

2.2 監視機制的觸發

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

def touchWatchKey(db, key):
  # 如果鍵key存在于資料庫的watched_keys字典中
  # 那麼說明至少有一個用戶端在監視這個key
  if key in db.watched_keys:
    # 周遊所有監視鍵key的用戶端
    for client in db.watched_keys[key]:
      # 打開辨別
      client.flags |= REDIS_DIRTY_CAS
           
Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

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

2.3 判斷事務是否安全

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

  • 如果用戶端的REDIS_DIRTY_CAS辨別已經被打開,那麼說明用戶端所監視的鍵當中,至少有一個鍵已經被修改過了,在這種情況下,用戶端送出的事務已經不再安全,是以伺服器會拒絕執行用戶端送出的事務。
  • 如果用戶端的REDIS_DIRTY_CAS辨別沒有被打開,那麼說明用戶端監視的所有鍵都沒有被修改過(或者用戶端沒有監視任何鍵),事務仍然是安全的,伺服器将執行用戶端送出的這個事務。
Redis設計與實作讀書筆記七、釋出、訂閱和事務的原理一、釋出與訂閱二、事務

3、事務的ACID性質

在傳統的關系式資料庫中,常常用ACID性質來檢驗事務功能的可靠性和安全性。

在Redis中,事務總是具有原子性(Atomicity)、一緻性(Consistency)和隔離性(Isolation),并且當Redis運作在某種特定的持久化模式下時,事務也具有耐久性(Durability)。

3.1 原子性

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

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

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

redis> SET msg "hello" # msg鍵是一個字元串
OK 
redis> MULTI
OK
redis> SADD fruit "apple" "banana" "cherry"
QUEUED
redis> RPUSH msg "good bye" "bye bye" # 錯誤地對字元串鍵msg執行清單鍵的指令
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
           

即使RPUSH指令在執行期間出現了錯誤,事務的後續指令也會繼續執行下去,并且之前執行的指令也不會有任何影響。

Redis的作者在事務功能的文檔中解釋說,不支援事務復原是因為這種複雜的功能和Redis追求簡單高效的設計主旨不相符,并且他認為,Redis事務的執行時錯誤通常都是程式設計錯誤産生的,這種錯誤通常隻會出現在開發環境中,而很少會在實際的生産環境中出現,是以他認為沒有必要為Redis開發事務復原功能。

3.2 一緻性

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

1.入隊錯誤

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

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.
           

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

2.執行錯誤

除了入隊時可能發生錯誤以外,事務還可能在執行的過程中發生錯誤。

關于這種錯誤有兩個需要說明的地方:

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

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

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
           

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

3.伺服器停機

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

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

3.3 隔離性

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

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

3.4 耐久性

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

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

  • 當伺服器在無持久化的記憶體模式下運作時,事務不具有耐久性:一旦伺服器停機,包括事務資料在内的所有伺服器資料都将丢失。
  • 當伺服器在RDB持久化模式下運作時,伺服器隻會在特定的儲存條件被滿足時,才會執行BGSAVE指令,對資料庫進行儲存操作,并且異步執行的BGSAVE不能保證事務資料被第一時間儲存到硬碟裡面,是以RDB持久化模式下的事務也不具有耐久性。
  • 當伺服器運作在AOF持久化模式下,并且appendfsync選項的值為always時,程式總會在執行指令之後調用同步(sync)函數,将指令資料真正地儲存到硬碟裡面,是以這種配置下的事務是具有耐久性的。
  • 當伺服器運作在AOF持久化模式下,并且appendfsync選項的值為everysec時,程式會每秒同步一次指令資料到硬碟。因為停機可能會恰好發生在等待同步的那一秒鐘之内,這可能會造成事務資料丢失,是以這種配置下的事務不具有耐久性。
  • 當伺服器運作在AOF持久化模式下,并且appendfsync選項的值為no時,程式會交由作業系統來決定何時将指令資料同步到硬碟。因為事務資料可能在等待同步的過程中丢失,是以這種配置下的事務不具有耐久性。

4、重點回顧

  • 事務提供了一種将多個指令打包,然後一次性、有序地執行的機制。
  • 多個指令會被入隊到事務隊列中,然後按先進先出(FIFO)的順序執行。
  • 事務在執行過程中不會被中斷,當事務隊列中的所有指令都被執行完畢之後,事務才會結束。
  • 帶有WATCH指令的事務會将用戶端和被監視的鍵在資料庫的watched_keys字典中進行關聯,當鍵被修改時,程式會将所有監視被修改鍵的用戶端的REDIS_DIRTY_CAS标志打開。
  • 隻有在用戶端的REDIS_DIRTY_CAS标志未被打開時,伺服器才會執行用戶端送出的事務,否則的話,伺服器将拒絕執行用戶端送出的事務。
  • Redis的事務總是具有ACID中的原子性、一緻性和隔離性,當伺服器運作在AOF持久化模式下,并且appendfsync選項的值為always時,事務也具有耐久性。

繼續閱讀