<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【高頻 Redis 面試題】Redis 事務(wù)是否具備原子性?

          共 7065字,需瀏覽 15分鐘

           ·

          2020-05-24 23:20

          一、事務(wù)的實現(xiàn)原理

          一個事務(wù)從開始到結(jié)束通常會經(jīng)歷以下三個階段:

          1、事務(wù)開始

          客戶端發(fā)送 MULTI 命令,服務(wù)器執(zhí)行 MULTI 命令邏輯。

          服務(wù)器會在客戶端狀態(tài)(redisClient)的 flags 屬性打開 REDIS_MULTI 標識,將客戶端從非事務(wù)狀態(tài)切換到事務(wù)狀態(tài)。


          void multiCommand(redisClient *c) {
          // 不能在事務(wù)中嵌套事務(wù) if (c->flags & REDIS_MULTI) { addReplyError(c,"MULTI calls can not be nested"); return; }
          // 打開事務(wù) FLAG c->flags |= REDIS_MULTI;
          addReply(c,shared.ok);}


          2、命令入隊

          接著,用戶可以在客戶端輸入當(dāng)前事務(wù)要執(zhí)行的多個命令。

          當(dāng)客戶端切換到事務(wù)狀態(tài)時,服務(wù)器會根據(jù)客戶端發(fā)來的命令來執(zhí)行不同的操作。

          • 如果客戶端發(fā)送的命令為 EXEC、DISCARD、WATCH、MULTI 四個命令的其中一個,那么服務(wù)器立即執(zhí)行這個命令。

          • 與此相反,如果客戶端發(fā)送的命令是 EXEC、DISCARD、WATCH、MULTI 四個命令以外的其他命令,那么服務(wù)器并不立即執(zhí)行這個命令。

            • 首先檢查此命令的格式是否正確,如果不正確,服務(wù)器會在客戶端狀態(tài)(redisClient)的 flags 屬性打開 REDIS_MULTI 標識,并且返回錯誤信息給客戶端。

            • 如果正確將這個命令放入一個事務(wù)隊列里面,然后向客戶端返回 QUEUED 回復(fù)。

          我們先看看事務(wù)隊列是如何實現(xiàn)的?

          每個 Redis 客戶端都有自己的事務(wù)狀態(tài),對應(yīng)的是客戶端狀態(tài)(redisClient)的 mstate 屬性。


          typeof struct redisClient{   // 事務(wù)狀態(tài)   multiState mstate;}redisClient;


          事務(wù)狀態(tài)(mstate)包含一個事務(wù)隊列(FIFO 隊列),以及一個已入隊命令的計數(shù)器。


          /** 事務(wù)狀態(tài)*/typedef struct multiState {
          // 事務(wù)隊列,F(xiàn)IFO 順序 multiCmd *commands; /* Array of MULTI commands */ // 已入隊命令計數(shù) int count; /* Total number of MULTI commands */ int minreplicas; /* MINREPLICAS for synchronous replication */ time_t minreplicas_timeout; /* MINREPLICAS timeout as unixtime. */} multiState;


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


          /** 事務(wù)命令*/typedef struct multiCmd {
          // 參數(shù) robj **argv; // 參數(shù)數(shù)量 int argc; // 命令指針 struct redisCommand *cmd;} multiCmd;


          最后我們再看看入隊列的源碼:


          /* Add a new command into the MULTI commands queue** 將一個新命令添加到事務(wù)隊列中*/void queueMultiCommand(redisClient *c) {   multiCmd *mc;   int j;
          // 為新數(shù)組元素分配空間 c->mstate.commands = zrealloc(c->mstate.commands, sizeof(multiCmd)*(c->mstate.count+1));
          // 指向新元素 mc = c->mstate.commands+c->mstate.count;
          // 設(shè)置事務(wù)的命令、命令參數(shù)數(shù)量,以及命令的參數(shù) mc->cmd = c->cmd; mc->argc = c->argc; mc->argv = zmalloc(sizeof(robj*)*c->argc); memcpy(mc->argv,c->argv,sizeof(robj*)*c->argc); for (j = 0; j < c->argc; j++) incrRefCount(mc->argv[j]);
          // 事務(wù)命令數(shù)量計數(shù)器增一 c->mstate.count++;}


          當(dāng)然了,還有我們上面提到的,如果命令入隊出錯時,會打開客戶端狀態(tài)的 REDIS_DIRTY_EXEC 標識。


          /* Flag the transacation as DIRTY_EXEC so that EXEC will fail.** 將事務(wù)狀態(tài)設(shè)為 DIRTY_EXEC ,讓之后的 EXEC 命令失敗。** Should be called every time there is an error while queueing a command.** 每次在入隊命令出錯時調(diào)用*/void flagTransaction(redisClient *c) {   if (c->flags & REDIS_MULTI)       c->flags |= REDIS_DIRTY_EXEC;}


          3、事務(wù)執(zhí)行

          客戶端發(fā)送 EXEC 命令,服務(wù)器執(zhí)行 EXEC 命令邏輯。

          • 如果客戶端狀態(tài)的 flags 屬性不包含 REDIS_MULTI 標識,或者包含 REDIS_DIRTY_CAS 或者 REDIS_DIRTY_EXEC 標識,那么就直接取消事務(wù)的執(zhí)行。

          • 否則客戶端處于事務(wù)狀態(tài)(flags 有 REDIS_MULTI 標識),服務(wù)器會遍歷客戶端的事務(wù)隊列,然后執(zhí)行事務(wù)隊列中的所有命令,最后將返回結(jié)果全部返回給客戶端;


          void execCommand(redisClient *c) {   int j;   robj **orig_argv;   int orig_argc;   struct redisCommand *orig_cmd;   int must_propagate = 0; /* Need to propagate MULTI/EXEC to AOF / slaves? */
          // 客戶端沒有執(zhí)行事務(wù) if (!(c->flags & REDIS_MULTI)) { addReplyError(c,"EXEC without MULTI"); return; }
          /* Check if we need to abort the EXEC because: * * 檢查是否需要阻止事務(wù)執(zhí)行,因為: * * 1) Some WATCHed key was touched. * 有被監(jiān)視的鍵已經(jīng)被修改了 * * 2) There was a previous error while queueing commands. * 命令在入隊時發(fā)生錯誤 * (注意這個行為是 2.6.4 以后才修改的,之前是靜默處理入隊出錯命令) * * A failed EXEC in the first case returns a multi bulk nil object * (technically it is not an error but a special behavior), while * in the second an EXECABORT error is returned. * * 第一種情況返回多個批量回復(fù)的空對象 * 而第二種情況則返回一個 EXECABORT 錯誤 */ if (c->flags & (REDIS_DIRTY_CAS|REDIS_DIRTY_EXEC)) {
          addReply(c, c->flags & REDIS_DIRTY_EXEC ? shared.execaborterr : shared.nullmultibulk);
          // 取消事務(wù) discardTransaction(c);
          goto handle_monitor; }
          /* Exec all the queued commands */ // 已經(jīng)可以保證安全性了,取消客戶端對所有鍵的監(jiān)視 unwatchAllKeys(c); /* Unwatch ASAP otherwise we'll waste CPU cycles */
          // 因為事務(wù)中的命令在執(zhí)行時可能會修改命令和命令的參數(shù) // 所以為了正確地傳播命令,需要現(xiàn)備份這些命令和參數(shù) orig_argv = c->argv; orig_argc = c->argc; orig_cmd = c->cmd;
          addReplyMultiBulkLen(c,c->mstate.count);
          // 執(zhí)行事務(wù)中的命令 for (j = 0; j < c->mstate.count; j++) {
          // 因為 Redis 的命令必須在客戶端的上下文中執(zhí)行 // 所以要將事務(wù)隊列中的命令、命令參數(shù)等設(shè)置給客戶端 c->argc = c->mstate.commands[j].argc; c->argv = c->mstate.commands[j].argv; c->cmd = c->mstate.commands[j].cmd;
          /* Propagate a MULTI request once we encounter the first write op. * * 當(dāng)遇上第一個寫命令時,傳播 MULTI 命令。 * * 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. * * 這可以確保服務(wù)器和 AOF 文件以及附屬節(jié)點的數(shù)據(jù)一致性。 */ if (!must_propagate && !(c->cmd->flags & REDIS_CMD_READONLY)) {
          // 傳播 MULTI 命令 execCommandPropagateMulti(c);
          // 計數(shù)器,只發(fā)送一次 must_propagate = 1; }
          // 執(zhí)行命令 call(c,REDIS_CALL_FULL);
          /* Commands may alter argc/argv, restore mstate. */ // 因為執(zhí)行后命令、命令參數(shù)可能會被改變 // 比如 SPOP 會被改寫為 SREM // 所以這里需要更新事務(wù)隊列中的命令和參數(shù) // 確保附屬節(jié)點和 AOF 的數(shù)據(jù)一致性 c->mstate.commands[j].argc = c->argc; c->mstate.commands[j].argv = c->argv; c->mstate.commands[j].cmd = c->cmd; }
          // 還原命令、命令參數(shù) c->argv = orig_argv; c->argc = orig_argc; c->cmd = orig_cmd;
          // 清理事務(wù)狀態(tài) discardTransaction(c);
          /* Make sure the EXEC command will be propagated as well if MULTI * was already propagated. */ // 將服務(wù)器設(shè)為臟,確保 EXEC 命令也會被傳播 if (must_propagate) 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);}



          二、為什么很多人說 Redis 事務(wù)不支持原子性?

          1、Redis 事務(wù)不支持事務(wù)回滾機制

          Redis 事務(wù)執(zhí)行過程中,如果一個命令執(zhí)行出錯,那么就返回錯誤,然后還是會接著繼續(xù)執(zhí)行下面的命令。

          下面我們演示一下:

          db3c8ae423ca43a7a534504e057f0f81.webp

          正是因為 Redis 事務(wù)不支持事務(wù)回滾機制,如果事務(wù)執(zhí)行中出現(xiàn)了命令執(zhí)行錯誤(例如對 String 類型的數(shù)據(jù)庫鍵執(zhí)行 LPUSH 操作),只會返回當(dāng)前命令執(zhí)行的錯誤給客戶端,并不會影響下面的命令的執(zhí)行。所以很多人覺得和關(guān)系型數(shù)據(jù)庫(MySQL) 不一樣,而 MySQL 的事務(wù)是具有原子性的,所以大家都認為 Redis 事務(wù)不支持原子性。

          2、但是其實 Redis 意義上是支持原子性的。

          正常情況下,它也是要不所有命令執(zhí)行成功,要不一個命令都不執(zhí)行。

          我們下面演示一下:

          全部執(zhí)行成功的:

          e8514b813c921317ed89c8bf15a2655b.webp

          一個都不執(zhí)行:

          cf5768d72af609d1197283becae6ad49.webp

          這就是上面提到的,在事務(wù)開始后,用戶可以輸入事務(wù)要執(zhí)行的命令;在命令入事務(wù)隊列前,會對命令進行檢查,如果命令不存在或者是命令參數(shù)不對,則會返回錯誤給客戶端,并且修改客戶端狀態(tài)。

          當(dāng)后面客戶端執(zhí)行 EXEC 命令時,服務(wù)器就會直接拒絕執(zhí)行此事務(wù)了。

          所以說,Redis 事務(wù)其實是支持原子性的!即使 Redis 不支持事務(wù)回滾機制,但是它會檢查每一個事務(wù)中的命令是否錯誤。

          但是我們要注意一個點就是:Redis 事務(wù)不支持檢查那些程序員自己邏輯錯誤。例如對 String 類型的數(shù)據(jù)庫鍵執(zhí)行對 HashMap 類型的操作!


          我很贊同 Redis 作者的想法:

          首先,MySQL 和 Redis 的定位不一樣,一個是關(guān)系型數(shù)據(jù)庫,一個是 NoSQL。

          MySQL 的 SQL 查詢是可以相當(dāng)復(fù)雜的,而且 MySQL 沒有事務(wù)隊列這種說法,SQL 真正開始執(zhí)行才會進行分析和檢查,MySQL 不可能提前知道下一條 SQL 是否正確。所以支持事務(wù)回滾是非常有必要的~

          但是,Redis 使用了事務(wù)隊列來預(yù)先將執(zhí)行命令存儲起來,并且會對其進行格式檢查的,提前就知道命令是否可執(zhí)行了。所以如果只要有一個命令是錯誤的,那么這個事務(wù)是不能執(zhí)行的。

          并且, Redis 作者認為基本只會出現(xiàn)在開發(fā)環(huán)境的編程錯誤其實在生產(chǎn)環(huán)境基本是不可能出現(xiàn)的(例如對 String 類型的數(shù)據(jù)庫鍵執(zhí)行 LPUSH 操作),所以他覺得沒必要為了這事務(wù)回滾機制而改變 Redis 追求簡單高效的設(shè)計主旨。

          所以最后,其實 Redis 事務(wù)真正支持原子性的前提:開發(fā)者不要傻不拉幾的寫有邏輯問題的代碼!


          推薦閱讀:


          76736f01f22a2aa3688a17d3c4e90df6.webp喜歡我可以給我設(shè)為星標哦76736f01f22a2aa3688a17d3c4e90df6.webp

          6199cb19e85f1c85f56621bde0b5fc08.webp好文章,我“在看”824d8a8af198b0d6e729fcf930626355.webp
          瀏覽 55
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产大片无码内射 | 狠狠久久婷五月 | 国产在线无码视频56CC中文字幕 | www插插插无码视频 | AV天堂电影在线 |