Redis基礎

Redis基礎

1.1 Redis 誕生歷程

1.1.1從一個故事開始

08 年的時候有一個意大利西西里島的小夥子,筆名 antirez(http://invece.org/),創建了一個訪客信息網站 LLOOGG.COM。有的時候我們需要知道網站的訪問情況,比如訪客的 IP、操作系統、瀏覽器、使用的搜索關鍵詞、所在地區、訪問的網頁地址等等。在國內,有很多網站提供了這個功能,比如 CNZZ,百度統計,國外也有谷歌的 GoogleAnalytics。我們不用自己寫代碼去實現這個功能,只需要在全局的 footer 裏面嵌入一段JS 代碼就行了,當頁面被訪問的時候,就會自動把訪客的信息發送到這些網站統計的服務器,然後我們登錄後臺就可以查看數據了。
LLOOGG.COM 提供的就是這種功能,它可以查看最多 10000 條的最新瀏覽記錄。這樣的話,它需要爲每一個網站創建一個列表(List),不同網站的訪問記錄進入到不同的列表。如果列表的長度超過了用戶指定的長度,它需要把最早的記錄刪除(先進先出)。

1570752179502.png

當 LLOOGG.COM 的用戶越來越多的時候,它需要維護的列表數量也越來越多,這種記錄最新的請求和刪除最早的請求的操作也越來越多。LLOOGG.COM 最初使用的數據庫是 MySQL,可想而知,因爲每一次記錄和刪除都要讀寫磁盤,因爲數據量和併發量太大,在這種情況下無論怎麼去優化數據庫都不管用了。
考慮到最終限制數據庫性能的瓶頸在於磁盤,所以 antirez 打算放棄磁盤,自己去實現一個具有列表結構的數據庫的原型,把數據放在內存而不是磁盤,這樣可以大大地提升列表的 push 和 pop 的效率。antirez 發現這種思路確實能解決這個問題,所以用 C 語言重寫了這個內存數據庫,並且加上了持久化的功能,09 年,Redis 橫空出世了。從最開始只支持列表的數據庫,到現在支持多種數據類型,並且提供了一系列的高級特性,Redis 已經成爲一個在全世界被廣泛使用的開源項目。爲什麼叫 REDIS 呢?它的全稱是 REmote DIctionary Service,直接翻譯過來是遠程字典服務。
從 Redis 的誕生歷史我們看到了,在某些場景中,關係型數據庫並不適合用來存儲
我們的 Web 應用的數據。那麼,關係型數據庫和非關係型數據庫,或者說 SQL 和 NoSQL,
到底有什麼不一樣呢?

1.2 Redis 定位與特性

1.2.1 SQL與NoSQL

在絕大部分時候,我們都會首先考慮用關係型數據庫來存儲我們的數據,比如SQLServer,Oracle,MySQL 等等。
關係型數據庫的特點:
    1、它以表格的形式,基於行存儲數據,是一個二維的模式。
    2、它存儲的是結構化的數據,數據存儲有固定的模式(schema),數據需要適應表結構。
    3、表與表之間存在關聯(Relationship)。
    4、大部分關係型數據庫都支持 SQL(結構化查詢語言)的操作,支持複雜的關聯查詢。
    5、通過支持事務(ACID 酸)來提供嚴格或者實時的數據一致性。


但是使用關係型數據庫也存在一些限制,比如:
    1、要實現擴容的話,只能向上(垂直)擴展,比如磁盤限制了數據的存儲,就要擴大磁盤容量,通過堆硬件的方式,不支持動態的擴縮容。水平擴容需要複雜的技術來實現,比如分庫分表。
    2、表結構修改困難,因此存儲的數據格式也受到限制。
    3、在高併發和高數據量的情況下,我們的關係型數據庫通常會把數據持久化到磁盤,基於磁盤的讀寫壓力比較大。

爲了規避關係型數據庫的一系列問題,我們就有了非關係型的數據庫,我們一般把它叫做“non-relational”或者“Not Only SQL”。NoSQL 最開始是不提供 SQL 的數據庫的意思,但是後來意思慢慢地發生了變化。

非關係型數據庫的特點:
    1、存儲非結構化的數據,比如文本、圖片、音頻、視頻。
    2、表與表之間沒有關聯,可擴展性強。
    3、保證數據的最終一致性。遵循 BASE(鹼)理論。 Basically Available(基本可用); Soft-state(軟狀態); Eventually Consistent(最終一致性)。
    4、支持海量數據的存儲和高併發的高效讀寫。
    5、支持分佈式,能夠對數據進行分片存儲,擴縮容簡單。

對於不同的存儲類型,我們又有各種各樣的非關係型數據庫,比如有幾種常見的類型
    1、KV 存儲,用 Key Value 的形式來存儲數據。比較常見的有 Redis 和MemcacheDB。
    2、文檔存儲,MongoDB。
    3、列存儲,HBase。
    4、圖存儲,這個圖(Graph)是數據結構,不是文件格式。Neo4j。
    5、對象存儲。
    6、XML 存儲等等等等。

這個網頁列舉了各種各樣的 NoSQL 數據庫 http://nosql-database.org/

NewSQL 結合了 SQL 和 NoSQL 的特性(例如 PingCAP 的 TiDB)。

1.2.2 Redis 特性

官網介紹:https://redis.io/topics/introduction
中文網站:http://www.redis.cn
硬件層面有 CPU 的緩存;瀏覽器也有緩存;手機的應用也有緩存。我們把數據緩存起來的原因就是從原始位置取數據的代價太大了,放在一個臨時位置存儲起來,取回就可以快一些。
Redis 的特性:
1)更豐富的數據類型
2)進程內與跨進程;單機與分佈式
3)功能豐富:持久化機制、過期策略
4)支持多種編程語言
5)高可用,集羣

