一、概述
參考作者( 架構師余勝軍 ,寫的非常好)
Redis 是速度非常快的非關系型(NoSQL)記憶體鍵值資料庫,可以存盤鍵和五種不同型別的值之間的映射,
鍵的型別只能為字串,值支持五種資料型別:字串、串列、集合、散串列、有序集合,
Redis 支持很多特性,例如將記憶體中的資料持久化到硬碟中,使用復制來擴展讀性能,使用分片來擴展寫性能,
二、資料型別
| 資料型別 | 可以存盤的值 |
|---|---|
| STRING | 字串、整數或者浮點數 |
| LIST | 串列 |
| SET | 無序集合 |
| HASH | 包含鍵值對的無序散串列 |
| ZSET | 有序集合 |
STRING
set key value 將字串值 value 關聯到 key ,如果 key 已經持有其他值, SET 就覆寫舊值,無視型別,
get key 取key的值value
del key 洗掉key:洗掉操作成功 回傳(integer)1;洗掉操作失敗 回傳(integer)0
高級命令:
mset k1 v1 k2 v2 k3 v3 ... 一次性添加或修改多個鍵值對
mget k1 k2 k3... 一次性獲取k1 k2 k3...的value
strlen k 獲取k對應的v的字串長度
append k v 往k對應的v尾部追加資料,如果不存在就新建,這時候相當于set k v
自增自減操作控制資料庫主鍵:
incr key 對應的value加1
decr key 對應的value減1
incrby key increment 對應的value+increment
decrby key increment 對應的value-increment
incrbyfloat key increment 對應的value+一個浮點數
資料庫的熱點資料key命名規范:
表名:主鍵名:主鍵值:欄位名
LIST
內部是使用雙向鏈表(double linked list)實作的,所以向串列兩端添加元素的時間復雜度為0(1),獲取越接近兩端的元素速度就越快,這意味著即使是一個有幾千萬個元素的串列,獲取頭部或尾部的10條記錄也是極快的
lpush key value1 value2 ... 左側插入
rpush key value1 value2 ... 右側插入
lrange key start stop 從start開始到stop結束的下標的資料,索引從0開始,如果是負數結束,比如stop=-1,那就是截止到倒數第一個
lindex key index 找到index位置的資料
lpop key 移除并回傳第一個元素
rpop key 移除并回傳最后一個元素
llen 獲取串列中元素個數
lrem key count value 洗掉list串列中number個value(因為list元素可以重復,所以要指定count) 1)當count>0時, lrem會從串列左邊開始洗掉
2)當count<0時, lrem會從串列后邊開始洗掉
3)當count=0時, lrem洗掉所有值為value的元素
ltrim key start stop 只保留串列中start開始到stop結束之間指定片段的資料
linsert key before|after pivot value 該命令首先會在串列中從左到右查找值為pivot的元素,然后根據 第二個引數是BEFORE還是AFTER來決定將value插入到該元素的前面還是后面
> rpush list-key item
(integer) 1
> rpush list-key item2
(integer) 2
> rpush list-key item
(integer) 3
> lrange list-key 0 -1
1) "item"
2) "item2"
3) "item"
> lindex list-key 1
"item2"
> lpop list-key
"item"
> lrange list-key 0 -1
1) "item2"
2) "item"
應用場景: 朋友圈評論,按順序顯示評論的朋友
SET
(1) 存盤大量資料、查詢速度快
(2) 集合中的資料是不重復且沒有順序
(3) 集合型別的Redis內部是使用值為空的散串列實作,所有這些操作的時間復雜度都為0(1)
(4) 集合型別的常用操作是向集合中加入或洗掉元素、判斷某個元素是否存在等,除此之外Redis還提供了多個集合之間的交集、并集、差集的運算,
hash和set的結構:
① hash: key-{field:value}
② set: key-{value:null}
sadd key value1 value2 ... 添加資料,如果重復添加,會添加失敗
smembers key 獲取全部資料
scard key 獲取資料總量
srem key value 洗掉資料
sismember key value 判斷value是否是key集合內的資料
> sadd set-key item
(integer) 1
> sadd set-key item2
(integer) 1
> sadd set-key item3
(integer) 1
> sadd set-key item
(integer) 0
> smembers set-key
1) "item"
2) "item2"
3) "item3"
> sismember set-key item4
(integer) 0
> sismember set-key item
(integer) 1
HASH
hash叫散列型別,它提供了欄位和欄位值的映射,欄位值只能是字串型別,不支持其它型別,

