關於Redis的ACID


事務是一個數據庫必備的元素,對於redis也不例外,對於一個傳統的關系型數據庫來說,數據庫事務滿足ACID四個特性:

  • A代表原子性:一個事務(transaction)中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
  • C代表一致性:事務應確保數據庫的狀態從一個一致狀態轉變為另一個一致狀態。一致狀態的含義是數據庫中的數據應滿足完整性約束
  • I代表隔離性:多個事務並發執行時,一個事務的執行不應影響其他事務的執行
  • D代表持久性:已被提交的事務對數據庫的修改應該永久保存在數據庫中

然而,對於redis來說,只滿足其中的:

一致性和隔離性兩個特性,其他特性是不支持的。

關於redis對ACID四個特性暫時先說這么多,在本文后面會詳細說明。在詳述之前,我們先來了解redis事務具體的使用和實現,這樣我們接下來討論ACID時才能更好的理解。

redis事務的使用

redis事務主要包括MULTI、EXEC、DISCARD和WATCH命令

MULTI命令

MULTI命令用來開啟一個事務,當MULTI執行之后,客戶端可以繼續向服務器發送多條命令,這些命令會緩存在隊列里面,只有當執行EXEC命令時,這些命令才會執行。

而DISCARD命令可以清空事務隊列,放棄執行事務。

一個使用MULTI和EXEC執行事務的例子:

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. 開始事務。
  2. 命令入隊。
  3. 執行事務。

redis事務產生錯誤

使用redis事務可能會產生錯誤,主要分為兩大類:

  • 事務在執行EXEC之前,入隊的命令可能出錯。
  • 命令可能在 EXEC 調用之后失敗

redis在2.6.5之后,如果發現事務在執行EXEC之前出現錯誤,那么會放棄這個事務。

在EXEC命令之后產生的錯誤,會被忽略,其他正確的命令會被繼續執行。

redis事務不支持回滾

如果你有使用關系式數據庫的經驗, 那么 “Redis 在事務失敗時不進行回滾,而是繼續執行余下的命令”這種做法可能會讓你覺得有點奇怪。

以下是這種做法的優點:

  • Redis 命令只會因為錯誤的語法而失敗(並且這些問題不能在入隊時發現),或是命令用在了錯誤類型的鍵上面:這也就是說,從實用性的角度來說,失敗的命令是由編程錯誤造成的,而這些錯誤應該在開發的過程中被發現,而不應該出現在生產環境中。
  • 因為不需要對回滾進行支持,所以 Redis 的內部可以保持簡單且快速。 有種觀點認為 Redis 處理事務的做法會產生 bug , 然而需要注意的是, 在通常情況下, 回滾並不能解決編程錯誤帶來的問題。 舉個例子, 如果你本來想通過 INCR 命令將鍵的值加上 1 , 卻不小心加上了 2 , 又或者對錯誤類型的鍵執行了 INCR , 回滾是沒有辦法處理這些情況的。

鑒於沒有任何機制能避免程序員自己造成的錯誤, 並且這類錯誤通常不會在生產環境中出現, 所以 Redis 選擇了更簡單、更快速的無回滾方式來處理事務。

關於WATCH命令

WATCH命令可以添加監控的鍵,如果這些監控的鍵沒有被其他客戶端修改,那么事務可以順利執行,如果被修改了,那么事務就不能執行。

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 時,Redis 會發現 name 這個被監視的鍵已經被修改, 因此客戶端 A 的事務不會被執行,而是直接返回失敗

當客戶端發送 EXEC 命令、觸發事務執行時, 服務器會對客戶端的狀態進行檢查:

  • 如果客戶端的 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()

 

redis事務的實現

在了解了redis事務的使用之后,我們再來看看redis事務的實現,主要是對上面說的MULTI、EXEC、DISCARD和WATCH命令的源碼的分析。

MULTI命令實現

void multiCommand(redisClient *c) {
    if (c->flags & REDIS_MULTI) {
        addReplyError(c,"MULTI calls can not be nested");
        return;
    }
    c->flags |= REDIS_MULTI;
    addReply(c,shared.ok);
}

從上面的源碼可以看出,MULTI只能執行一次,而且就做一件事,把客戶端的標志打上REDIS_MULTI。

EXEC命令實現

