Redis源码剖析(七)监视功能
Redis提供這樣一個功能,客戶端在開啟事務之前,可以設置對一個或多個鍵的監視,在執行EXEC命令之前的這段時間,如果其他客戶端對該客戶端監視的鍵做了修改,那么Redis會取消該客戶端事務的運行,也就是說如果執行EXEC,那么Redis什么也不會做。
由于別的客戶端對當前客戶端關心的鍵做了修改,Redis會將當前客戶端的事務視為不安全,從而不再執行。Redis設計與實現一書中將監視功能稱作樂觀鎖
監視命令
監視功能由WATCH命令實現,可以設置對一個或多個鍵的監視
127.0.0.1:6379> set time 14:33 //設置鍵值對 OK 127.0.0.1:6379> WATCH time //設置對鍵time的監視 OK 127.0.0.1:6379> MULTI //開啟事務 OK 127.0.0.1:6379> set db redis QUEUED 127.0.0.1:6379> get db QUEUED 127.0.0.1:6379> get time QUEUED 127.0.0.1:6379> //此時還處于事務狀態,沒有執行EXEC命令此時開啟另一個客戶端對鍵time進行修改
注:修改的意思通常是改變鍵對應的值,使用各種SET命令
127.0.0.1:6379> set time 14:34 //修改鍵time的值 OK 127.0.0.1:6379>現在回到之前的客戶端,如果執行EXEC命令,會發現事務隊列中的命令沒有執行,而是返回了nil
//執行事務之前 127.0.0.1:6379> set time 14:33 OK 127.0.0.1:6379> WATCH time OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> set db redis QUEUED 127.0.0.1:6379> get db QUEUED 127.0.0.1:6379> get time QUEUED //執行事務之后 127.0.0.1:6379> EXEC //執行事務,返回空 (nil) 127.0.0.1:6379>存儲結構
要想知道一個客戶端都監視了哪些鍵,就需要將其記錄下來,Redis采用字典記錄每個被監視的鍵和監視該鍵的所有客戶端,不過這個字典不在redisServer結構中,而是在數據庫redisDb結構中,該結構中保存了很多字典,其中就有數據鍵值對字典和過期時間字典,之前提到過的
//server.h typedef struct redisDb {dict *dict; /* 保存鍵值對的字典 */ dict *expires; /* 保存鍵和其到期時間 */ dict *watched_keys; /* 監視字典,保存每個被監視的鍵和所有監視該鍵的客戶端 */ ... } redisDb;監視字典中保存了所有客戶端的監視信息,鍵是每個被監視的鍵,值是監視該鍵的客戶端鏈表
和訂閱模塊相同,客戶端同時也會記錄自己監視了哪些鍵,不過客戶端不需要使用字典,使用鏈表就夠了。在client結構中,可以找到相關的定義
//server.h typedef struct client {list *watched_keys; //監視鏈表,記錄當前客戶端監視的所有鍵... } client;此外,客戶端的監視鏈表中并不是單單保存鍵,而是保存一個watchedKey結構,其中記錄著監視的鍵和鍵所在的數據庫
//multi.c /* 客戶端監視鏈表中保存的結構,記錄監視的鍵和鍵所在的數據庫 */ typedef struct watchedKey {robj *key;redisDb *db; } watchedKey;監視功能的實現
添加監視的鍵
監視功能由watchCommand函數實現,函數中主要是將WATCH命令的參數依次添加到數據庫的監視字典和客戶端的監視鏈表中,由watchForKey函數完成
//multi.c /* 對鍵key進行監聽,需要更新數據庫的監視字典和客戶端的監視鏈表 */ void watchForKey(client *c, robj *key) {list *clients = NULL;listIter li;listNode *ln;watchedKey *wk;/* 對客戶端的監視鏈表進行遍歷,判斷鍵key是否已經被監視 */listRewind(c->watched_keys,&li);while((ln = listNext(&li))) {/* 取出鏈表的節點 */wk = listNodeValue(ln);/* 判斷是否監視過鍵key */if (wk->db == c->db && equalStringObjects(key,wk->key))return; /* Key already watched */}/* 從數據庫的監視字典中取出鍵key對應的客戶端鏈表,如果不存在,則創建一個 */clients = dictFetchValue(c->db->watched_keys,key);if (!clients) {/* 不存在客戶端鏈表(當前沒有客戶端對該鍵進行監聽),創建一個客戶端鏈表作為鍵key對應的值 */clients = listCreate();dictAdd(c->db->watched_keys,key,clients);incrRefCount(key);}/* 將當前客戶端追加到客戶端鏈表中 */listAddNodeTail(clients,c);/* 增加鍵key到客戶端的監視鏈表中 */wk = zmalloc(sizeof(*wk));wk->key = key;wk->db = c->db;incrRefCount(key);listAddNodeTail(c->watched_keys,wk); }修改被監視的鍵對事務的影響
當客戶端開始監視功能后,其他客戶端任何對監視鍵的修改都會破壞當前客戶端的事務狀態,導致Redis不再執行這次的事務。所以在對鍵進行修改的命令中一定有對監視鍵的處理,以SET命令為例,可以看到在setKey函數中執行了signalModifiedKey函數,目的是將所有監視該鍵的客戶端的事務狀態標記為已破壞(Redis不會執行已破壞的事務)
//db.c /* 添加或覆蓋鍵值對 */ void setKey(redisDb *db, robj *key, robj *val) {if (lookupKeyWrite(db,key) == NULL) {dbAdd(db,key,val);} else {dbOverwrite(db,key,val);}incrRefCount(val);removeExpire(db,key);/* 因為對鍵key進行了修改,所以會導致監聽該鍵的客戶端事務被破壞* 調用該函數更改這些客戶端的事務狀態 */signalModifiedKey(db,key); }signalModifiedKey又調用touchWatchedKey函數,完成實際的修改任務。該函數將所有監視該鍵的客戶端的事務標志設置為已破壞,當客戶端輸入EXEC命令時,Redis會先判斷事務狀態,如果已破壞,則不再執行事務隊列中的命令
//multi.c /* 掃描服務器的監視字典,檢查鍵key是否被某些客戶端監視,如果有,將對應客戶端標記為事務破壞狀態 */ void touchWatchedKey(redisDb *db, robj *key) {list *clients;listIter li;listNode *ln;/* 如果數據庫中沒有鍵被監視,則返回 */if (dictSize(db->watched_keys) == 0) return;/* 嘗試從監視字典中取出鍵key對應的值 */clients = dictFetchValue(db->watched_keys, key);/* 如果不存在,說明沒有客戶端監視該鍵,直接返回 */if (!clients) return;/* 設置迭代器方向從頭到尾,開始遍歷監視鍵key的所有客戶端 */listRewind(clients,&li);while((ln = listNext(&li))) {/* 取出節點對應的值 */client *c = listNodeValue(ln);/* 設置客戶端的事務狀態,表示該客戶端的事務已經被破壞,* 如果該客戶端使用EXEC執行事務,則什么也不做直接返回 */c->flags |= CLIENT_DIRTY_CAS;} }在EXEC命令處理函數中,可以看到對于事務狀態的判斷
/* 啟動事務命令 */ void execCommand(client *c) {.../* CLIENT_DIRTY_CAS標識代表客戶端監視的鍵是否被修改過* 如果被修改過,說明事務已被破壞,那么執行事務就不再安全,直接返回 */if (c->flags & (CLIENT_DIRTY_CAS|CLIENT_DIRTY_EXEC)) {addReply(c, c->flags & CLIENT_DIRTY_EXEC ? shared.execaborterr :shared.nullmultibulk);discardTransaction(c);goto handle_monitor;}/* 開始執行事務,監視任務就可以結束了,將該客戶端的監視字典清空 */unwatchAllKeys(c); ... }可以看到,在EXEC命令處理函數中有unwatchAllKeys這樣一個函數調用,原因是監視功能只對單次事務有效,當事務結束后,當前客戶端所有的監視也都會被清空。如果需要再進行監視,需要重新設置,當然,這就涉及到下一輪的事務
小結
監視功能和事務模塊是結合在一起的,如果想要對事務進行保護,確保當其他客戶端修改了某些必要的鍵時取消事務,就可以使用監視功能。另外,UNWATCH命令用于取消對一個或多個鍵的監視,不過該命令只能在事務之外執行,如果在事務狀態中使用,則會被添加到事務隊列中
總結
以上是生活随笔為你收集整理的Redis源码剖析(七)监视功能的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 每天一道LeetCode-----杨辉三
- 下一篇: Redis源码剖析(八)链表