格式:一個存盤空間(key)存盤多個鍵值對,底層通過哈希表進行存盤,
? key {filed1 - v1, filed2 - v2,…}
注意:如果filed數量較少時,會被優化為類陣列的結構,如果filed數量多,就是HashMap,
hset key field value 新增/修改某個field的v,新增時回傳1,修改時回傳0
hsetnx key field value 如果key中沒有field欄位則設定field值為value,否則不做任何操作
hget key field 獲取某個field的v
hgetall key 獲取這個key的所有f-v
hdel key field 洗掉某個field,可以洗掉一個或多個,回傳值是被洗掉的欄位個數
del key 洗掉整個key
hmset key f1 v1 f2 v2 f3 v3 ... 新增/修改某個field的f1、f2、f3,值分別為v1、v2、v3
hmget key f1 f2 f3... 獲取某個field的f1、f2、f3 的值
hlen key 獲取key的欄位數量,就是field的數量
hexists key field 判斷key中是否存在field這個欄位
> hset hash-key sub-key1 value1
(integer) 1
> hset hash-key sub-key2 value2
(integer) 1
> hset hash-key sub-key1 value1
(integer) 0
> hdel hash-key sub-key2
(integer) 1
> hdel hash-key sub-key2
(integer) 0
> hget hash-key sub-key1
"value1"
注意事項
(1) hash型別的value只能存盤string,不允許嵌套存盤,如果獲取不到對應的資料,回傳的是(nil),
(2) 每個hash最多存盤2^32 -1 個鍵值對,
(3) hash最初設計不是為了存物件,不要把hash當成物件串列使用,
(4) hgetall 可以獲取全部屬性,如果field過多,遍歷一次會很慢,影響程式效率,
應用場景
(1) 電商購物車:添加購物車、瀏覽購物車商品、更改購物車商品數量、洗掉商品、清空商品均可實作,
????key : userID
????field : 商品ID
????value : 商品購買數量
演示案例
127.0.0.1:6379> hmset userid:1001 id 10011 name phone number 10
OK
127.0.0.1:6379> hmset userid:1002 id 10012 name xiaomi number 15
OK
127.0.0.1:6379> hgetall userid:1001
1) "id"
2) "10011"
3) "name"
4) "phone"
5) "number"
6) "10"
ZSET(有序集合)
zset使用散串列實作
如果添加重復的資料,score會被最后一次的覆寫
zadd key score1 value1 score2 value2.. --添加資料,向有序集合中加入一個元素和該元素的分數,如果該 元素已經存在則會用新的分數替換原有的分數
回傳值是新加入到集合中的元素個數,不包含之前已經存在的元素
zrange key start stop [WITHSCORES] 獲取資料按照元素分數從小到大的順序回傳索引從start到stop之間的所有元素(包含兩端的元素),
如果WITHSCORES在末尾,則會把score也輸出出來
zrevrange key srart stop [WITHSCORES] 按照元素分數從大到小的順序回傳索引從start到stop之間的所有 元素,如果WITHSCORES在末尾,則會把score也輸出出來,
zcard key 獲取資料總量
zcount key min max 獲取[min, max]范圍內的資料數量
zrem key value 洗掉資料
> zadd zset-key 728 member1
(integer) 1
> zadd zset-key 982 member0
(integer) 1
> zadd zset-key 982 member0
(integer) 0
> zrange zset-key 0 -1 withscores
1) "member1"
2) "728"
3) "member0"
4) "982"
> zrangebyscore zset-key 0 800 withscores
1) "member1"
2) "728"
> zrem zset-key member1
(integer) 1
> zrem zset-key member1
(integer) 0
> zrange zset-key 0 -1 withscores
1) "member0"
2) "982"
獲取排名(索引)
zrank key value 獲取value在key中的升序排名,score小的排在前面
zrevrank key value 降序排名
zscore key value 拿到value的score
zincrby key increment value 給value加上對應的increment
127.0.0.1:6379> zadd scores 30 a 50 b 10 c 35 d
(integer) 4
127.0.0.1:6379> zrank scores c <!--獲取c 在key中的升序排名,score小的排在前面-->
(integer) 0
127.0.0.1:6379> zrevrank scores c <!--獲取c 在key中的降序排名,score大的排在前面-->
(integer) 3
127.0.0.1:6379> zscore scores b <!--獲取b 在key中的值 50-->
"50"
127.0.0.1:6379> zincrby scores 5 c <!--給c 加上 5-->
"15"
三、redis底層資料結構(重點)
String
sds 字串即 Simple Dynamic String(即簡單動態字串),其中動態的含義是記憶體的分配是動態的,sds的定義如下: 但是這個sds型別僅作為引數和回傳值使用,并不是真正用于操作的型別,真正核心的部分是下面的這些類:
//當長度不同的時候,型別也不一樣
struct __attribute__ ((__packed__)) sdshdr5 {//當字串長度 <32,用這種
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 { //當字串長度 < 2^8 -1,用這種
uint8_t len;
uint8_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {//當字串長度 < 2^16 -1,用這種
uint16_t len;
uint16_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len;
uint32_t alloc;
unsigned char flags;
char buf[];
};
redis同時寫重寫了大量的與sds型別相關的方法,那redis為什么要這么下功夫呢,有以下4個優點:
- 降低獲取字串長度的時間復雜度到O(1)
- 減少了修改字串時的記憶體重分配次數
- 兼容c字串的同時,提高了一些字串工具方法的效率
- 二進制安全(資料寫入的格式和讀取的格式一致)
可以用位圖來實作活躍用戶的統計
串列(List)
? 一個串列結構可以有序地存盤多個字串,擁有例如:lpush lpop rpush rpop等操作命令,
ziplist的結構
由表頭和N個entry節點和壓縮串列尾部識別符號zlend組成的一個連續的記憶體塊,然后通過一系列的編碼規則,提高記憶體的利用率,主要用于存盤整數和比較短的字串,實際上,ziplist充分體現了Redis對于存盤效率的追求,一個普通的雙向鏈表,鏈表中每一項都占用獨立的一塊記憶體,各項之間用地址指標(或參考)連接起來,這種方式會帶來大量的記憶體碎片,而且地
址指標也會占用額外的記憶體,而ziplist卻是將表中每一項存放在前后連續的地址空間內,一個ziplist整體占用一大塊記憶體,它是一個表(list),但其實不是一個鏈表(linked list),另外,ziplist為了在細節上節省記憶體,對于值的存盤采用了變長的編碼方式,大概意思是說,對于大的整數,就多用一些位元組來存盤,而對于小的整數,就少用一些位元組來存盤,我們接下來很快就會討論到這些實作細節,

但是由于資料量非常大的話,頻繁的進行記憶體分配和釋放,會很麻煩,造成更新效率低下的情況,就引入了一個quicklist的資料結構,配合ziplist,意思就是一個由ziplist組成的雙向鏈表,
每個節點都是以壓縮串列ziplist的結構保存著資料,而每個ziplist又可以包含多個entry,也可以說一個quicklist節點保存的是一片資料,而不是一個資料,即每個quicklist節點就是一個ziplist,具備壓縮串列的特性,整體上quicklist就是一個雙向鏈表結構,和普通的鏈表操作一樣,插入洗掉效率很高,但查詢的效率卻是O(n),不過,這樣的鏈表訪問兩端的元素的時間復雜度卻是O(1),所以,對list的操作多數都是poll和push,

字典(hash)(重點)
? Redis的字典使用哈希表作為底層實作,一個哈希表里面可以有多個哈希表節點,而每個哈希表節點就保存了字典中的一個鍵值對,
接下來將分別介紹Redis的哈希表節點、哈希表以及字典的實作,
typedef struct redisDb {
dict *dict;
dict *expires;
dict *blocking_ keys;
dict *ready_ keys;
dict *watched keys;
int id;
long long avg_ tt;
unsigned long expires cursor;
list *defrag_ later;
} redisDb;
//其中dict的結構為:
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; //rehash的索引,沒有進行rehash操作時值都為-1.
unsigned long iterators; /* number of iterators currently running */
} dict;
//其中dictht的結構為
typedef struct dictht {
dictEntry **table;//table里面放的就是一個一個dictEntry,使用拉鏈法解決哈希沖突,
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
//其中dictEntry的結構為 可以把dictEntry理解為node
typedef struct dictEntry {
void *key; //key就相當于一個下標索引
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next; //指向下一個節點
} dictEntry;
//其中dictEntry中union中的val的資料為redisObject
typedef struct redisObject {
unsigned type:4; //表示值的資料型別,
unsigned encoding:4;//值的編碼方式,就是其底層的資料結構實作,
unsigned lru:LRU_BITS;//記錄了物件最后一次被訪問的時間,用于淘汰過期的鍵值對,
int refcount;//記錄了物件的參考計數,
void *ptr;//指向資料的指標,比如ptr指向String,zest
} robj;

Redis 的字典 dict 中包含兩個哈希表 dictht,這是為了方便進行 rehash 操作,在擴容時,將其中一個 dictht 上的鍵值對 rehash 到另一個 dictht 上面,完成之后釋放空間并交換兩個 dictht 的角色,
rehash程序:隨著操作的不斷執行,哈希表保存的鍵值對有可能增多或者減少,當哈希表保存的鍵值對數量太多或者太少時,程式需要對哈希表的大小進行相應的擴展或者收縮,
擴展和收縮哈希表的作業可以通過執行rehash(重新散列)操作來完成,步驟如下:
1)為字典的ht[1]哈希表分配空間,空間大小根據實際情況而定;
2)將ht[0]中所有鍵值對rehash到ht[1]中
注意:rehash指的是重新計算鍵的哈希值和索引值,然后將鍵值對放置到ht[1]哈希表的指定位置上
3)釋放ht[0],將ht[1]設定為ht[0],并在ht[1]新建一個空表,為下次rehash做準備

