1 概述
數據結構和內部編碼
無傳統關系型數據庫的 Table 模型
schema 所對應的db僅以編號區分。同一 db 內,key 作為頂層模型,它的值是扁平化的。即 db 就是key的命名空間。
key的定義通常以 : 分隔,如:Article:Count:1
常用的redis數據類型有:string、list、set、map、sorted-set
redisObject通用結構
Redis中的所有value 都是以object 的形式存在的,其通用結構如下
- type 數據類型
指 string、list 等類型 - encoding 編碼方式
指的是這些結構化類型具體的實現方式,同一個類型可以有多種實現。e.g. string 可以用int 來實現,也可以使用char[] 來實現;list 可以用ziplist 或者鏈表來實現 - lru
本對象的空轉時長,用于有限內存下長時間不訪問的對象清理 - refcount
對象引用計數,用于GC - ptr 數據指針
指向以 encoding 方式實現這個對象實際實現者的地址。如:string 對象對應的SDS地址(string的數據結構/簡單動態字符串)
單線程
單線程為何這么快?
- 純內存
- 非阻塞I/O
- 避免線程切換和競態消耗
- 一次只運行一條命令
- 拒絕長(慢)命令
keys, flushall, flushdb, slow lua script, mutil/exec, operate big value(collection) - 其實不是單線程
fysnc file descriptorclose file descriptor
2 string
Redis中的 string 可表示很多語義
- 字節串(bits)
- 整數
- 浮點數
redis會根據具體的場景完成自動轉換,并根據需要選取底層的實現方式。
例如整數可以由32-bit/64-bit、有符號/無符號承載,以適應不同場景對值域的要求。
- 字符串鍵值結構,也能是 JSON 串或 XML 結構
內存結構
在Redis內部,string的內部以 int、SDS(簡單動態字符串 simple dynamic string)作為存儲結構
- int 用來存放整型
- SDS 用來存放字節/字符和浮點型SDS結構
SDS
typedef struct sdshdr {
// buf中已經占用的字符長度
unsigned int len;
// buf中剩余可用的字符長度
unsigned int free;
// 數據空間
char buf[];
}
- 結構圖
存儲的內容為“Redis”,Redis采用類似C語言的存儲方法,使用’’結尾(僅是定界符)。SDS的free 空間大小為0,當free > 0時,buf中的free 區域的引入提升了SDS對字符串的處理性能,可以減少處理過程中的內存申請和釋放次數。
buf 的擴容與縮容
當對SDS 進行操作時,如果超出了容量。SDS會對其進行擴容,觸發條件如下:
- 字節串初始化時,buf的大小 = len + 1,即加上定界符’’剛好用完所有空間
- 當對串的操作后小于1M時,擴容后的buf 大小 = 業務串預期長度 * 2 + 1,也就是擴大2倍。
- 對于大小 > 1M的長串,buf總是留出 1M的 free空間,即2倍擴容,但是free最大為 1M。
字節串與字符串
SDS中存儲的內容可以是ASCII 字符串,也可以是字節串。由于SDS通過len 字段來確定業務串的長度,因此業務串可以存儲非文本內容。對于字符串的場景,buf[len] 作為業務串結尾的’’ 又可以復用C的已有字符串函數。
SDS編碼的優化
value 在內存中有2個部分:redisObject和ptr指向的字節串部分。
在創建時,通常要分別為2個部分申請內存,但是對于小字節串,可以一次性申請。
incr userid:pageview (單線程:無競爭)。緩存視頻的基本信息(數據源在MySQL)
public VideoInfo get(Long id) {
String redisKey = redisPrefix + id;
VideoInfo videoInfo e redis.get(redisKey);
if (videoInfo == null) {
videoInfo = mysql.get(id);
if (videoInfo != null) {
// 序列化
redis.set(redisKey serialize(videoInfo)):
}
}
}
除此之外,string 類型的value還有一些CAS的原子操作,如:get、set、set value nx(如果不存在就設置)、set value xx(如果存在就設置)。
String 類型是二進制安全的,也就是說在Redis中String類型可以包含各種數據,比如一張JPEG圖片或者是一個序列化的Ruby對象。一個String類型的值最大長度可以是512M。
在Redis中String有很多有趣的用法
- 把String當做原子計數器,這可以使用INCR家族中的命令來實現:INCR, DECR, INCRBY。
- 使用AppEND命令來給一個String追加內容。
- 把String當做一個隨機訪問的向量(Vector),這可以使用GETRANGE和 SETRANGE命令來實現
- 使用GETBIT 和SETBIT方法,在一個很小的空間中編碼大量的數據,或者創建一個基于Redis的Bloom Filter 算法。
List
可從頭部(左側)加入元素,也可以從尾部(右側)加入元素。有序列表。
像微博粉絲,即可以list存儲做緩存。
key = 某大v
value = [zhangsan, lisi, wangwu]
所以可存儲一些list型的數據結構,如:
- 粉絲列表
- 文章的評論列表
可通過lrange命令,即從某元素開始讀取多少元素,可基于list實現分頁查詢,這就是基于redis實現簡單的高性能分頁,可以做類似微博那種下拉不斷分頁的東西,性能高,就一頁一頁走。
搞個簡單的消息隊列,從list頭推進去,從list尾拉出來。
List類型中存儲一系列String值,這些String按照插入順序排序。
5.1 內存數據結構
List 類型的 value對象,由 linkedlist 或 ziplist 實現。
當 List 元素個數少并且元素內容長度不大采用ziplist 實現,否則使用linkedlist
5.1.1 linkedlist實現
鏈表的代碼結構
typedef struct list {
// 頭結點
listNode *head;
// 尾節點
listNode *tail;
// 節點值復制函數
void *(*dup)(void * ptr);
// 節點值釋放函數
void *(*free)(void *ptr);
// 節點值對比函數
int (*match)(void *ptr, void *key);
// 鏈表長度
unsigned long len;
} list;
// Node節點結構
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
linkedlist 結構圖
5.1.2 ziplist實現
存儲在連續內存
- zlbytes
ziplist 的總長度 - zltail
指向最末元素。 - zllen
元素的個數。 - entry
元素內容。 - zlend
恒為0xFF,作為ziplist的定界符
linkedlist和ziplist的rpush、rpop、llen的時間復雜度都是O(1):
- ziplist的lpush、lpop都會牽扯到所有數據的移動,時間復雜度為O(N)
由于List的元素少,體積小,這種情況還是可控的。
ziplist的Entry結構:
記錄前一個相鄰的Entry的長度,便于雙向遍歷,類似linkedlist的prev指針。
ziplist是連續存儲,指針由偏移量來承載。
Redis中實現了2種方式實現:
- 當前鄰 Entry的長度小于254 時,使用1字節實現
- 否則使用5個字節
當前一個Entry長度變化時,可能導致后續的所有空間移動,雖然這種情況發生可能性較小。
Entry內容本身是自描述的,意味著第二部分(Entry內容)包含了幾個信息:Entry內容類型、長度和內容本身。而內容本身包含:類型長度部分和內容本身部分。類型和長度同樣采用變長編碼:
- 00xxxxxx :string類型;長度小于64,0~63可由6位bit 表示,即xxxxxx表示長度
- 01xxxxxx|yyyyyyyy : string類型;長度范圍是[64, 16383],可由14位 bit 表示,即xxxxxxyyyyyyyy這14位表示長度。
- 10xxxxxx|yy…y(32個y) : string類型,長度大于16383.
- 1111xxxx :integer類型,integer本身內容存儲在xxxx 中,只能是1~13之間取值。也就是說內容類型已經包含了內容本身。
- 11xxxxxx :其余的情況,Redis用1個字節的類型長度表示了integer的其他幾種情況,如:int_32、int_24等。
由此可見,ziplist 的元素結構采用的是可變長的壓縮方法,針對于較小的整數/字符串的壓縮效果較好 - LPUSH命令
在頭部加入一個新元素 - RPUSH命令
在尾部加入一個新元素
當在一個空的K執行這些操作時,會創建一個新列表。當一個操作清空了一個list時,該list對應的key會被刪除。若使用一個不存在的K,就會使用一個空list。
LPUSH mylist a # 現在list是 "a"
LPUSH mylist b # 現在list是"b","a"
RPUSH mylist c # 現在list是 "b","a","c" (注意這次使用的是 RPUSH)
list的最大長度是2^32 – 1個元素(4294967295,一個list中可以有多達40多億個元素)。
從時間復雜度的角度來看,Redis list類型的最大特性是:即使是在list的頭端或者尾端做百萬次的插入和刪除操作,也能保持穩定的很少的時間消耗。在list的兩端訪問元素是非常快的,但是如果要訪問一個很大的list中的中間部分的元素就會比較慢了,時間復雜度是O(N)
適用場景
- 社交中使用List進行時間表建模,使用 LPUSH 在用戶時間線中加入新元素,然后使用 LRANGE 獲得最近加入元素
- 可以把[LPUSH] 和[LTRIM] 命令結合使用來實現定長的列表,列表中只保存最近的N個元素
- 做MQ,依賴BLPOP這種阻塞命令
Set
類似List,但無序且其元素不重復。
向集合中添加多次相同的元素,集合中只存在一個該元素。在實際應用中,這意味著在添加一個元素前不需要先檢查元素是否存在。
支持多個服務器端命令來從現有集合開始計算集合,所以執行集合的交集,并集,差集都很快。
set的最大長度是2^32 – 1個元素(一個set中可多達40多億個元素)。
內存數據結構
Set在Redis中以intset 或 hashtable存儲:
- 對于Set,HashTable的value永遠為NULL
- 當Set中只包含整型數據時,采用intset作為實現
intset
核心元素是一個字節數組,從小到大有序的存放元素
結構圖
因為元素有序排列,所以SET的獲取操作采用二分查找,復雜度為O(log(N))。
進行插入操作時:
- 首先通過二分查找到要插入位置
- 再對元素進行擴容
- 然后將插入位置之后的所有元素向后移動一個位置
- 最后插入元素
時間復雜度為O(N)。為使二分查找的速度足夠快,存儲在content 中的元素是定長的。
當插入2018 時,所有的元素向后移動,并且不會發生覆蓋。
當Set 中存放的整型元素集中在小整數范圍[-128, 127]內時,可大大的節省內存空間。
IntSet支持升級,但是不支持降級。
- Set 基本操作
適用場景
無序集合,自動去重,數據太多時不太推薦使用。
直接基于set將系統里需要去重的數據扔進去,自動就給去重了,如果你需要對一些數據進行快速的全局去重,你當然也可以基于JVM內存里的HashSet進行去重,但若你的某個系統部署在多臺機器呢?就需要基于redis進行全局的set去重。
可基于set玩交集、并集、差集操作,比如交集:
- 把兩個人的粉絲列表整一個交集,看看倆人的共同好友
- 把兩個大v的粉絲都放在兩個set中,對兩個set做交集
全局這種計算開銷也大。
- 記錄唯一的事物
比如想知道訪問某個博客的IP地址,不要重復的IP,這種情況只需要在每次處理一個請求時簡單的使用SADD命令就可以了,可確保不會插入重復IP - 表示關系
你可以使用Redis創建一個標簽系統,每個標簽使用一個Set表示。然后你可以使用SADD命令把具有特定標簽的所有對象的所有ID放在表示這個標簽的Set中如果你想要知道同時擁有三個不同標簽的對象,那么使用SINTER - 可使用SPOP 或 SRANDMEMBER 命令從集合中隨機的提取元素。
Hash/Map
一般可將結構化的數據,比如一個對象(前提是這個對象未嵌套其他的對象)給緩存在redis里,然后每次讀寫緩存的時候,即可直接操作hash里的某個字段。
key=150
value={
“id”: 150,
“name”: “zhangsan”,
“age”: 20
}
hash類的數據結構,主要存放一些對象,把一些簡單的對象給緩存起來,后續操作的時候,可直接僅修改該對象中的某字段的值。
value={
“id”: 150,
“name”: “zhangsan”,
“age”: 21
}
因為Redis本身是一個K.V存儲結構,Hash結構可理解為subkey - subvalue
這里面的subkey - subvalue只能是
- 整型
- 浮點型
- 字符串
因為Map的 value 可表示整型和浮點型,因此Map也可以使用hincrby 對某個field的value值做自增操作。
內存數據結構
hash有HashTable 和 ziplist 兩種實現。對于數據量較小的hash,使用ziplist 實現。
HashTable 實現
HashTable在Redis 中分為3 層,自底向上分別是:
- dictEntry:管理一個field - value 對,保留同一桶中相鄰元素的指針,以此維護Hash 桶中的內部鏈
- dictht:維護Hash表的所有桶鏈
- dict:當dictht需要擴容/縮容時,用戶管理dictht的遷移
dict是Hash表存儲的頂層結構
// 哈希表(字典)數據結構,Redis 的所有鍵值對都會存儲在這里。其中包含兩個哈希表。
typedef struct dict {
// 哈希表的類型,包括哈希函數,比較函數,鍵值的內存釋放函數
dictType *type;
// 存儲一些額外的數據
void *privdata;
// 兩個哈希表
dictht ht[2];
// 哈希表重置下標,指定的是哈希數組的數組下標
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 綁定到哈希表的迭代器個數
int iterators; /* number of iterators currently running */
} dict;
Hash表的核心結構是dictht,它的table 字段維護著 Hash 桶,桶(bucket)是一個數組,數組的元素指向桶中的第一個元素(dictEntry)。
typedef struct dictht {
//槽位數組
dictEntry **table;
//槽位數組長度
unsigned long size;
//用于計算索引的掩碼
unsigned long sizemask;
//真正存儲的鍵值對數量
unsigned long used;
} dictht;
結構圖
Hash表使用【鏈地址法】解決Hash沖突。當一個 bucket 中的 Entry 很多時,Hash表的插入性能會下降,此時就需要增加bucket的個數來減少Hash沖突。
Hash表擴容
和大多數Hash表實現一樣,Redis引入負載因子判定是否需要增加bucket個數:
負載因子 = Hash表中已有元素 / bucket數量
擴容后,bucket 數量是原先2倍。目前有2 個閥值:
- 小于1 時一定不擴容
- 大于5 時一定擴容
- 在1 ~ 5 之間時,Redis 如果沒有進行bgsave/bdrewrite 操作時則會擴容
- 當key - value 對減少時,低于0.1時會進行縮容。縮容之后,bucket的個數是原先的0.5倍
ziplist 實現
這里的 ziplist 和List#ziplist的實現類似,都是通過Entry 存放元素。
不同的是,Map#ziplist的Entry個數總是2的整數倍:
- 第奇數個Entry存放key
- 下個相鄰Entry存放value
ziplist承載時,Map的大多數操作不再是O(1)了,而是由Hash表遍歷,變成了鏈表的遍歷,復雜度變為O(N)
由于Map相對較小時采用ziplist,采用Hash表時計算hash值的開銷較大,因此綜合起來ziplist的性能相對好一些
哈希鍵值結構
特點:
- Map的map
- Small redis
- field不能相同,value可相同
hget key field O(1)
# 獲取 hash key 對應的 field 的 value
hset key field value O(1)
# 設置 hash key 對應的 field 的 value
hdel key field O(1)
# 刪除 hash key 對應的 field 的 value
實操
127.0.0.1:6379> hset user:1:info age 23
(integer) 1
127.0.0.1:6379> hget user:1:info age
"23"
127.0.0.1:6379> hset user:1:info name JAVAEdge
(integer) 1
127.0.0.1:6379> hgetall user:1:info
1) "age"
2) "23"
3) "name"
4) "JavaEdge"
127.0.0.1:6379> hdel user:1:info age
(integer) 1
127.0.0.1:6379> hgetall user:1:info
1) "name"
2) "JavaEdge"
hexists key field O(1)
# 判斷hash key是否有field
hlen key O(1)
# 獲取hash key field的數量
127.0.0.1:6379> hgetall user:1:info
1) "name"
2) "JavaEdge"
127.0.0.1:6379> HEXISTS user:1:info name
(integer) 1
127.0.0.1:6379> HLEN user:1:info
(integer) 1
hmget key field1 field2... fieldN O(N)
# 批量獲取 hash key 的一批 field 對應的值
hmset key field1 value1 field2 value2...fieldN valueN O(N)
# 批量設置 hash key的一批field value
Redis Hashes 保存String域和String值之間的映射,所以它們是用來表示對象的絕佳數據類型(比如一個有著用戶名,密碼等屬性的User對象)
| `1` | `@cli` |
| `2` | `HMSET user:1000 username antirez password P1pp0 age 34` |
| `3` | `HGETALL user:1000` |
| `4` | `HSET user:1000 password 12345` |
| `5` | `HGETALL user:1000` |
一個有著少量數據域(這里的少量大概100上下)的hash,其存儲方式占用很小的空間,所以在一個小的Redis實例中就可以存儲上百萬的這種對象
Hash的最大長度是2^32 – 1個域值對(4294967295,一個Hash中可以有多達40多億個域值對)
Sorted sets(zset)
有序集合,去重但可排序,寫進去時候給個分數,可以自定義排序規則。比如想根據時間排序,則寫時可以使用時間戳作為分數。
排行榜:將每個用戶以及其對應的什么分數寫進去。
127.0.0.1:6379> zadd board 1.0 JavaEdge
(integer) 1
獲取排名前100的用戶:
127.0.0.1:6379> zrevrange board 0 99
1) "JavaEdge"
用戶在排行榜里的排名:
127.0.0.1:6379> zrank board JavaEdge
(integer) 0
127.0.0.1:6379> zadd board 85 zhangsan
(integer) 1
127.0.0.1:6379> zadd board 72 wangwu
(integer) 1
127.0.0.1:6379> zadd board 96 lisi
(integer) 1
127.0.0.1:6379> zadd board 62 zhaoliu
(integer) 1
# 獲取排名前3的用戶
127.0.0.1:6379> zrevrange board 0 3
1) "lisi"
2) "zhangsan"
3) "wangwu"
4) "zhaoliu"
127.0.0.1:6379> zrank board zhaoliu
(integer) 1
類似于Map的key-value對,但有序
- key :key-value對中的鍵,在一個Sorted-Set中不重復
- value : 浮點數,稱為 score
- 有序 :內部按照score 從小到大的順序排列
基本操作
由于Sorted-Set 本身包含排序信息,在普通Set 的基礎上,Sorted-Set 新增了一系列和排序相關的操作:
- Sorted-Set的基本操作
內部數據結構
Sorted-Set類型的valueObject 內部結構有兩種:
- ziplist
實現方式和Map類似,由于Sorted-Set包含了Score的排序信息,ziplist內部的key-value元素對的排序方式也是按照Score遞增排序的,意味著每次插入數據都要移動之后的數據,因此ziplist適于元素個數不多,元素內容不大的場景。 - skiplist+hashtable
更通用的場景,Sorted-Set使用sliplist來實現。
8.2.1 zskiplist
和通用的跳表不同的是,Redis為每個level 對象增加了span 字段,表示該level 指向的forward節點和當前節點的距離,使得getByRank類的操作效率提升
- 數據結構
- 結構示意圖
每次向skiplist 中新增或者刪除一個節點時,需要同時修改圖標中紅色的箭頭,修改其forward和span的值。
需要修改的箭頭和對skip進行查找操作遍歷并廢棄過的路徑是吻合的。span修改僅是+1或-1。
zskiplist 的查找平均時間復雜度 O(Log(N)),因此add / remove的復雜度也是O(Log(N))。因此Redis中新增的span 提升了獲取rank(排序)操作的性能,僅需對遍歷路徑相加即可(矢量相加)。
還有一點需要注意的是,每個skiplist的節點level 大小都是隨機生成的(1-32之間)。
- zskiplistNode源碼
8.2.2 hashtable
zskiplist 是zset 實現順序相關操作比較高效的數據結構,但是對于簡單的zscore操作效率并不高。Redis在實現時,同時使用了Hashtable和skiplist,代碼結構如下:
Hash表的存在使得Sorted-Set中的Map相關操作復雜度由O(N)變為O(1)。
Redis有序集合類型與Redis的集合類型類似,是非重復的String元素的集合。不同之處在于,有序集合中的每個成員都關聯一個Score,Score是在排序時候使用的,按照Score的值從小到大進行排序。集合中每個元素是唯一的,但Score有可能重復。
使用有序集合可以很高效的進行,添加,移除,更新元素的操作(時間消耗與元素個數的對數成比例)。由于元素在集合中的位置是有序的,使用get ranges by score或者by rank(位置)來順序獲取或者隨機讀取效率都很高。(本句不確定,未完全理解原文意思,是根據自己對Redis的淺顯理解進行的翻譯)訪問有序集合中間部分的元素也非常快,所以可以把有序集合當做一個不允許重復元素的智能列表,你可以快速訪問需要的一切:獲取有序元素,快速存在測試,快速訪問中間的元素等等。
簡短來說,使用有序集合可以實現很多高性能的工作,這一點在其他數據庫是很難實現的。
應用
- 在大型在線游戲中創建一個排行榜,每次有新的成績提交,使用[ZADD]命令加入到有序集合中。可以使用[ZRANGE]命令輕松獲得成績名列前茅的玩家,你也可以使用[ZRANK]根據一個用戶名獲得該用戶的分數排名。把ZRANK 和 ZRANGE結合使用你可以獲得與某個指定用戶分數接近的其他用戶。這些操作都很高效。
- 有序集合經常被用來索引存儲在Redis中的數據。比如,如果你有很多用戶,用Hash來表示,可以使用有序集合來為這些用戶創建索引,使用年齡作為Score,使用用戶的ID作為Value,這樣的話使用[ZRANGEBYSCORE]命令可以輕松和快速的獲得某一年齡段的用戶。zset有個ZSCORE的操作,用于返回單個集合member的分數,它的操作復雜度是O(1),這就是收益于你這看到的hash table。這個hash table保存了集合元素和相應的分數,所以做ZSCORE操作時,直接查這個表就可以,復雜度就降為O(1)了。
而跳表主要服務范圍操作,提供O(logN)的復雜度。
Bitmaps
位圖類型,String類型上的一組面向bit操作的集合。由于 strings是二進制安全的blob,并且它們的最大長度是512m,所以bitmaps能最大設置 2^32個不同的bit。
HyperLogLogs
pfadd/pfcount/pfmerge。
在redis的實現中,使用標準錯誤小于1%的估計度量結束。這個算法的神奇在于不再需要與需要統計的項相對應的內存,取而代之,使用的內存一直恒定不變。最壞的情況下只需要12k,就可以計算接近2^64個不同元素的基數。
GEO
geoadd/geohash/geopos/geodist/georadius/georadiusbymember
Redis的GEO特性在 Redis3.2版本中推出,這個功能可以將用戶給定的地理位置(經、緯度)信息儲存起來,并對這些信息進行操作。