1. 程式人生 > 資料庫 >面試官為了讓我學好Redis,送給我這10W+字的詳解筆記(二)

面試官為了讓我學好Redis,送給我這10W+字的詳解筆記(二)

文章目錄

說明

唉,寫得太長了,CSDN編輯器不允許我在一篇文章上繼續發揮了。

這是上一篇博文 面試官為了讓我學好Redis,送給我這10W+字的詳解筆記

四、Redis的其他功能

(一)慢查詢

慢查詢簡介 慢查詢顧名思義是將redis執行命令較慢的命令記錄下來。

一條命令的生命週期

  1. client通過網路向Redis傳送一條命令
  2. 由於Redis是單執行緒應用,可以把Redis想像成一個佇列,client執行的所有命令都在排隊等著server端執行
  3. Redis服務端按順序執行命令
  4. server端把命令結果通過網路返回給client
    在這裡插入圖片描述

兩點說明
(1)慢查詢發生在第3階段
(2)客戶端超時不一定慢查詢,但慢查詢是客戶端超時的一個可能因素

慢查詢是一個先進先出的佇列,如果一條命令在執行過程中被列入慢查詢範圍內,就會被放入一個佇列,這個佇列是基於Redis的列表來實現,而且這個佇列是固定長度的,當佇列的長度達到固定長度時,最先被放入佇列就會被pop出去。慢查詢佇列儲存在記憶體之中,不會做持久化,當Redis重啟之後就會消失。

在這裡插入圖片描述
結合上面圖示這裡涉及到兩個配置和三個慢查詢命令。

先看兩個配置
(1) slowing-max-len
(2)slowing-log- slower-than

slowlog-max-len             慢查詢佇列的長度
slowlog-log-slower-than     慢查詢閾值(單位:微秒),執行時間超過閥值的命令會被加入慢查詢命令
    如果設定為0,則會記錄所有命令,通常在需要記錄每條命令的執行時間時使用
    如果設定為小於0,則不記錄任何命令
slowlog list                慢查詢記錄

慢查詢配置方法
1.修改配置檔案重啟

修改/etc/redis.conf配置檔案,配置慢查詢
修改配置方式應該在第一次配置Redis中時配置完成,生產後不建議修改配置檔案

2.動態配置

127.0.0.1:6379> config get slowlog-max-len
1) "slowlog-max-len"
2) "128"
127.0.0.1:6379> config get slowlog-log-slower-than
1) "slowlog-log-slower-than"
2) "10000"
127.0.0.1:6379> config set slowlog-max-len 1000
OK
127.0.0.1:6379> config get slowlog-max-len
1) "slowlog-max-len"
2) "1000"
127.0.0.1:6379> config set slowlog-log-slower-than 1000
OK
127.0.0.1:6379> config get slowlog-log-slower-than
1) "slowlog-log-slower-than"
2) "1000"

與配置對應的是三個慢查詢命令

  1. slowlog get [n]:獲取慢查詢佇列
  2. slowlog len:獲取慢查詢佇列長度
  3. slowlog reset:清空慢查詢佇列

值得注意的是:

  1. slowing-max-len不要設定過大,預設10ms,通常設定1ms
  2. slowing-log- slower-than不要設定過小,通常設定1000左右。
  3. 理解命令生命週期。
  4. 定期持久化慢查詢。

(二)pipeline

pipeline的中文意思是管道。

下面通過圖示,我們看看認清楚什麼是流水線:

批量網路命令通訊模型:

n次時間=n次網路時間+n次命令時間
在這裡插入圖片描述
Pipeline模型:
在這裡插入圖片描述

pipeline就是把一批命令進行打包,然後傳輸給server端進行批量計算,然後按順序將執行結果返回給client端
使用Pipeline模型進行n次網路通訊需要的時間:

1次pipeline(n條命令) = 1次網路時間 + n次命令時間

為了更具體,我們可以測試一下時間:(python實現)

import redis
import time

client = redis.StrictRedis(host='192.168.81.100',port=6379)
start_time = time.time()

for i in range(10000):
    client.hset('hashkey','field%d' % i,'value%d' % i)

ctime = time.time()
print(client.hlen('hashkey'))
print(ctime - start_time)

程式執行結果:
10000
2.0011684894561768

在上面的例子裡,直接向Redis中寫入10000條hash記錄,需要的時間大約為2.00秒

使用pipeline的方式向Redis中寫入1萬條hash記錄

import redis
import time

client = redis.StrictRedis(host='192.168.81.100',port=6379)
start_time = time.time()

for i in range(100):
    pipeline = client.pipeline()
    j = i * 100
    while j < (i+ 1) * 100:
        pipeline.hset('hashkey1','field%d' % j * 100,'value%d' % i)
        j += 1
    pipeline.execute()

ctime = time.time()
print(client.hlen('hashkey1'))
print(ctime - start_time)

程式執行結果:

10000
0.3175079822540283

可以看到使用Pipeline方式每次向Redis服務端傳送100條命令,傳送100次所需要的時間僅為0.31秒,可以看到使用Pipeline可以節省網路傳輸時間

值得注意的是

  1. 每次pipeline攜帶資料量不能太大
  2. pipeline可以提高Redis批量處理的併發的能力,但是並不能無節制的使用
  3. 如果批量執行的命令數量過大,則很容易對網路及客戶端造成很大影響,此時可以把命令分割,每次傳送少量的命令到服務端執行
  4. pipeline每次只能作用在一個Redis節點上