rehash 操作不是一次性完成,而是采用漸進方式,這是為了避免一次性執行過多的 rehash 操作給服務器帶來過大的負擔,漸進式 rehash 通過記錄 dict 的 rehashidx 完成,它從 0 開始,然后每執行一次 rehash 都會遞增,例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],這一次會把 dict[0] 上 table[rehashidx] 的鍵值對 rehash 到 dict[1] 上,dict[0] 的 table[rehashidx] 指向 null,并令 rehashidx++,
跳躍表(zest)(重點)
是有序集合的底層實作之一,
skiplist的資料結構定義:
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
typedef struct zskiplistNode { //這個就是一個一個資料結點
robj *obj;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned int span;
} level[];
} zskiplistNode;
跳躍表是基于多指標有序鏈表實作的,可以看成多個有序鏈表,
在查找時,從上層指標開始查找,找到對應的區間之后再到下一層去查找,下圖演示了查找 22 的程序,
在這個鏈表結構上,如果我們還是查找22,那么沿著最上層鏈表首先要比較的是7,發現22比7大,接下來我們就知道只需要到7的后面去繼續查找,從而一下子跳過了7前面的所有節點,可以想象,當鏈表足夠長的時候,這種多層鏈表的查找方式能讓我們跳過很多下層節點,大大加快查找的速度,然后找到25,22比25小,然后往25的下一層比較,發現22比15大,比25小,然后繼續降層,比15大,然后找下一個,喲,找到了22,
創建結點的時候,為每個節點隨機出一個層數(level),比如,一個節點隨機出的層數是3,那么就把它鏈入到第1層到第3層這三層鏈表中,下圖展示了如何通過一步步的插入操作從而形成一個skiplist的程序:

