本文基於社群版Redis 4.0.8
1、復現條件
- 版本:社群版Redis 4.0.10以下版本
- 使用場景:開啟讀寫分離的主從架構或者叢集架構(master只負責寫流量,slave負責讀流量)
案例:
# 寫入一條帶過期時間10s的key
10.90.73.147:12345> set luxiu1 1 ex 10
OK
10.90.73.147:12345> get luxiu1
"1"
10.90.73.147:12345> exists luxiu1
(integer) 1
......
#等待10s,待key過期
......
10.90.73.147:12345> ttl luxiu1
(integer) -2 #確定key已經過期了
10.90.73.147:12345> get luxiu1
(nil) #沒有問題,該key不存在了
10.90.73.147:12345> exists luxiu1
(integer) 1 #還能查到
10.90.73.147:12345> exists luxiu1
(integer) 1 #還能查到
2、原始碼分析
在分析該問題前,需要了解Redis在讀寫分離模式下讀到過期資料的問題:
Redis過期key的刪除策略採用惰性刪除和定時刪除:
惰性刪除:主節點每次處理讀取命令時,都會檢查鍵是否超時,如果超時則執行del命令刪除鍵物件,之後del命令也會非同步傳送給從節點。需要注意的是為了保證複製的一致性,從節點自身永遠不會主動刪除超時資料;
定時刪除:Redis主節點在內部定時任務會迴圈取樣一定數量的鍵,當發現取樣的鍵過期時執行del命令,之後再同步給從節點;
如果此時資料大量過期,主節點取樣速度跟不上過期速度且主節點沒有讀取過期鍵的操作,那麼從節點將無法收到del命令。這時在從節點上可以讀取到已經超時的資料。Redis在3.2版本解決了這個問題,在從節點上讀取資料之前也會檢查鍵的過期時間來決定是否返回資料。但是,4.0.10版本以下的exists命令實現方式有問題,導致該命令還是查詢到過期資料問題。
下面是4.0.10以下版本exists命令實現原始碼:
問題就在於expireIfNeeded這個函式,它的功能就是惰性刪除,判斷如果key過期了就進行del,我們是讀寫分離架構,slave不進行del,如下程式碼:
直接返回1,並不進行到del操作。
所以exists查詢到過期key一直存在。
3、問題解決
在社群版4.0.11以上版本已經修復了該bug:
lookupKeyRead函式呼叫lookupKeyReadWithFlags(db,key,LOOKUP_NONE)
lookupKeyReadWithFlags函式邏輯如下:
最後,可以升級到4.0.12版本解決該問題。