1.3 Redis 安裝啓動 參考下面的鏈接

CentOS Redis環境搭建

1.4 Redis基本操作

默認有 16 個庫(0-15),可以在配置文件中修改,默認使用第一個 db0。

1570753926946.png

databases 16

因爲Redis沒有完全把庫隔離,不像數據庫的 database,不適合把不同的庫分配給不同的業務使用。
切換數據庫

select 0

清空當前數據庫

flushdb

清空所有數據庫

flushall
Redis 是字典結構的存儲方式,採用 key-value 存儲。key 和 value 的最大長度限制是 512M(來自官網 https://redis.io/topics/data-types-intro/ )。

1570754230840.png

鍵的基本操作。
命令參考: http://redisdoc.com/index.html

密碼驗證

 auth qazwsx

存值

set java 123

取值

get java

查看所有鍵

keys *

獲取鍵總數

dbsize

查看鍵是否存在 (存在返回1 不存在返回0)

exists java

刪除鍵

del java 

重命名鍵

rename java php

查看類型

type php

Redis 一共有幾種數據類型?(注意是數據類型不是數據結構)
官網:https://redis.io/topics/data-types-intro
String、Hash、Set、List、Zset、Hyperloglog、Geo、Streams

1.5 Redis數據類型

最基本也是最常用的數據類型就是 String。set 和 get 命令就是 String 的操作命令。爲什麼叫 Binary-safe strings 呢?

1.5.1 String 字符串

存儲 類型

可以用來存儲字符串、整數、浮點數。

操作命令

設置多個值(批量操作,原子性)如果有重複的 會覆蓋

mset sunda 1314 Java 520

設置值,如果 key 存在,則不成功

setnx sunda

基於此可實現分佈式鎖。用 del key 釋放鎖。
但如果釋放鎖的操作失敗了,導致其他節點永遠獲取不到鎖,怎麼辦?
加過期時間。單獨用 expire 加過期,也失敗了,無法保證原子性,怎麼辦?多參數

set key value [expiration EX seconds|PX milliseconds][NX|XX]

使用參數的方式

set lock1 1 EX 10 NX

(整數)值遞增

incr java
incrby Java 100

(整數)值遞減

decr java
decrby java 100

浮點數增量

set f 2.6
incrbyfloat f 7.3

獲取多個值

mget java php

獲取值長度

strlen Java

字符串追加內容

append sunda good

獲取指定範圍的字符

getrange sunda 0 8

查看對外類型

type sunda

存儲(實現)原理

數據 模型
set hello word 爲例,因爲 Redis 是 KV 的數據庫,它是通過 hashtable 實現的(我們把這個叫做外層的哈希)。所以每個鍵值對都會有一個 dictEntry(源碼位置:dict.h),裏面指向了 key 和 value 的指針。next 指向下一個 dictEntry。
typedef struct dictEntry {
    void *key;    /* key 關鍵字定義  */
    union {
        void *val;
        uint64_t u64;  /* value 定義 */
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next; /* 指向下一個鍵值對節點 */
} dictEntry;

1570758435951.png

key 是字符串,但是 Redis 沒有直接使用 C 的字符數組,而是存儲在自定義的 SDS中。
value 既不是直接作爲字符串存儲,也不是直接存儲在 SDS 中,而是存儲在redisObject 中。實際上五種常用的數據類型的任何一種,都是通過 redisObject 來存儲的。

redisObject

redisObject 定義在 src/server.h 文件中。

typedef struct redisObject {
    unsigned type:4;  /* 對象的類型,包括:OBJ_STRING、OBJ_LIST、OBJ_HASH、OBJ_SET、OBJ_ZSET */
    unsigned encoding:4;/* 具體的數據結構  */ //實際使用的編碼
    unsigned lru:LRU_BITS;  /* 24 位,對象最後一次被命令程序訪問的時間,與內存回收有關 */
    int refcount;  /* 引用計數。當 refcount 爲 0 的時候,表示該對象已經不被任何對象引用,則可以進行垃圾回收了
*/
    void *ptr; /* 指向對象實際的數據結構 */
} robj;
內部編碼

1570758902327.png

字符串類型的內部編碼有三種:
1、int,存儲 8 個字節的長整型(long,2^63-1)。
2、embstr, 代表 embstr 格式的 SDS(Simple Dynamic String 簡單動態字符串),存儲小於 44 個字節的字符串。
3、raw,存儲大於 44 個字節的字符串(3.2 版本之前是 39 字節)。爲什麼是 39?

在源代碼object.c裏面定義44個字節

1570759142117.png

問題1、什麼是SDS

Redis 中字符串的實現。
在 3.2 以後的版本中,SDS 又有多種結構(sds.h):sdshdr5、sdshdr8、sdshdr16、sdshdr32、sdshdr64,用於存儲不同的長度的字符串,分別代表 2^5=32byte,2^8=256byte,2^16=65536byte=64KB,2^32byte=4GB

1570759261686.png

問題2、爲什麼 Redis 要用 SDS 實現字符串?
我們知道,C 語言本身沒有字符串類型(只能用字符數組 char[]實現)。
1、使用字符數組必須先給目標變量分配足夠的空間,否則可能會溢出。
2、如果要獲取字符長度,必須遍歷字符數組,時間複雜度是 O(n)。
3、C 字符串長度的變更會對字符數組做內存重分配。
4、通過從字符串開始到結尾碰到的第一個'\0'來標記字符串的結束,因此不能保存圖片、音頻、視頻、壓縮文件等二進制(bytes)保存的內容,二進制不安全。

SDS 的特點:
1、不用擔心內存溢出問題,如果需要會對 SDS 進行擴容。
2、獲取字符串長度時間複雜度爲 O(1),因爲定義了 len 屬性。
3、通過“空間預分配”( sdsMakeRoomFor)和“惰性空間釋放”,防止多次重分配內存。
4、判斷是否結束的標誌是 len 屬性(它同樣以'\0'結尾是因爲這樣就可以使用 C語言中函數庫操作字符串的函數了),可以包含'\0'

C字符串 SDS
獲取字符串長度的複雜度爲 O(N) 獲取字符串長度的複雜度爲 O(1)
API 是不安全的,可能會造成緩衝區溢出 API 是安全的,不會早晨個緩衝區溢出
修改字符串長度N次必然需要執行N次內存重分配 修改字符串長度N次最多需要執行N次內存重分配
只能保存文本數據 可以保存文本或者二進制數據
可以使用所有<string.h>庫中的函數 可以使用一部分<string.h>庫中的函數

問題三 embstr 和 raw 的區別?

embstr 的使用只分配一次內存空間(因爲 RedisObject 和 SDS 是連續的),而 raw需要分配兩次內存空間(分別爲 RedisObject 和 SDS 分配空間)。
因此與 raw 相比,embstr 的好處在於創建時少分配一次空間,刪除時少釋放一次空間,以及對象的所有數據連在一起,尋找方便。
而 embstr 的壞處也很明顯,如果字符串的長度增加需要重新分配內存時,整個RedisObject 和 SDS 都需要重新分配空間,因此 Redis 中的 embstr 實現爲只讀

問題 4:int 和 embstr 什麼時候轉化爲 raw?

當 int 數 據 不 再 是 整 數 , 或 大 小 超 過 了 long 的 範 圍(2^63-1=9223372036854775807)時,自動轉化爲 embstr。
192.168.2.171:6379> set k1 1
OK
192.168.2.171:6379> append k1 a
(integer) 2
192.168.2.171:6379> object encoding k1
"raw"

問題 5:明明沒有超過閾值,爲什麼變成 raw 了?

192.168.2.171:6379> set k2 a
OK
192.168.2.171:6379> object encoding k2
"embstr"
192.168.2.171:6379> append k2 b
(integer) 2
192.168.2.171:6379> object encoding k2
"raw"

對於 embstr,由於其實現是隻讀的,因此在對 embstr 對象進行修改時,都會先轉化爲 raw 再進行修改。
因此,只要是修改 embstr 對象,修改後的對象一定是 raw 的,無論是否達到了 44個字節。

問題 6:當長度小於閾值時,會還原嗎?

關於 Redis 內部編碼的轉換,都符合以下規律:編碼轉換在 Redis 寫入數據時完成,且轉換過程不可逆,只能從小內存編碼向大內存編碼轉換(但是不包括重新 set)

問題 7:爲什麼要對底層的數據結構進行一層包裝呢?

通過封裝,可以根據對象的類型動態地選擇存儲結構和可以使用的命令,實現節省

空間和優化查詢速度。

應用 場景
緩存

String 類型
例如:熱點數據緩存(例如報表,明星出軌),對象緩存,全頁緩存。
可以提升熱點數據的訪問速度。

數據 共享分佈式

STRING 類型,因爲 Redis 是分佈式的獨立服務,可以在多個應用之間共享
例如:分佈式 Session

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
分佈式鎖

STRING 類型 setnx 方法,只有不存在時才能添加成功,返回 true。
http://redisdoc.com/string/set.html 建議用參數的形式

public Boolean getLock(Object lockObject){
jedisUtil = getJedisConnetion();
boolean flag = jedisUtil.setNX(lockObj, 1);
if(flag){
expire(locakObj,10);
}
return flag;
}

public void releaseLock(Object lockObject){
del(lockObj);
}
全局 ID

INT 類型,INCRBY,利用原子性

incrby userid 1000

(分庫分表的場景,一次性拿一段)

計數器

INT 類型,INCR 方法
例如:文章的閱讀量,微博點贊數,允許一定的延遲,先寫入 Redis 再定時同步到
數據庫。

限流

INT 類型,INCR 方法
以訪問者的 IP 和其他信息作爲 key,訪問一次增加一次計數,超過次數則返回 false。

位統計

String 類型的 BITCOUNT(1.6.6 的 bitmap 數據結構介紹)。
字符是以 8 位二進制存儲的。

set k1 a
setbit k1 6 1
setbit k1 7 0
get k1

a 對應的 ASCII 碼是 97,轉換爲二進制數據是 01100001
b 對應的 ASCII 碼是 98,轉換爲二進制數據是 01100010
因爲 bit 非常節省空間(1 MB=8388608 bit),可以用來做大數據量的統計。
例如:在線用戶統計,留存用戶統計

setbit onlineusers 0 1
setbit onlineusers 1 1
setbit onlineusers 2 0

支持按位與、按位或等等操作。

BITOP AND destkey key [key ...] ,對一個或多個 key 求邏輯並,並將結果保存到 destkey 。
BITOP OR destkey key [key ...] ,對一個或多個 key 求邏輯或,並將結果保存到 destkey 。
BITOP XOR destkey key [key ...] ,對一個或多個 key 求邏輯異或,並將結果保存到 destkey 。
BITOP NOT destkey key ,對給定 key 求邏輯非,並將結果保存到 destkey 。

計算出 7 天都在線的用戶

BITOP "AND" "7_days_both_online_users" "day_1_online_users" "day_2_online_users" ... "day_7_online_users"

如果一個對象的 value 有多個值的時候,怎麼存儲?
例如用一個 key 存儲一張表的數據。

1570761778716.png

序列化?例如 JSON/Protobuf/XML,會增加序列化和反序列化的開銷,並且不能
單獨獲取、修改一個值。
可以通過 key 分層的方式來實現,例如:

mset student:1:sno 16666 student:1:sname 孫達 student:1:company 騰訊

獲取值的時候一次獲取多個值:

mget student:1:sno student:1:sname student:1:company

缺點:key 太長,佔用的空間太多。有沒有更好的方式?

1.5.2 Hash

1570788874049.png

存儲 類型

包含鍵值對的無序散列表。value 只能是字符串,不能嵌套其他類型。

同樣是存儲字符串,Hash 與 String 的主要區別?
1、把所有相關的值聚集到一個 key 中,節省內存空間
2、只使用一個 key,減少 key 衝突
3、當需要批量獲取值的時候,只需要使用一個命令,減少內存/IO/CPU 的消耗
Hash 不適合的場景:
1、Field 不能單獨設置過期時間
2、沒有 bit 操作
3、需要考慮數據量分佈的問題(value 值非常大的時候,無法分佈到多個節點)

操作命令

hset h1 f 6
hset h1 e 5
hmset h1 a 1 b 2 c 3 d 4
hget h1 a
hmget h1 a b c d
hkeys h1
hvals h1
hgetall h1

key 操作

hget exists h1
hdel h1
hlen h1

存儲(實現)原理

​ Redis 的 Hash 本身也是一個 KV 的結構,類似於 Java 中的 HashMap。
​ 外層的哈希(Redis KV 的實現)只用到了 hashtable。當存儲 hash 數據類型時,
我們把它叫做內層的哈希。內層的哈希底層可以使用兩種數據結構實現:
​ ziplist:OBJ_ENCODING_ZIPLIST(壓縮列表)
​ hashtable:OBJ_ENCODING_HT(哈希表)

ziplist 壓縮 列表
ziplist 壓縮列表是什麼?
/* ziplist.c 源碼頭部註釋 */
/* The ziplist is a specially encoded dually linked list that is designed
 * to be very memory efficient. It stores both strings and integer values,
 * where integers are encoded as actual integers instead of a series of
 * characters. It allows push and pop operations on either side of the list
 * in O(1) time. However, because every operation requires a reallocation of
 * the memory used by the ziplist, the actual complexity is related to the
 * amount of memory used by the ziplist.
 */

​ ziplist 是一個經過特殊編碼的雙向鏈表,它不存儲指向上一個鏈表節點和指向下一
個鏈表節點的指針,而是存儲上一個節點長度和當前節點長度,通過犧牲部分讀寫性能,
來換取高效的內存空間利用率,是一種時間換空間的思想。只用在字段個數少,字段值
小的場景裏面。

ziplist 的內部結構?

ziplist.c 源碼第 16 行的註釋:

<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>

1570789289138.png

typedef struct zlentry {
unsigned int prevrawlensize; /* 上一個鏈表節點佔用的長度 */
unsigned int prevrawlen; /* 存儲上一個鏈表節點的長度數值所需要的字節數 */
unsigned int lensize; /* 存儲當前鏈表節點長度數值所需要的字節數 */
unsigned int len; /* 當前鏈表節點佔用的長度 */
unsigned int headersize; /* 當前鏈表節點的頭部大小(prevrawlensize + lensize),即非數據域的大小 */
unsigned char encoding; /* 編碼方式 */
unsigned char *p; /* 壓縮鏈表以字符串的形式保存,該指針指向當前節點起始位置 */
} zlentry;

1570789403016.png

編碼 encoding(ziplist.c 源碼第 204 行)
1570789460384.png

define ZIP_STR_06B (0 << 6) //長度小於等於 63 字節

define ZIP_STR_14B (1 << 6) //長度小於等於 16383 字節

define ZIP_STR_32B (2 << 6) //長度小於等於 4294967295 字節

1570789480640.png

問題:什麼時候使用 ziplist 存儲?

當 hash 對象同時滿足以下兩個條件的時候,使用 ziplist 編碼:
1)所有的鍵值對的健和值的字符串長度都小於等於 64byte(一個英文字母一個字節);
2)哈希對象保存的鍵值對數量小於 512 個。