實際上,Redis中sorted set的實作是這樣的:
- 當資料較少時,sorted set是由一個ziplist來實作的,
- 當資料多的時候,sorted set是由一個dict + 一個skiplist來實作的,簡單來講,dict用來查詢資料到分數的對應關系,而skiplist用來根據分數查詢資料(可能是范圍查找),
總結起來,Redis中的skiplist跟前面介紹的經典的skiplist相比,有如下不同:
- 分數(score)允許重復,即skiplist的key允許重復,這在最開始介紹的經典skiplist中是不允許的,
- 在比較時,不僅比較分數(相當于skiplist的key),還比較資料本身,在Redis的skiplist實作中,資料本身的內容唯一標識這份資料,而不是由key來唯一標識,另外,當多個元素分數相同的時候,還需要根據資料內容來進字典排序,
- 第1層鏈表不是一個單向鏈表,而是一個雙向鏈表,這是為了方便以倒序方式獲取一個范圍內的元素,
- 在skiplist中可以很方便地計算出每個元素的排名(rank),
與紅黑樹等平衡樹相比,跳躍表具有以下優點:
- 插入速度非常快速,因為不需要進行旋轉等操作來維護平衡性;
- 更容易實作;
- 支持無鎖操作,
四、使用場景和淘汰策略
快取
將熱點資料放到記憶體中,設定記憶體的最大使用量以及淘汰策略來保證快取的命中率,
訊息佇列
List 是一個雙向鏈表,可以通過 lpush 和 rpop 寫入和讀取訊息
不過最好使用 Kafka、RabbitMQ 等訊息中間件,
會話快取
可以使用 Redis 來統一存盤多臺應用服務器的會話資訊,
當應用服務器不再存盤用戶的會話資訊,也就不再具有狀態,一個用戶可以請求任意一個應用服務器,從而更容易實作高可用性以及可伸縮性,
分布式鎖實作
在分布式場景下,無法使用單機環境下的鎖來對多個節點上的行程進行同步,
可以使用 Redis 自帶的 SETNX 命令實作分布式鎖,他跟set相反,set會把值覆寫,而SETNX不會覆寫,會除此之外,還可以使用官方提供的 RedLock 分布式鎖實作,
Redis 支持兩種持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化,
redis資料淘汰策略
可以設定記憶體最大使用量,當記憶體使用量超出時,會施行資料淘汰策略,
Redis 具體有 6 種淘汰策略:
作為記憶體資料庫,出于對性能和記憶體消耗的考慮,Redis 的淘汰演算法實際實作上并非針對所有 key,而是抽樣一小部分并且從中選出被淘汰的 key,
使用 Redis 快取資料時,為了提高快取命中率,需要保證快取資料都是熱點資料,可以將記憶體最大使用量設定為熱點資料占用的記憶體量,然后啟用 allkeys-lru 淘汰策略,將最近最少使用的資料淘汰,
五、持久化(重點)
Redis 是記憶體型資料庫,為了保證資料在斷電后不會丟失,需要將記憶體中的資料持久化到硬碟上,
RDB (快照)持久化
在某些時刻,Redis通過fork產生子行程,一個父行程的快照(副本),其中有和父行程當前時刻相同的資料,父行程繼續處理client請求執行IO操作,子行程負責將快照(資料副本)寫入臨時檔案,子行程寫完后,用臨時檔案替換原來的快照檔案,然后子行程退出,然后呢就會隔一段時間自動進行一次備份,
#save時間,以下分別表示更改了1個key時間隔900s進行持久化存盤;更改了10個key300s進行存盤;更改10000個key60s進行存盤,
save 900 1
save 300 10
save 60 10000
RDB檔案存在是以一個壓縮后的二進制檔案,這個RDB檔案一般是保存在Redis安裝目錄下,通過啟動Redis服務器執行rdbLoad函式加載RDB檔案,執行rdbSave函式保存RDB檔案, RDB會每個一段時間去更新一下redis的資料,會生成一個二進制檔案,RDB會通過一個bgsave的命令,會fork出一個子行程,通過一個寫實復制的方式,生成一個RDB檔案,
注意:SAVE和BGSAVE兩個命令都會呼叫rdbSave函式,
SAVE(不推薦使用)直接呼叫rdbSave,阻塞Redis主行程,直到保存完成為止,在主行程阻塞期間,服務器不能處理客戶端的任何請求,
BGSAVE則開啟一個子行程,子行程負責呼叫rdbSave,Redis服務器在BGSAVE執行期間仍然可以繼續處理客戶端的請求,并在保存完成之后向主行程發送信號,通知保存已完成,
snapshot執行流程:

