字符串對象的編碼可以是?`int`?、?`raw`?或者?`embstr`?。
如果一個字符串對象保存的是整數值, 并且這個整數值可以用?`long`?類型來表示, 那么字符串對象會將整數值保存在字符串對象結構的?`ptr`屬性里面(將?`void*`?轉換成?`long`?), 并將字符串對象的編碼設置為?`int`?。
舉個例子, 如果我們執行以下?SET?命令, 那么服務器將創建一個如圖 8-1 所示的?`int`?編碼的字符串對象作為?`number`?鍵的值:
~~~
redis> SET number 10086
OK
redis> OBJECT ENCODING number
"int"
~~~

如果字符串對象保存的是一個字符串值, 并且這個字符串值的長度大于?`39`?字節, 那么字符串對象將使用一個簡單動態字符串(SDS)來保存這個字符串值, 并將對象的編碼設置為?`raw`?。
舉個例子, 如果我們執行以下命令, 那么服務器將創建一個如圖 8-2 所示的?`raw`?編碼的字符串對象作為?`story`?鍵的值:
~~~
redis> SET story "Long, long, long ago there lived a king ..."
OK
redis> STRLEN story
(integer) 43
redis> OBJECT ENCODING story
"raw"
~~~

如果字符串對象保存的是一個字符串值, 并且這個字符串值的長度小于等于?`39`?字節, 那么字符串對象將使用?`embstr`?編碼的方式來保存這個字符串值。
`embstr`?編碼是專門用于保存短字符串的一種優化編碼方式, 這種編碼和?`raw`?編碼一樣, 都使用?`redisObject`?結構和?`sdshdr`?結構來表示字符串對象, 但?`raw`?編碼會調用兩次內存分配函數來分別創建?`redisObject`?結構和?`sdshdr`?結構, 而?`embstr`?編碼則通過調用一次內存分配函數來分配一塊連續的空間, 空間中依次包含?`redisObject`?和?`sdshdr`?兩個結構, 如圖 8-3 所示。

`embstr`?編碼的字符串對象在執行命令時, 產生的效果和?`raw`?編碼的字符串對象執行命令時產生的效果是相同的, 但使用?`embstr`?編碼的字符串對象來保存短字符串值有以下好處:
1. `embstr`?編碼將創建字符串對象所需的內存分配次數從?`raw`?編碼的兩次降低為一次。
2. 釋放?`embstr`?編碼的字符串對象只需要調用一次內存釋放函數, 而釋放?`raw`?編碼的字符串對象需要調用兩次內存釋放函數。
3. 因為?`embstr`?編碼的字符串對象的所有數據都保存在一塊連續的內存里面, 所以這種編碼的字符串對象比起?`raw`?編碼的字符串對象能夠更好地利用緩存帶來的優勢。
作為例子, 以下命令創建了一個?`embstr`?編碼的字符串對象作為?`msg`?鍵的值, 值對象的樣子如圖 8-4 所示:
~~~
redis> SET msg "hello"
OK
redis> OBJECT ENCODING msg
"embstr"
~~~

