標籤:在伺服器 跳躍表 刪除 set 效率 lru ring car 分配
對象
前面我們介紹了Redis的主要資料結構,如:簡單動態字串SDS、雙端鏈表、字典、壓縮列表、整數集合等。Redis並沒有直接使用這些資料結構來實現索引值對資料庫,而是基於這些資料結構建立了一個對象系統,這個系統包含字串對象、列表對象、雜湊對象、集合對象和有序集合對象這五種類型的對象,每種對象都用到了至少一種我們之前介紹的資料結構
通過這五種不同類型的對象,Redis可以在執行命令之前,根據對象的類型來判斷一個對象是否可以執行給定的命令。使用對象的另一個好處是,我們可以根據不同的使用情境,為對象設定多種不同的資料結構實現,從而最佳化對象在不同情境下的使用效率
除此之外,Redis的對象系統還實現了基於引用計數的記憶體回收機制,當程式不再使用某個對象時,這個對象所佔用的記憶體就會被自動釋放;另外,Redis還通過引用計數實現了對象共用機制,這一機制可以在適當的條件下,通過讓多個資料庫鍵共用一個對象來節約記憶體
最後,Redis的對象帶有訪問時間記錄資訊,該資訊可以用於計算資料庫鍵的空轉時間,在伺服器啟用了maxmemory功能的情況下,空轉時間較大的那些鍵可能會優先被伺服器刪除
對象的類型編碼
Redis使用對象來表示資料庫中的鍵和值,每次當我們在Redis的資料庫中新建立一個索引值對時,我們至少會建立兩個對象,一個對象用作索引值對的鍵(鍵對象),另一個對象用作索引值對的值(值 對象)
舉個栗子,以下SET命令在資料庫將建立一個新的索引值對,其中索引值對的鍵是一個包含了字串"msg"的對象,而索引值對的值則是一個包含了字串"hello world"的對象
127.0.0.1:6379> SET msg "hello world"OK
Redis中的每個對象都由一個redisObject結構表示,該結構中儲存資料相關的三個屬性分別是:type、encoding、ptr
redis.h
typedef struct redisObject {//類型 unsigned type:4; unsigned notused:2; //編碼 unsigned encoding:4; unsigned lru:22; //引用計數 int refcount;//指向底層實現資料結構的指標 void *ptr;} robj;
類型
對象的type屬性記錄了對象的類型,這個屬性的值可以是表1-1列出對的常量中的一個
表1-1 對象的類型
| 類型常量 |
對象的名稱 |
| REDIS_STRING |
字串對象 |
| REDIS_LIST |
列表對象 |
| REDIS_HASH |
雜湊對象 |
| REDIS_SET |
集合對象 |
| REDIS_ZSET |
有序集合對象 |
對於Redis資料庫儲存的索引值對來說,鍵總是一個字串對象,而值則可以是字串對象、列表對象、雜湊對象、集合對象或者有序集合對象的其中一種,因此:
- 當我們稱呼一個資料庫鍵為“字串鍵”時,我們指的是“這個資料庫鍵所對應的值為字串對象”
- 當我們稱呼一個資料庫鍵為“列表鍵”時,我們指的是“這個資料庫鍵所對應的值為列表對象”
TYPE命令的實現方式也與此類似,當我們對一個資料庫鍵執行TYPE命令時,命令返回的結果為資料庫鍵對應的值物件類型,而不是鍵物件類型:
# 鍵為字串對象,值為字串對象127.0.0.1:6379> SET msg "hello world"OK127.0.0.1:6379> TYPE msgstring# 鍵為字串對象,值為列表對象127.0.0.1:6379> RPUSH numbers 1 3 5(integer) 3127.0.0.1:6379> TYPE numberslist# 鍵為字串對象,值為雜湊對象127.0.0.1:6379> HMSET profile name Tome age 25 career ProgrammerOK127.0.0.1:6379> TYPE profilehash# 鍵為字串對象,值為集合對象127.0.0.1:6379> SADD fruits apple banana cherry(integer) 3127.0.0.1:6379> TYPE fruitsset# 鍵為字串對象,值為有序集合對象127.0.0.1:6379> ZADD price 8.5 apple 5.0 banana 6.0 cherry(integer) 3127.0.0.1:6379> TYPE pricezset
表1-2列出了TYPE命令在面對不同類型的值對象時所產生的輸出
表1-2 不同類型值對象的TYPE命令輸出
| 對象 |
對象type屬性的值 |
TYPE命令的輸出 |
| 字串對象 |
REDIS_STRING |
string |
| 列表對象 |
REDIS_LIST |
list |
| 雜湊對象 |
REDIS_HASH |
hash |
| 集合對象 |
REDIS_SET |
set |
| 有序集合對象 |
REDIS_ZSET |
zset |
編碼和底層實現
對象的ptr指標指向對象的底層實現資料結構,而這些資料結構由對象的encoding屬性決定。encoding屬性記錄了對象使用的編碼,也即是說這個對象使用了什麼資料結構作為對象的底層實現,這個屬性的值可以是表1-3列出的常量的其中一個
表1-3 對象的編碼
| 編碼常量 |
編碼所對應的底層資料結構 |
| REDIS_ENCODING_INT |
long類型的整數 |
| REDIS_ENCODING_EMBSTR |
embstr編碼的簡單動態字串 |
| REDIS_ENCODING_RAW |
簡單動態字串 |
| REDIS_ENCODING_HT |
字典 |
| REDIS_ENCODING_LINKEDLIST |
雙端鏈表 |
| REDIS_ENCODING_ZIPLIST |
壓縮列表 |
| REDIS_ENCODING_INTSET |
整數集合 |
| REDIS_ENCODING_SKIPLIST |
跳躍表和字典 |
每種類型的對象都至少使用了兩種不同的編碼,表1-4列出了每種類型的對象可以使用的編碼
表1-4 不同類型和編碼的對象
| 類型 |
編碼 |
對象 |
| REDIS_STRING |
REDIS_ENCODING_INT |
使用整數值實現的字串對象 |
| REDIS_STRING |
REDIS_ENCODING_EMBSTR |
使用embstr編碼的簡單動態字串實現的字串對象 |
| REDIS_STRING |
REDIS_ENCODING_RAW |
使用簡單動態字串實現的字串對象 |
| REDIS_LIST |
REDIS_ENCODING_ZIPLIST |
使用壓縮列表實現的列表對象 |
| REDIS_LIST |
REDIS_ENCODING_LINKEDLIST |
使用雙端鏈表實現的列表對象 |
| REDIS_HASH |
REDIS_ENCODING_ZIPLIST |
使用壓縮列表實現的雜湊對象 |
| REDIS_HASH |
REDIS_ENCODING_HT |
使用字典實現的雜湊對象 |
| REDIS_SET |
REDIS_ENCODING_INTSET |
使用整數集合實現的集合對象 |
| REDIS_SET |
REDIS_ENCODING_HT |
使用字典實現的集合對象 |
| REDIS_ZSET |
REDIS_ENCODING_ZIPLIST |
使用壓縮列表實現的有序集合對象 |
| REDIS_ZSET |
REDIS_ENCODING_SKIPLIST |
使用跳躍表和字典實現的有序集合對象 |
使用OBJECT ENCODING命令可以查看一個資料庫鍵的值對象的編碼:
127.0.0.1:6379> SET msg "hello wrold"OK127.0.0.1:6379> OBJECT ENCODING msg"embstr"127.0.0.1:6379> SADD numbers 1 3 5(integer) 3127.0.0.1:6379> OBJECT ENCODING numbers"intset"127.0.0.1:6379> SADD numbers "seven"(integer) 1127.0.0.1:6379> OBJECT ENCODING numbers"hashtable"
表1-5列出了不同編碼的對象所對應的OBJECT ENCODING命令輸出:
表1-5 OBJECT ENCODING對不同編碼的輸出
| 對象所使用的底層資料結構 |
編碼常量 |
OBJECT ENCODING命令輸出 |
| 整數 |
REDIS_ENCODING_INT |
int |
| embstr編碼的簡單動態字串(SDS) |
REDIS_ENCODING_EMBSTR |
embstr |
| 簡單動態字串 |
REDIS_ENCODING_RAW |
raw |
| 字典 |
REDIS_ENCODING_HT |
hashtable |
| 雙端鏈表 |
REDIS_ENCODING_LINKEDLIST |
linkedlist |
| 雙端鏈表 |
REDIS_ENCODING_ZIPLIST |
ziplist |
| 整數集合 |
REDIS_ENCODING_INTSET |
intset |
| 跳躍表和字典 |
REDIS_ENCODING_SKIPLIST |
skiplist |
通過encoding屬性來設定對象所使用的編碼,而不是為特定類型的對象關聯一種固定的編碼,極大地提升了Redis的靈活性和效率,因為Redis可以根據不同的使用情境來為一個對象設定不同的編碼,從而最佳化對象在某一情境下的效率。舉個栗子,在列表對象包含的元素比較少時,Redis使用壓縮列表作為列表對象的底層實現:
- 因為壓縮列表比雙端鏈表更節約記憶體,並且在元素比較少時,在記憶體中以連續塊方式儲存的壓縮列表比起雙端鏈表可以更快被載入到緩衝中
- 隨著列表對象包含的元素越來越多,使用壓縮列表來儲存元素的優勢逐漸消失時,對象就會將底層實現從壓縮列錶轉換成功能更強、更適合儲存大量元素的雙端鏈表
其他類型的對象也會通過使用多種不同的編碼來進行類似的最佳化,在接下來的內容中,我們將分別介紹Redis中的五種不同類型的對象,說明這些對象底層所使用的編碼方式,列出對象從一種編碼轉換成另一種編碼所需的條件,以及同一個命令在多種不同編碼上的實現方法
字串對象
字串對象的編碼可以是int、raw或者embstr。如果一個字串對象儲存的是整數值,並且這個整數值可以用long類型來表示,那麼字串對象會將整數值儲存在字串結構的ptr屬性中(將void *轉換成long),並將字串對象的編碼設定為int
舉個栗子,如果我們執行以下SET命令,那麼伺服器將建立一個1-1所示的int編碼的字串對象作為number鍵的值:
127.0.0.1:6379> SET number 10086OK127.0.0.1:6379> OBJECT ENCODING number"int"
圖1-1 int編碼的字串對象
如果字串對象儲存的是一個字串值,並且這個字串值的長度大於32位元組,那麼字串對象將使用一個簡單動態字串(SDS)來儲存這個字串值,並將對象的編碼設定為raw。舉個栗子,如果我們執行以下命令,那麼伺服器將建立一個1-2所示的raw編碼的字串作為store鍵的值
127.0.0.1:6379> SET story "Long, long, long, long, long ago there lived a king ..."OK127.0.0.1:6379> STRLEN story(integer) 55127.0.0.1:6379> OBJECT ENCODING story"raw"
圖1-2 raw編碼的字串對象
如果字串對象儲存的是一個字串,並且這個字串長度小於等於44位元組,那麼字串將使用embstr編碼,看下面的樣本
127.0.0.1:6379> SET story "Long, long, long, long, long ago there lived"OK127.0.0.1:6379> STRLEN story(integer) 44127.0.0.1:6379> OBJECT ENCODING story"embstr"127.0.0.1:6379> SET story "Long, long, long, long, long ago there lived "OK127.0.0.1:6379> STRLEN story(integer) 45127.0.0.1:6379> OBJECT ENCODING story"raw"
embstr編碼是專門用於儲存短字串的一種最佳化編碼方式,這種編碼方式和raw編碼一樣,都使用redisObject結構和sdshdr結構來表示字串對象,但raw編碼會調用兩次記憶體配置函數來分別建立redisObject結構和sdshdr結構,而embstr編碼則通過調用一次記憶體配置函數來分配一塊連續的空間,空間中依次包含redisObject和sdshdr結構,1-3所示
圖1-3 embstr編碼建立的記憶體塊結構
embstr編碼的字串對象在執行命令時,產生的效果和raw編碼的字串對象執行命令時產生的效果是相同的,但使用embstr編碼的字串來儲存短字串值有以下好處:
- embstr編碼將建立字串對象所需的記憶體配置次數從raw編碼的兩次降低為一次
- 釋放embstr編碼的字串對象只需調用一次記憶體釋放函數,而釋放raw編碼的字串對象需要調用兩次記憶體釋放函數
- 因為embstr編碼的字串對象的所有資料都儲存在一塊連續的記憶體中,所以這種編碼的字串對象比起raw編碼的字串對象能夠更好地利用緩衝帶來的優勢
作為例子,以下命令建立一個embstr編碼的字串對象作為msg鍵的值,值對象的樣子1-4所示
127.0.0.1:6379> SET msg "hello world"OK127.0.0.1:6379> OBJECT ENCODING msg"embstr"
圖1-4 embstr編碼的字串對象
最後要說的是,可以用long double類型表示的浮點數在Redis中也是作為字串值來儲存的。如果我們要儲存一個浮點數到字串對象裡面,那麼程式先將這個浮點數轉換成字串值,然後將其儲存。舉個栗子,執行以下代碼將建立一個包含3.14的字串對象
127.0.0.1:6379> SET pi 3.14OK127.0.0.1:6379> OBJECT ENCODING pi"embstr"
在有需要的時候,程式會將儲存在字串對象中的字串值轉換回浮點數值,執行某些操作,然後再將執行操作所得的浮點數轉換回字串值,並繼續儲存在字串對象裡面。舉個栗子,我們執行以下代碼:
127.0.0.1:6379> SET pi 3.14OK127.0.0.1:6379> INCRBYFLOAT pi 3.0"6.14"127.0.0.1:6379> OBJECT ENCODING pi"embstr
程式首先會取出字串對象中儲存的字串值"3.14",將它轉換回浮點數值3.14,然後把3.14和2.0相加得到5.14後在轉換回字串,並將字串"5.14"儲存到字串對象中。表1-6總結並列出字串對象儲存各種不同類型的值所使用的編碼方式
表1-6 字串對象儲存各類型值的編碼方式
| 值 |
編碼 |
| 可以用long類型儲存的整數 |
int |
| 可以用long double類型儲存的浮點數 |
embstr或者raw |
| 字串值,或者因為長度太大而沒辦法用long類型表示的整數,又或者因為長度太大而沒辦法用long double類型表示的浮點數 |
embstr或者raw |
編碼的轉換
int編碼的字串對象和embstr編碼的字串對象在條件滿足的情況下,會被轉換為raw編碼的字串對象。對於int編碼的字串對象來說,如果我們向對象執行了一些命令,使得對象儲存的不再是整數值,而是一個字串值,那麼字串對象將從int變為raw
下面的樣本中,我們通過APPEND命令,向一個儲存整數值的字串追加一個字串值,因為追加操作只能對字串值執行,所以程式會將之前儲存的整數值轉換為字串,然後再執行追加操作,操作的執行結果就是一個raw編碼的、儲存了字串值的字串對象
127.0.0.1:6379> SET number 10086OK127.0.0.1:6379> OBJECT ENCODING number"int"127.0.0.1:6379> APPEND number " is a good number!"(integer) 23127.0.0.1:6379> GET number"10086 is a good number!"127.0.0.1:6379> OBJECT ENCODING number"raw"
另外,因為Redis沒有為embstr編碼的字串對象編寫任何相應的修改程式(只有int編碼的字串對象和raw編碼的字串對象有這些程式),所以embstr編碼的字串對象實際上是唯讀。當我們對embstr編碼的字串對象執行任何修改命令時,程式先將對象的編碼從embstr轉換成raw,然後再執行修改命令。因為這個原因,embstr編碼的字串對象在執行修改命令之後,總會變成一個raw編碼的字串對象
以下代碼展示了一個embstr編碼的字串對象在執行APPEND命令之後,對象的編碼從embstr變為raw的例子:
127.0.0.1:6379> SET msg "hello world"OK127.0.0.1:6379> OBJECT ENCODING msg"embstr"127.0.0.1:6379> APPEND msg " again!"(integer) 18127.0.0.1:6379> OBJECT ENCODING msg"raw"
字串命令的實現
因為字串鍵的值為字串對象,所以用於字串鍵的所有命令都是針對字串對象來構建的,表1-7例舉了其中一部分字串命令,以及這些命令在不同編碼的字串對象下的實現方法
表1-7 字串命令的實現
| 命令 |
int編碼的實現方法 |
embstr編碼的實現方法 |
raw編碼的實現方法 |
| SET |
使用int編碼儲存值 |
使用embstr編碼儲存值 |
使用raw編碼儲存值 |
| GET |
拷貝對象所儲存的整數值,將這個拷貝轉換成字串值,然後向用戶端返回這個字串值 |
直接向用戶端返回字串值 |
直接向用戶端返回字串值 |
| APPEND |
將對象轉換成raw編碼,然後按raw編碼的方式執行此操作 |
將對象轉換成raw編碼,然後按raw編碼的方式執行此操作 |
調用sdscatlen函數,將給定字串追加到現有字串的末尾 |
| INCRBYFLOAT |
取出整數值並將其轉換成long double類型的浮點數,對這個浮點數進行加法計算,然後將得出的浮點數結果儲存起來 |
取出字串值並嘗試將其轉換成long double類型的浮點數,對這個浮點數進行加法計算,然後將得出的浮點數結果儲存起來。
如果字串值不能被轉換成浮點數,那麼向用戶端返回一個錯誤 |
取出字串值並嘗試將其轉換成long double類型的浮點數,對這個浮點數進行加法計算,然後將得出的浮點數結果儲存起來。
如果字串值不能被轉換成浮點數,那麼向用戶端返回一個錯誤 |
| INCRBY |
對整數值進行加法計算,得出的計算結果會作為整數被儲存起來 |
embstr編碼不能執行此命令,向用戶端返回一個錯誤 |
raw編碼不能執行此命令,向用戶端返回一個錯誤 |
| DECRBY |
對整數值進行減法計算,得出的計算結果會作為整數被儲存起來 |
embstr編碼不能執行此命令,向用戶端返回一個錯誤 |
raw編碼不能執行此命令,向用戶端返回一個錯誤 |
| STRLEN |
拷貝對象所儲存的整數值,將這個拷貝轉換成字串值,計算並返回這個字串值的長度 |
調用sdslen函數,返回字串的長度 |
調用sdslen函數,返回字串的長度 |
| SETRANGE |
將對象轉換成raw編碼,然後按raw編碼的方式執行此命令 |
將對象轉換成raw編碼,然後按raw編碼的方式執行此命令 |
將字串特定索引上的值設定為給定的字元 |
| GETRANGE |
拷貝對象所儲存的整數值,將這個拷貝轉換成字串值,然後取出並返回字串指定索引上的字元 |
直接取出並返回字串指定索引上的字元 |
直接取出並返回字串指定索引上的字元 |
Redis實現之對象(一)