開發與維運

Redis 基礎

Redis 特性:

  • 速度快,數據在內存中,通過 key 查找,時間複雜度 O(1)
  • 支持多種數據類型,string,list,hash,set,sort set 等
  • 支持事物,操作都是原子性的
  • 豐富的特性,可用於緩存等

Redis 是單線程還多線程?

答:Redis基於Reactor模式開發了網絡事件處理器,這個處理器被稱為文件事件處理器。它的組成結構為4部分:多個套接字、IO 多路複用程序、文件事件分派器、事件處理器。因為文件事件分派器隊列的消費是單線程的,所以Redis才叫單線程模型。

參考和圖片鏈接:https://www.jianshu.com/p/6264fa82ac33

Redis 單線程模型

1、Redis 基礎

1.1 為什麼要使用 Redis?

當系統訪問量大的時候,比如某個商品促銷導致訂單量激增(比如秒殺場景),如果直接在 MySQL 扣減庫存很容易導致 MySQL 崩掉,所以需要一個 Redis 這樣的緩存中間件。

1.2 常用的緩存中間件有那些?

知道的有 Redis 和 Memcache

  • 共同點:都是內存數據庫,
  • 不同點:Redis 支持持久化寫入到磁盤,而 Mencache 掛掉後就消失無法恢復。

1.3 Redis 有那些數據結構

Redis 的基本數據結構如下:

圖片和參考來源:https://www.cnblogs.com/haoprogrammer/p/11065461.html

  • String :存儲字符串,比較浪費內存,不推薦。可以用來 Session 共享,分佈式鎖等;
  • Hash:比 String 省內存,可以比較直觀的緩存多維信息,而 String 需要通過 JSON 等形式存儲多維信息
  • List:鏈表,異步隊列需要延後處理的任務塞進列表,存儲微博、朋友圈的時間軸列表
  • Set:去重,點贊,還有收藏等
  • Sort Set:去重,有個 score,可以用來排名等

1.4、Redis 數據過期時間過於集中會導致那些問題?

會導致卡頓,解決方法在設置過期時間的時候加一個隨機值。使得過期時間分散一點。消息過期太集中容易導致緩存雪崩。

1.5 Redis 分佈式鎖,需要主要什麼?

更多參考 Spring Boot 和 Redis 的分佈式鎖: https://ylooq.gitee.io/learn-spring-boot-2/#/12-DistributedLock

同一商品的信息、促銷優惠等,可能會被多個人同時更改,如果沒有加分佈式鎖會導致數據前後不一致的問題。

# 設置 鎖的命令,正確示範
SET key value [EX seconds] [PX milliseconds] [NX|XX]

SET 參數說明,從 Redis 2.6.12 版本開始:

  • 沒有 EX、NX、PX、XX 的情況下,如果 key 已經存在,則覆蓋 value 值不管其什麼類型;如果不存在,則新建一個 key -- value
  • EX seconds: 設置過期時間為 seconds 秒,SET key value EX second 效果等同於 SETEX key second value
  • PX millisecond :設置鍵的過期時間為 millisecond 毫秒。 SET key value PX millisecond 效果等同於 PSETEX key millisecond value
  • NX :只在 key 不存在時,才對鍵進行設置操作。 SET key value NX 效果等同於 SETNX key value
  • XX :只在 key 已經存在時,才對鍵進行設置操作。
  • value 值:需要加入隨機的字符串,作為釋放鎖的唯一口令(Token),預防持有過期鎖的線程誤刪其他持線程的鎖

需要注意:設置鎖的時候不能先設置 key,然後在設置過期時間,如果執行兩個命令之間 Redis 進程發生意外(crash,重啟維護)了,就會導致鎖無法自動釋放

# 設置 key value ,這是錯誤的
SET key value
# 然後再設置過期時間 10s,萬一中間服務器抽風了,那就沒法自動釋放鎖,容易導致死鎖了
EXPIRE key 10