AOF (日志)持久化
將寫命令添加到 AOF 檔案(Append Only File)的末尾,
Append-only file,將“操作 + 資料”以格式化指令的方式追加到緩沖區中,然后緩沖區根據對應的策略向硬碟進行同步操作,在append操作回傳后(已經寫入到檔案或者即將寫入),才進行實際的資料變更,AOF保存了歷史所有的操作程序,當server需要資料恢復時,可以直接加載日志檔案,即可還原所有的操作程序,
使用 AOF 持久化需要設定同步選項,從而確保寫命令同步到磁盤檔案上的時機,這是因為對檔案進行寫入并不會馬上將內容同步到磁盤上,而是先存盤到緩沖區,然后由作業系統決定什么時候同步到磁盤,有以下同步選項:Redis目前支持三種AOF保存模式:
everysec: 表示每秒同步一次(默認值,很快,但可能會丟失一秒以內的資料
no:表示等作業系統進行資料快取同步到磁盤,效率快,但是持久化沒保證
always:同步持久化,每次發生資料變更時,立即記錄到磁盤,效率慢,嚴重減低服務器的性能,但是安全

隨著服務器寫請求的增多,AOF 檔案會越來越大,Redis 提供了一種將 AOF 重寫的特性,能夠去除 AOF 檔案中的冗余寫命令,我們可以看到經常會對一個key進行多次修改,那么我們可以把這個key的最后一次操作保存起來這樣我們就輕易的給AOF"瘦身",當然我們還有一種方式,就是遍歷整個Redis,set每個key和它的值,也跟RDB全備一樣我們需要一個子行程讀取當前的Redis庫,這里會出現一個問題,我們如果是遍歷整個Redis需要考慮此時的客戶端必定會有指令更改里面的值,此時我們怎么保證AOF重寫后不丟下重寫后的指令呢?
操作步驟:
AOF創建一個子行程進行AOF重寫,其指定記憶體跟主行程一致
客戶端執行寫命令,主執行緒處理指令,指令追加到AOF緩沖區,并且追加到AOF重寫緩沖區
AOF重寫完成后替換現有的AOF檔案
那么為什么會把這個指令同時追加到AOF緩沖區和AOF重寫區呢?原因是如果我們在重寫的時候突然服務器掛了,那么我們AOF檔案中會保存這個指令,追加到AOF緩沖區是為了保證操作指令能及時同步到AOF重寫區,
AOF優點:AOF會進行實時的寫操作,不管你是每秒鐘執行一次還是手動執行,都會將資料寫入磁盤,即使系統崩潰,也只會丟失一秒鐘的資料,比RDB更適于做更實時的持久化,
AOF缺點:在一直進行寫操作,AOF檔案會不斷增長(可能比快照檔案大幾倍),在極端情況下,可能會對硬碟空間造成壓力,即使有重寫機制可能也無法保證他很小,Redis在重啟時,需要重新執行一個可能非常大的AOF,時間會很長AOF與RDB的區別
RDB持久化是在指定的時間內將資料寫入磁盤,實際操作時主行程fork出一個子行程,讓子行程將資料寫入,寫入成功后在替換點之前的檔案,
AOF是一個簡短的寫指令,在每一秒進行一次寫指令,單次的消耗遠低于RDB,所以AOF更適合做實時的持久化,將新加入的資料寫入檔案,RDB執行bgsave時和AOF重寫一樣,開啟一個子行程,他們的記憶體與父行程共享
Redis4.0混合持久化
當開啟混合持久化,把資料以 RDB 的方式寫入檔案,再將后續的操作命令以 AOF 的格式存入檔案,并將新的含有RDB格式和AOF格式的AOF檔案替換舊的的AOF檔案,既保證了 Redis 重啟速度,又降低資料丟失風險,
AOF rewrite 的時候就直接把 RDB的內容寫到 AOF 檔案開頭,AOF 檔案內容會變成如下:

這樣做的好處是可以結合 RDB和 AOF的優點, 快速加載同時避免丟失過多的資料,
混合 AOF 加載
開啟混合存盤模式后 AOF檔案加載的流程如下:
- AOF檔案開頭是 RDB的格式, 先加載 RDB內容再加載剩余的 AOF
- AOF檔案開頭不是 RDB的格式,直接以 AOF格式加載整個檔案
優點:混合持久化結合了RDB持久化和AOF持久化的優點,由于絕大部分都是RDB格式,加載速度快,同時結合
AOF,增量的資料以AOF方式保存了,資料更少的丟失,
缺點:兼容性差,在4.0之前版本都不識別該混合持久化AOF檔案
六、Redis如何實作高可用
1、主從復制
為了分擔壓力,Redis支持主從復制,Redis的主從結構可以采用一主多從或者級聯結構,Redis主從同步策略的策略就是先是全量同步,再為增量同步,
全量同步:Redis全量復制一般發生在Slave初始化階段,這時Slave需要將Master上的所有資料都復制一份,具體步驟如下:
- 從服務器連接主服務器;
- 主服務器接收到命名后,開始執行BGSAVE命令生成RDB檔案并使用緩沖區記錄此后執行的所有寫命令;
- 主服務器BGSAVE執行完后,向所有從服務器發送快照檔案,并在發送期間繼續記錄被執行的寫命令;
- 從服務器收到快照檔案后丟棄所有舊資料,載入收到的快照;
- 主服務器快照發送完畢后開始向從服務器發送緩沖區中的寫命令;
- 從服務器完成對快照的載入,開始接收命令請求,此后主服務器每執行一次寫命令,就向從服務器發送相同的寫命令(也就是增量同步),
增量同步:Redis增量復制是指Slave初始化后開始正常作業時主服務器發生的寫操作同步到從服務器的程序, 增量復制的程序主要是主服務器每執行一個寫命令就會向從服務器發送相同的寫命令,從服務器接收并執行收到的寫命令,
主從復制的作用:主從復制,讀寫分離,容災恢復,一臺主機負責寫入資料,多臺從機負責備份資料,在高并發的場景下,即便是主機掛了,可以用從機代替主機繼續作業,避免單點故障導致系統性能問題,讀寫分離,讓讀多寫少的應用性能更佳,
2、哨兵(Sentinel)
使用 Redis 主從服務的時候,會有一個問題,就是當 Redis 的主從服務器出現故障宕機時,需要手動進行恢復,為了解決這個問題,Redis 增加了哨兵模式(因為哨兵模式做到了可以監控主從服務器,并且提供自動容災恢復的功能), Sentinel(哨兵)可以監聽集群中的服務器,并在主服務器進入下線狀態時,自動從從服務器中選舉出新的主服務器,

3、集群 (Redis Cluster)
使用哨兵模式在資料上有副本資料做保證,在可用性上又有哨兵監控,一旦master宕機會選舉salve節點為master節點,這種已經滿足了我們的生產環境需要,那為什么還需要使用集群模式呢?
答:因為主服務器掛掉的時候,要進行主從切換,這瞬間存在訪問瞬斷的情況,雖然在主從模式下我們可以通過增加salve節點來擴展讀并發能力,但是沒辦法擴展寫能力和存盤能力,所以為了擴展寫能力和存盤能力,我們就需要引入集群模式,
redis集群是一個由多個主從節點群組成的分布式服務器群,它具有復制、高可用和分片特性,Redis集群不需要哨兵也能完成節點移除和故障轉移的功能,需要將每個節點設定成集群模式,這種集群模式沒有中心節點,可水平擴展,

集群中那么多Master節點,redis cluster在存盤的時候如何確定選擇哪個節點呢?
答:Redis Cluster采用的是類一致性哈希演算法實作節點選擇的,
Redis Cluster將自己分成了16384個Slot(槽位),哈希槽類似于資料磁區,每個鍵值對都會根據它的 key,被映射到一個哈希槽中,具體執行程序分為兩大步,
- 根據鍵值對的 key,按照 CRC16 演算法計算一個 16 bit 的值,
- 再用 16bit 值對 16384 取模,得到
0~16383范圍內的模數,每個模數代表一個相應編號的哈希槽,
每個Redis節點負責處理一部分槽位,假如你有三個master節點 ABC,每個節點負責的槽位如下:
| 節點 | 處理槽位 |
|---|---|
| A | 0-5000 |
| B | 5001 - 10000 |
| C | 10001 - 16383 |
七、快取更新機制
? 當執行寫操作后,需要保證從快取讀取到的資料與資料庫中的資料是一致的,因此需要對快取進行更新,
? 因為涉及到資料庫和快取兩步操作,難以保證更新的原子性,在設計更新策略時,我們需要考慮多個方面的問題,對系統吞吐量的影響、并發安全性、更新失敗的影響
更新快取有兩種方式:
- 洗掉失效快取: 讀取時會因為未命中快取而從資料庫中讀取新的資料并更新到快取中
- 更新快取: 直接將新的資料寫入快取覆寫過期資料
更新快取和更新資料庫有兩種順序:
- 先資料庫后快取
- 先快取后資料庫
兩兩組合共有四種更新策略,現在我們逐一進行分析,
先做一個說明,從理論上來說,給快取設定過期時間,是保證最終一致性的解決方案,這種方案下,我們可以對存入快取的資料設定過期時間,所有的寫操作以資料庫為準,對快取操作只是盡最大努力即可,也就是說如果資料庫寫成功,快取更新失敗,那么只要到達過期時間,則后面的讀請求自然會從資料庫中讀取新值然后回填快取,
1.先更新資料庫,再洗掉快取(推薦)
? 若資料庫更新成功,洗掉快取操作失敗,則此后讀到的都是快取中過期的資料,造成不一致問題,
? 假設這會有兩個請求,一個請求A做查詢操作,一個請求B做更新操作,那么會有如下情形產生
(1)快取剛好失效(2)請求A查詢資料庫,得一個舊值(3)請求B將新值寫入資料庫(4)請求B洗掉快取(5)請求A將查到的舊值寫入快取
假設,有人非要抬杠,有強迫癥,一定要解決怎么辦?
如何解決上述并發問題?首先,給快取設定有效時間是一種方案,其次,采用異步延時洗掉策略,redis自己起一個執行緒,異步洗掉保證讀請求完成以后,再進行洗掉操作,
2.先更新資料庫,再更新快取(反對)
同洗掉快取策略一樣,若資料庫更新成功快取更新失敗則會造成資料不一致問題,反對此方案
- 原因一(執行緒安全角度)
同時有請求A和請求B進行更新操作,那么會出現
(1)執行緒A更新了資料庫(2)執行緒B更新了資料庫(3)執行緒B更新了快取(4)執行緒A更新了快取
? 這就出現請求A更新快取應該比請求B更新快取早才對,但是因為網路等原因,B卻比A更早更新了快取,這就導致了臟資料,因此不考慮,
- 原因二(業務場景角度)
有如下兩點:
(1)如果你是一個寫資料庫場景比較多,而讀資料場景比較少的業務需求,采用這種方案就會導致,資料壓根還沒讀到,快取就被頻繁的更新,浪費性能,
(2)如果你寫入資料庫的值,并不是直接寫入快取的,而是要經過一系列復雜的計算再寫入快取,那么,每次寫入資料庫后,都再次計算寫入快取的值,無疑是浪費性能的,顯然,洗掉快取更為適合,
接下來討論的就是爭議最大的,先刪快取,再更新資料庫,還是先更新資料庫,再刪快取的問題,
3.先洗掉快取,再更新資料庫
該方案會導致不一致的原因是,同時有一個請求A進行操作,另一個請求B進行查詢操作,那么會出現如下情形:
(1)請求A進行寫操作,洗掉快取(2)請求B查詢發現快取不存在(3)請求B去資料庫查詢得到舊值(4)請求B將舊值寫入快取(5)請求A將新值寫入資料庫

上述情況就會導致不一致的情形出現,而且,如果不采用給快取設定過期時間策略,該資料永遠都是臟資料,
那么,如何解決呢?
采用延時雙刪策略:
(1)先淘汰洗掉快取(2)再寫資料庫(這兩步和原來一樣)(3)休眠1秒,再次淘汰快取
這么做,可以將1秒內所造成的快取臟資料,再次洗掉,
那么,這個1秒怎么確定的,具體該休眠多久呢?這確實需要根據實際情況而定
如果你用了MySQL的讀寫分離架構怎么辦?還是使用延時雙刪策略,
采用這種同步淘汰策略,吞吐量降低怎么辦?ok,那就將第二次洗掉作為異步的,自己起一個執行緒,異步洗掉,
第二次洗掉,如果洗掉失敗怎么辦?這是個非常好的問題,因為第二次洗掉失敗,就會出現如下情形,還是有兩個請求,一個請求A進行更新操作,另一個請求B進行查詢操作,為了方便,假設是單庫:
(1)請求A進行寫操作,洗掉快取(2)請求B查詢發現快取不存在(3)請求B去資料庫查詢得到舊值(4)請求B將舊值寫入快取(5)請求A將新值寫入資料庫(6)請求A試圖去洗掉請求B寫入對快取值,結果失敗了,
ok,這也就是說,如果第二次洗掉快取失敗,會再次出現快取和資料庫不一致的問題,
如何解決呢?
4.先更新快取,再更新資料庫(zz才這么做)
若快取更新成功資料庫更新失敗, 則此后讀到的都是未持久化的資料,因為快取中的資料是易失的,這種狀態非常危險,
八、快取雪崩/擊穿/穿透
正常情況下的流程是這樣的,先查快取,快取無就查資料庫

快取雪崩
快取雪崩是指快取中的資料大批量的過期 ,而查詢量巨大,造成資料庫壓力過大而崩潰,
解決方法:
快取的過期時間隨機設定,防止大量資料同時過期,
盡量保證redis集群的高可用性,當發現機器墜機時盡快補上,
選擇合適的快取淘汰策略,
快取擊穿
快取擊穿是指快取中沒有資料,而資料庫中有資料,一般是快取中的資料過期了,然后很多用戶并發查詢該資料,同時在快取中讀取該資料沒讀取到,就同時去資料庫中查,造成資料庫壓力過大,快取擊穿強調的是一個資料過期,同時并發地去資料庫訪問該資料;而快取雪崩是強調大量的資料過期,
解決方法:
設定熱點資料永不過期,
加互斥鎖,邏輯如下:從快取中獲取當前資料,如果快取中沒有,則嘗試去獲取鎖,如果獲取成功則查詢資料庫,然后寫進快取,然后釋放鎖,
快取穿透
快取穿透是指快取中沒有該資料,資料庫中也沒有該資料,而用戶不斷地發請求,比如不斷發出一些id=-1或者是根本就很不合理的資料來發生請求,這種一般是別人想攻擊你,攻擊會導致資料庫壓力過大,
? 對于這種情況很好解決,我們可以在redis快取一個空字串或者特殊字串,比如&&,下次我們去redis中查詢的時候,當取到的值是慷訓者&&,我們就知道這個值在資料庫中是沒有的,就不會在去資料庫中查詢,ps:這里快取不存在key的時候一定要設定過期時間,不然當資料庫已經新增了這一條記錄的時候,這樣會導致快取和資料庫不一致的情況
上面這個只是重復查詢同一個不存在的值的情況,如果每次查詢的不存在的值是不一樣的呢?那怎么辦,難道自己手動快取許多特殊字串嗎?別人想攻擊你,即使你每次快取很多特殊字串也沒用,太有概率性了,這時候資料庫的壓力是相當大,怎么辦呢,布隆過濾器就登場了,
布隆過濾器使用場景:
①、原本有10億個數,現在又來了10萬個數,要快速準確判斷這10萬個數是否在10億個數庫中?
辦法一:將10億個數存入資料庫,再資料庫查詢,查出值為null,代表不存在,準確性有了,但是速度會比較慢,
辦法二:將10億數放入記憶體中,比如Redis中,這里我們算一下占用記憶體大小:10億*8位元組=8GB,通過記憶體查詢,準確性和速度都有了,但是大約8GB的記憶體空間,挺浪費記憶體空間的,
那么對于類似這種,大資料量集合,如何準確快速的判斷某個資料是否在大資料量集合中,并且不占用記憶體,布隆過濾器應運而生了,
布隆過濾器:使用位圖實作,是由一串很長的二進制向量組成,陣列中只存在0.1
當要向布隆過濾器中添加一個元素key時,我們通過多個hash函式,算出一個值,然后將這個值所在的方格置為1, 如下圖(圖片來源)

如何查詢是否存在呢?
我們只需要將這個新的資料通過上面自定義的幾個哈希函式,分別算出各個值,然后看其對應的地方是否都是1,如果存在一個不是1的情況,那么我們可以說,該新資料一定不存在于這個布隆過濾器中,
反過來說,如果通過哈希函式算出來的值,對應的地方都是1,那么我們能夠肯定的得出:這個資料一定存在于這個布隆過濾器中嗎?
答案是否定的,因為多個不同的資料通過hash函式算出來的結果是會有重復的,所以會存在某個位置是別的資料通過hash函式置為的1,比如這個d,通過三次計算發現得到的結果也都是1,那么我們能說d在布隆過濾器中是存在的嗎,顯然是不行的,我們仔細看d得到的三個1其實是f1(a),f1(b),f2?存進去的,并不是d自己存進去的,這個還是哈希碰撞導致的,
結論:布隆過濾器可以判斷某個資料一定不存在,但是無法判斷一定存在,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/277834.html
標籤:其他
下一篇:哈希表及其企業級應用
