對象
前面我們介紹了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列出對的常量中的一個
類型常量 | 對象的名稱 |
REDIS_STRING | 字符串對象 |
REDIS_LIST | 列表對象 |
REDIS_HASH | 哈希對象 |
REDIS_SET | 集合對象 |
REDIS_ZSET | 有序集合對象 |
對於Redis數據庫保存的鍵值對來說,鍵總是一個字符串對象,而值則可以是字符串對象、列表對象、哈希對象、集合對象或者有序集合對象的其中一種,因此:
- 當我們稱呼一個數據庫鍵為“字符串鍵”時,我們指的是“這個數據庫鍵所對應的值為字符串對象”
- 當我們稱呼一個數據庫鍵為“列表鍵”時,我們指的是“這個數據庫鍵所對應的值為列表對象”
TYPE命令的實現方式也與此類似,當我們對一個數據庫鍵執行TYPE命令時,命令返回的結果為數據庫鍵對應的值對象類型,而不是鍵對象類型:
# 鍵為字符串對象,值為字符串對象 127.0.0.1:6379> SET msg "hello world" OK 127.0.0.1:6379> TYPE msg string # 鍵為字符串對象,值為列表對象 127.0.0.1:6379> RPUSH numbers 1 3 5 (integer) 3 127.0.0.1:6379> TYPE numbers list # 鍵為字符串對象,值為哈希對象 127.0.0.1:6379> HMSET profile name Tome age 25 career Programmer OK 127.0.0.1:6379> TYPE profile hash # 鍵為字符串對象,值為集合對象 127.0.0.1:6379> SADD fruits apple banana cherry (integer) 3 127.0.0.1:6379> TYPE fruits set # 鍵為字符串對象,值為有序集合對象 127.0.0.1:6379> ZADD price 8.5 apple 5.0 banana 6.0 cherry (integer) 3 127.0.0.1:6379> TYPE price zset
表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列出的常量的其中一個
編碼常量 | 編碼所對應的底層數據結構 |
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列出了每種類型的對象可以使用的編碼
類型 | 編碼 | 對象 |
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" OK 127.0.0.1:6379> OBJECT ENCODING msg "embstr" 127.0.0.1:6379> SADD numbers 1 3 5 (integer) 3 127.0.0.1:6379> OBJECT ENCODING numbers "intset" 127.0.0.1:6379> SADD numbers "seven" (integer) 1 127.0.0.1:6379> OBJECT ENCODING numbers "hashtable"
表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 10086 OK 127.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 ..." OK 127.0.0.1:6379> STRLEN story (integer) 55 127.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" OK 127.0.0.1:6379> STRLEN story (integer) 44 127.0.0.1:6379> OBJECT ENCODING story "embstr" 127.0.0.1:6379> SET story "Long, long, long, long, long ago there lived " OK 127.0.0.1:6379> STRLEN story (integer) 45 127.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" OK 127.0.0.1:6379> OBJECT ENCODING msg "embstr"
圖1-4 embstr編碼的字符串對象
最后要說的是,可以用long double類型表示的浮點數在Redis中也是作為字符串值來保存的。如果我們要保存一個浮點數到字符串對象里面,那么程序先將這個浮點數轉換成字符串值,然后將其保存。舉個栗子,執行以下代碼將創建一個包含3.14的字符串對象
127.0.0.1:6379> SET pi 3.14 OK 127.0.0.1:6379> OBJECT ENCODING pi "embstr"
在有需要的時候,程序會將保存在字符串對象中的字符串值轉換回浮點數值,執行某些操作,然后再將執行操作所得的浮點數轉換回字符串值,並繼續保存在字符串對象里面。舉個栗子,我們執行以下代碼:
127.0.0.1:6379> SET pi 3.14 OK 127.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總結並列出字符串對象保存各種不同類型的值所使用的編碼方式
值 | 編碼 |
可以用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 10086 OK 127.0.0.1:6379> OBJECT ENCODING number "int" 127.0.0.1:6379> APPEND number " is a good number!" (integer) 23 127.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" OK 127.0.0.1:6379> OBJECT ENCODING msg "embstr" 127.0.0.1:6379> APPEND msg " again!" (integer) 18 127.0.0.1:6379> OBJECT ENCODING msg "raw"
字符串命令的實現
因為字符串鍵的值為字符串對象,所以用於字符串鍵的所有命令都是針對字符串對象來構建的,表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 | 拷貝對象所保存的整數值,將這個拷貝轉換成字符串值,然后取出並返回字符串指定索引上的字符 | 直接取出並返回字符串指定索引上的字符 | 直接取出並返回字符串指定索引上的字符 |