如果在代碼,參考如下設置設置,同時設置 key vlaue 和過期時間:

# 加鎖成功,lockStat !=null && lockStat == true
Boolean lockStat = stringRedisTemplate.execute((RedisCallback<Boolean>) connection ->
                connection.set(key.getBytes(StandardCharsets.UTF_8), value.getBytes(StandardCharsets.UTF_8),
                 Expiration.from(timeout, timeUnit), RedisStringCommands.SetOption.SET_IF_ABSENT));

需要注意:釋放鎖的時候不能直接 DEL,要使用 lua 腳本驗證 key 和 value,一致才能釋放鎖,可能導致先前持有過期鎖的線程誤刪除鎖。

if redis.call('get', KEYS[1]) == ARGV[1] 
  then return redis.call('del', KEYS[1]) 
    else return 0 end

需要注意:加鎖失敗的處理

  • 拋出異常
  • sleep,然後再次嘗試
  • 使用延時隊列

1.6 Redis 的 keys 命令和 sacn 命令有什麼區別?

  • keys abc* : keys 可以匹配後面的正則表達式,例如列出所有 abc 開頭的 key
  • scan 0【遊標】 MATCH abc* count 1【返回多少條】:列出所有 abc 開頭的 key

參考 : http://doc.redisfans.com/key/scan.html

keys 會阻塞,一直到所有符合條件的 key 列出來,如果key 的總數或者符合條件的 key 過多會導致卡頓,而 SCAN 是增量式迭代命令,每次從遊標開始,遊標參數被設置為 0 時, 服務器將開始一次新的迭代, 而當服務器向用戶返回值為 0 的遊標時, 表示迭代已結束。

不過,增量式迭代命令也不是沒有缺點的:舉個例子, 使用 SMEMBERS 命令可以返回集合鍵當前包含的所有元素, 但是對於 SCAN 這類增量式迭代命令來說, 因為在對鍵進行增量式迭代的過程中, 鍵可能會被修改, 所以增量式迭代命令只能對被返回的元素提供有限的保證

Redis> keys key* # 測試數據
1)key1
*********
10000)key10000
redis>scan 0 match key* count 100 
# 0 表示開始迭代的遊標,match key* 正則表達式匹配,count 100 返回 100 條
1)120  #---> 下一次使用的遊標
2)1) "key233" #--->返回的結果,不一定按照 key1 到 key10000 的順序
  2) "key1000"
  3) "key330"
  4) "key23"

1、如果 redis 中有 1 億個 key,其中有 10w 個 abc 開頭的 key,怎麼列出來? 用 keys 命令

2、如果 redis 正在提供服務,keys 會導致什麼後果?卡頓,可以通過 sacn 命令解決,但 scan 命令返回的結果不能完全保證,因為在增量迭代的時候 key 可能會被修改。

1.7 Redis 如何實現消息隊列?

在 Redis 中,有個 list 的數據結構,通過如下命令生產消息和消費消息,List 是雙鏈表,遵循先入先出的原則,通過 lpush/rpush 和 rpop/lpop 發佈和消費消息。

# 消息入隊,L 左邊,R 右邊,PUSH 入隊,POP 出隊
redis> LPUSH languages python
redis> LPUSH languages java
# 消息出隊,返回 python ,如果隊列裡面為空,則返回 nil
redis> RPOP languages

只能被 1 個消費端消費,LPOP 命令在隊列為空的時候返回 null,如果一直沒有消息,可以讓消費者線程睡睡覺 sleep 免得浪費資源。

如果消費者線程不使用 sleep() ,該怎麼辦?使用 BLPOP / 或者 BRPOP 命令,會阻塞到消息隊列有消息才返回。

1.8 Redis 如何實現發佈、訂閱模式?也就是 1:N 消費?

使用 Redis 的 pub/sub 發佈定義模式,但不推薦使用,因為消息者下線的情況下,消息會丟失。推薦使用專門的消息中間件如 RocketMQ,RabbitMQ,Kafka。