在redis.conf配置文件

hash-max-ziplist-value 64 // ziplist 中最大能存放的值長度
hash-max-ziplist-entries 512 // ziplist 中最多能存放的 entry 節點數量

在源代碼t_hash.c

if (hashTypeLength(o) > server.hash_max_ziplist_entries)
        hashTypeConvert(o, OBJ_ENCODING_HT);
    /*源碼位置: t_hash.c,當字段值長度過大,轉爲 HT */
for (i = start; i <= end; i++) {
    if (sdsEncodedObject(argv[i]) && sdslen(argv[i]->ptr) > server.hash_max_ziplist_value){
        hashTypeConvert(o, OBJ_ENCODING_HT);
        break;
    }
}

1570789752160.png

1570789784961.png

一個哈希對象超過配置的閾值(鍵和值的長度有>64byte,鍵值對個數>512 個)時,會轉換成哈希表(hashtable)。

hashtable (dict)

在 Redis 中,hashtable 被稱爲字典(dictionary),它是一個數組+鏈表的結構。
源碼位置:dict.h
前面我們知道了,Redis 的 KV 結構是通過一個 dictEntry 來實現的。
Redis 又對 dictEntry 進行了多層的封裝。

typedef struct dictEntry {
    void *key;    /* key 關鍵字定義  */
    union {
        void *val;
        uint64_t u64;  /* value 定義 */
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next; /* 指向下一個鍵值對節點 */
} dictEntry;

1570789927844.png

dictEntry 放到了 dictht(hashtable 裏面):

/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table; /* 哈希表數組 */
unsigned long size; /* 哈希表大小 */
unsigned long sizemask; /* 掩碼大小,用於計算索引值。總是等於 size-1 */
unsigned long used; /* 已有節點數 */
} dictht;

