1、問題及背景 redis
公司去年上線一個抽獎系統,主要用來拉新、提高流量,全部新註冊的用戶在指定時間均可以抽獎,爲了保證安全性,程序中作了頻率限制,每一個用戶30秒只能抽1次,具體作法是以用戶id爲key,保存在redis中,過時時間爲30秒;抽獎時會先讀取這個key是否存在,若是存在則認爲用戶在30秒內已經抽過,返回稍後再試。sql
由於讀多寫少,爲了提升系統的吞吐量,系統採用了redis讀、寫分離的架構,即寫入的時候往master上寫,讀取用戶是否抽過獎則從slave上讀取,redis版本爲2.8.6。安全
這個系統上線後前幾天運行比較良好,某天忽然報大量的稍後重試的錯誤,很多用戶反饋抽了一次獎後再也沒法抽獎。架構
經過日誌分析和數據覈對發現某個key過時了,但在slave上還能夠讀取的到。函數
復現以下:oop
在master上設置一個key,並設置過時時間this
set name edward expire name 5
過了5秒等key過時後再到slave上讀取,但get返回不爲空spa
但這個時候若是在master上get1次,再到slave上get,結果就是空了。設計
2、故障分析3d
爲何會出現這種狀況呢,咱們來分析下redis中key過時刪除策略,redis中key過時刪除策略有二種:主動刪除、惰性刪除。
一、主動刪除
是在服務端的定時任務中執行,相關代碼以下:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { ... databasesCron(); ...
serverCron函數在redis啓動的時候註冊到定時器中,執行頻率大概爲100毫秒1次,具體參考aeCreateTimeEvent函數。
其中databasesCron函數爲將過時的key進行隨機刪除:
if (server.active_expire_enabled && server.masterhost == NULL) activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
這裏會判斷當前實例是否爲主實例,只有主實例而且active_expire_enabled 啓用(默認會啓用)纔會啓動刪除機制,activeExpireCycle函數中會隨機刪除一些過時的key,注意是隨機刪除一些過時的key,而不是所有刪除,由於redis要考慮系統的負載,怕執行時間太長會搶佔太多CPU,增長系統負載,這裏就不細講,有興趣的同窗能夠細看下代碼。
結論:主動刪除只有在master上生效。
二、惰性刪除
再看get命令:
int getGenericCommand(redisClient *c) { robj *o; if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL) return REDIS_OK; if (o->type != REDIS_STRING) { addReply(c,shared.wrongtypeerr); return REDIS_ERR; } else { addReplyBulk(c,o); return REDIS_OK; } }
lookupKeyReadOrReply函數會調用lookupKeyRead函數,後者會調用到expireIfNeeded先檢測是否過時了:
int expireIfNeeded(redisDb *db, robj *key) { mstime_t when = getExpire(db,key); mstime_t now; /* 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 now > when; }
經過代碼發現,若是當前實例爲slave則直接返回,不會作進一步的處理;做者也作了註釋,說slave上過時的key會依賴master發過來的DEL命令來刪除。
結論:redis的惰性刪除機制是在執行用戶請求的時候判斷key是否過時,惰性刪除也只在master上生效,slave上是不生效的。
咱們來總結下:
一、redis中過時key的刪除有2種策略:主動刪除、惰性刪除。
二、主動刪除和惰性刪除只在master上發生,slave的刪除機制依賴於master。
回到上面的問題,是什麼緣由致使key過時了,而slave上還有值,由於master沒有及時將過時的key刪除,即沒有觸發主動刪除機制,這時候也沒有在master上讀取數據,即執行get命令,因此也不會觸發master上的惰性刪除機制,因此slave上的key沒有及時刪除。
爲何master的主動刪除沒有觸發呢?,緣由有二:
一、redis的定時任務執行有延遲
redis儘可能保證按指定時間執行指定任務,不過若是當時CPU搶佔的比較厲害,定時任務執行時間可能有很大的延遲,這個期間一些key沒有及時刪除。
這種狀況通常發生在redis實例所在機器cpu負載很高的狀況。
二、由於redis的是隨機刪除的,可能會致使部分過時key沒有被及時刪除掉
這個只發生在redis中有大量的過時的key的狀況下
3、解決方案
好了,問題緣由找到了,那咱們的解決方案是什麼呢?
禁止在slave上查詢一些關鍵信息:像鎖、登陸信息,這些信息必須從master上查詢,若是壓力較大能夠經過集羣方案多堆些機器。