訂閱某個 key 的消息:http://doc.redisfans.com/pub_sub/subscribe.html

# 訂閱 msg 頻道
# 1 - 3 行是執行 subscribe 之後的反饋信息
# 第 4 - 6 行才是接收到的第一條信息
redis> subscribe msg
Reading messages... (press Ctrl-C to quit)
1) "subscribe"       # 返回值的類型:顯示訂閱成功
2) "msg"             # 訂閱的頻道名字
3) (integer) 1       # 目前已訂閱的頻道數量

1) "message"         # 返回值的類型:信息
2) "msg"             # 來源(從那個頻道發送過來)
3) "hello moto"      # 信息內容

發佈某個 key 的消息:http://doc.redisfans.com/pub_sub/publish.html

redis> publish msg "good morning"
(integer) 1 # 返回當前 msg 主題的消息訂閱客戶端數量

1.9 Redis 如何實現延遲隊列?

如果是多線程環境處理延遲隊列,可以通過 zrangebyscore 和 zrem 一同挪到服務器端使用 lua 腳本進行原子操作。

  • zrangebyscore 只取一條數據
  • zrem 移除該條數據

使用 Sorted Set 數據類型,使用時間戳作為分數

# 添加隊列任務
redis> ZADD key score(時間戳) value

獲取 N 秒前的數據