1570790000066.png

ht 放到了 dict 裏面:

typedef struct dict {
dictType *type; /* 字典類型 */
void *privdata; /* 私有數據 */
dictht ht[2]; /* 一個字典有兩個哈希表 */
long rehashidx; /* rehash 索引 */
unsigned long iterators; /* 當前正在使用的迭代器數量 */
} dict;

1570790039165.png

從最底層到最高層 dictEntry——dictht——dict——OBJ_ENCODING_HT

總結:哈希的存儲結構

1570790059299.png

注意:dictht 後面是 NULL 說明第二個 ht 還沒用到。dictEntry*後面是 NULL 說明沒有 hash 到這個地址。dictEntry 後面是NULL 說明沒有發生哈希衝突。

應用 場景

String

String 可以做的事情,Hash 都可以做。

存儲對象類型的數據

比如對象或者一張表的數據,比 String 節省了更多 key 的空間,也更加便於集中管理。

購物車

1570790546989.png

key:用戶 id;field:商品 id;value:商品數量。
+1:hincr。-1:hdecr。刪除:hdel。全選:hgetall。商品數:hlen。

問題:爲什麼要定義兩個哈希表呢?ht[2]

redis 的 hash 默認使用的是 ht[0],ht[1]不會初始化和分配空間。
哈希表 dictht 是用鏈地址法來解決碰撞問題的。在這種情況下,哈希表的性能取決於它的大小(size 屬性)和它所保存的節點的數量(used 屬性)之間的比率:

