Redis數(shù)據(jù)庫的鍵管理示例詳解
一、Redis 數(shù)據(jù)庫管理
Redis 是一個鍵值對(key-value pair)的數(shù)據(jù)庫服務(wù)器,其數(shù)據(jù)保存在 src/server.h/redisDb 中(網(wǎng)上很多帖子說在 redis.h 文件中,但是 redis 6.x版本目錄中都沒有這個文件。redisDb 結(jié)構(gòu)應(yīng)該在 server.h文件中)
typedef redisServer { .... // Redis數(shù)據(jù)庫 redisDb *db; .... }
Redis 默認(rèn)會創(chuàng)建 16 個數(shù)據(jù)庫,每個數(shù)據(jù)庫是獨立互不影響。其默認(rèn)的目標(biāo)數(shù)據(jù)庫是 0 號數(shù)據(jù)庫,可以通過 select 命令來切換目標(biāo)數(shù)據(jù)庫。在 redisClient 結(jié)構(gòu)中記錄客戶端當(dāng)前的目標(biāo)數(shù)據(jù)庫:
typedef struct redisClient { // 套接字描述符 int fd; // 當(dāng)前正在使用的數(shù)據(jù)庫 redisDb *db; // 當(dāng)前正在使用的數(shù)據(jù)庫的 id (號碼) int dictid; // 客戶端的名字 robj *name; /* As set by CLIENT SETNAME */ } redisClient;
下面是客戶端和服務(wù)器狀態(tài)之間的關(guān)系實例,客戶端的目標(biāo)數(shù)據(jù)庫目前為 1 號數(shù)據(jù)庫:
通過修改 redisClient.db 的指針來指向不同數(shù)據(jù)庫,這也就是 select 命令的實現(xiàn)原理。但是,到目前為止,Redis 仍然沒有可以返回客戶端目標(biāo)數(shù)據(jù)庫的命令。雖然在 redis-cli 客戶端中輸入時會顯示:
redis> SELECT 1 Ok redis[1]>
但是在其他語言客戶端沒有顯示目標(biāo)數(shù)據(jù)庫的號端,所以在頻繁切換數(shù)據(jù)庫后,會導(dǎo)致忘記目前使用的是哪一個數(shù)據(jù)庫,也容易產(chǎn)生誤操作。因此要謹(jǐn)慎處理多數(shù)據(jù)庫程序,必須要執(zhí)行時,可以先顯示切換指定數(shù)據(jù)庫,然后再執(zhí)行別的命令。
二、Redis 數(shù)據(jù)庫鍵
2.1 數(shù)據(jù)庫鍵空間
Redis 服務(wù)器中的每一個數(shù)據(jù)庫是由一個 server.h/redisDb 結(jié)構(gòu)來表示的,其具體結(jié)構(gòu)如下:
typedef struct redisDb { //數(shù)據(jù)庫鍵空間 dict *dict; /* The keyspace for this DB */ //鍵的過期時間,字典的值為過期事件 UNIX 時間戳 dict *expires; /* Timeout of keys with a timeout set */ //正處于阻塞狀態(tài)的鍵 dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/ //可以解除阻塞的鍵 dict *ready_keys; /* Blocked keys that received a PUSH */ //正在被 WATCH 命令監(jiān)視的鍵 dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */ //數(shù)據(jù)庫號端 int id; /* Database ID */ //數(shù)據(jù)庫鍵的平均 TTL,統(tǒng)計信息 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;
鍵空間和用戶所見的數(shù)據(jù)庫是直接對應(yīng):
- 鍵空間的 key 就是數(shù)據(jù)庫的 key, 每個 key 都是一個字符串對象
- 鍵空間的 value 是數(shù)據(jù)庫的 value, 每個 value 可以是字符串對象、列表對象和集合對象等等任意一種 Redis 對象
舉個實例,若在空白數(shù)據(jù)庫中執(zhí)行一下命令:插入字符串對象、列表對象和哈希對象
# 插入一個字符串對象 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 對數(shù)據(jù)的增刪改查是通過操作 dict 來操作 Redis 中的數(shù)據(jù)
2.2 數(shù)據(jù)庫鍵的過期
我們可以通過兩種方式設(shè)置鍵的生命周期:
通過 EXPIRE 或者 PEXPIRE 命令來為數(shù)據(jù)庫中的某個鍵設(shè)置生存時間(TTL,Time To Live)。在經(jīng)過 TTL 個生存時間后,服務(wù)器會自動刪除生存時間為0 的鍵。比如:
redis> set key value OK # 設(shè)置鍵的 TTL 為 5 redis> EXPIRE key 5 (integer)1
此外,客戶端也可以通過 EXPIREAT 或者PEXPIREAT 命令,為數(shù)據(jù)庫中的某個鍵設(shè)置過期時間(expire time)。過期時間是一個 UNIX 時間戳,當(dāng)過期時間來臨時,服務(wù)器就會自動從數(shù)據(jù)庫中刪除這個鍵。比如
redis> SET key value OK redis> EXPIREAT key 1377257300 (integer) 1 # 當(dāng)前系統(tǒng)時間 redis> TIME 1)"1377257296" # 過一段時間后,再查詢key redis> GET key // 1377257300 (nil)
2.2.1 過期時間
redisDb 中的dict *dict
和 dict *expires
字典 分別保存了數(shù)據(jù)庫中的鍵和鍵的過期時間,分別叫做鍵空間和過期字典。
- 過期字典的鍵是一個指向鍵空間中的某個鍵對象
- 過期字典的值是一個 long long 類型的整數(shù),這個整數(shù)保存了鍵所指向的數(shù)據(jù)庫鍵的過期時間
2.3 過期鍵的刪除策略
對于已經(jīng)過期的數(shù)據(jù)是如何刪除這些過期鍵的呢?主要有兩種方式:惰性刪除和定期刪除:
1.惰性刪除
是指 Redis 服務(wù)器不主動刪除過期的鍵值,而是通過訪問鍵值時,檢查當(dāng)前的鍵值是否過期
- 如果過期則執(zhí)行刪除并返回 null
- 沒有過期則正常訪問值信息給客戶端
惰性刪除的源碼在 src/db.c/expireIfNeeded 方法中
int expireIfNeeded(redisDb *db, robj *key) { // 判斷鍵是否過期 if (!keyIsExpired(db,key)) return 0; if (server.masterhost != NULL) return 1; /* 刪除過期鍵 */ // 增加過期鍵個數(shù) 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 服務(wù)器會每隔一段時間就會檢查一下數(shù)據(jù)庫,看看是否有過期鍵可以清除,默認(rèn)情況下,Redis 定期檢查的頻率是每秒掃描 10 次,這個值在 redis.conf 中的 "hz" , 默認(rèn)是 10 ,可以進(jìn)行修改。
定期刪除的掃描并不是遍歷所有的鍵值對,這樣的話比較費時且太消耗系統(tǒng)資源。Redis 服務(wù)器采用的是隨機(jī)抽取形式,每次從過期字典中,取出 20 個鍵進(jìn)行過期檢測,過期字典中存儲的是所有設(shè)置了過期時間的鍵值對。如果這批隨機(jī)檢查的數(shù)據(jù)中有 25% 的比例過期,那么會再抽取 20 個隨機(jī)鍵值進(jìn)行檢測和刪除,并且會循環(huán)執(zhí)行這個流程,直到抽取的這批數(shù)據(jù)中過期鍵值小于 25%,此次檢測才算完成。
定期刪除的源碼在 expire.c/activeExpireCycle 方法中:
void activeExpireCycle(int type) { static unsigned int current_db = 0; /* 上次定期刪除遍歷到的數(shù)據(jù)庫ID */ static int timelimit_exit = 0; static long long last_fast_cycle = 0; /* 上次執(zhí)行定期刪除的時間點 */ int j, iteration = 0; int dbs_per_call = CRON_DBS_PER_CALL; // 需要遍歷數(shù)據(jù)庫的數(shù)量 long long start = ustime(), timelimit, elapsed; if (clientsArePaused()) return; if (type == ACTIVE_EXPIRE_CYCLE_FAST) { if (!timelimit_exit) return; // ACTIVE_EXPIRE_CYCLE_FAST_DURATION 快速定期刪除的執(zhí)行時長 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; // 慢速定期刪除的執(zhí)行時長 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; // 每個數(shù)據(jù)庫中檢查的鍵的數(shù)量 if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP) num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP; // 從數(shù)據(jù)庫中隨機(jī)選取 num 個鍵進(jìn)行檢查 while (num--) { dictEntry *de; long long ttl; if ((de = dictGetRandomKey(db->expires)) == NULL) break; ttl = dictGetSignedInteger // 過期檢查,并對過期鍵進(jìn)行刪除 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; } } /* 判斷過期鍵刪除數(shù)量是否超過 25% */ } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4); } // ....... }
以上就是Redis 的刪除策略。下面來看一個面試題:
面試題:你知道 Redis 內(nèi)存淘汰策略和鍵的刪除策略的區(qū)別嗎?
Redis 內(nèi)存淘汰策略
我們可以通過 config get maxmemory-policy 命令來查看當(dāng)前 Redis 的內(nèi)存淘汰策略:
127.0.0.1:6379> config get maxmemory-policy 1) "maxmemory-policy" 2) "noeviction"
當(dāng)前服務(wù)器設(shè)置的是 noeviction 類型的,對于 redis 6.x版本,主要有以下幾種內(nèi)存淘汰策略
- noeviction:不淘汰任何數(shù)據(jù),當(dāng)內(nèi)存不足時,執(zhí)行緩存新增操作會報錯,它是 Redis 默認(rèn)內(nèi)存淘汰策略。
- allkeys-lru:淘汰整個鍵值中最久未使用的鍵值。
- allkeys-random:隨機(jī)淘汰任意鍵值。
- volatile-lru:淘汰所有設(shè)置了過期時間的鍵值中最久未使用的鍵值。
- volatile-random:隨機(jī)淘汰設(shè)置了過期時間的任意鍵值。
- volatile-ttl:優(yōu)先淘汰更早過期的鍵值。
- volatile-lfu: 淘汰所有設(shè)置了過期時間的鍵值中最少使用的鍵值。
- alkeys-lfu: 淘汰整個鍵值中最少使用的鍵值
也就是 alkeys 開頭的表示從所有鍵值中淘汰相關(guān)數(shù)據(jù),而 volatile 表示從設(shè)置了過期鍵的鍵值中淘汰數(shù)據(jù)。
Redis 內(nèi)存淘汰算法
內(nèi)存淘汰算法主要分為 LRU 和 LFU 淘汰算法
LRU(Least Recently Used) 淘汰算法
是一種常用的頁面置換算法,LRU 是基于鏈表結(jié)構(gòu)實現(xiàn),鏈表中的元素按照操作順序從前往后排列。最新操作的鍵會被移動到表頭,當(dāng)需要進(jìn)行內(nèi)存淘汰時,只需要刪除鏈表尾部的元素。
Redis 使用的是一種近似 LRU 算法,目的是為了更好的節(jié)約內(nèi)存,給現(xiàn)有的數(shù)據(jù)結(jié)構(gòu)添加一個額外的字段,用于記錄此鍵值的最后一次訪問時間。Redis 內(nèi)存淘汰時,會使用隨機(jī)采樣的方式來淘汰數(shù)據(jù),隨機(jī)取5個值,然后淘汰最久沒有使用的數(shù)據(jù)。
LFU(Least Frequently Used)淘汰算法
根據(jù)總訪問次數(shù)來淘汰數(shù)據(jù),核心思想是如果數(shù)據(jù)過去被訪問多次,那么將來被訪問的頻率也更高
以上就是Redis數(shù)據(jù)庫的鍵管理示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Redis數(shù)據(jù)庫鍵管理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
聊聊使用RedisTemplat實現(xiàn)簡單的分布式鎖的問題
這篇文章主要介紹了使用RedisTemplat實現(xiàn)簡單的分布式鎖問題,文中給大家介紹在SpringBootTest中編寫測試模塊的詳細(xì)代碼,需要的朋友可以參考下2021-11-11Redisson實現(xiàn)Redis分布式鎖的幾種方式
本文在講解如何使用Redisson實現(xiàn)Redis普通分布式鎖,以及Redlock算法分布式鎖的幾種方式的同時,也附帶解答這些同學(xué)的一些疑問,感興趣的可以了解一下2021-08-08Redisson實現(xiàn)分布式鎖、鎖續(xù)約的案例
這篇文章主要介紹了Redisson如何實現(xiàn)分布式鎖、鎖續(xù)約,本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-03-03redis鍵值出現(xiàn)\xac\xed\x00\x05t\x00&的問題及解決
這篇文章主要介紹了redis鍵值出現(xiàn)\xac\xed\x00\x05t\x00&的問題及解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07Redis字典實現(xiàn)、Hash鍵沖突及漸進(jìn)式rehash詳解
這篇文章主要介紹了Redis字典實現(xiàn)、Hash鍵沖突以及漸進(jìn)式rehash的相關(guān)知識,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-09-09