# 使用 ZRANGEBYSORT 獲取前 N 秒的數據 ,0,1 類似 MySQL limit,偏移量 0,取一條
redis> ZRANGEBYSORT key (時間戳開始點 時間戳結束點 0,1
# score 開始點, score 結束點, 也就是列出 時間戳開始點 <= 時間戳 < 時間戳結束點 的數據
# 如果開始點或者結束點前面有 ( 這個符號表示 大於等於或者小於等於

1.10 Redis 持久化

前面提到,Redis 支持持久化的操作,Redis 的數據全部在內存中,如果突然掛機,如果不持久化寫入磁盤就會造成數據的全部丟失。Redis 的持久化機制有兩種:一種是 RDB 快照,一種是 AOF 日誌

通常情況下,Redis 主節點不進行持久化操作,持久化操作一般在從節點進行的。

1.10.1 RDB 快照原理:全量持久化

RDB 會將內存的數據全量的寫入到磁盤,這也是為什麼叫“快照”的原因。Redis 使用多進程的 COW(Copy On Write)實現內存數據的快照持久化。數據恢復比較快

RDB 的缺點的:會丟失很多的數據。看 COW 原理理解。

什麼是 COW? 參考 Copy On Write機制瞭解一下 https://cloud.tencent.com/developer/article/1369027

Redis 在持久化的時候,會將當前主進程調用 glibc 的 fork 產生一個子進程。fork 出來的子進程和父進程共享相同的內存頁,只有在數據更改的時候才會分離給子進程分配新的物理內存。在 Redis 中,子進程不會對內存中的數據進行修改。當父進程對其中一個 page 修改的時候,會使用操作系統的 COW 機制,將內存分離出來。

父子進程虛擬地址不同,如果內存頁數據在 fork 進程後沒有發生改變,則實際物理內存地址相同。

image

Redis 主進程對紅色的 page 進行修改,COW 會將父子進程共享的紅色 page 在修改前分離。子進程還是會使用未修改的內存頁。這也就是說,Redis 的內存在子進程產生的一瞬間瞬間凝固了,相當於給內存照相(快照)了。

因為在 Redis 中,父進程(主進程)負責響應請求,也就是會修改內存中的數據。而子進程專門負責遍歷內存,進行系列化寫入磁盤。

主進程修改內存頁,會拷貝未修改的內存頁給子進程分配新的物理內存。

1.10.2 AOF 日誌

AOF 日誌,也就是記錄對 Redis 內存數據進行修改的指令,當 Redis 宕機重啟後,會根據日誌記錄重放一遍(重新執行一遍)。這樣子就可以恢復到宕機前的數據了。

Redis 收到客戶端修改指令後,進行參數檢驗,邏輯處理,如果沒有執行成功,就會記錄一條數據。這跟其他的 HBASE 等不同的是,Redis 是先執行再記錄日誌的。

AOF 日誌的缺點是:如果 AOF 日誌很大很大重新執行一遍 AOF 日誌很慢很慢。

AOF 瘦身: 原理就是 fork 一個子進程,對當前的內存數據遍歷,生成新的 AOF 日誌,然後跟 fork 後的增量數據合併產生的 AOF。因為 Redis 經常 set 、del,有些已經刪除了的數據沒必要還記錄著。

AOF 會丟失數據嗎? 其實 AOF 為了快速寫入,還是會先寫入到內存中,所以沒有 fsync 刷寫到磁盤的數據還是會丟失的。不調用 fsync ,交給操作系統完成寫入磁盤的操作性能比較高,但數據丟失的更多。一條日誌調用一次 fync,則性能比較低。在生產中一般配置 1s fsync 刷寫磁盤,也就是說最多丟失 1s 的數據

Kafka 默認是將刷寫磁盤的操作交給操作系統的。所以 Kafka 比較適合允許丟失少量數據的大數據應用。

1.10.3 Redis 4 開始的混合持久化

Redis 重啟後,會加載 RDB,然後在重放 AOF,這樣子數據丟失的可能比較小,重啟的效率也比較高。

1.11 Redis Pipeline 管道

管道是加速 Redis 的存取效率,減少網絡 IO 次數,減少 IO 時間。也就是將多個讀取、寫入的操作指令封裝成一個網絡請求。

Kafka 客戶端,將發往同一個 Broker 的消息封裝成 batchs,然後再發送,提高吞吐量。

如何測試 Redis 的 QPS,redis-benchmark:

> redis-benchmark -t set -p 3 -q
# -t set 對 set 命令進行壓測
# -p 3 管道的命令數量,不使用表示 一個命令一個請求
# -q 強制退出 redis。僅顯示 query/sec 值

1.12 Redis 同步機制

Redis 支持主從同步和從從同步:

1.12.1 快照同步

第一次同步或者新節點加入的時候使用快照同步。首先在主節點進行 bgsave 產生 RDB 快照,然後發送給要同步的從節點。

從節點接收 RDB 後會清空當前從節點的數據,全量加載 RDB 快照。對於快照以後的數據採用增量同步的方式。

1.12.2 增量同步

Redis 增量同步的是指令流,主節點會將會改變內存數據的指令記錄到本地的環形數組 buffer,然後異步將 buffer 發送到從節點,從節點一邊記錄同步到那裡一邊反饋給主節點同步到哪裡了(偏移量)。

環形數組 buffer,循環寫入,寫滿後會覆蓋前面寫的內容。如果網絡不好,從節點短時間無法和主節點同步。恢復網絡後,主節點環形 buffer 那些還沒有被同步的數據可能被後續的指令覆蓋了。那麼就會採用快照同步的方式。

快照同步死循環:主節點 bgsave 後的增量數據的操作指令流,又發生覆蓋了前面還沒有同步的指令。從節點又得重新採用快照同步方式。然後死循環了。因此需要配置一個合適的 buffer 大小。

1.13 Redis 集群高可用

Redis Sentinel :哨兵,master 宕機後,將 slave 提升為 master 繼續提供服務。數據丟失:因為 buffer 增量同步的方式是異步的,可能會丟失部分數據。高可用

Redis Cluster:去中心化,多個節點負責集群的一部分數據,多個節點也是對等的。默認分成 16384 個槽位,每個節點負責部分數據。擴展性。

參考資料:

Redis基礎:https://mp.weixin.qq.com/s/aOiadiWG2nNaZowmoDQPMQ

錢文品《Redis 深度歷險》

Leave a Reply

Your email address will not be published. Required fields are marked *