redis源碼分析之事務Transaction(上)

来源:http://www.cnblogs.com/lfls/archive/2017/11/14/7835333.html
-Advertisement-
Play Games

這周學習了一下redis事務功能的實現原理,本來是想用一篇文章進行總結的,寫完以後發現這塊內容比較多,而且多個命令之間又互相依賴,放在一篇文章里一方面篇幅會比較大,另一方面文章組織結構會比較亂,不容易閱讀。因此把事務這個模塊整理成上下兩篇文章進行總結。 原文地址:http://www.jianshu ...


這周學習了一下redis事務功能的實現原理,本來是想用一篇文章進行總結的,寫完以後發現這塊內容比較多,而且多個命令之間又互相依賴,放在一篇文章里一方面篇幅會比較大,另一方面文章組織結構會比較亂,不容易閱讀。因此把事務這個模塊整理成上下兩篇文章進行總結。

原文地址:http://www.jianshu.com/p/acb97d620ad7

這篇文章我們重點分析一下redis事務命令中的兩個輔助命令:watch跟unwatch。

一、redis事務輔助命令簡介

依然從server.c文件的命令表中找到相應的命令以及它們對應的處理函數。

//watch,unwatch兩個命令我們把它們叫做redis事務輔助命令
{"watch",watchCommand,-2,"sF",0,NULL,1,-1,1,0,0},
{"unwatch",unwatchCommand,1,"sF",0,NULL,0,0,0,0,0},
  1. watch,用於客戶端關註某個key,當這個key的值被修改時,整個事務就會執行失敗(註:該命令需要在事務開啟前使用)。
  2. unwatch,用於客戶端取消已經watch的key。

用法舉例如下:
clientA

127.0.0.1:6379> watch a
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set b b
QUEUED
//在執行前插入clientB的操作如下,事務就會執行失敗
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379>

clientB

127.0.0.1:6379> set a aa
OK
127.0.0.1:6379>

二、redis事務輔助命令源碼分析

在看具體執行函數之前首先瞭解幾個數據結構:

//每個客戶端對象中有一個watched_keys鏈表來保存已經watch的key
typedef struct client {
    list *watched_keys;  
}
//上述鏈表中每個節點的數據結構
typedef struct watchedKey {
    //watch的key
    robj *key;
    //指向的DB,後面細說
    redisDb *db;
} watchedKey;

關於事務的幾個命令所對應的函數都放在了multi.c文件中。
一起看下watch命令對應處理函數的源碼:

void watchCommand(client *c) {
    int j;
    //如果客戶端處於事務狀態,則返回錯誤信息
    //由此可以看出,watch必須在事務開啟前使用
    if (c->flags & CLIENT_MULTI) {
        addReplyError(c,"WATCH inside MULTI is not allowed");
        return;
    }
    //依次watch客戶端的各個參數(這裡說明watch命令可以一次watch多個key)
    //註:0表示命令本身,所以參數從1開始
    for (j = 1; j < c->argc; j++)
        watchForKey(c,c->argv[j]);
    //返回結果
    addReply(c,shared.ok);
}

//具體的watch操作,代碼較長,慢慢分析
void watchForKey(client *c, robj *key) {
    list *clients = NULL;
    listIter li;
    listNode *ln;
    //上面已經提到了數據結構
    watchedKey *wk;

    //首先判斷key是否已經被客戶端watch
    //listRewind這個函數在發佈訂閱那篇文章里也有,就是把客戶端的watched_keys賦值給li
    listRewind(c->watched_keys,&li);
    while((ln = listNext(&li))) {
        wk = listNodeValue(ln);
        //這裡一個wk節點中有db,key兩個欄位
        if (wk->db == c->db && equalStringObjects(key,wk->key))
            return; 
    }
    //開始watch指定key
    //整個watch操作保存了兩套數據結構,一套是在db->watched_keys中的字典結構,如下:
    clients = dictFetchValue(c->db->watched_keys,key);
    //如果是key第一次出現,則進行初始化
    if (!clients) {
        clients = listCreate();
        dictAdd(c->db->watched_keys,key,clients);
        incrRefCount(key);
    }
    //把當前客戶端加到該key的watch鏈表中
    listAddNodeTail(clients,c);
    //另一套是在c->watched_keys中的鏈表結構:如下
    wk = zmalloc(sizeof(*wk));
    //初始化各個欄位
    wk->key = key;
    wk->db = c->db;
    incrRefCount(key);
    //加入到鏈表最後
    listAddNodeTail(c->watched_keys,wk);
}

整個watch的數據結構比較複雜,我這裡畫了一張圖方便理解:
watch數據結構
簡單解釋一下上面的圖,首先redis把每個客戶端連接包裝成了一個client對象,上圖中db,watch_keys就是其中的兩個欄位(client對象裡面還有很多其他欄位,包括上篇文章中提到的pub/sub)。

  1. db欄位指向給該client對象分配的儲存空間,db對象中也含有一個watched_keys欄位,是字典類型(也就是哈希表),以想要watch的key做key,存儲的鏈表則是所有watch該key的客戶端。
  2. watch_keys欄位則是一個鏈表類型,每個節點類型為watch_key,其中包含兩個欄位,key表示watch的key,db則指向了當前client對象的db欄位,如上圖。

看完watch命令的源碼以後,再來看一下unwatch命令,如果搞明白了上面提到的兩套數據結構,那麼看unwatch的源碼應該會比較容易,畢竟就是刪除數據結構中對應的內容。