還有,記得pipeline命令不是原子命令(要麼全部一下子執行,要麼不執行),pipeline中命令以子命令的形式穿插在Redis執行的其他命令當中
在這裡插入圖片描述

(三)釋出訂閱

我們在字元型別那部分已經探討過 Redis簡易的訊息佇列(點對點,後面有解釋和對比)。這裡則是高階點的實現。

對於有接觸過釋出訂閱模型(生產者消費者模型)的訊息佇列的朋友來說,這部分是So easy的。

釋出訂閱模型分成三個角色:

  1. 釋出者( publisher)
  2. 訂閱者( subscriber)
  3. 頻道( channel)

它們的關係如下:

  • 每個訂閱者可以訂閱多個頻道
  • 釋出者釋出訊息後,訂閱者就可以收到不同頻道的訊息
  • 訂閱者不可以接收未訂閱頻道的訊息
  • 訂閱者訂閱某個頻道後,Redis無法做訊息的堆積,不能接收頻道被訂閱之前釋出的訊息

Redis server就相當於頻道
釋出者是一個redis-cli,通過redis server釋出訊息
訂閱者也是於一個redis-cli,如果訂閱了這個頻道,就可以通過redis server獲取訊息
在這裡插入圖片描述
釋出訂閱的命令

publish channel message         釋出訊息
subscribe [channel]             訂閱頻道
unsubscribe [channel]           取消訂閱
psubscribe [pattern...]         訂閱指定模式的頻道
punsubscribe [pattern...]       退訂指定模式的頻道
pubsub channels                 列出至少有一個訂閱者的頻道
pubsub numsub [channel...]      列表給定頻道的訂閱者數量
pubsub numpat                   列表被訂閱模式的數量 

開啟一個終端1

127.0.0.1:6379> subscribe sohu_tv               # 訂閱sohu_tv頻道
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "sohu_tv"
3) (integer) 1

開啟一個終端2

127.0.0.1:6379> publish sohu_tv 'hello python'      # sohu_tv頻道釋出訊息
(integer) 1
127.0.0.1:6379> publish sohu_tv 'hello world'       # sohu_tv頻道釋出訊息
(integer) 3

可以看到終端1中已經接收到sohu_tv釋出的訊息

127.0.0.1:6379> subscribe sohu_tv
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "sohu_tv"
3) (integer) 1
1) "message"
2) "sohu_tv"
3) "hello python"
1) "message"
2) "sohu_tv"
3) "hello world"

開啟終端3,取消訂閱sohu_tc頻道

127.0.0.1:6379> unsubscribe sohu_tv
1) "unsubscribe"
2) "sohu_tv"
3) (integer) 0

訊息佇列點對點與釋出訂閱區別
1.點對點
訊息生產者訊息傳送到queue中,然後消費者從queue中取。
注意:訊息被消費以後,佇列中不再有儲存 。客戶端和客戶端之間是 的關係。生產者傳送一條訊息到 queue,只有一個消費者能收到。

2.釋出/訂閱
生產者將訊息傳送到topic中,同時多個消費者消費這個訊息。 和點對點不同,釋出到topic的訊息會被所有訂閱者消費。

(四)Bitmap

在我們平時開發過程中,會有⼀些 布林型資料需要存取,⽐如CSDN APP⽤戶⼀年的簽到記錄(我快簽到100天了,還是比較活躍的,歡迎與我交流),簽了是 1,沒簽是 0,要記錄 365 天。如果使⽤普通的 key/value,每個⽤戶要記錄 365 個,⽤戶上千萬的時候,需要的儲存空間是比較大的。

為了解決這個問題,Redis 提供了點陣圖資料結構,這樣每天的簽到記錄只佔據⼀個位,365 天就是 365 個位,46 個位元組 (⼀個稍⻓⼀點的字串) 就可以完全容納下,這就⼤⼤節約了儲存空間。

點陣圖不是特殊的資料結構,它的內容其實就是普通的字串,也就是byte 陣列。我們可以使⽤普通的 get/set 直接獲取和設定整個點陣圖的內容,也可以使⽤點陣圖操作 getbit/setbit 等將 byte 陣列看成「位陣列」來處理。

首先來看一個例子,字串big,

字母b的ASCII碼為98,轉換成二進位制為 01100010
字母i的ASCII碼為105,轉換成二進位制為 01101001
字母g的ASCII碼為103,轉換成二進位制為 01100111

如果在Redis中,設定一個key,其值為big,此時可以get到big這個值,也可以獲取到 big的ASCII碼每一個位對應的值,也就是0或1

127.0.0.1:6379> set hello big
OK
127.0.0.1:6379> getbit hello 0      # b的二進位制形式的第1位,即為0
(integer) 0
127.0.0.1:6379> getbit hello 1      # b的二進位制形式的第2位,即為1
(integer) 1

在這裡插入圖片描述
在這裡插入圖片描述

我們看一下它常用的API

1.setbit
SETBIT key offset value

時間複雜度: O(1)
對 key 所儲存的字串值,設定或清除指定偏移量上的位(bit)。位的設定或清除取決於 value 引數,可以是 0 也可以是 1 。當 key 不存在時,自動生成一個新的字串值。字串會進行伸展(grown)以確保它可以將 value 儲存在指定的偏移量上。當字串值進行伸展時,空白位置以 0 填充。

offset 引數必須大於或等於 0 ,小於 2^32 (bit 對映被限制在 512 MB 之內)。

redis> SETBIT bit 10086 1
(integer) 0

redis> GETBIT bit 10086
(integer) 1

redis> GETBIT bit 100   # bit 預設被初始化為 0
(integer) 0

setbit
在這裡插入圖片描述

偏移量不要太大,向上面的 SETBIT bit 10086 1 0-10085都要初始化成0

2.getbit
GETBIT key offset

時間複雜度: O(1)
對 key 所儲存的字串值,獲取指定偏移量上的位(bit)。當 offset 比字串值的長度大,或者 key 不存在時,返回 0 。

# 對不存在的 key 或者不存在的 offset 進行 GETBIT, 返回 0
redis> EXISTS bit
(integer) 0

redis> GETBIT bit 10086
(integer) 0

# 對已存在的 offset 進行 GETBIT
redis> SETBIT bit 10086 1
(integer) 0

redis> GETBIT bit 10086
(integer) 1

3.bitcount
時間複雜度: O(N)
計算給定字串中,被設定為 1 的位元位的數量。

一般情況下,給定的整個字串都會被進行計數,通過指定額外的 start 或 end 引數,可以讓計數只在特定的位上進行。

start 和 end 引數的設定和 GETRANGE key start end 命令類似,都可以使用負數值: 比如 -1 表示最後一個位元組, -2 表示倒數第二個位元組,以此類推。

不存在的 key 被當成是空字串來處理,因此對一個不存在的 key 進行 BITCOUNT 操作,結果為 0 。

redis> BITCOUNT bits
(integer) 0

redis> SETBIT bits 0 1          # 0001
(integer) 0

redis> BITCOUNT bits
(integer) 1

redis> SETBIT bits 3 1          # 1001
(integer) 0

redis> BITCOUNT bits
(integer) 2

對了哦,前面提到的CSDN APP簽到的應用,就是用這個命令實現的。

4.bitop
BITOP operation destkey key [key …]

對一個或多個儲存二進位制位的字串 key 進行位元操作,並將結果儲存到 destkey 上。返回儲存到 destkey 的字串的長度,和輸入 key 中最長的字串長度相等。

operation 可以是 AND 、 OR 、 NOT 、 XOR 這四種操作中的任意一種:

  • 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 。

除了 NOT 操作之外,其他操作都可以接受一個或多個 key 作為輸入。

處理不同長度的字串

當 BITOP 處理不同長度的字串時,較短的那個字串所缺少的部分會被看作 0 。

空的 key 也被看作是包含 0 的字串序列。

redis> SETBIT bits-1 0 1        # bits-1 = 1001
(integer) 0

redis> SETBIT bits-1 3 1
(integer) 0

redis> SETBIT bits-2 0 1        # bits-2 = 1011
(integer) 0

redis> SETBIT bits-2 1 1
(integer) 0

redis> SETBIT bits-2 3 1
(integer) 0

redis> BITOP AND and-result bits-1 bits-2
(integer) 1

redis> GETBIT and-result 0      # and-result = 1001
(integer) 1

redis> GETBIT and-result 1
(integer) 0

redis> GETBIT and-result 2
(integer) 0

redis> GETBIT and-result 3
(integer) 1

5.bitpos
BITPOS key bit [start] [end]
時間複雜度: O(N),其中 N 為點陣圖包含的二進位制位數量
返回點陣圖中第一個值為 bit 的二進位制位的位置。在預設情況下, 命令將檢測整個點陣圖, 但使用者也可以通過可選的 start 引數和 end 引數指定要檢測的範圍。

127.0.0.1:6379> SETBIT bits 3 1    # 1000
(integer) 0

127.0.0.1:6379> BITPOS bits 0
(integer) 0

127.0.0.1:6379> BITPOS bits 1
(integer) 3

下面我們再說一個應用

如果一個網站有1億使用者,假如user_id用的是整型,長度為32位,每天有5千萬獨立使用者訪問,如何判斷是哪5千萬使用者訪問了網站

方式一:用set來儲存
使用set來儲存資料執行一天需要佔用的記憶體為
32bit * 50000000 = (4 * 50000000) / 1024 /1024 MB,約為200MB

執行一個月需要佔用的記憶體為6G,執行一年佔用的記憶體為72G
30 * 200 = 6G

方式二:使用bitmap的方式
如果user_id訪問網站,則在user_id的索引上設定為1,沒有訪問網站的user_id,其索引設定為0,此種方式執行一天佔用的記憶體為
1 * 100000000 = 100000000 / 1014 /1024/ 8MB,約為12.5MB
執行一個月佔用的記憶體為375MB,一年佔用的記憶體容量為4.5G

由此可見,使用bitmap可以節省大量的記憶體資源

值得注意的是

  1. bitmap是string型別,單個值最大可以使用的記憶體容量為512MB
  2. setbit時是設定每個value的偏移量,可以有較大耗時(偏移量不要太大)
  3. bitmap不是絕對好,用在合適的場景最好

(五)HyperLogLog

基於HyperLogLog演算法,極小空間完成獨立數量統計,本質還是字串。演算法描述參考維基百科介紹,實現起來50行程式碼左右的樣子。

HyperLogLog 提供了兩個指令 pfadd 和 pfcount,⼀個是增加計數,⼀個是獲取計數。pfadd ⽤法和 set集合的 sadd 是⼀樣的,pfcount 和 scard ⽤法是⼀樣的,直接獲取計數值。

PFADD key element [element …]
將任意數量的元素新增到指定的 HyperLogLog 裡面。

PFCOUNT key [key …]
計算hyperloglog的獨立總數

prmerge destkey sourcekey [sourcekey…]
合併多個hyperloglog

127.0.0.1:6379> pfadd unique_ids1 'uuid_1' 'uuid_2' 'uuid_3' 'uuid_4'       # 向unique_ids1中新增4個元素
(integer) 1
127.0.0.1:6379> pfcount unique_ids1         # 檢視unique_ids1中元素的個數
(integer) 4
127.0.0.1:6379> pfadd unique_ids1 'uuid_1' 'uuid_2' 'uuid_3' 'uuid_10'      # 再次向unique_ids1中新增4個元素
(integer) 1
127.0.0.1:6379> pfcount unique_ids1         # 由於兩次新增的value有重複,所以unique_ids1中只有5個元素
(integer) 5
127.0.0.1:6379> pfadd unique_ids2 'uuid_1' 'uuid_2' 'uuid_3' 'uuid_4'       # 向unique_ids2中新增4個元素
(integer) 1
127.0.0.1:6379> pfcount unique_ids2         # 檢視unique_ids2中元素的個數
(integer) 4
127.0.0.1:6379> pfadd unique_ids2 'uuid_4' 'uuid_5' 'uuid_6' 'uuid_7'       # 再次向unique_ids2中新增4個元素
(integer) 1
127.0.0.1:6379> pfcount unique_ids2         # 再次檢視unique_ids2中元素的個數,由於兩次新增的元素中有一個重複,所以有7個元素
(integer) 7
127.0.0.1:6379> pfmerge unique_ids1 unique_ids2     # 合併unique_ids1和unique_ids2
OK
127.0.0.1:6379> pfcount unique_ids1         # unique_ids1和unique_ids2中有重複元素,所以合併後的hyperloglog中只有8個元素
(integer) 8

hyperloglog也有非常明顯的侷限性:

  1. hyperloglog有一定的錯誤率,在使用hyperloglog進行資料統計的過程中,hyperloglog給出的資料不一定是對的
    按照維基百科的說法,使用hyperloglog處理10億條資料,佔用1.5Kb記憶體時,錯誤率為2%
  2. 沒法從hyperloglog中取出單條資料,這很容易理解,使用16KB的記憶體儲存100萬條資料,此時還想把100萬條資料取出來,顯然是不可能的

所以具體的應用還需要考量實際的場景。

(六)GEO

GEO即地址資訊定位,可以用來儲存經緯度,計算兩地距離,範圍計算等。這意味著我們可以使⽤ Redis 來實現美團和餓了麼「附近的餐館」,微信搖一搖等功能了。
在這裡插入圖片描述
我們看一下它的常用API

geoadd key longitude latitude member [longitude latitude member…] 增加地理位置資訊

127.0.0.1:6379> geoadd cities:locations 116.28 39.55 beijing                # 新增北京的經緯度
(integer) 1
127.0.0.1:6379> geoadd cities:locations 117.12 39.08 tianjin 114.29 38.02 shijiazhuang    # 新增天津和石家莊的經緯度
(integer) 2
127.0.0.1:6379> geoadd cities:locations 118.01 39.38 tangshan 115.29 38.51 baoding         # 新增唐山和保定的經緯度
(integer) 2

geopos key member [member…] 獲取地理位置資訊

27.0.0.1:6379> geopos cities:locations tianjin     # 獲取天津的地址位置資訊
1) 1) "117.12000042200088501"
   2) "39.0800000535766543"

geodist key member1 member2 [unit] 獲取兩個地理位置的距離,unit:m(米),km(千米),mi(英里),ft(尺)

127.0.0.1:6379> geodist cities:locations tianjin beijing km
"89.2061"
127.0.0.1:6379> geodist cities:locations tianjin baoding km
"170.8360"

georedius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key][storedist key]
georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key][storedist key]
獲取指定位置範圍內的地理位置資訊集合

  • withcoord:返回結果中包含經緯度
  • withdist:返回結果中包含距離中心節點位置
  • withhash:返回結果中包含geohash
  • COUNT count:指定返回結果的數量
  • asc|desc:返回結果按照距離中心節點的距離做升序或者降序
  • store key:將返回結果的地理位置資訊儲存到指定鍵
  • storedist key:將返回結果距離中心節點的距離儲存到指定鍵
127.0.0.1:6379> georadiusbymember cities:locations beijing 150 km   # 獲取距離北京150km範圍內的城市
1) "beijing"
2) "tianjin"
3) "tangshan"
4) "baoding"

最後還需要補充

  1. Redis的GEO功能是從3.2版本新增
  2. geo功能基於zset實現
  3. geo沒有刪除命令

五、Redis持久化的取捨和選擇

Redis 的資料全部在記憶體⾥,如果突然宕機,資料就會全部丟失,因此必須有⼀種機制來保證 Redis 的資料不會因為故障⽽丟失,這種機制就是 Redis 的持久化機制。

Redis的持久化就是將儲存在記憶體裡面的資料以檔案形式儲存硬盤裡面,這樣即使Redis服務端被關閉,已經同步到硬盤裡面的資料也不會丟失,除此之外,持久化也可以使Redis伺服器重啟時,通過載入同步的持久檔案來還原之前的資料,或者使用持久化檔案來進行資料備份和資料遷移等工作

Redis 的持久化機制有兩種,一種是RDB、一種是AOF。

(一)RDB

RDB持久化功能可以將Redis中所有資料生成快照,快照是記憶體資料的⼆進位制序列化形式,在儲存上⾮常緊湊,將其儲存在硬盤裡,檔名為.RDB檔案

在Redis啟動時載入RDB檔案,Redis讀取RDB檔案內容,還原伺服器原有的資料庫資料

在這裡插入圖片描述
觸發Redis服務端建立RDB檔案,有三種方式:

(1)使用SAVE命令手動同步建立RDB檔案

客戶端向Redis服務端傳送SAVE命令,服務端把當前所有的資料同步儲存為一個RDB檔案。通過向伺服器傳送SAVE命令,Redis會建立一個新的RDB檔案。

由於Redis單執行緒的特點,在執行SAVE命令的過程中(也就是即時建立RDB檔案的過程中),Redis服務端將被阻塞,無法處理客戶端傳送的其他命令請求。只有在SAVE命令執行完畢之後(也就時RDB檔案建立完成之後),伺服器才會重新開始處理客戶端傳送的命令請求。如果已經存在RDB檔案,那麼伺服器將自動使用新的RDB檔案去代替舊的RDB檔案。

在這裡插入圖片描述

演示
1、修改Redis的配置檔案/etc/redis.conf,把下面三行註釋掉(後面會解釋原因)

#save 900 1
#save 300 10
#save 60 10000

2、執行下面三條命令

127.0.0.1:6379> flushall                # 清空Redis中所有的鍵值對
OK
127.0.0.1:6379> dbsize                  # 檢視Redis中鍵值對數量
(integer) 0
127.0.0.1:6379> info memory             # 檢視Redis佔用的記憶體數為834.26K
# Memory
used_memory:854280
used_memory_human:834.26K
used_memory_rss:5931008
used_memory_rss_human:5.66M
used_memory_peak:854280
used_memory_peak_human:834.26K
total_system_memory:2080903168
total_system_memory_human:1.94G
used_memory_lua:37888
used_memory_lua_human:37.00K
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
mem_fragmentation_ratio:6.94
mem_allocator:jemalloc-3.6.0

3、從Redis的配置檔案可以知道,Redis的RDB檔案儲存在/var/lib/redis/目錄中

[root@mysql redis]# pwd
/var/lib/redis
[root@mysql redis]# ll      # 檢視Redis的RDB目錄下的檔案
total 0

4、在客戶端執行程式,向Redis中插入500萬條資料
5、向Redis中寫入500萬條資料完成後,執行SAVE命令

127.0.0.1:6379> save        # 執行SAVE命令,花費5.72秒
OK
(5.72s)

6.切換另一個Redis-cli視窗執行命令

127.0.0.1:6379> spop key1   # 執行spop命令彈出'key1'的值,因為SAVE命令在執行的原因,spop命令會阻塞直到save命令執行完成,執行spop命令共花費4.36秒
"value1"
(4.36s)

7、檢視Redis佔用的記憶體數

127.0.0.1:6379> info memory     # 向Redis中寫入500萬條資料後,Redis佔用1.26G記憶體容量
# Memory
used_memory:1347976664
used_memory_human:1.26G
used_memory_rss:1381294080
used_memory_rss_human:1.29G
used_memory_peak:1347976664
used_memory_peak_human:1.26G
total_system_memory:2080903168
total_system_memory_human:1.94G
used_memory_lua:37888
used_memory_lua_human:37.00K
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
mem_fragmentation_ratio:1.02
mem_allocator:jemalloc-3.6.0
127.0.0.1:6379> dbsize          # 檢視Redis中資料總數
(integer) 4999999

8、在系統命令提示符中檢視生成的RDB檔案

[root@mysql redis]# ls -lah         # Redis的RDB檔案經過壓縮後的大小為122MB
total 122M
drwxr-x---   2 redis redis   22 Oct 13 15:31 .
drwxr-xr-x. 64 root  root  4.0K Oct 13 13:38 ..
-rw-r--r--   1 redis redis 122M Oct 13 15:31 dump.rdb

(2)使用BGSAVE命令非同步建立RDB檔案
執行BGSAVE命令也會建立一個新的RDB檔案,BGSAVE不會造成redis伺服器阻塞:在執行BGSAVE命令的過程中,Redis服務端仍然可以正常的處理其他的命令請求。

BGSAVE命令執行步驟:

  1. Redis服務端接受到BGSAVE命令
  2. Redis服務端通過fork()來生成一個名叫redis-rdb-bgsave的程序,由redis-rdb-bgsave子程序來建立RDB檔案,而Redis主程序則繼續處理客戶端的命令請求
  3. 當redis-rdb-bgsave子程序建立完成RDB檔案,會向Redis主程序傳送一個訊號,告知Redis主程序RDB檔案已經建立完畢,然後redis-rdb-bgsave子程序退出
  4. Redis伺服器(父程序)接手子程序建立的RDB檔案,BGSAVE命令執行完畢

在這裡插入圖片描述
Redis主程序因為建立子程序,會消耗額外的記憶體。不過,如果在Redis主程序fork子程序的過程中花費的時間過多,Redis仍然可能會阻塞

SAVE命令與BGSAVE命令的區別

命令 save bgsave
IO型別 同步 非同步
是否阻塞 是(阻塞發生在fork)
時間複雜度 O(n) O(n)
優點 不會消耗額外記憶體) 不阻塞客戶端命令
缺點 阻塞客戶端命令 需要fork消耗記憶體。

總結:
SAVE建立RDB檔案的速度會比BGSAVE快,SAVE可以集中資源來建立RDB檔案。如果資料庫正在上線當中,就要使用BGSAVE
;如果資料庫需要維護,可以使用SAVE命令。

(3)自動生成RDB
開啟Redis的配置檔案/etc/redis.conf,可以看到我們剛才註釋的內容

save 900 1
save 300 10
save 60 10000

save 900 1表示:如果距離上一次建立RDB檔案已經過去的900秒時間內,Redis中的資料發生了1次改動,則自動執行BGSAVE命令
save 300 10表示:如果距離上一次建立RDB檔案已經過去的300秒時間內,Redis中的資料發生了10次改動,則自動執行BGSAVE命令
save 60 10000表示:如果距離上一次建立RDB檔案已經過去了60秒時間內,Redis中的資料發生了10000次改動,則自動執行BGSAVE命令

每次執行BGSAVE命令建立RDB檔案之後,伺服器為實現自動持久化而設定的時間計數器和次數計數器就會被清零,並重新開始計數,所以多個儲存條件的效果是不會疊加。使用者也可以通過設定多個SAVE選項來設定自動儲存條件,

Redis關於自動持久化的配置

rdbcompression yes              建立RDB檔案時,是否啟用壓縮
stop-writes-on-bgsave-error yes 執行BGSAVE命令時發生錯誤是否停止寫入
rdbchecksum yes                 是否對生成RDB檔案進行檢驗
dbfilename dump.rdb             持久化生成的備份檔案的名字
# dbfilename dump-$(port).rdb  可以以埠號 進行區分
dir /var/lib/redis/6379         RDB檔案儲存的目錄

除了上面的三種方式,注意還有一些觸發機制:

  1. 全量複製 (與主從複製有關 後面會說 主會生成RDB檔案)
  2. debug reload debug級別的重啟(不清空記憶體資料)
  3. shutdown 關閉 會執行rdb檔案的生成

(二)AOF

RDB有兩個問題
1.耗時耗效能
Redis把記憶體中的資料dump到硬碟中生成RDB檔案,首先要把所有的資料都進行持久化,所需要的時間複雜度為O(N),同時把資料dump到檔案中,也需要消耗CPU資源,由於BGSAVE命令有一個fork子程序的過程,雖然不是完整的記憶體拷貝,而是基於copy-on-write的策略,但是如果Redis中的資料非常多,佔用的記憶體頁也會非常大,fork子程序時消耗的記憶體資源也會很多
磁碟IO效能的消耗,生成RDB檔案本來就是把記憶體中的資料儲存到硬碟當中,如果生成的RDB檔案非常大,儲存到硬碟的過程中消耗非常多的硬碟IO

2.不可控,丟失資料
自動建立RDB檔案的過程中,在上一次建立RDB檔案以後,又向Redis中寫入多條資料,如果此時Redis服務停止,則從上一次建立RDB檔案到Redis服務掛機這個時間段內的資料就丟失了

AOF((AppendOnlyFile))相當於日誌的記錄。

下圖是AOF建立原理。
在這裡插入圖片描述
恢復的時候 AOF載入,執行命令恢復資料。
在這裡插入圖片描述
AOF安全性問題 – 資料丟失

雖然伺服器執行一次修改資料庫的命令,執行的命令就會被寫入到AOF檔案,但這並不意味著AOF持久化方式不會丟失任何資料

在linux系統中,系統呼叫write函式,將一些資料儲存到某檔案時,為了提高效率,系統通常不會直接將內容寫入硬盤裡面,而是先把資料儲存到硬碟的緩衝區之中。等到緩衝區被填滿,或者使用者執行fsync呼叫和fdatasync呼叫時,作業系統才會將儲存在緩衝區裡的內容真正的寫入到硬盤裡。

對於AOF持久化來說,當一條命令真正的被寫入到硬碟時,這條命令才不會因為停機而意外丟失。因此,AOF持久化在遭遇停機時丟失命令的數量,取決於命令被寫入硬碟的時間。越早將命令寫入到硬碟,發生意外停機時丟失的資料就越少,而越遲將命令寫入硬碟,發生意外停機時丟失的資料就越多。

AOF提供三種策略讓我們在AOF安全性和效能上進行權衡。
1、always
Redis每寫入一個命令,always會把每條命令都重新整理到硬碟的緩衝區當中然後將緩衝區裡的資料寫入到硬盤裡。
這種模式下,Redis即使用遭遇意外停機,也不會丟失任何自己已經成功執行的資料
在這裡插入圖片描述
2.everysec
Redis每一秒呼叫一次fdatasync,將緩衝區裡的命令寫入到硬盤裡,這種模式下,當Redis的資料交換很多的時候可以保護硬碟。即使Redis遭遇意外停機時,最多隻丟失一秒鐘內的執行的資料

在這裡插入圖片描述

3.no
伺服器不主動呼叫fdatasync,由作業系統決定任何將緩衝區裡面的命令寫入到硬盤裡,這種模式下,伺服器遭遇意外停機時,丟失的命令的數量是不確定的

在這裡插入圖片描述
三種方式對比:

命令 always everysec no
優點 不丟失資料 每秒一次 fsync丟1秒資料 不用管
缺點 IO開銷較大,一般的sata盤只有幾百TPS 丟1秒資料 不可控

一般不會選擇第三種。

AOF重寫功能
隨著伺服器的不斷執行,為了記錄Redis中資料的變化,Redis會將越來越多的命令寫入到AOF檔案中,使得AOF檔案的體積來斷增大,為了讓AOF檔案的大小控制在合理的範圍,redis提供了AOF重寫功能,通過這個功能,伺服器可以產生一個新的AOF檔案:
Redis重寫 將過期的 沒有用的 可以優化的命令 進行化簡,從而達到減少硬碟佔用量和加速Redis恢復速度的目的重寫將過期的沒有用的可以優化的命令進行化簡,從而達到減少硬碟佔用量和加速Redis恢復速度的目的

具體內容:

  1. 新的AOF檔案記錄的資料庫資料和原有AOF檔案記錄的資料庫資料完全一樣
  2. 新的AOF檔案會使用盡可能少的命令來記錄資料庫資料,因此新的AOF檔案的體積通常會比原有AOF檔案的體積要小得多
  3. AOF重寫期間,伺服器不會被阻塞,可以正常處理客戶端傳送的命令請求
    在這裡插入圖片描述

AOF重寫觸發方式
1.向Redis傳送BGREWRITEAOF命令

類似於BGSAVE命令,Redis主程序會fork一個子程序,由子程序去完成AOF重寫
在這裡插入圖片描述

這裡的AOF重寫是將Redis記憶體中的資料進行一次回溯,得到一個AOF檔案,而不是將已有的AOF檔案重寫成一個新的AOF檔案

2、通過配置選項自動執行BGREWRITEAOF命令
(1)auto-aof-rewrite-min-size 觸發AOF重寫所需的最小體積:
只要在AOF檔案的大小超過設定的size時,Redis會進行AOF重寫,這個選項用於避免對體積過小的AOF檔案進行重寫

(2)auto-aof-rewrite-percentage 指定觸發重寫所需的AOF檔案體積百分比:
當AOF檔案的體積大於auto-aof-rewrite-min-size指定的體積,並且超過上一次重寫之後的AOF檔案體積的percent%時,就會觸發AOF重寫,如果伺服器剛啟動不久,還沒有進行過AOF重寫,那麼使用伺服器啟動時載入的AOF檔案的體積來作為基準值。
將這個值設定為0表示關閉自動AOF重寫功能

涉及的兩個統計項:
aof_current_size AOF當前尺寸(單位:位元組)
aof_base_size AOF上次啟動和重寫的尺寸(單位:位元組)

只有當上面兩個條件同時滿足時才會觸發Redis的AOF重寫功能

自動觸發時機 根據統計項 尺寸大小 增長率
在這裡插入圖片描述

AOF重寫流程可用下圖表示

  1. 無論是執行bgrewriteaof命令還是自動進行AOF重寫,實際上都是執行BGREWRITEAOF命令
  2. 執行bgrewriteaof命令,Redis會fork一個子程序,
  3. 子程序對記憶體中的Redis資料進行回溯,生成新的AOF檔案
  4. Redis主程序會處理正常的命令操作
  5. 同時Redis把會新的命令寫入到aof_rewrite_buf當中,當bgrewriteaof命令執行完成,新的AOF檔案生成完畢,Redis主程序會把aof_rewrite_buf中的命令追加到新的AOF檔案中
  6. 用新生成的AOF檔案替換舊的AOF檔案
    在這裡插入圖片描述

配置檔案中AOF相關選項

appendonly   no                     # 改為yes,開啟AOF功能
appendfilename  "appendonly.aof"    # 生成的AOF的檔名
appendfsync everysec                # AOF同步的策略
no-appendfsync-on-rewrite no        # AOF重寫時,是否做append的操作
    AOF重寫非常消耗伺服器的效能,子程序要將記憶體中的資料刷到硬碟中,肯定會消耗硬碟的IO
    而正常的AOF也要將記憶體中的資料寫入到硬碟當中,此時會有一定的衝突
    因為rewrite的過程在資料量比較大的時候,會佔用大量的硬碟的IO
    在AOF重寫後,生成的新的AOF檔案是完整且安全的資料
    如果AOF重寫失敗,如果設定為no則正常的AOF檔案中會丟失一部分資料
    生產環境中會在yes和no之間進行一定的權衡,通過優先從效能方面進行考慮,設定為yes
auto-aof-rewrite-percentage 100     # 觸發重寫所需的AOF檔案體積增長率
auto-aof-rewrite-min-size 64mb      # 觸發重寫所需的AOF檔案大小

(三)RDB和AOF的選擇

RDB和AOF的選擇可以參考下表:

命令 RDB AOF
啟動優先順序
體積
恢復速度
資料安全性 丟資料 根據策略決定
輕重

啟動優先順序解釋: 如果兩者都選擇了情況下 重啟redis redis載入資料 會先選擇aof

RDB最佳策略
RDB是一個重操作

Redis主從複製中的全量複製(之前有提到)是需要主節點執行一次BGSAVE命令,然後把RDB檔案同步給從Redis從節點來實現複製的效果。即使你RDB檔案生成的配置給關閉了,全量複製並不受此限制。

如果對Redis按小時或者按天這種比較大的量級進行備份,使用RDB是一個不錯的選擇,集中備份管理比較方便。

在Redis主從架構中,可以在Redis從節點開啟RDB,可以在本機儲存RDB的歷史檔案,但是生成RDB檔案的週期不要太頻繁。

Redis的單機多部署模式對伺服器的CPU,記憶體,硬碟有較大開銷,實際生產環境根據需要進行設定。

AOF最佳策略

建議把appendfsync選項設定為everysec,進行持久化,這種情況下Redis宕機最多隻會丟失一秒鐘的資料。

如果使用Redis做為快取時,即使資料丟失也不會造成任何影響,只需要在下次載入時重新從資料來源載入就可以了。

Redis單機多部署模式下,AOF集中操作時會fork大量的子程序,可能會出現記憶體爆滿或者導致作業系統使用SWAP分割槽的情況
一般分配伺服器60%到70%的記憶體給Redis使用,剩餘的記憶體分留給類似fork的操作

RDB和AOF的最佳使用策略

  1. 使用max_memory對Redis進行規劃,例如Redis使用單機多部署模式時,每個Redis可用記憶體設定為4G,這樣無論是使用RDB模式還是AOF模式進行持久化,fork子程序操作都只需要較小的開銷。
  2. Redis分散式時,小分片會產生更多的程序,可能會對CPU的消耗更大。
  3. 使用監控軟體對伺服器的硬碟,記憶體,負載,網路進行監控,以對伺服器各硬碟有更全面的瞭解,方便發生故障時進行定位
    不要佔用100%的記憶體。

Redis持久化開發涉及的問題:

1.fork操作

Redis的fork操作是同步操作

執行BGSAVE和BGAOF命令時,實際上都是先執行fork操作,fork操作只是記憶體頁的拷貝,而不是完全對記憶體的拷貝。

fork操作在大部分情況下是非常快的,但是如果fork操作被阻塞,也會阻塞Redis主執行緒的執行。畢竟fork與記憶體量息息相關:Redis中資料佔用的記憶體越大,耗時越長(與機器型別有關),可以通過info memory命令檢視上次fork操作消耗的微秒數:latest_fork_usec:0

改善fork

  1. 優先使用物理機或者高效支援fork操作的虛擬化技術
  2. 控制Redis例項最大可用記憶體:maxmemory
  3. 合理配置linux記憶體分配策略:vm.overcommit_memory = 1
  4. 降低fork頻率,例如放寬AOF重寫自動觸發機制,不必要的全量複製

2.程序外開銷

(1.1)CPU開銷
RDB和AOF檔案的生成操作都屬於CPU密集型
通常子程序的開銷會佔用90%以上的CPU,檔案寫入是非常密集的過程

(1.2)CPU開銷優化

  1. 不做CPU繫結,不要把Redis程序繫結在一顆CPU上,這樣Redis fork子程序時,會分散消耗的CPU資源,不會對Redis主程序造成影響
  2. 不和CPU密集型應用在一臺伺服器上部署,這樣不會產生CPU資源的過度競爭
  3. 在使用單機部署Redis時,不要發生大量的RDB,BGSAVE,AOF的過程,保證可以節省一定的CPU資源

(2.1)記憶體開銷

在linux系統中,有一種顯式複製的機制:copy-on-write,父子程序會共享相同的實體記憶體頁,當父程序有寫請求的時候,會建立一個父本,此時才會消耗一定的記憶體。

在這個過程中,子程序會共享fork時父程序的記憶體的快照。

如果父程序沒有多少寫入操作時,fork操作不會佔用過多的記憶體資源,可以在Redis的日誌中看到

(2.2)記憶體開銷優化:

  1. 在單機部署Redis時,不要產生大量的重寫,這樣記憶體開銷也會比較小
  2. 儘量主程序寫入量比較小時,執行BGSAVE或者AOF操作
  3. linux系統優化:echo never > /sys/kernel/mm/transparent_hugepage/enabled

(3.1)硬碟開銷

AOF和RDB檔案的寫入,會佔用硬碟的IO及容量,可以使用iostat命令和iotop命令檢視分析

(3.2)硬碟開銷優化:

  1. 不要和硬碟高負載服務部署在一起,如儲存服務,訊息佇列等
  2. 修改Redis配置檔案:在AOF重寫期間不要執行AOF操作,以減少記憶體開銷 : no-appendfsync-on-rewrite = yes
  3. 根據硬碟寫入量決定磁碟型別:例如使用SSD
  4. 單機多部署模式持久化時,檔案目錄可以考慮分盤。即對不同的Redis例項以埠來進行區分,持久化檔案也以埠來區分

AOF追加阻塞(AOF一般都是一秒中執行一次)

  1. 主執行緒負責寫入AOF緩衝區
  2. AOF同步執行緒每秒鐘執行一次同步硬碟操作,同時還會記錄一次最近一次的同步時間
  3. 主執行緒會對比上次AOF同步時間,如果距離上次同步時間在2秒之內,則返回主執行緒
  4. 如果距離上次AOF同步時間超過2秒,則主執行緒會阻塞,直到同步完成
    在這裡插入圖片描述

AOF追加阻塞是保證AOF檔案安全性的一種策略

為了達到每秒刷盤的效果,主執行緒會阻塞直到同步完成

這樣就會產生一些問題:
因為主執行緒是在負責Redis日常命令的處理,所以Redis主執行緒不能阻塞,而此時Redis的主執行緒被阻塞。如果AOF追加被阻塞,每秒刷盤的策略並不會每秒都執行,可能會丟失2秒的資料

AOF阻塞定位:
如果AOF追加被阻塞,可以通過命令檢視:

127.0.0.1:6379> info persistence
# Persistence
loading:0
rdb_changes_since_last_save:1
rdb_bgsave_in_progress:0
rdb_last_save_time:1539409132
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:-1
rdb_current_bgsave_time_sec:-1
aof_enabled:0
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
aof_delayed_fsync:100               # AOF被阻塞的歷史次數,無法看到某次AOF被阻塞的時間點

這五個專題串過之後,你會對Redis單體,有著非常好的理解了,後面再走就是看原始碼了。相信你到這一步已經可以獨當一面了。

我再往下面寫,就是Redis分散式領域相關的東西了,比如說Redis的主從複製、哨兵機制、 Redis cluster特性以及快取設計存在的問題與優化等。等我~

對了,兄dei,如果你覺得這篇文章可以的話,給俺點個贊再走,管不管?這樣可以讓更多的人看到這篇文章,對我來說也是一種激勵。還有如果你有什麼問題的話,歡迎留言或者CSDN APP直接與我交流。