從一次線上故障來看redis刪除機制

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上查詢,若是壓力較大能夠經過集羣方案多堆些機器。

PHP內存池分析

直播評論系統分析設計

一次線上Mysql死鎖分析

相關文章
相關標籤/搜索