​ 比率在 1:1 時(一個哈希表 ht 只存儲一個節點 entry),哈希表的性能最好;

​ 如果節點數量比哈希表的大小要大很多的話(這個比例用 ratio 表示,5 表示平均一個 ht 存儲 5 個 entry),那麼哈希表就會退化成多個鏈表,哈希表本身的性能優勢就不再存在。

在這種情況下需要擴容。Redis 裏面的這種操作叫做 rehash。

rehash 的步驟:

​ 1、爲字符 ht[1]哈希表分配空間,這個哈希表的空間大小取決於要執行的操作,以及 ht[0]當前包含的鍵值對的數量。擴展:ht[1]的大小爲第一個大於等於 ht[0].used*2。
​ 2、將所有的 ht[0]上的節點 rehash 到 ht[1]上,重新計算 hash 值和索引,然後放入指定的位置。
​ 3、當 ht[0]全部遷移到了 ht[1]之後,釋放 ht[0]的空間,將 ht[1]設置爲 ht[0]表,並創建新的 ht[1],爲下次 rehash 做準備。

問題:什麼時候觸發擴容?

static int dict_can_resize = 1;
static unsigned int dict_force_resize_ratio = 5;

ratio = used / size,已使用節點與字典大小的比例
dict_can_resize 爲 1 並且 dict_force_resize_ratio 已使用節點數和字典大小之間的比率超過 1:5,觸發擴容

擴容判斷 _dictExpandIfNeeded(源碼 dict.c)

if (d->ht[0].used >= d->ht[0].size && (dict_can_resize ||d->ht[0].used/d->ht[0].size > dict_force_resize_ratio))
{
    return dictExpand(d, d->ht[0].used*2);
}
    return DICT_OK;

擴容方法 dictExpand(源碼 dict.c)