void execCommand(redisClient *c) {
    int j;
    robj **orig_argv;
    int orig_argc;
    struct redisCommand *orig_cmd;

    if (!(c->flags & REDIS_MULTI)) {
        addReplyError(c,"EXEC without MULTI");
        return;
    }

    /* Check if we need to abort the EXEC if some WATCHed key was touched.
    * A failed EXEC will return a multi bulk nil object. */
    if (c->flags & REDIS_DIRTY_CAS) {
        freeClientMultiState(c);
        initClientMultiState(c);
        c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);
        unwatchAllKeys(c);
        addReply(c,shared.nullmultibulk);
        goto handle_monitor;
    }

    /* Replicate a MULTI request now that we are sure the block is executed.
    * This way we'll deliver the MULTI/..../EXEC block as a whole and
    * both the AOF and the replication link will have the same consistency
    * and atomicity guarantees. */
    execCommandReplicateMulti(c);

    /* Exec all the queued commands */
    unwatchAllKeys(c); /* Unwatch ASAP otherwise we'll waste CPU cycles */
    orig_argv = c->argv;
    orig_argc = c->argc;
    orig_cmd = c->cmd;
    addReplyMultiBulkLen(c,c->mstate.count);
    for (j = 0; j < c->mstate.count; j++) {
        c->argc = c->mstate.commands[j].argc;
        c->argv = c->mstate.commands[j].argv;
        c->cmd = c->mstate.commands[j].cmd;
        call(c,REDIS_CALL_FULL);

        /* Commands may alter argc/argv, restore mstate. */
        c->mstate.commands[j].argc = c->argc;
        c->mstate.commands[j].argv = c->argv;
        c->mstate.commands[j].cmd = c->cmd;
    }
    c->argv = orig_argv;
    c->argc = orig_argc;
    c->cmd = orig_cmd;
    freeClientMultiState(c);
    initClientMultiState(c);
    c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);
    /* Make sure the EXEC command is always replicated / AOF, since we
    * always send the MULTI command (we can't know beforehand if the
    * next operations will contain at least a modification to the DB). */
    server.dirty++;

handle_monitor:
    /* Send EXEC to clients waiting data from MONITOR. We do it here
    * since the natural order of commands execution is actually:
    * MUTLI, EXEC, ... commands inside transaction ...
    * Instead EXEC is flagged as REDIS_CMD_SKIP_MONITOR in the command
    * table, and we do it here with correct ordering. */
    if (listLength(server.monitors) && !server.loading)
        replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
}

其主要步驟是:

  • 檢查是否已經是處在MULTI狀態下,如果不是,直接返回
  • 檢查WATCH的key是否已經被其他客戶端修改,如果是,放棄事務
  • 執行事務命令隊列里面的所有命令

執行命令隊列里面的所有命令的代碼如下:

for (j = 0; j < c->mstate.count; j++) {
    c->argc = c->mstate.commands[j].argc;
    c->argv = c->mstate.commands[j].argv;
    c->cmd = c->mstate.commands[j].cmd;
    call(c,REDIS_CALL_FULL);

     /* Commands may alter argc/argv, restore mstate. */
    c->mstate.commands[j].argc = c->argc;
    c->mstate.commands[j].argv = c->argv;
    c->mstate.commands[j].cmd = c->cmd;
}

遍歷queue隊列的所有命令,一條條的執行

DISCARD命令實現

DICARD命令主要由下面兩個函數完成:

void discardCommand(redisClient *c) {
    if (!(c->flags & REDIS_MULTI)) {
        addReplyError(c,"DISCARD without MULTI");
        return;
    }
    discardTransaction(c);
    addReply(c,shared.ok);
}

void discardTransaction(redisClient *c) {
    freeClientMultiState(c);
    initClientMultiState(c);
    c->flags &= ~(REDIS_MULTI|REDIS_DIRTY_CAS);;
    unwatchAllKeys(c);
}

void freeClientMultiState(redisClient *c) {
    int j;

    for (j = 0; j < c->mstate.count; j++) {
        int i;
        multiCmd *mc = c->mstate.commands+j;

        for (i = 0; i < mc->argc; i++)
            decrRefCount(mc->argv[i]);
        zfree(mc->argv);
    }
    zfree(c->mstate.commands);
}

函數調用的關系如下

  discardCommand
       |
       |
       |
discardTransaction
       |
       |
       |
freeClientMultiState

freeClientMultiState就是完成DISCARD命令主要功能的函數,把其命令隊列里面的所有命令所在的存儲空間都釋放。

WATCH命令

void watchForKey(redisClient *c, robj *key) {
    list *clients = NULL;
    listIter li;
    listNode *ln;
    watchedKey *wk;

    /* Check if we are already watching for this key */
    listRewind(c->watched_keys,&li);
    while((ln = listNext(&li))) {
        wk = listNodeValue(ln);
        if (wk->db == c->db && equalStringObjects(key,wk->key))
            return; /* Key already watched */
    }
    /* This key is not already watched in this DB. Let's add it */
    clients = dictFetchValue(c->db->watched_keys,key);
    if (!clients) { 
        clients = listCreate();
        dictAdd(c->db->watched_keys,key,clients);
        incrRefCount(key);
    }
    listAddNodeTail(clients,c);
    /* Add the new key to the lits of keys watched by this client */
    wk = zmalloc(sizeof(*wk));
    wk->key = key;
    wk->db = c->db;
    incrRefCount(key);
    listAddNodeTail(c->watched_keys,wk);
}