void unwatchCommand(client *c) {
    //取消watch所有key
    unwatchAllKeys(c);
    //修改客戶端狀態
    c->flags &= (~CLIENT_DIRTY_CAS);
    addReply(c,shared.ok);
}

//取消watch的key
void unwatchAllKeys(client *c) {
    listIter li;
    listNode *ln;
    //如果客戶端沒有watch任何key,則直接返回
    if (listLength(c->watched_keys) == 0) return;
    //註意這裡操作的是鏈表欄位
    listRewind(c->watched_keys,&li);
    while((ln = listNext(&li))) {
        list *clients;
        watchedKey *wk;
        //遍歷取出該客戶端watch的key
        wk = listNodeValue(ln);
        //取出所有watch了該key的客戶端,這裡則是字典(即哈希表)
        clients = dictFetchValue(wk->db->watched_keys, wk->key);
        //空指針判斷
        serverAssertWithInfo(c,NULL,clients != NULL);
        //從watch列表中刪除該客戶端
        listDelNode(clients,listSearchKey(clients,c));
        //如果key只有一個當前客戶端watch,則刪除
        if (listLength(clients) == 0)
            dictDelete(wk->db->watched_keys, wk->key);
        //從當前client的watch列表中刪除該key
        listDelNode(c->watched_keys,ln);
        //減少引用數
        decrRefCount(wk->key);
        //釋放記憶體
        zfree(wk);
    }
}

最後我們考慮一下watch機制的觸發時機,現在我們已經把想要watch的key加入到了watch的數據結構中,可以想到觸發watch的時機應該是修改key的內容時,通知到所有watch了該key的客戶端。

感興趣的用戶可以任意選一個修改命令跟蹤一下源碼,例如set命令,我們發現所有對key進行修改的命令最後都會調用touchWatchedKey()函數,而該函數源碼就位於multi.c文件中,該函數就是觸發watch機制的關鍵函數,源碼如下:

//這裡入參db就是客戶端對象中的db,上文已經提到,不贅述
void touchWatchedKey(redisDb *db, robj *key) {
    list *clients;
    listIter li;
    listNode *ln;
    //保存watchkey的字典為空,則返回
    if (dictSize(db->watched_keys) == 0) return;
    //註意這裡操作的是字典(即哈希表)數據結構
    clients = dictFetchValue(db->watched_keys, key);
    //如果沒有客戶端watch該key,則返回
    if (!clients) return;
    //把client賦值給li
    listRewind(clients,&li);
    //遍歷watch了該key的客戶端,修改他們的狀態
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        c->flags |= CLIENT_DIRTY_CAS;
    }
}

跟我們猜測的一樣,就是每當key的內容被修改時,則遍歷所有watch了該key的客戶端,設置相應的狀態為CLIENT_DIRTY_CAS。

三、redis事務輔助命令總結

上面就是redis事務命令中watch,unwatch的實現原理,其中最複雜的應該就是watch對應的那兩套數據結構了,跟之前的pub/sub類似,都是使用鏈表+哈希表的結構存儲,另外也是通過修改客戶端的狀態位FLAG來通知客戶端。

代碼比較多,而且C++代碼看上去會比較費勁,需要慢慢讀,反覆讀。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 最近在一個剛結束的一個項目中使用到了UEditor編輯器,下麵總結一下遇到的問題以及使用時需要註意的地方: 1. 使用UEditor插件需要先對其進行路徑配置: 在ueditor.config.js文件中 配置 ueditor.config.js文件相對Ueditor文件夾的位置; 2. UEdit ...
  • 終於過完雙十一,伺服器頂住了壓力,不知道為啥,突然的輕鬆,反而感覺有點無所適從,好久沒寫博客了,竟然發現還有人回我,很是開心,問題都是關於阿裡雲的,阿裡雲的吭確實多,其實關鍵在於,官方文檔還是少,出了問題,很多都要靠自己去嘗試,自己去找方法。 好了,今天開始學習bootstrap,自己本身是後端出身 ...
  • 緊跟任何開發工具包的更新都是一件需要持續努力的事,特別是前端開發工具。 把你的註意力從方法和技術的洪流中移開一會,你就可能會錯過什麼! 上周我遇到我的一個前端開發朋友,他很興奮地跟我談論他使用的一些新工具。其中最有意思的是使用 Grunt 來編譯 SCSS。 人們很容易忘記不是每個人都和你走在同一條 ...
  • 效果圖: Css: ...
  • 最近公司開發的h5項目,需要用到sass,所以領導推薦讓我去阮一峰大神的SASS用法指南博客學習,為方便以後自己使用,所以在此記錄。 一、代碼的重用 1、繼承:SASS允許一個選擇器,繼承另一個選擇器。 class2要繼承class1,就要使用@extend命令: 2、Mixin:Mixin有點像C ...
  • 現今對於大多數公司來說,信息安全工作尤為重要,就像京東,阿裡巴巴這樣的大公司來說,信息安全是最為重要的一個話題,舉個簡單的例子: 就像這樣的密碼公開化,很容易造成一定的信息的泄露。所以今天我們要講的就是如何來實現密碼的加密和解密來提高數據的安全性。 在這首先要引入springboot融合mybati ...
  • http://www.pinhuba.com/oracle/101128.htm ...
  • 接著上一篇,這篇文章分析一下redis事務操作中multi,exec,discard三個核心命令。 原文地址:http://www.jianshu.com/p/e22615586595 看本篇文章前需要先對上面文章有所瞭解: "redis源碼分析之事務Transaction(上)" 一、redis事 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...