最后要說的是, 可以用?`long?double`?類型表示的浮點數在 Redis 中也是作為字符串值來保存的: 如果我們要保存一個浮點數到字符串對象里面, 那么程序會先將這個浮點數轉換成字符串值, 然后再保存起轉換所得的字符串值。
舉個例子, 執行以下代碼將創建一個包含?`3.14`?的字符串表示?`"3.14"`?的字符串對象:
~~~
redis> SET pi 3.14
OK
redis> OBJECT ENCODING pi
"embstr"
~~~
在有需要的時候, 程序會將保存在字符串對象里面的字符串值轉換回浮點數值, 執行某些操作, 然后再將執行操作所得的浮點數值轉換回字符串值, 并繼續保存在字符串對象里面。
舉個例子, 如果我們執行以下代碼的話:
~~~
redis> INCRBYFLOAT pi 2.0
"5.14"
redis> OBJECT ENCODING pi
"embstr"
~~~
那么程序首先會取出字符串對象里面保存的字符串值?`"3.14"`?, 將它轉換回浮點數值?`3.14`?, 然后把?`3.14`?和?`2.0`?相加得出的值?`5.14`?轉換成字符串?`"5.14"`?, 并將這個?`"5.14"`?保存到字符串對象里面。
表 8-6 總結并列出了字符串對象保存各種不同類型的值所使用的編碼方式。
* * *
表 8-6 字符串對象保存各類型值的編碼方式
| 值 | 編碼 |
| --- | --- |
| 可以用?`long`?類型保存的整數。 | `int` |
| 可以用?`long?double`?類型保存的浮點數。 | `embstr`?或者?`raw` |
| 字符串值, 或者因為長度太大而沒辦法用?`long`?類型表示的整數, 又或者因為長度太大而沒辦法用`long?double`?類型表示的浮點數。 | `embstr`?或者?`raw` |
* * *
## 編碼的轉換
`int`?編碼的字符串對象和?`embstr`?編碼的字符串對象在條件滿足的情況下, 會被轉換為?`raw`?編碼的字符串對象。
對于?`int`?編碼的字符串對象來說, 如果我們向對象執行了一些命令, 使得這個對象保存的不再是整數值, 而是一個字符串值, 那么字符串對象的編碼將從?`int`?變為?`raw`?。
在下面的示例中, 我們通過?APPEND?命令, 向一個保存整數值的字符串對象追加了一個字符串值, 因為追加操作只能對字符串值執行, 所以程序會先將之前保存的整數值?`10086`?轉換為字符串值?`"10086"`?, 然后再執行追加操作, 操作的執行結果就是一個?`raw`?編碼的、保存了字符串值的字符串對象:
~~~
redis> SET number 10086
OK
redis> OBJECT ENCODING number
"int"
redis> APPEND number " is a good number!"
(integer) 23
redis> GET number
"10086 is a good number!"
redis> OBJECT ENCODING number
"raw"
~~~
另外, 因為 Redis 沒有為?`embstr`?編碼的字符串對象編寫任何相應的修改程序 (只有?`int`?編碼的字符串對象和?`raw`?編碼的字符串對象有這些程序), 所以?`embstr`?編碼的字符串對象實際上是只讀的: 當我們對?`embstr`?編碼的字符串對象執行任何修改命令時, 程序會先將對象的編碼從?`embstr`?轉換成?`raw`?, 然后再執行修改命令; 因為這個原因,?`embstr`?編碼的字符串對象在執行修改命令之后, 總會變成一個?`raw`?編碼的字符串對象。
以下代碼展示了一個?`embstr`?編碼的字符串對象在執行?APPEND?命令之后, 對象的編碼從?`embstr`?變為?`raw`?的例子:
~~~
redis> SET msg "hello world"
OK
redis> OBJECT ENCODING msg
"embstr"
redis> APPEND msg " again!"
(integer) 18
redis> OBJECT ENCODING msg
"raw"
~~~
## 字符串命令的實現
因為字符串鍵的值為字符串對象, 所以用于字符串鍵的所有命令都是針對字符串對象來構建的, 表 8-7 列舉了其中一部分字符串命令, 以及這些命令在不同編碼的字符串對象下的實現方法。
* * *
表 8-7 字符串命令的實現
| 命令 | `int`?編碼的實現方法 | `embstr`?編碼的實現方法 | `raw`?編碼的實現方法 |
| --- | --- | --- | --- |
| SET | 使用?`int`?編碼保存值。 | 使用?`embstr`?編碼保存值。 | 使用?`raw`?編碼保存值。 |
| GET | 拷貝對象所保存的整數值, 將這個拷貝轉換成字符串值, 然后向客戶端返回這個字符串值。 | 直接向客戶端返回字符串值。 | 直接向客戶端返回字符串值。 |
| APPEND | 將對象轉換成?`raw`?編碼, 然后按`raw`?編碼的方式執行此操作。 | 將對象轉換成?`raw`?編碼, 然后按`raw`?編碼的方式執行此操作。 | 調用?`sdscatlen`?函數, 將給定字符串追加到現有字符串的末尾。 |
| INCRBYFLOAT | 取出整數值并將其轉換成?`longdouble`?類型的浮點數, 對這個浮點數進行加法計算, 然后將得出的浮點數結果保存起來。 | 取出字符串值并嘗試將其轉換成`long?double`?類型的浮點數, 對這個浮點數進行加法計算, 然后將得出的浮點數結果保存起來。 如果字符串值不能被轉換成浮點數, 那么向客戶端返回一個錯誤。 | 取出字符串值并嘗試將其轉換成?`longdouble`?類型的浮點數, 對這個浮點數進行加法計算, 然后將得出的浮點數結果保存起來。 如果字符串值不能被轉換成浮點數, 那么向客戶端返回一個錯誤。 |
| INCRBY | 對整數值進行加法計算, 得出的計算結果會作為整數被保存起來。 | `embstr`?編碼不能執行此命令, 向客戶端返回一個錯誤。 | `raw`?編碼不能執行此命令, 向客戶端返回一個錯誤。 |
| DECRBY | 對整數值進行減法計算, 得出的計算結果會作為整數被保存起來。 | `embstr`?編碼不能執行此命令, 向客戶端返回一個錯誤。 | `raw`?編碼不能執行此命令, 向客戶端返回一個錯誤。 |
| STRLEN | 拷貝對象所保存的整數值, 將這個拷貝轉換成字符串值, 計算并返回這個字符串值的長度。 | 調用?`sdslen`?函數, 返回字符串的長度。 | 調用?`sdslen`?函數, 返回字符串的長度。 |
| SETRANGE | 將對象轉換成?`raw`?編碼, 然后按`raw`?編碼的方式執行此命令。 | 將對象轉換成?`raw`?編碼, 然后按`raw`?編碼的方式執行此命令。 | 將字符串特定索引上的值設置為給定的字符。 |
| GETRANGE | 拷貝對象所保存的整數值, 將這個拷貝轉換成字符串值, 然后取出并返回字符串指定索引上的字符。 | 直接取出并返回字符串指定索引上的字符。 | 直接取出并返回字符串指定索引上的字符。 |
- 介紹
- 前言
- 致謝
- 簡介
- 第一部分:數據結構與對象
- 簡單動態字符串
- SDS 的定義
- SDS 與 C 字符串的區別
- SDS API
- 重點回顧
- 參考資料
- 鏈表
- 鏈表和鏈表節點的實現
- 鏈表和鏈表節點的 API
- 重點回顧
- 字典
- 字典的實現
- 哈希算法
- 解決鍵沖突
- rehash
- 漸進式 rehash
- 字典 API
- 重點回顧
- 跳躍表
- 跳躍表的實現
- 跳躍表 API
- 重點回顧
- 整數集合
- 整數集合的實現
- 升級
- 升級的好處
- 降級
- 整數集合 API
- 重點回顧
- 壓縮列表
- 壓縮列表的構成
- 壓縮列表節點的構成
- 連鎖更新
- 壓縮列表 API
- 重點回顧
- 對象
- 對象的類型與編碼
- 字符串對象
- 列表對象
- 哈希對象
- 集合對象
- 有序集合對象
- 類型檢查與命令多態
- 內存回收
- 對象共享
- 對象的空轉時長
- 重點回顧
- 第二部分:單機數據庫的實現
- 數據庫
- 數據庫鍵空間
- 重點回顧
- RDB 持久化
- RDB 文件結構
- 重點回顧
- AOF 持久化
- AOF 持久化的實現
- 重點回顧
- 事件
- 文件事件
- 重點回顧
- 參考資料
- 客戶端
- 客戶端屬性
- 重點回顧
- 服務器
- 命令請求的執行過程
- 重點回顧
- 第三部分:多機數據庫的實現
- 復制
- 舊版復制功能的實現
- 重點回顧
- Sentinel
- 啟動并初始化 Sentinel
- 重點回顧
- 參考資料
- 集群
- 節點
- 重點回顧
- 第四部分:獨立功能的實現
- 發布與訂閱
- 頻道的訂閱與退訂
- 重點回顧
- 參考資料
- 事務
- 事務的實現
- 重點回顧
- Lua 腳本
- 創建并修改 Lua 環境
- 重點回顧
- 排序
- SORT <key> 命令的實現
- 重點回顧
- 二進制位數組
- GETBIT 命令的實現
- 重點回顧
- 慢查詢日志
- 慢查詢記錄的保存
- 慢查詢日志的閱覽和刪除
- 添加新日志
- 重點回顧
- 監視器
- 成為監視器
- 向監視器發送命令信息
- 重點回顧
- 源碼、相關資源和勘誤