1、Redis中key的的過期時間
通過EXPIRE key seconds命令來設置數據的過期時間。返回1表明設置成功,返回0表明key不存在或者不能成功設置過期時間。在key上設置了過期時間后key將在指定的秒數后被自動刪除。被指定了過期時間的key在Redis中被稱為是不穩定的。
當key被DEL命令刪除或者被SET、GETSET命令重置后與之關聯的過期時間會被清除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
127.0.0.1:6379> setex s 20 1 OK 127.0.0.1:6379> ttl s (integer) 17 127.0.0.1:6379> setex s 200 1 OK 127.0.0.1:6379> ttl s (integer) 195 127.0.0.1:6379> setrange s 3 100 (integer) 6 127.0.0.1:6379> ttl s (integer) 152 127.0.0.1:6379> get s "1\x00\x00100" 127.0.0.1:6379> ttl s (integer) 108 127.0.0.1:6379> getset s 200 "1\x00\x00100" 127.0.0.1:6379> get s "200" 127.0.0.1:6379> ttl s (integer) -1 |
使用PERSIST可以清除過期時間
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
127.0.0.1:6379> setex s 100 test OK 127.0.0.1:6379> get s "test" 127.0.0.1:6379> ttl s (integer) 94 127.0.0.1:6379> type s string 127.0.0.1:6379> strlen s (integer) 4 127.0.0.1:6379> persist s (integer) 1 127.0.0.1:6379> ttl s (integer) -1 127.0.0.1:6379> get s "test" |
使用rename只是改了key值
1
2
3
4
5
6
7
8
9
10
11
12
|
127.0.0.1:6379> expire s 200 (integer) 1 127.0.0.1:6379> ttl s (integer) 198 127.0.0.1:6379> rename s ss OK 127.0.0.1:6379> ttl ss (integer) 187 127.0.0.1:6379> type ss string 127.0.0.1:6379> get ss "test" |
說明:Redis2.6以后expire精度可以控制在0到1毫秒內,key的過期信息以絕對Unix時間戳的形式存儲(Redis2.6之后以毫秒級別的精度存儲),所以在多服務器同步的時候,一定要同步各個服務器的時間
2、Redis過期鍵刪除策略
Redis key過期的方式有三種:
- 被動刪除:當讀/寫一個已經過期的key時,會觸發惰性刪除策略,直接刪除掉這個過期key
- 主動刪除:由于惰性刪除策略無法保證冷數據被及時刪掉,所以Redis會定期主動淘汰一批已過期的key
- 當前已用內存超過maxmemory限定時,觸發主動清理策略
被動刪除
只有key被操作時(如GET),REDIS才會被動檢查該key是否過期,如果過期則刪除之并且返回NIL。
1、這種刪除策略對CPU是友好的,刪除操作只有在不得不的情況下才會進行,不會其他的expire key上浪費無謂的CPU時間。
2、但是這種策略對內存不友好,一個key已經過期,但是在它被操作之前不會被刪除,仍然占據內存空間。如果有大量的過期鍵存在但是又很少被訪問到,那會造成大量的內存空間浪費。expireIfNeeded(redisDb *db, robj *key)
函數位于src/db.c。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
|
/*----------------------------------------------------------------------------- * Expires API *----------------------------------------------------------------------------*/ int removeExpire(redisDb *db, robj *key) { /* An expire may only be removed if there is a corresponding entry in the * main dict. Otherwise, the key will never be freed. */ redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL); return dictDelete(db->expires,key->ptr) == DICT_OK; } void setExpire(redisDb *db, robj *key, long long when) { dictEntry *kde, *de; /* Reuse the sds from the main dict in the expire dict */ kde = dictFind(db->dict,key->ptr); redisAssertWithInfo(NULL,key,kde != NULL); de = dictReplaceRaw(db->expires,dictGetKey(kde)); dictSetSignedIntegerVal(de,when); } /* Return the expire time of the specified key, or -1 if no expire * is associated with this key (i.e. the key is non volatile) */ long long getExpire(redisDb *db, robj *key) { dictEntry *de; /* No expire? return ASAP */ if (dictSize(db->expires) == 0 || (de = dictFind(db->expires,key->ptr)) == NULL) return -1; /* The entry was found in the expire dict, this means it should also * be present in the main dict (safety check). */ redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL); return dictGetSignedIntegerVal(de); } /* Propagate expires into slaves and the AOF file. * When a key expires in the master, a DEL operation for this key is sent * to all the slaves and the AOF file if enabled. * * This way the key expiry is centralized in one place, and since both * AOF and the master->slave link guarantee operation ordering, everything * will be consistent even if we allow write operations against expiring * keys. */ void propagateExpire(redisDb *db, robj *key) { robj *argv[2]; argv[0] = shared.del; argv[1] = key; incrRefCount(argv[0]); incrRefCount(argv[1]); if (server.aof_state != REDIS_AOF_OFF) feedAppendOnlyFile(server.delCommand,db->id,argv,2); replicationFeedSlaves(server.slaves,db->id,argv,2); decrRefCount(argv[0]); decrRefCount(argv[1]); } 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. */ if (server.masterhost != NULL) return now > when; /* Return when this key has not expired */ if (now <= when) return 0; /* Delete the key */ server.stat_expiredkeys++; propagateExpire(db,key); notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED, "expired",key,db->id); return dbDelete(db,key); } /*----------------------------------------------------------------------------- * Expires Commands *----------------------------------------------------------------------------*/ /* This is the generic command implementation for EXPIRE, PEXPIRE, EXPIREAT * and PEXPIREAT. Because the commad second argument may be relative or absolute * the "basetime" argument is used to signal what the base time is (either 0 * for *AT variants of the command, or the current time for relative expires). * * unit is either UNIT_SECONDS or UNIT_MILLISECONDS, and is only used for * the argv[2] parameter. The basetime is always specified in milliseconds. */ void expireGenericCommand(redisClient *c, long long basetime, int unit) { robj *key = c->argv[1], *param = c->argv[2]; long long when; /* unix time in milliseconds when the key will expire. */ if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK) return; if (unit == UNIT_SECONDS) when *= 1000; when += basetime; /* No key, return zero. */ if (lookupKeyRead(c->db,key) == NULL) { addReply(c,shared.czero); return; } /* EXPIRE with negative TTL, or EXPIREAT with a timestamp into the past * should never be executed as a DEL when load the AOF or in the context * of a slave instance. * * Instead we take the other branch of the IF statement setting an expire * (possibly in the past) and wait for an explicit DEL from the master. */ if (when <= mstime() && !server.loading && !server.masterhost) { robj *aux; redisAssertWithInfo(c,key,dbDelete(c->db,key)); server.dirty++; /* Replicate/AOF this as an explicit DEL. */ aux = createStringObject("DEL",3); rewriteClientCommandVector(c,2,aux,key); decrRefCount(aux); signalModifiedKey(c->db,key); notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",key,c->db->id); addReply(c, shared.cone); return; } else { setExpire(c->db,key,when); addReply(c,shared.cone); signalModifiedKey(c->db,key); notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"expire",key,c->db->id); server.dirty++; return; } } void expireCommand(redisClient *c) { expireGenericCommand(c,mstime(),UNIT_SECONDS); } void expireatCommand(redisClient *c) { expireGenericCommand(c,0,UNIT_SECONDS); } void pexpireCommand(redisClient *c) { expireGenericCommand(c,mstime(),UNIT_MILLISECONDS); } void pexpireatCommand(redisClient *c) { expireGenericCommand(c,0,UNIT_MILLISECONDS); } void ttlGenericCommand(redisClient *c, int output_ms) { long long expire, ttl = -1; /* If the key does not exist at all, return -2 */ if (lookupKeyRead(c->db,c->argv[1]) == NULL) { addReplyLongLong(c,-2); return; } /* The key exists. Return -1 if it has no expire, or the actual * TTL value otherwise. */ expire = getExpire(c->db,c->argv[1]); if (expire != -1) { ttl = expire-mstime(); if (ttl < 0) ttl = 0; } if (ttl == -1) { addReplyLongLong(c,-1); } else { addReplyLongLong(c,output_ms ? ttl : ((ttl+500)/1000)); } } void ttlCommand(redisClient *c) { ttlGenericCommand(c, 0); } void pttlCommand(redisClient *c) { ttlGenericCommand(c, 1); } void persistCommand(redisClient *c) { dictEntry *de; de = dictFind(c->db->dict,c->argv[1]->ptr); if (de == NULL) { addReply(c,shared.czero); } else { if (removeExpire(c->db,c->argv[1])) { addReply(c,shared.cone); server.dirty++; } else { addReply(c,shared.czero); } } } |
但僅是這樣是不夠的,因為可能存在一些key永遠不會被再次訪問到,這些設置了過期時間的key也是需要在過期后被刪除的,我們甚至可以將這種情況看作是一種內存泄露----無用的垃圾數據占用了大量的內存,而服務器卻不會自己去釋放它們,這對于運行狀態非常依賴于內存的Redis服務器來說,肯定不是一個好消息
主動刪除
先說一下時間事件,對于持續運行的服務器來說, 服務器需要定期對自身的資源和狀態進行必要的檢查和整理, 從而讓服務器維持在一個健康穩定的狀態, 這類操作被統稱為常規操作(cron job)
在 Redis 中, 常規操作由 redis.c/serverCron 實現, 它主要執行以下操作
- 更新服務器的各類統計信息,比如時間、內存占用、數據庫占用情況等。
- 清理數據庫中的過期鍵值對。
- 對不合理的數據庫進行大小調整。
- 關閉和清理連接失效的客戶端。
- 嘗試進行 AOF 或 RDB 持久化操作。
- 如果服務器是主節點的話,對附屬節點進行定期同步。
- 如果處于集群模式的話,對集群進行定期同步和連接測試。
Redis 將 serverCron 作為時間事件來運行, 從而確保它每隔一段時間就會自動運行一次, 又因為 serverCron 需要在 Redis 服務器運行期間一直定期運行, 所以它是一個循環時間事件: serverCron 會一直定期執行,直到服務器關閉為止。
在 Redis 2.6 版本中, 程序規定 serverCron 每秒運行 10 次, 平均每 100 毫秒運行一次。 從 Redis 2.8 開始, 用戶可以通過修改 hz選項來調整 serverCron 的每秒執行次數, 具體信息請參考 redis.conf 文件中關于 hz 選項的說明
也叫定時刪除,這里的“定期”指的是Redis定期觸發的清理策略,由位于src/redis.c的activeExpireCycle(void)
函數來完成。
serverCron是由redis的事件框架驅動的定位任務,這個定時任務中會調用activeExpireCycle函數,針對每個db在限制的時間REDIS_EXPIRELOOKUPS_TIME_LIMIT內遲可能多的刪除過期key,之所以要限制時間是為了防止過長時間 的阻塞影響redis的正常運行。這種主動刪除策略彌補了被動刪除策略在內存上的不友好。
因此,Redis會周期性的隨機測試一批設置了過期時間的key并進行處理。測試到的已過期的key將被刪除。
典型的方式為,Redis每秒做10次如下的步驟:
- 隨機測試100個設置了過期時間的key
- 刪除所有發現的已過期的key
- 若刪除的key超過25個則重復步驟1
這是一個基于概率的簡單算法,基本的假設是抽出的樣本能夠代表整個key空間,redis持續清理過期的數據直至將要過期的key的百分比降到了25%以下。這也意味著在任何給定的時刻已經過期但仍占據著內存空間的key的量最多為每秒的寫操作量除以4.
Redis-3.0.0中的默認值是10,代表每秒鐘調用10次后臺任務。
除了主動淘汰的頻率外,Redis對每次淘汰任務執行的最大時長也有一個限定,這樣保證了每次主動淘汰不會過多阻塞應用請求,以下是這個限定計算公式:
1
2
3
|
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* CPU max % for keys collection */ ... timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100; |
hz調大將會提高Redis主動淘汰的頻率,如果你的Redis存儲中包含很多冷數據占用內存過大的話,可以考慮將這個值調大,但Redis作者建議這個值不要超過100。我們實際線上將這個值調大到100,觀察到CPU會增加2%左右,但對冷數據的內存釋放速度確實有明顯的提高(通過觀察keyspace個數和used_memory大?。?。
可以看出timelimit和server.hz是一個倒數的關系,也就是說hz配置越大,timelimit就越小。換句話說是每秒鐘期望的主動淘汰頻率越高,則每次淘汰最長占用時間就越短。這里每秒鐘的最長淘汰占用時間是固定的250ms(1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/100),而淘汰頻率和每次淘汰的最長時間是通過hz參數控制的。
從以上的分析看,當redis中的過期key比率沒有超過25%之前,提高hz可以明顯提高掃描key的最小個數。假設hz為10,則一秒內最少掃描200個key(一秒調用10次*每次最少隨機取出20個key),如果hz改為100,則一秒內最少掃描2000個key;另一方面,如果過期key比率超過25%,則掃描key的個數無上限,但是cpu時間每秒鐘最多占用250ms。
當REDIS運行在主從模式時,只有主結點才會執行上述這兩種過期刪除策略,然后把刪除操作”del key”同步到從結點。
maxmemory
當前已用內存超過maxmemory限定時,觸發主動清理策略
- volatile-lru:只對設置了過期時間的key進行LRU(默認值)
- allkeys-lru : 刪除lru算法的key
- volatile-random:隨機刪除即將過期key
- allkeys-random:隨機刪除
- volatile-ttl : 刪除即將過期的
- noeviction : 永不過期,返回錯誤當mem_used內存已經超過maxmemory的設定,對于所有的讀寫請求,都會觸發redis.c/freeMemoryIfNeeded(void)函數以清理超出的內存。注意這個清理過程是阻塞的,直到清理出足夠的內存空間。所以如果在達到maxmemory并且調用方還在不斷寫入的情況下,可能會反復觸發主動清理策略,導致請求會有一定的延遲。
當mem_used內存已經超過maxmemory的設定,對于所有的讀寫請求,都會觸發redis.c/freeMemoryIfNeeded(void)
函數以清理超出的內存。注意這個清理過程是阻塞的,直到清理出足夠的內存空間。所以如果在達到maxmemory并且調用方還在不斷寫入的情況下,可能會反復觸發主動清理策略,導致請求會有一定的延遲。
清理時會根據用戶配置的maxmemory-policy來做適當的清理(一般是LRU或TTL),這里的LRU或TTL策略并不是針對redis的所有key,而是以配置文件中的maxmemory-samples個key作為樣本池進行抽樣清理。
maxmemory-samples在redis-3.0.0中的默認配置為5,如果增加,會提高LRU或TTL的精準度,redis作者測試的結果是當這個配置為10時已經非常接近全量LRU的精準度了,并且增加maxmemory-samples會導致在主動清理時消耗更多的CPU時間,建議:
- 盡量不要觸發maxmemory,最好在mem_used內存占用達到maxmemory的一定比例后,需要考慮調大hz以加快淘汰,或者進行集群擴容。
- 如果能夠控制住內存,則可以不用修改maxmemory-samples配置;如果Redis本身就作為LRU cache服務(這種服務一般長時間處于maxmemory狀態,由Redis自動做LRU淘汰),可以適當調大maxmemory-samples。
以下是上文中提到的配置參數的說明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
# Redis calls an internal function to perform many background tasks, like # closing connections of clients in timeout, purging expired keys that are # never requested, and so forth. # # Not all tasks are performed with the same frequency, but Redis checks for # tasks to perform according to the specified "hz" value. # # By default "hz" is set to 10. Raising the value will use more CPU when # Redis is idle, but at the same time will make Redis more responsive when # there are many keys expiring at the same time, and timeouts may be # handled with more precision. # # The range is between 1 and 500, however a value over 100 is usually not # a good idea. Most users should use the default of 10 and raise this up to # 100 only in environments where very low latency is required. hz 10 # MAXMEMORY POLICY: how Redis will select what to remove when maxmemory # is reached. You can select among five behaviors: # # volatile-lru -> remove the key with an expire set using an LRU algorithm # allkeys-lru -> remove any key according to the LRU algorithm # volatile-random -> remove a random key with an expire set # allkeys-random -> remove a random key, any key # volatile-ttl -> remove the key with the nearest expire time (minor TTL) # noeviction -> don't expire at all, just return an error on write operations # # Note: with any of the above policies, Redis will return an error on write # operations, when there are no suitable keys for eviction. # # At the date of writing these commands are: set setnx setex append # incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd # sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby # zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby # getset mset msetnx exec sort # # The default is: # maxmemory-policy noeviction # LRU and minimal TTL algorithms are not precise algorithms but approximated # algorithms (in order to save memory), so you can tune it for speed or # accuracy. For default Redis will check five keys and pick the one that was # used less recently, you can change the sample size using the following # configuration directive. # # The default of 5 produces good enough results. 10 Approximates very closely # true LRU but costs a bit more CPU. 3 is very fast but not very accurate. # maxmemory-samples 5 |
Replication link和AOF文件中的過期處理
為了獲得正確的行為而不至于導致一致性問題,當一個key過期時DEL操作將被記錄在AOF文件并傳遞到所有相關的slave。也即過期刪除操作統一在master實例中進行并向下傳遞,而不是各salve各自掌控。這樣一來便不會出現數據不一致的情形。當slave連接到master后并不能立即清理已過期的key(需要等待由master傳遞過來的DEL操作),slave仍需對數據集中的過期狀態進行管理維護以便于在slave被提升為master會能像master一樣獨立的進行過期處理。
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作能帶來一定的幫助,如果有疑問大家可以留言交流。