制定Redis過期策略萨蚕,是整個Redis緩存策略的關(guān)鍵之一,因為內(nèi)存來說,公司不可能無限大办陷,所以就要對key進(jìn)行一系列的管控绍移。
文章結(jié)構(gòu):(1)理解Redis過期設(shè)置API(命令與Java描述版本)框弛;(2)理解Redis內(nèi)部的過期策略扁誓;(3)對開發(fā)需求而言邓梅,Redis過期策略的設(shè)計實現(xiàn)經(jīng)驗冶共。
本系列文章:
一、理解Redis過期設(shè)置API(命令與Java描述版本):
(1)TTL命令:
redis 127.0.0.1:6379> TTL KEY_NAME
返回值
當(dāng) key 不存在時捅僵,返回 -2 家卖。 當(dāng) key 存在但沒有設(shè)置剩余生存時間時,返回 -1 庙楚。 否則上荡,以秒為單位,返回 key 的剩余生存時間馒闷。
注意:在 Redis 2.8 以前酪捡,當(dāng) key 不存在,或者 key 沒有設(shè)置剩余生存時間時纳账,命令都返回 -1 逛薇。
(2)EXPIRE命令
定義:為給定 key 設(shè)置生存時間,當(dāng) key 過期時(生存時間為 0 )疏虫,它會被自動刪除永罚。
redis 127.0.0.1:6379> EXPIRE runooobkey 60
(integer) 1
返回值
設(shè)置成功返回 1 。 當(dāng) key 不存在或者不能為 key 設(shè)置過期時間時(比如在低于 2.1.3 版本的 Redis 中你嘗試更新 key 的過期時間)返回 0 议薪。
key生存時間注意點:
生存時間可以通過使用 DEL 命令來刪除整個 key 來移除尤蛮,或者被 SET 和 GETSET 命令覆寫(overwrite),這意味著斯议,如果一個命令只是修改(alter)一個帶生存時間的 key 的值而不是用一個新的 key 值來代替(replace)它的話,那么生存時間不會被改變醇锚。
比如說哼御,對一個 key 執(zhí)行 INCR 命令,對一個列表進(jìn)行 LPUSH 命令焊唬,或者對一個哈希表執(zhí)行 HSET 命令恋昼,這類操作都不會修改 key 本身的生存時間。
另一方面赶促,如果使用 RENAME 對一個 key 進(jìn)行改名液肌,那么改名后的 key 的生存時間和改名前一樣。
RENAME 命令的另一種可能是鸥滨,嘗試將一個帶生存時間的 key 改名成另一個帶生存時間的 another_key 嗦哆,這時舊的 another_key (以及它的生存時間)會被刪除谤祖,然后舊的 key 會改名為 another_key ,因此老速,新的 another_key 的生存時間也和原本的 key 一樣粥喜。
(3)PEXPIRE命令
設(shè)置成功返回 1 。 當(dāng) key 不存在或者不能為 key 設(shè)置過期時間時(比如在低于 2.1.3 版本的 Redis 中你嘗試更新 key 的過期時間)返回 0 橘券。
(4)PERSIST 命令
返回值:
當(dāng)過期時間移除成功時额湘,返回 1 。 如果 key 不存在或 key 沒有設(shè)置過期時間旁舰,返回 0 锋华。
127.0.0.1:6379> PEXPIRE k2 10000000
(integer) 1
(5)SETEX命令
用于在Redis鍵中的指定超時,設(shè)置鍵的字符串值
返回值:
字符串箭窜,如果在鍵中設(shè)置了值則返回OK毯焕。如果值未設(shè)置則返回 Null。
127.0.0.1:6379> SETEX k1 100 v1
OK
127.0.0.1:6379> ttl k1
(integer) 92
127.0.0.1:6379> get k1
"v1"
(6)補充:(精度不同的時間設(shè)置):
EXPIREAT <key> < timestamp> 命令用于將鍵key 的過期時間設(shè)置為timestamp所指定的秒數(shù)時間戳绽快。
PEXPIREAT <key> < timestamp > 命令用于將鍵key 的過期時間設(shè)置為timestamp所指定的毫秒數(shù)時間戳芥丧。
例子:
//TTL命令
127.0.0.1:6379> FLUSHDB
OK
127.0.0.1:6379> ttl key
(integer) -2
127.0.0.1:6379> set key value
OK
127.0.0.1:6379> ttl key
(integer) -1
//expire命令
127.0.0.1:6379> expire key 10
(integer) 1
127.0.0.1:6379> ttl key
(integer) 7
127.0.0.1:6379> ttl key
(integer) 3
127.0.0.1:6379> ttl key
(integer) -2
//PEXPIRE命令
127.0.0.1:6379> set k2 v2
OK
127.0.0.1:6379> PEXPIRE k2 10000000
(integer) 1
127.0.0.1:6379> ttl k2
(integer) 9994
//PERSIST 命令
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> EXPIRE k1 100
(integer) 1
127.0.0.1:6379> ttl k1
(integer) 86
127.0.0.1:6379> PERSIST k1
(integer) 1
127.0.0.1:6379> ttl k1
(integer) -1
(6)Java代碼控制:
@Autowired
private JedisPool jedisPool;
Jedis jedis = jedisPool.getResource();
System.out.println("判斷key是否存在:"+shardedJedis.exists("key"));
// 設(shè)置 key001的過期時間
System.out.println("設(shè)置 key的過期時間為5秒:"+jedis.expire("key", 5));
// 查看某個key的剩余生存時間,單位【秒】.永久生存或者不存在的都返回-1
System.out.println("查看key的剩余生存時間:"+jedis.ttl("key"));
// 移除某個key的生存時間
System.out.println("移除key的生存時間:"+jedis.persist("key"));
System.out.println("查看key的剩余生存時間:"+jedis.ttl("key"));
// 查看key所儲存的值的類型
System.out.println("查看key所儲存的值的類型:"+jedis.type("key"));
二、理解Redis內(nèi)部的過期策略:
(1)總述:
Redis采用的是定期刪除策略和懶漢式的策略互相配合坊罢。
注意续担!是Redis內(nèi)部自主完成!是Redis內(nèi)部自主完成活孩!是Redis內(nèi)部自主完成物遇!
我們只可以通過調(diào)整外圍參數(shù),以及設(shè)計數(shù)據(jù)淘汰模式去調(diào)控我們的Redis緩存系統(tǒng)過期策略憾儒。
(2)定期刪除策略:
1)含義:每隔一段時間執(zhí)行一次刪除過期key操作
2)優(yōu)點:
通過限制刪除操作的時長和頻率询兴,來減少刪除操作對CPU時間的占用--處理"定時刪除"的缺點
定期刪除過期key--處理"懶漢式刪除"的缺點
3)缺點:
在內(nèi)存友好方面,會造成一定的內(nèi)存占用起趾,但是沒有懶漢式那么占用內(nèi)存(相對于定時刪除則不如)
在CPU時間友好方面诗舰,不如"懶漢式刪除"(會定期的去進(jìn)行比較和刪除操作,cpu方面不如懶漢式训裆,但是比定時好)
4)關(guān)鍵點:
合理設(shè)置刪除操作的執(zhí)行時長(每次刪除執(zhí)行多長時間)和執(zhí)行頻率(每隔多長時間做一次刪除)(這個要根據(jù)服務(wù)器運行情況來定了)眶根,每次執(zhí)行時間太長,或者執(zhí)行頻率太高對cpu都是一種壓力边琉。
每次進(jìn)行定期刪除操作執(zhí)行之后属百,需要記錄遍歷循環(huán)到了哪個標(biāo)志位,以便下一次定期時間來時变姨,從上次位置開始進(jìn)行循環(huán)遍歷族扰。
對于懶漢式刪除而言,并不是只有獲取key的時候才會檢查key是否過期,在某些設(shè)置key的方法上也會檢查(例子:setnx key2 value2:如果設(shè)置的key2已經(jīng)存在渔呵,那么該方法返回false怒竿,什么都不做;如果設(shè)置的key2不存在厘肮,那么該方法設(shè)置緩存key2-value2愧口。假設(shè)調(diào)用此方法的時候,發(fā)現(xiàn)redis中已經(jīng)存在了key2类茂,但是該key2已經(jīng)過期了耍属,如果此時不執(zhí)行刪除操作的話,setnx方法將會直接返回false巩检,也就是說此時并沒有重新設(shè)置key2-value2成功厚骗,所以對于一定要在setnx執(zhí)行之前,對key2進(jìn)行過期檢查)兢哭。
5)刪除鍵流程(簡單而言领舰,對指定個數(shù)個庫的每一個庫隨機刪除小于等于指定個數(shù)個過期key):
1. 遍歷每個數(shù)據(jù)庫(就是redis.conf中配置的"database"數(shù)量,默認(rèn)為16)
2. 檢查當(dāng)前庫中的指定個數(shù)個key(默認(rèn)是每個庫檢查20個key迟螺,注意相當(dāng)于該循環(huán)執(zhí)行20次冲秽,循環(huán)體是下邊的描述)
如果當(dāng)前庫中沒有一個key設(shè)置了過期時間,直接執(zhí)行下一個庫的遍歷
隨機獲取一個設(shè)置了過期時間的key矩父,檢查該key是否過期锉桑,如果過期,刪除key
判斷定期刪除操作是否已經(jīng)達(dá)到指定時長窍株,若已經(jīng)達(dá)到民轴,直接退出定期刪除。
對于定期刪除球订,在程序中有一個全局變量current_db來記錄下一個將要遍歷的庫后裸,假設(shè)有16個庫,我們這一次定期刪除遍歷了10個冒滩,那此時的current_db就是11微驶,下一次定期刪除就從第11個庫開始遍歷,假設(shè)current_db等于15了开睡,那么之后遍歷就再從0號庫開始(此時current_db==0)
6)源碼機制閱讀:
在redis源碼中祈搜,實現(xiàn)定期淘汰策略的是函數(shù)activeExpireCycle,每當(dāng)周期性函數(shù)serverCron執(zhí)行時士八,該函數(shù)會調(diào)用databasesCron函數(shù);然后databasesCron會調(diào)用activeExpireCycle函數(shù)進(jìn)行主動的過期鍵刪除。具體方法是在規(guī)定的時間內(nèi)梁呈,多次從expires中隨機挑一個鍵婚度,檢查它是否過期,如果過期則刪除。
首先這個函數(shù)有兩種執(zhí)行模式蝗茁,一個是快速模式一個是慢速模式醋虏,體現(xiàn)在代碼中就是timelimit這個變量中,這個變量是用來約束這個函數(shù)的運行時間的哮翘,我們可以考慮這樣一個場景颈嚼,就是數(shù)據(jù)庫中有很多過期的鍵需要清理,那么這個函數(shù)就會一直運行很長時間饭寺,這樣一直占用CPU顯然是不合理的阻课,所以需要這個變量來約束,當(dāng)函數(shù)運行時間超過了這個閾值艰匙,就算還有很多過期鍵沒有清理限煞,函數(shù)也強制退出。
在快速模式下员凝,timelimit的值是固定的署驻,是一個預(yù)定義的常量ACTIVE_EXPIRE_CYCLE_FAST_DURATION,在慢速模式下健霹,這個變量的值是通過下面的代碼計算的旺上。
timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
他的計算依據(jù)是之前預(yù)定義好的每次迭代只能占用的CPU時間比例,以及這個函數(shù)被調(diào)用的頻率糖埋。
Redis中也可能有多個數(shù)據(jù)庫宣吱,所以這個函數(shù)會遍歷多個數(shù)據(jù)庫來清楚過期鍵 ,但是是根據(jù)下面代碼的原則來確定要遍歷的數(shù)據(jù)庫的個數(shù)的阶捆。
if (dbs_per_call > server.dbnum || timelimit_exit)
dbs_per_call = server.dbnum;
dbs_per_call變量就是函數(shù)會遍歷的數(shù)據(jù)庫的個數(shù)凌节,他有一個預(yù)定義的值REDIS_DBCRON_DBS_PER_CALL,但是如果這個值大于現(xiàn)在redis中本身的數(shù)據(jù)庫的個數(shù)洒试,我們就要將它的值變成當(dāng)前的數(shù)據(jù)庫的實際個數(shù)倍奢,或者上次的函數(shù)是因為超時強制退出了,說明可能有的數(shù)據(jù)庫在上次函數(shù)調(diào)用時沒有遍歷到垒棋,里面的過期鍵沒有清理掉卒煞,所以也要將這次遍歷的數(shù)據(jù)庫的個數(shù)改成實際數(shù)據(jù)庫的個數(shù)。
for (j = 0; j < dbs_per_call; j++) {
int expired;
redisDb *db = server.db+(current_db % server.dbnum);
current_db++;
上面代碼可以看出:數(shù)據(jù)庫的遍歷是在這個大的for循環(huán)里叼架,其中值得留意的是current_db這個變量是一個static變量畔裕,這么做的好處是,如果真的發(fā)生了我們上面說的情況乖订,上一次函數(shù)調(diào)用因為超時而強制退出扮饶,這個變量就會記錄下這一次函數(shù)應(yīng)該從哪個數(shù)據(jù)庫開始遍歷,這樣會使得函數(shù)用在每個數(shù)據(jù)庫的時間盡量平均乍构,就不會出現(xiàn)有的數(shù)據(jù)庫里面的過期鍵一直沒有清理的情況甜无。
每個數(shù)據(jù)庫的過期鍵清理的操作是在下面的這個do while 循環(huán)中(由于代碼過長,所以中間有很多代碼我把它隱藏了,現(xiàn)在看到的只是一個大框架岂丘,稍后我會對其中的部分詳細(xì)講解)
do {
...
/* If there is nothing to expire try next DB ASAP. */
if ((num = dictSize(db->expires)) == 0) {
...
}
slots = dictSlots(db->expires);
now = mstime();
if (num && slots > DICT_HT_INITIAL_SIZE &&
(num*100/slots < 1)) break;
...
if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
while (num--) {
...
}
/* Update the average TTL stats for this database. */
if (ttl_samples) {
...
}
iteration++;
if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
...
}
if (timelimit_exit) return;
} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
注意while循環(huán)條件陵究,ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP是我們每個循環(huán)希望查到的過期鍵的個數(shù),如果我們每次循環(huán)過后奥帘,被清理的過期鍵的個數(shù)超過了我們期望的四分之一铜邮,我們就會繼續(xù)這個循環(huán),因為這說明當(dāng)前數(shù)據(jù)庫中過期鍵的個數(shù)比較多寨蹋,需要繼續(xù)清理松蒜,如果沒有達(dá)到我們期望的四分之一,就跳出while循環(huán)钥庇,遍歷下一個數(shù)據(jù)庫牍鞠。
這個函數(shù)最核心的功能就是清除過期鍵,這個功能的實現(xiàn)就是在while(num--)這個循環(huán)里面评姨。
while (num--) {
dictEntry *de;
long long ttl;
if ((de = dictGetRandomKey(db->expires)) == NULL) break;
ttl = dictGetSignedIntegerVal(de)-now;
if (activeExpireCycleTryExpire(db,de,now)) expired++;
if (ttl < 0) ttl = 0;
ttl_sum += ttl;
ttl_samples++;
}
他先從數(shù)據(jù)庫中設(shè)置了過期時間的鍵的集合中隨機抽取一個鍵难述,然后調(diào)用activeExpireCycleTryExpire函數(shù)來判斷這個鍵是否過期,如果過期就刪除鍵吐句,activeExpireCycleTryExpire函數(shù)的源碼如下:
int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) {
long long t = dictGetSignedIntegerVal(de);
if (now > t) {
sds key = dictGetKey(de);
robj *keyobj = createStringObject(key,sdslen(key));
propagateExpire(db,keyobj);
dbDelete(db,keyobj);
notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
"expired",keyobj,db->id);
decrRefCount(keyobj);
server.stat_expiredkeys++;
return 1;
} else {
return 0;
}
}
這個函數(shù)的邏輯很簡單胁后,就是先獲取鍵de的過期時間,和現(xiàn)在的時間比較嗦枢,如果過期攀芯,就生成該鍵de的對象,然后傳播該鍵de的過期信息文虏,并且刪除這個鍵侣诺,然后增加過期鍵總數(shù)。
最后就是控制函數(shù)運行時間的部分了氧秘,代碼如下:
/* We can't block forever here even if there are many keys to
* expire. So after a given amount of milliseconds return to the
* caller waiting for the other active expire cycle. */
iteration++;
if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
long long elapsed = ustime()-start;
latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
if (elapsed > timelimit) timelimit_exit = 1;
}
if (timelimit_exit) return;
這里有一個迭代次數(shù)的變量iteration年鸳,每迭代16次就來計算函數(shù)已經(jīng)運行的時間,如果這個時間超過了之前的限定時間timelimit丸相,就將timelimit_exit這個標(biāo)志置為1搔确,說明程序超時,需要強制退出了灭忠。
(3)懶惰淘汰策略:
1)含義:key過期的時候不刪除膳算,每次通過key獲取值的時候去檢查是否過期,若過期弛作,則刪除涕蜂,返回null。
2)優(yōu)點:刪除操作只發(fā)生在通過key取值的時候發(fā)生映琳,而且只刪除當(dāng)前key宇葱,所以對CPU時間的占用是比較少的瘦真,而且此時的刪除是已經(jīng)到了非做不可的地步(如果此時還不刪除的話,我們就會獲取到了已經(jīng)過期的key了)
3)缺點:若大量的key在超出超時時間后黍瞧,很久一段時間內(nèi),都沒有被獲取過原杂,那么可能發(fā)生內(nèi)存泄露(無用的垃圾占用了大量的內(nèi)存)
4)懶惰式策略刪除流程:
1. 在進(jìn)行g(shù)et或setnx等操作時印颤,先檢查key是否過期;
2. 若過期穿肄,刪除key年局,然后執(zhí)行相應(yīng)操作; 若沒過期咸产,直接執(zhí)行相應(yīng)操作矢否;
5)源碼閱讀:
在redis源碼中,實現(xiàn)懶惰淘汰策略的是函數(shù)expireIfNeeded脑溢,所有讀寫數(shù)據(jù)庫命令在執(zhí)行之前都會調(diào)用expireIfNeeded函數(shù)對輸入鍵進(jìn)行檢查僵朗。如果過期就刪除,如果沒過期就正常訪問屑彻。
int expireIfNeeded(redisDb *db, robj *key) {
mstime_t when = getExpire(db,key);
mstime_t now;
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 in the context of a Lua script, we claim that time is
* blocked to when the Lua script started. This way a key can expire
* only the first time it is accessed and not in the middle of the
* script execution, making propagation to slaves / AOF consistent.
* See issue #1525 on Github for more information. */
now = server.lua_caller ? server.lua_time_start : mstime();
/* 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. */
/*如果我們正在slaves上執(zhí)行讀寫命令验庙,就直接返回,
*因為slaves上的過期是由master來發(fā)送刪除命令同步給slaves刪除的社牲,
*slaves不會自主刪除*/
if (server.masterhost != NULL) return now > when;
/*只是回了一個判斷鍵是否過期的值粪薛,0表示沒有過期,1表示過期
*但是并沒有做其他與鍵值過期相關(guān)的操作*/
/* Return when this key has not expired */
/*如果沒有過期搏恤,就返回當(dāng)前鍵*/
if (now <= when) return 0;
/* Delete the key */
/*增加過期鍵個數(shù)*/
server.stat_expiredkeys++;
/*傳播鍵過期的消息*/
propagateExpire(db,key);
notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,"expired",key,db->id);
/*刪除過期鍵*/
return dbDelete(db,key);
}
以上是expireIfNeeded函數(shù)的源碼违寿,源碼中的注釋已經(jīng)很清楚的描述出了它的邏輯,我只是將他翻譯成中文熟空,然后加了一點自己的注釋藤巢。值得注意的如果是slaves,它是不能自主刪除鍵的,需要由master發(fā)del命令痛阻,然后同步到所有的slaves菌瘪,這樣就不會造成主從數(shù)據(jù)不一致的問題。
(4)策略總述:
懶惰淘汰機制和定時淘汰機制是一起合作的阱当,就好像你開一家餐館一樣俏扩,定時淘汰機制就是你每隔幾小時去查看所有的菜品是否還有,如果有的菜品現(xiàn)在賣光了弊添,就將他從菜單上劃掉录淡。懶惰淘汰機制就是有客人要點宮保雞丁,你馬上去查看還有沒有油坝,如果今天的已經(jīng)賣完了嫉戚,就告訴客人不好意思刨裆,我們賣完了,然后將宮保雞丁從菜單上劃掉彬檀。只有等下次有原料再做的時候帆啃,才又把它放到菜單上去。
所以窍帝,在實際中努潘,如果我們要自己設(shè)計過期策略,在使用懶漢式刪除+定期刪除時坤学,控制時長和頻率這個尤為關(guān)鍵疯坤,需要結(jié)合服務(wù)器性能,已經(jīng)并發(fā)量等情況進(jìn)行調(diào)整深浮,以致最佳压怠。
三、對開發(fā)需求而言飞苇,Redis過期策略的設(shè)計實現(xiàn)經(jīng)驗:代碼在此工程里
(1)分析緩存鍵值的客戶方角度菌瘫,調(diào)和服務(wù)器內(nèi)存壓力
基于服務(wù)器內(nèi)存是有限的,但是緩存是必須的玄柠,所以我們就要結(jié)合起來選擇一個平衡點突梦。所以一般來說,我們采取高訪問量緩存策略---就是給那些經(jīng)常被訪問的數(shù)據(jù)羽利,維持它較長的key生存周期宫患。
(2)估算過期時間
這個就要結(jié)合我們自己的業(yè)務(wù)去估量了。
參考因素:數(shù)據(jù)的訪問量这弧、并發(fā)量娃闲,數(shù)據(jù)的變化更新的時間,服務(wù)器數(shù)據(jù)內(nèi)存大小......
(3)Java演示一策略做法匾浪。
每次訪問刷新對應(yīng)key生存時間:
針對經(jīng)常訪問的數(shù)據(jù)的策略
//加進(jìn)redis時皇帮,設(shè)置生存時間
@Override
public String set(String key, String value) {
Jedis jedis = jedisPool.getResource();
String string = jedis.set(key, value);
jedis.expire(key,5);
System.out.println("key : "+key);
System.out.println("查看key的剩余生存時間:"+jedis.ttl(key));
jedis.close();
return string;
}
//從redis獲取時
@Override
public String get(String key) {
Jedis jedis = jedisPool.getResource();
String string = jedis.get(key);
jedis.expire(key,5);//每次訪問刷新時間
jedis.close();
return string;
}
好了,Redis系列(三)--過期策略講完了蛋辈,這是redis使用優(yōu)化必須理解的原理属拾,這是積累的必經(jīng)一步,我會繼續(xù)出這個系列文章冷溶,分享經(jīng)驗給大家渐白。歡迎在下面指出錯誤,共同學(xué)習(xí)3哑怠纯衍!你的點贊是對我最好的支持!苗胀!