int dictExpand(dict *d, unsigned long size)
{
    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    dictht n; /* the new hash table */
    unsigned long realsize = _dictNextPower(size);

    /* Rehashing to the same table size is not useful. */
    if (realsize == d->ht[0].size) return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;

    /* Is this the first initialization? If so it's not really a rehashing
     * we just set the first hash table so that it can accept keys. */
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    /* Prepare a second hash table for incremental rehashing */
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

縮容: server.c

int htNeedsResize(dict *dict) {
    long long size, used;

    size = dictSlots(dict);
    used = dictSize(dict);
    return (size > DICT_HT_INITIAL_SIZE &&
            (used*100/size < HASHTABLE_MIN_FILL));
}

1.5.3 List 列表

存儲 類型

存儲有序的字符串(從左到右),元素可以重複。可以充當隊列和棧的角色。

1570791287377.png

操作命令

元素增減:

lpush queue a
lpush queue b c
rpush queue d e
lpop queue
rpop queue
blpop queue
brpop queue

取值

lindex queue 0
lrange queue 0 -1

1570791338226.png

存儲(實現)原理

​ 在早期的版本中,數據量較小時用 ziplist 存儲,達到臨界值時轉換爲 linkedlist 進行存儲,分別對應 OBJ_ENCODING_ZIPLIST 和 OBJ_ENCODING_LINKEDLIST 。
​ 3.2 版本之後,統一用 quicklist 來存儲。quicklist 存儲了一個雙向鏈表,每個節點都是一個 ziplist。

quicklist

quicklist(快速列表)是 ziplist 和 linkedlist 的結合體。
quicklist.h,head 和 tail 指向雙向列表的表頭和表尾

typedef struct quicklist {
quicklistNode *head; /* 指向雙向列表的表頭 */
quicklistNode *tail; /* 指向雙向列表的表尾 */
unsigned long count; /* 所有的 ziplist 中一共存了多少個元素 */
unsigned long len; /* 雙向鏈表的長度,node 的數量 */
int fill : 16; /* fill factor for individual nodes */
unsigned int compress : 16; /* 壓縮深度,0:不壓縮; */
} quicklist;

1570791490944.png

redis.conf 相關參數:

參數 含義
list-max-ziplist-size(fill) 正數表示單個 ziplist 最多所包含的 entry 個數。
負數代表單個 ziplist 的大小,默認 8k。
-1:4KB;-2:8KB;-3:16KB;-4:32KB;-5:64KB
list-compress-depth(compress) 壓縮深度,默認是 0。
1:首尾的 ziplist 不壓縮;2:首尾第一第二個 ziplist 不壓縮,以此類推

quicklistNode 中的*zl 指向一個 ziplist,一個 ziplist 可以存放多個元素

typedef struct quicklistNode {
struct quicklistNode *prev; /* 前一個節點 */
struct quicklistNode *next; /* 後一個節點 */
unsigned char *zl; /* 指向實際的 ziplist */
unsigned int sz; /* 當前 ziplist 佔用多少字節 */
unsigned int count : 16; /* 當前 ziplist 中存儲了多少個元素,佔 16bit(下同),最大 65536 個 */
unsigned int encoding : 2; /* 是否採用了 LZF 壓縮算法壓縮節點,1:RAW 2:LZF */
unsigned int container : 2; /* 2:ziplist,未來可能支持其他結構存儲 */
unsigned int recompress : 1; /* 當前 ziplist 是不是已經被解壓出來作臨時使用 */
unsigned int attempted_compress : 1; /* 測試用 */
unsigned int extra : 10; /* 預留給未來使用 */
} quicklistNode;

1570791623750.png

1570791662225.png

ziplist 的結構前面已經說過了,不再重複。

應用 場景

用戶 消息時間線 timeline

因爲 List 是有序的,可以用來做用戶時間線

1570792005382.png

消息 隊列

​ List 提供了兩個阻塞的彈出操作:BLPOP/BRPOP,可以設置超時時間。
​ BLPOP:BLPOP key1 timeout 移出並獲取列表的第一個元素, 如果列表沒有元素會阻塞列表直到等待超時或發現可彈出元素爲止。
​ BRPOP:BRPOP key1 timeout 移出並獲取列表的最後一個元素, 如果列表沒有元素會阻塞列表直到等待超時或發現可彈出元素爲止。
​ 隊列:先進先出:rpush blpop,左頭右尾,右邊進入隊列,左邊出隊列。
​ 棧:先進後出:rpush brpop

1.5.4 Set 集合

存儲類型

String 類型的無序集合,最大存儲數量 2^32-1(40 億左右)。

1570792262135.png

操作命令

添加一個或者多個元素

sadd myset a b c d e f g

獲取所有元素

smembers myset

統計元素個數

scard myset

隨機獲取一個元素

srandmember key

隨機彈出一個元素

spop myset

移除一個或者多個元素

srem myset d e f

查看元素是否存在

sismember myset a

存儲(實現)原理

​ Redis 用 intset 或 hashtable 存儲 set。如果元素都是整數類型,就用 inset 存儲。如果不是整數類型,就用 hashtable(數組+鏈表的存來儲結構)。
​ 問題:KV 怎麼存儲 set 的元素?key 就是元素的值,value 爲 null。
​ 如果元素個數超過 512 個,也會用 hashtable 存儲。

應用 場景

抽獎

​ 隨機獲取元素
​ spop myset

點贊、簽到、打卡

1570792428126.png

這條微博的 ID 是 t1001,用戶 ID 是 u3001。
用 like:t1001 來維護 t1001 這條微博的所有點贊用戶。
點讚了這條微博:sadd like:t1001 u3001
取消點贊:srem like:t1001 u3001
是否點贊:sismember like:t1001 u3001
點讚的所有用戶:smembers like:t1001
點贊數:scard like:t1001
比關係型數據庫簡單許多。

商品 標籤

用 tags:i5001 來維護商品所有的標籤。

1570792454552.png

sadd tags:i5001 畫面清晰細膩
sadd tags:i5001 真彩清晰顯示屏
sadd tags:i5001 流暢至極

商品篩選

獲取差集

sdiff set1 set2

獲取交集( intersection )

sinter set1 set2

獲取並集

sunion set1 set2

1570792503214.png

Phone11 上市了。

sadd brand:apple iPhone11
sadd brand:ios iPhone11
sad screensize:6.0-6.24 iPhone11
sad screentype:lcd iPhone11

篩選商品,蘋果的,iOS 的,屏幕在 6.0-6.24 之間的,屏幕材質是 LCD 屏幕

sinter brand:apple brand:ios screensize:6.0-6.24 screentype:lcd

1.5.5 ZSet 有序集合

存儲 類型

1570796166278.png

sorted set,有序的 set,每個元素有個 score。
score 相同時,按照 key 的 ASCII 碼排序。
數據結構對比:

數據 結構 是否允許 重複元素 是否有序 有序實現方式
列表 list 索引下標
集合 set
有序集合 zset 分值 score

操作命令

添加元素

zadd myzset 10 java 20 php 30 ruby 40 cpp 50 python

獲取全部元素

zrange myzset 0 -1 withscores
zrevrange myzset 0 -1 withscores

根據分值區間獲取元素

zrangebyscore myzset 20 30

移除元素
也可以根據 score rank 刪除

zrem myzset php cpp

統計元素個數

zcard myzset

分值遞增

zincrby myzset 5 python

根據分值統計個數

zcount myzset 20 60

獲取元素 rank

zrank myzset java

獲取元素 score

zsocre myzset java

也有倒序的 rev 操作(reverse)

存儲(實現)原理

​ 同時滿足以下條件時使用 ziplist 編碼:
​ 元素數量小於 128 個
​ 所有 member 的長度都小於 64 字節

​ 在 ziplist 的內部,按照 score 排序遞增來存儲。插入的時候要移動之後的數據。

在 redis.conf 參數

zset-max-ziplist-entries 128
zset-max-ziplist-value 64

超過閾值之後,使用 skiplist+dict 存儲

問題:什麼是 skiplist?

我們先來看一下有序鏈表:

1570796505550.png

在這樣一個鏈表中,如果我們要查找某個數據,那麼需要從頭開始逐個進行比較,直到找到包含數據的那個節點,或者找到第一個比給定數據大的節點爲止(沒找到)。也就是說,時間複雜度爲 O(n)。同樣,當我們要插入新數據的時候,也要經歷同樣的查找過程,從而確定插入位置。
而二分查找法只適用於有序數組,不適用於鏈表。
假如我們每相鄰兩個節點增加一個指針(或者理解爲有三個元素進入了第二層),讓指針指向下下個節點。

1570796542106.png

這樣所有新增加的指針連成了一個新的鏈表,但它包含的節點個數只有原來的一半(上圖中是 7, 19, 26)。在插入一個數據的時候,決定要放到那一層,取決於一個算法
(在 redis 中 t_zset.c 有一個 zslRandomLevel 這個方法)。

現在當我們想查找數據的時候,可以先沿着這個新鏈表進行查找。當碰到比待查數據大的節點時,再回到原來的鏈表中的下一層進行查找。比如,我們想查找 23,查找的路徑是沿着下圖中標紅的指針所指向的方向進行的:

1570796590420.png

  1. 23 首先和 7 比較,再和 19 比較,比它們都大,繼續向後比較。
  2. 但 23 和 26 比較的時候,比 26 要小,因此回到下面的鏈表(原鏈表),與 22比較。
  3. 23 比 22 要大,沿下面的指針繼續向後和 26 比較。23 比 26 小,說明待查數據 23 在原鏈表中不存在

在這個查找過程中,由於新增加的指針,我們不再需要與鏈表中每個節點逐個進行比較了。需要比較的節點數大概只有原來的一半。這就是跳躍表。

​ 爲什麼不用 AVL 樹或者紅黑樹?因爲 skiplist 更加簡潔。

源碼:server.h

typedef struct zskiplistNode {
    sds ele; /* zset 的元素 */
    double score;/* 分值 */
    struct zskiplistNode *backward; /* 後退指針 */
    struct zskiplistLevel {
        struct zskiplistNode *forward; /* 前進指針,對應 level 的下一個節點 */
        unsigned long span; /* 從當前節點到下一個節點的跨度(跨越的節點數) */
    } level[]; /* 層 */
} zskiplistNode;

typedef struct zskiplist {
    struct zskiplistNode *header, *tail; /* 指向跳躍表的頭結點和尾節點 */
    unsigned long length;/* 跳躍表的節點數 */
    int level;/* 最大的層數 */
} zskiplist;

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

隨機獲取層數的函數:

源代碼 t_zset.c

/* Returns a random level for the new skiplist node we are going to create.
 * The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL
 * (both inclusive), with a powerlaw-alike distribution where higher
 * levels are less likely to be returned. */
int zslRandomLevel(void) {
    int level = 1;
    while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

應用 場景

排行榜

d 爲 6001 的新聞點擊數加 1:zincrby hotNews:20190926 1 n6001
獲取今天點擊最多的 15 條:zrevrange hotNews:20190926 0 15 withscores

1570796820289.png

1.5.6 其他 數據結構

https://redis.io/topics/data-types-intro

BitMaps

Bitmaps 是在字符串類型上面定義的位操作。一個字節由 8 個二進制位組成。

1570796953395.png

set k1 a

獲取 value 在 offset 處的值(a 對應的 ASCII 碼是 97,轉換爲二進制數據是 01100001)

getbit k1 0

修改二進制數據(b 對應的 ASCII 碼是 98,轉換爲二進制數據是 01100010)

setbit k1 6 1
setbit k1 7 0
get k1

統計二進制位中 1 的個數

bitcount k1

獲取第一個 1 或者 0 的位置

bitpos k1 1
bitpos k1 0

BITOP 命令支持 AND 、 OR 、 NOT 、 XOR 這四種操作中的任意一種參數:
BITOP AND destkey srckey1 … srckeyN ,對一個或多個 key 求邏輯與,並將結果保存到 destkey
BITOP OR destkey srckey1 … srckeyN,對一個或多個 key 求邏輯或,並將結果保存到 destkey
BITOP XOR destkey srckey1 … srckeyN,對一個或多個 key 求邏輯異或,並將結果保存到 destkey
BITOP NOT destkey srckey,對給定 key 求邏輯非,並將結果保存到 destkey

應用場景:
用戶訪問統計
在線用戶統計

Hyperloglogs

Hyperloglogs:提供了一種不太準確的基數統計方法,比如統計網站的 UV,存在
一定的誤差。HyperLogLogTest.java

Streams

​ 5.0 推出的數據類型。支持多播的可持久化的消息隊列,用於實現發佈訂閱功能,借
鑑了 kafka 的設計。

1.5.7 總結

數據 結構總結

對象 對象 type 屬性 值 type 命令輸出 底層可能的存儲結構 object encoding
字符串對象 OBJ_STRING string OBJ_ENCODING_INT
OBJ_ENCODING_EMBSTR
OBJ_ENCODING_RAW
int
embstr
raw
列表對象 OBJ_LIST list OBJ_ENCODING_QUICKLIST quicklist
哈希對象 OBJ_HASH hash OBJ_ENCODING_ZIPLIST
OBJ_ENCODING_HT
ziplist
hashtable
集合對象 OBJ_SET set OBJ_ENCODING_INTSET
OBJ_ENCODING_HT
intset
hashtable
有序集合對象 OBJ_ZSET zset OBJ_ENCODING_ZIPLIST
OBJ_ENCODING_SKIPLIST
ziplist
skiplist(包含 ht)

編碼 轉換總結

對象 原始編碼 升級編碼
字符串對象 INT
整數並且小於 long 2^63-1
embstr
超過 44 字節,被修改
raw
哈希對象 ziplist
鍵和值的長度小於 64byte,鍵值對個數不
超過 512 個,同時滿足
hashtable
列表對象 quicklist
集合對象 intset
元素都是整數類型,元素個數小於 512 個,
同時滿足
hashtable
有序集合對象 ziplist
元素數量不超過 128 個,任何一個 member
的長度小於 64 字節,同時滿足。
skiplist

2.0問答

1 Redis 爲什麼要自己實現一個 SDS ?

問題2、爲什麼 Redis 要用 SDS 實現字符串?
C 語言本身沒有字符串類型(只能用字符數組 char[]實現)。
1、使用字符數組必須先給目標變量分配足夠的空間,否則可能會溢出。
2、如果要獲取字符長度,必須遍歷字符數組,時間複雜度是 O(n)。
3、C 字符串長度的變更會對字符數組做內存重分配。
4、通過從字符串開始到結尾碰到的第一個'\0'來標記字符串的結束,因此不能保存圖片、音頻、視頻、壓縮文件等二進制(bytes)保存的內容,二進制不安全。

SDS 的特點:
1、不用擔心內存溢出問題,如果需要會對 SDS 進行擴容。
2、獲取字符串長度時間複雜度爲 O(1),因爲定義了 len 屬性。
3、通過“空間預分配”( sdsMakeRoomFor)和“惰性空間釋放”,防止多次重分配內存。
4、判斷是否結束的標誌是 len 屬性(它同樣以'\0'結尾是因爲這樣就可以使用 C語言中函數庫操作字符串的函數了),可以包含'\0'

C字符串 SDS
獲取字符串長度的複雜度爲 O(N) 獲取字符串長度的複雜度爲 O(1)
API 是不安全的,可能會造成緩衝區溢出 API 是安全的,不會早晨個緩衝區溢出
修改字符串長度N次必然需要執行N次內存重分配 修改字符串長度N次最多需要執行N次內存重分配
只能保存文本數據 可以保存文本或者二進制數據
可以使用所有<string.h>庫中的函數 可以使用一部分<string.h>庫中的函數

2、基於Set 如何實現用戶關注模型?

我:me
他:he
我關注的人:focus
關注我的人:attention

操作命令

sadd attention:me he u3 u4
sadd attention:me u3 he
sadd focus:me he u3
sadd focus:he u4

​ 1)相互關注?

sinter attention:me focus:me
  1)  "he"
 2)  "u3"

