01 Redis源码起航
源碼解讀
Redis 源碼文件分布大致如下:
redisDb結(jié)構(gòu)介紹
首先看下redisDb的結(jié)構(gòu),在server.h文件中:
typedef struct redisDb {dict *dict; /* 保存數(shù)據(jù)庫中所有的鍵值對 */dict *expires; /* 保存是所有有過期時間的鍵值對 */dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) 客戶端等待數(shù)據(jù)的鍵-客戶端和鍵值的映射*/dict *ready_keys; /* Blocked keys that received a PUSH */dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */int id; /* Database ID 數(shù)據(jù)庫ID*/long long avg_ttl; /* Average TTL, just for stats 平均TTL時間,僅用于統(tǒng)計*/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;- dict:保存db中所有的鍵值對
- expires:保存所有有過期時間的鍵值對
- expires_cursor:周期性刪除過期鍵的游標(biāo),應(yīng)該是6.0版本以后才有的,因為書里和網(wǎng)上的一些介紹都沒有這個字段
- dictType:指定的數(shù)據(jù)類型,中間可以指定hash函數(shù)
- dictht:hashtable的縮寫,【2】在單線程的情況下擴容會影響響應(yīng)應(yīng)能造成卡頓,通過漸進式的方式對舊 hasht 槽位中的dictEntry進行數(shù)據(jù)遷移,沒有請求的時候也會進行數(shù)據(jù)遷移,通過事件輪巡遷移數(shù)據(jù),直到舊的數(shù)據(jù)遷移完。兩個hashtable中的數(shù)據(jù)相互倒換。
- hashFunction:指定hash函數(shù)
- keyCompare:類似于equals比較方法
數(shù)據(jù)擴容邏輯
在元數(shù)個數(shù)與容量是1:1的時候回觸發(fā)擴容,擴容邏輯是,請求訪問key值的時候,如果dict對象中的dictht[0]存在key,那數(shù)據(jù)會遷移到dictht[1]中,如果dictht[0]中不存在,則訪問dictht[1]中的數(shù)據(jù),等到dictht[0]中數(shù)據(jù)遷移完成后,就把dictht[0]的指針指向dictht[1],原有dictht[0]的指向null。redis數(shù)據(jù)擴容是成倍擴容的,擴容也是相互之前互相進行數(shù)據(jù)倒換。
typedef struct dictEntry {void *key; // sds 對象,所有的key都是String類型union {void *val; //指針指向具體的數(shù)據(jù),就是redis支持的幾種數(shù)據(jù)類型。這里的數(shù)據(jù)也不是直接存儲的,而是通過 redisObject 這個對象封裝,甚至可以指定自定義的數(shù)據(jù)類型uint64_t u64;int64_t s64;double d;} v; // C語言的數(shù)據(jù)類型,用于存放value值struct dictEntry *next; // 鏈表,解決hash沖突,把產(chǎn)生hash沖突的節(jié)點關(guān)聯(lián)起來 } dictEntry;// redisObject對象,對value的封裝 : string , list ,set ,hash ,zset ... typedef struct redisObject {unsigned type:4; // 4 bit, sting , hash 約束客戶端的操作命令,基于客戶端的操作命令,把數(shù)據(jù)封裝為相應(yīng)的數(shù)據(jù)結(jié)構(gòu),如 set->String, lpush->Listunsigned encoding:4; // 4 bit 編碼類型,用于描述value值在數(shù)據(jù)庫中的編碼形式unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or 最近最少使用* LFU data (least significant 8 bits frequency 最近不經(jīng)常使用* and most significant 16 bits access time). * 24 bit * */int refcount; // 4 byte 引用計數(shù)法管理內(nèi)存,沒人使用時釋放內(nèi)存 void *ptr; // 8 byte 總空間: 4 bit + 4 bit + 24 bit + 4 byte + 8 byte = 16 byte 真實存儲數(shù)據(jù)的地方,指向數(shù)據(jù)在內(nèi)存中的位置。 } robj; typedef char *sds;/* Note: sdshdr5 is never used, we just access the flags byte directly.* However is here to document the layout of type 5 SDS strings. */ struct __attribute__ ((__packed__)) sdshdr5 {unsigned char flags; /* 3 lsb of type, and 5 msb of string length */char buf[];// buf[0]: z: 0101001 }; struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len; /* used */uint8_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[]; // 兼容C語言的函數(shù)庫,會占用一個字節(jié) }; struct __attribute__ ((__packed__)) sdshdr16 {uint16_t len; /* used */uint16_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[]; // 兼容C語言的函數(shù)庫,會占用一個字節(jié) }; struct __attribute__ ((__packed__)) sdshdr32 {uint32_t len; /* used */uint32_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[]; // 兼容C語言的函數(shù)庫,會占用一個字節(jié) }; struct __attribute__ ((__packed__)) sdshdr64 {uint64_t len; /* used */uint64_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[]; };緩存值對象占用16個字節(jié)的總空間,內(nèi)存中的緩存也是64個字節(jié),一次讀取64字節(jié),還差48字節(jié),那怎樣讓這48個字節(jié)進行補齊,讓內(nèi)存頁地址是連續(xù)的呢?這里通過encoding這個字段確定具體的數(shù)據(jù)類型,那我們用那種類型的數(shù)據(jù)結(jié)構(gòu)剛好是48個字節(jié)呢?我們從上面的String類型數(shù)據(jù)結(jié)構(gòu)中可知,48個字節(jié)應(yīng)該落在sdshdr8這個區(qū)間類,sds這個數(shù)據(jù)結(jié)構(gòu)自身需要消耗4個字節(jié),那還剩余44個字節(jié),那我們存放一個44字節(jié)的數(shù)據(jù),那正好可以占用64個字節(jié),正好是一個緩存行的容量,這個時候數(shù)據(jù)類型是embstr,那我們讀取內(nèi)存數(shù)據(jù)時,可以一次讀取出來。
Redis 里面小于等于44個字節(jié)的字符串是embstr編碼、大于44個字節(jié)是raw編碼
當(dāng)字符串大于 44 字節(jié)時
SDS 的數(shù)據(jù)量就開始變多了,Redis 就不再把 SDS 和 RedisObject 布局在一起了,而是會給 SDS 分配獨立的空間,并用指針指向 SDS 結(jié)構(gòu)。 這種布局方式被稱為 raw 編碼模式。
?
RedisDb數(shù)據(jù)結(jié)構(gòu)
redis 數(shù)據(jù)結(jié)構(gòu)層次
第一個層面,是從使用者的角度。比如:
- string
- list
- hash
- set
- sorted set
這一層面也是Redis暴露給外部的調(diào)用接口。
第二個層面,是從內(nèi)部實現(xiàn)的角度,屬于更底層的實現(xiàn)。比如:
- dict
- sds
- ziplist
- quicklist
- skiplist
第一個層面的“數(shù)據(jù)結(jié)構(gòu)”,Redis的官方文檔(An introduction to Redis data types and abstractions – Redis)有詳細的介紹。本文的重點在于討論第二個層面,Redis數(shù)據(jù)結(jié)構(gòu)的內(nèi)部實現(xiàn),以及這兩個層面的數(shù)據(jù)結(jié)構(gòu)之間的關(guān)系:Redis如何通過組合第二個層面的各種基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)第一個層面的更高層的數(shù)據(jù)結(jié)構(gòu)。
思考:
1 redis的擴容機制是如何實現(xiàn)的?
redis在hashtable容量與其中的容量是1:1的時候回觸發(fā)redis的擴容機制,擴容機制觸發(fā)后,每當(dāng)有請求過來的時候都會去原有的hashtable中查找是否有當(dāng)前查詢的key存在,如果存在則把當(dāng)前的數(shù)據(jù)遷移到因擴容的hashtable中,然后返回數(shù)據(jù);如果原有的hashtable不存在當(dāng)前key的值,則直接去新的hashtable中查詢數(shù)據(jù)并返回。
2 redis key 的hash沖突是怎么解決的?
redis在存放key=AAA的時候,如果hashtable沒有hase(AAA)的下標(biāo),則值直接存放在槽位下面,如果有沖突則把值存放在槽位下面,并通過鏈表的方式把產(chǎn)生沖突的值用鏈表串起來。
3 redis是如何替換原有key值的?
在上一個問題的基礎(chǔ)上,通過遍歷鏈表,比較key值。
3 redis 的數(shù)據(jù)類型為什么
參考:
1?第十章:Redis中bitmap的妙用 - 唯伊 - 博客園
總結(jié)
以上是生活随笔為你收集整理的01 Redis源码起航的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: office卸载后无法重装终极解决办法
- 下一篇: mysql优化--博森瑞