這個函數是在WATCH命令執行后,主要完成兩件事情:

  • 把key添加到當前客戶端c->watched_keys的鏈表中,
  • 並且把當前客戶端添加到c->db->watched_keys的字典里面去。

當db中有能修改鍵狀態的命令(比如INCR等)執行時,會自動把watch這個key的客戶端的事務設置為放棄狀態:

void touchWatchedKey(redisDb *db, robj *key) {
    list *clients;
    listIter li;
    listNode *ln;

    if (dictSize(db->watched_keys) == 0) return;
    clients = dictFetchValue(db->watched_keys, key);
    if (!clients) return;

    /* Mark all the clients watching this key as REDIS_DIRTY_CAS */
    /* Check if we are already watching for this key */
    listRewind(clients,&li);
    while((ln = listNext(&li))) {
        redisClient *c = listNodeValue(ln);

        c->flags |= REDIS_DIRTY_CAS;
    }
}

如上源碼所示,當某個key被修改后,會遭到其字典中所在的hash鏈表,然后把鏈表中所有的客戶端都設置成REDIS_DIRTY_CAS狀態,即事務被DISCARD的狀態。

redis事務的ACID性質討論

A原子性

單個 Redis 命令的執行是原子性的,但 Redis 沒有在事務上增加任何維持原子性的機制,所以 Redis 事務的執行並不是原子性的

如果一個事務隊列中的所有命令都被成功地執行,那么稱這個事務執行成功

另一方面,如果 Redis 服務器進程在執行事務的過程中被停止 —— 比如接到 KILL 信號、宿主機器停機,等等,那么事務執行失敗

事務失敗時,Redis 也不會進行任何的重試或者回滾動作,不滿足要么全部全部執行,要么都不執行的條件

C一致性

一致性分下面幾種情況來討論:

首先,如果一個事務的指令全部被執行,那么數據庫的狀態是滿足數據庫完整性約束的

其次,如果一個事務中有的指令有錯誤,那么數據庫的狀態是滿足數據完整性約束的

最后,如果事務運行到某條指令時,進程被kill掉了,那么要分下面幾種情況討論:

  • 如果當前redis采用的是內存模式,那么重啟之后redis數據庫是空的,那么滿足一致性條件
  • 如果當前采用RDB模式存儲的,在執行事務時,Redis 不會中斷事務去執行保存 RDB 的工作,只有在事務執行之后,保存 RDB 的工作才有可能開始。所以當 RDB 模式下的 Redis 服務器進程在事 務中途被殺死時,事務內執行的命令,不管成功了多少,都不會被保存到 RDB 文件里。 恢復數據庫需要使用現有的 RDB 文件,而這個 RDB 文件的數據保存的是最近一次的數 據庫快照(snapshot),所以它的數據可能不是最新的,但只要 RDB 文件本身沒有因為 其他問題而出錯,那么還原后的數據庫就是一致的

  • 如果當前采用的是AOF存儲的,那么可能事務的內容還未寫入到AOF文件,那么此時肯定是滿足一致性的,如果事務的內容有部分寫入到AOF文件中,那么需要用工具把AOF中事務執行部分成功的指令移除,這時,移除之后的AOF文件也是滿足一致性的

所以,redis事務滿足一致性約束

I隔離性

Redis 是單進程程序,並且它保證在執行事務時,不會對事務進行中斷,事務可以運行直到執行完所有事務隊列中的命令為止。因此,Redis 的事務是總是帶有隔離性的。

D持久性

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

  • 在單純的內存模式下,事務肯定是不持久的
  • 在 RDB 模式下,服務器可能在事務執行之后、RDB 文件更新之前的這段時間失敗,所以 RDB 模式下的 Redis 事務也是不持久的
  • 在 AOF 的“總是 SYNC ”模式下,事務的每條命令在執行成功之后,都會立即調用 fsync 或 fdatasync 將事務數據寫入到 AOF 文件。但是,這種保存是由后台線程進行的,主線程不會阻塞直到保存成功,所以從命令執行成功到數據保存到硬盤之間,還是有一段非常小的間隔,所以這種模式下的事務也是不持久的。
  • 其他 AOF 模式也和“總是 SYNC ”模式類似,所以它們都是不持久的。

來源:https://github.com/Charles0429/redis_doc/blob/master/function/redis_transaction.md

http://redisbook.readthedocs.org/en/latest/feature/transaction.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM