【大廠面試】面試官看了讚不絕口的Redis筆記(二)

說明

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

這是上一篇文章 【大廠面試】面試官看了讚不絕口的Redis筆記(二)

目錄:
在這裏插入圖片描述

這是下一篇文章【大廠面試】面試官看了讚不絕口的Redis筆記(三)分佈式篇

目錄:
在這裏插入圖片描述

四、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恢復速度的目的

具體內容:

  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直接與我交流。

發佈了194 篇原創文章 · 獲贊 3472 · 訪問量 53萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章