​ 2)我關注的人也關注了他?

sunion focus:me attention:he
 2)  "he"
 3)  "u3"

​ 3)可能認識的人?

我關注的人 和他關注的人差集 但是要去除我和他

3、dict裏面爲什麼要定義兩個哈希表ht[0] ht[1]?hash擴容是怎麼實現的?

redis 的 hash 默認使用的是 ht[0],ht[1]不會初始化和分配空間。
哈希表 dictht 是用鏈地址法來解決碰撞問題的。在這種情況下,哈希表的性能取決於它的大小(size 屬性)和它所保存的節點的數量(used 屬性)之間的比率:

​ 比率在 1:1 時(一個哈希表 ht 只存儲一個節點 entry),哈希表的性能最好;

​ 如果節點數量比哈希表的大小要大很多的話(這個比例用 ratio 表示,5 表示平均一個 ht 存儲 5 個 entry),那麼哈希表就會退化成多個鏈表,哈希表本身的性能優勢就不再存在。

在這種情況下需要擴容。Redis 裏面的這種操作叫做 rehash。

hash擴容是怎麼實現的?

​ 1、爲字符 ht[1]哈希表分配空間,這個哈希表的空間大小取決於要執行的操作,以及 ht[0]當前包含的鍵值對的數量。擴展:ht[1]的大小爲第一個大於等於 ht[0].used*2。
​ 2、將所有的 ht[0]上的節點 rehash 到 ht[1]上,重新計算 hash 值和索引,然後放入指定的位置。
​ 3、當 ht[0]全部遷移到了 ht[1]之後,釋放 ht[0]的空間,將 ht[1]設置爲 ht[0]表,並創建新的 ht[1],爲下次 rehash 做準備。

公衆號

如果大家想要實時關注我更新的文章以及分享的乾貨的話,可以關注我的公衆號。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章