Redis學習筆記(三)redis 的鍵管理
Redis 的鍵管理
一、Redis 數據庫管理
Redis 是一個鍵值對(key-value pair)的數據庫服務器,其數據保存在 src/server.h/redisDb 中(網上很多帖子說在 redis.h 文件中,但是 redis 6.x版本目錄中都沒有這個文件。redisDb 結構應該在 server.h文件中)
typedef redisServer {
....
// Redis數據庫
redisDb *db;
....
}
Redis 默認會創建 16 個數據庫,每個數據庫是獨立互不影響。其默認的目標數據庫是 0 號數據庫,可以通過 select 命令來切換目標數據庫。在 redisClient 結構中記錄客戶端當前的目標數據庫:
typedef struct redisClient {
// 套接字描述符
int fd;
// 當前正在使用的數據庫
redisDb *db;
// 當前正在使用的數據庫的 id (號碼)
int dictid;
// 客戶端的名字
robj *name; /* As set by CLIENT SETNAME */
} redisClient;
下面是客戶端和服務器狀態之間的關係實例,客戶端的目標數據庫目前為 1 號數據庫:
通過修改 redisClient.db 的指針來指向不同數據庫,這也就是 select 命令的實現原理。但是,到目前為止,Redis 仍然沒有可以返回客戶端目標數據庫的命令。雖然在 redis-cli 客戶端中輸入時會顯示:
redis> SELECT 1
Ok
redis[1]>
但是在其他語言客戶端沒有顯示目標數據庫的號端,所以在頻繁切換數據庫後,會導致忘記目前使用的是哪一個數據庫,也容易產生誤操作。因此要謹慎處理多數據庫程序,必須要執行時,可以先顯示切換指定數據庫,然後再執行別的命令。
二、Redis 數據庫鍵
2.1 數據庫鍵空間
Redis 服務器中的每一個數據庫是由一個 server.h/redisDb 結構來表示的,其具體結構如下:
typedef struct redisDb {
//數據庫鍵空間
dict *dict; /* The keyspace for this DB */
//鍵的過期時間,字典的值為過期事件 UNIX 時間戳
dict *expires; /* Timeout of keys with a timeout set */
//正處於阻塞狀態的鍵
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/
//可以解除阻塞的鍵
dict *ready_keys; /* Blocked keys that received a PUSH */
//正在被 WATCH 命令監視的鍵
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
//數據庫號端
int id; /* Database ID */
//數據庫鍵的平均 TTL,統計信息
long long avg_ttl; /* Average TTL, just for stats */
//
unsigned long expires_cursor; /* Cursor of the active expire cycle. */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
鍵空間和用戶所見的數據庫是直接對應:
- 鍵空間的 key 就是數據庫的 key, 每個 key 都是一個字符串對象
- 鍵空間的 value 是數據庫的 value, 每個 value 可以是字符串對象、列表對象和集合對象等等任意一種 Redis 對象
舉個實例,若在空白數據庫中執行一下命令:插入字符串對象、列表對象和哈希對象
# 插入一個字符串對象
redis> SET message "hello world"
OK
# 插入包含三個元素的列表對象
redis> RPUSH alphabet "a" "b" "c"
(integer)3
# 插入包含三個元素的哈希表對象
redis> HSET book name "Redis in Action"
(integer) 1
redis> HSET book author "Josiah L. Carlson"
(integer) 1
redis> HSET book publisher "Manning"
(integer) 1
所以說 redis 對數據的增刪改查是通過操作 dict 來操作 Redis 中的數據
2.2 數據庫鍵的過期
我們可以通過兩種方式設置鍵的生命周期:
-
通過 EXPIRE 或者 PEXPIRE 命令來為數據庫中的某個鍵設置生存時間(TTL,Time To Live)。在經過 TTL 個生存時間後,服務器會自動刪除生存時間為0 的鍵。比如:
redis> set key value OK # 設置鍵的 TTL 為 5 redis> EXPIRE key 5 (integer)1
-
此外,客戶端也可以通過 EXPIREAT 或者PEXPIREAT 命令,為數據庫中的某個鍵設置過期時間(expire time)。過期時間是一個 UNIX 時間戳,當過期時間來臨時,服務器就會自動從數據庫中刪除這個鍵。比如
redis> SET key value OK redis> EXPIREAT key 1377257300 (integer) 1 # 當前系統時間 redis> TIME 1)"1377257296" # 過一段時間後,再查詢key redis> GET key // 1377257300 (nil)
2.2.1 過期時間
redisDb 中的dict *dict
和 dict *expires
字典 分別保存了數據庫中的鍵和鍵的過期時間,分別叫做鍵空間和過期字典。
- 過期字典的鍵是一個指向鍵空間中的某個鍵對象
- 過期字典的值是一個 long long 類型的整數,這個整數保存了鍵所指向的數據庫鍵的過期時間
2.3 過期鍵的刪除策略
對於已經過期的數據是如何刪除這些過期鍵的呢?主要有兩種方式:惰性刪除和定期刪除:
1.惰性刪除
是指 Redis 服務器不主動刪除過期的鍵值,而是通過訪問鍵值時,檢查當前的鍵值是否過期
- 如果過期則執行刪除並返回 null
- 沒有過期則正常訪問值信息給客戶端
惰性刪除的源碼在 src/db.c/expireIfNeeded 方法中
int expireIfNeeded(redisDb *db, robj *key) {
// 判斷鍵是否過期
if (!keyIsExpired(db,key)) return 0;
if (server.masterhost != NULL) return 1;
/* 刪除過期鍵 */
// 增加過期鍵個數
server.stat_expiredkeys++;
// 傳播鍵過期的消息
propagateExpire(db,key,server.lazyfree_lazy_expire);
notifyKeyspaceEvent(NOTIFY_EXPIRED,
"expired",key,db->id);
// server.lazyfree_lazy_expire 為 1 表示異步刪除,否則則為同步刪除
return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
dbSyncDelete(db,key);
}
// 判斷鍵是否過期
int keyIsExpired(redisDb *db, robj *key) {
mstime_t when = getExpire(db,key);
if (when < 0) return 0;
if (server.loading) return 0;
mstime_t now = server.lua_caller ? server.lua_time_start : mstime();
return now > when;
}
// 獲取鍵的過期時間
long long getExpire(redisDb *db, robj *key) {
dictEntry *de;
if (dictSize(db->expires) == 0 ||
(de = dictFind(db->expires,key->ptr)) == NULL) return -1;
serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
return dictGetSignedIntegerVal(de);
}
2.定期刪除
與惰性刪除不同,定期刪除是指 Redis 服務器會每隔一段時間就會檢查一下數據庫,看看是否有過期鍵可以清除,默認情況下,Redis 定期檢查的頻率是每秒掃描 10 次,這個值在 redis.conf 中的 “hz” , 默認是 10 ,可以進行修改。
定期刪除的掃描並不是遍歷所有的鍵值對,這樣的話比較費時且太消耗系統資源。Redis 服務器採用的是隨機抽取形式,每次從過期字典中,取出 20 個鍵進行過期檢測,過期字典中存儲的是所有設置了過期時間的鍵值對。如果這批隨機檢查的數據中有 25% 的比例過期,那麼會再抽取 20 個隨機鍵值進行檢測和刪除,並且會循環執行這個流程,直到抽取的這批數據中過期鍵值小於 25%,此次檢測才算完成。
定期刪除的源碼在 expire.c/activeExpireCycle 方法中:
void activeExpireCycle(int type) {
static unsigned int current_db = 0; /* 上次定期刪除遍歷到的數據庫ID */
static int timelimit_exit = 0;
static long long last_fast_cycle = 0; /* 上次執行定期刪除的時間點 */
int j, iteration = 0;
int dbs_per_call = CRON_DBS_PER_CALL; // 需要遍曆數據庫的數量
long long start = ustime(), timelimit, elapsed;
if (clientsArePaused()) return;
if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
if (!timelimit_exit) return;
// ACTIVE_EXPIRE_CYCLE_FAST_DURATION 快速定期刪除的執行時長
if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
last_fast_cycle = start;
}
if (dbs_per_call > server.dbnum || timelimit_exit)
dbs_per_call = server.dbnum;
// 慢速定期刪除的執行時長
timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
timelimit_exit = 0;
if (timelimit <= 0) timelimit = 1;
if (type == ACTIVE_EXPIRE_CYCLE_FAST)
timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* 刪除操作花費的時間 */
long total_sampled = 0;
long total_expired = 0;
for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
int expired;
redisDb *db = server.db+(current_db % server.dbnum);
current_db++;
do {
// .......
expired = 0;
ttl_sum = 0;
ttl_samples = 0;
// 每個數據庫中檢查的鍵的數量
if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
// 從數據庫中隨機選取 num 個鍵進行檢查
while (num--) {
dictEntry *de;
long long ttl;
if ((de = dictGetRandomKey(db->expires)) == NULL) break;
ttl = dictGetSignedInteger
// 過期檢查,並對過期鍵進行刪除
if (activeExpireCycleTryExpire(db,de,now)) expired++;
if (ttl > 0) {
ttl_sum += ttl;
ttl_samples++;
}
total_sampled++;
}
total_expired += expired;
if (ttl_samples) {
long long avg_ttl = ttl_sum/ttl_samples;
if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
}
if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
elapsed = ustime()-start;
if (elapsed > timelimit) {
timelimit_exit = 1;
server.stat_expired_time_cap_reached_count++;
break;
}
}
/* 判斷過期鍵刪除數量是否超過 25% */
} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
}
// .......
}
以上就是Redis 的刪除策略。下面來看一個面試題:
面試題:你知道 Redis 內存淘汰策略和鍵的刪除策略的區別嗎?
Redis 內存淘汰策略
我們可以通過 config get maxmemory-policy 命令來查看當前 Redis 的內存淘汰策略:
127.0.0.1:6379> config get maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"
當前服務器設置的是 noeviction 類型的,對於 redis 6.x版本,主要有以下幾種內存淘汰策略
- noeviction:不淘汰任何數據,當內存不足時,執行緩存新增操作會報錯,它是 Redis 默認內存淘汰策略。
- allkeys-lru:淘汰整個鍵值中最久未使用的鍵值。
- allkeys-random:隨機淘汰任意鍵值。
- volatile-lru:淘汰所有設置了過期時間的鍵值中最久未使用的鍵值。
- volatile-random:隨機淘汰設置了過期時間的任意鍵值。
- volatile-ttl:優先淘汰更早過期的鍵值。
- volatile-lfu: 淘汰所有設置了過期時間的鍵值中最少使用的鍵值。
- alkeys-lfu: 淘汰整個鍵值中最少使用的鍵值
也就是 alkeys 開頭的表示從所有鍵值中淘汰相關數據,而 volatile 表示從設置了過期鍵的鍵值中淘汰數據。
Redis 內存淘汰算法
內存淘汰算法主要分為 LRU 和 LFU 淘汰算法
LRU(Least Recently Used) 淘汰算法
是一種常用的頁面置換算法,LRU 是基於鏈表結構實現,鏈表中的元素按照操作順序從前往後排列。最新操作的鍵會被移動到表頭,當需要進行內存淘汰時,只需要刪除鏈表尾部的元素。
Redis 使用的是一種近似 LRU 算法,目的是為了更好的節約內存,給現有的數據結構添加一個額外的字段,用於記錄此鍵值的最後一次訪問時間。Redis 內存淘汰時,會使用隨機採樣的方式來淘汰數據,隨機取5個值,然後淘汰最久沒有使用的數據。
LFU(Least Frequently Used)淘汰算法
根據總訪問次數來淘汰數據,核心思想是如果數據過去被訪問多次,那麼將來被訪問的頻率也更高
參考資料
《Redis 設計與實現》
//kaiwu.lagou.com/course/courseInfo.htm?courseId=59#/detail/pc?id=1779