Redis主從讀資料不一致與hmget()獲取欄位為null的問題解析
大家在使用redis的時候,經常會用expire來設定key的過期時間,以為某個key到期就會馬上清除。但在設定為主寫隨機讀時,發現存在key未失效的情況,下面具體分析:
原因一過期策略的問題:
3.2之後的版本已不存在以下問題
Redis key的三種過期策略
惰性刪除:當讀/寫一個已經過期的key時,會觸發惰性刪除策略,直接刪除掉這個過期key,很明顯,這是被動的!
定期刪除:由於惰性刪除策略無法保證冷資料被及時刪掉,所以 redis 會定期主動淘汰一批已過期的key。(在第二節中會具體說明)
主動刪除:當前已用記憶體超過maxMemory限定時,觸發主動清理策略。主動設定的前提是設定了maxMemory的值
Redis刪除過期Key的原始碼
<pre name="code" class="java">int expireIfNeeded(redisDb *db, robj *key) { time_t when = getExpire(db,key); if (when < 0) return 0; /* No expire for this key */ /* Don't expire anything while loading. It will be done later. */ if (server.loading) return 0; /* If we are running in the context of a slave, return ASAP: * the slave key expiration is controlled by the master that will * send us synthesized DEL operations for expired keys. * * Still we try to return the right information to the caller, * that is, 0 if we think the key should be still valid, 1 if * we think the key is expired at this time. */ if (server.masterhost != NULL) { return time(NULL) > when; } /* Return when this key has not expired */ if (time(NULL) <= when) return 0; /* Delete the key */ server.stat_expiredkeys++; propagateExpire(db,key); return dbDelete(db,key); }
通過以上原始碼發現,4行:沒有設定超時時間,則不刪;7行:在"loading"時不刪;16行:非主庫不刪;21行未到期不刪。25行同步從庫和檔案。
所以說,在從庫執行主動刪除操作,或者通過惰性刪除的方式觸發刪除key的操作,最終都不會執行成功。原因就在上面的第16行程式碼。
原因二快照模式自身的特性:
大家都知道,redis支援兩 種持久化方式,一種是 Snapshotting(快照)也是預設方式,另一種是
Append-only file(縮寫 aof)的方式。Snapshotting(快照)的持久化方式:
這種方式是就是將記憶體中資料以快照的方式寫入到二進位制檔案中,預設的檔名為 dump.rdb。可以通過配置設定自動做快照持久 化的方式。我們可以配置 redis 在 n 秒內如果超過 m 個 key 被修改就自動做快照,下面是預設的快照儲存配置。
save 900 1 #900 秒內如果超過 1 個 key 被修改,則發起快照儲存
save 300 10 #300 秒內容如超過 10 個 key 被修改,則發起快照儲存
Save 60 10000 #60
秒內容如超過 10000個 key 被修改,則發起快照儲存
下面介紹詳細的快照儲存過程
1.redis 呼叫 fork,現在有了子程序和父程序。
2. 父程序繼續處理 client 請求,子程序負責將記憶體內容寫入到臨時檔案。由於 os 的寫時複製機制(copy on write)父子程序會共享相同的物理頁面,當父程序處理寫請求時 os 會為父程序要修改的頁面建立副本,而不是寫共享的頁面。所以子程序的地址空間內的數
據是 fork 時刻整個資料庫的一個快照。
3.當子程序將快照寫入臨時檔案完畢後,用臨時檔案替換原來的快照檔案,然後子程序退出。
由於快照模式是定時進行,可能會導致資料在上次持久化完成到本次持久化開始之前,redis伺服器宕機,丟失記憶體中相關資料,這也是主從讀取內容不一致的原因之一
二、Redis獲取欄位為null的問題解析
不知道大家是否碰到過一個問題,使用redis的hmget()獲取列表形式的資料時,有時偶然會發生獲取的列表中,所有欄位都是null的情況。本人專案中碰到過,下面解釋下部門大牛的調查結果:當儲存的資料時存在有效期時,且資料在過期的時間臨界點,此時,過期資料會被清理,關鍵是清理過程,是先刪除List裡每個元素的資料,再刪除整個List的,這就導致小概率事件的發生了,hmget時,結果是List<String>中裡面的元素都為null,也許這也是很多專案都使用hgetAll()的原因了。
所以我們可以在hmget增加一個校驗處理:
// 使用hexists的地方必須對查詢結果進行校驗
if (CollectionUtils.isEmpty(result)|| isAllItemNull(result)){
return new ArrayList<String>();
}
這可以算是hmget()的一個坑了,但同樣,hgetAll()也存在一個坑的,只是一般不會發生,詳細參見:http://huoding.com/2013/01/21/214