Redis 核心技術

1. Redis 核心數據結構與高性能原理
 1.1 Redis 核心數據結構
  1.1.1 string
  1.1.2 hash
  1.1.3 list
  1.1.4 set
  1.1.5 zset
  1.1.6 bit
  1.1.7 geo
  1.1.8 其他高級命令
 1.2 Redis 高性能核心原理
 1.3 管道(pipeline)
 1.4 Lua 腳本
 1.5 Redis 的設計與實現
2. Redis 持久化、主從與哨兵架構
 2.1 Redis 持久化
  2.1.1 RDB 快照(snapshot)
  2.1.2 AOF(append-only file)
  2.1.3 RDB 與 AOF 的選擇
  2.1.4 混合持久化
 2.2 Redis 主從架構
 2.3 Redis 哨兵高可用架構
  2.3.1 哨兵 leader 選舉流程
  2.3.2 哨兵架構缺點
3. Redis 集羣架構
 3.1 集羣原理分析
  3.1.1 槽位定位算法
  3.1.2 跳轉重定向
  3.1.3 Redis 集羣節點間的通信機制
  3.1.4 網絡抖動
 3.2 集羣選舉原理分析
  3.2.1 集羣是否完整才能提供服務
4. Redis 分佈式鎖
 4.1 分佈式鎖的適用場景
 4.2 Redis 分佈式鎖的實現
 4.3 Redis 實現分佈式鎖的問題
 4.4 如何保障一致性問題
 4.5 高性能的分佈式鎖如何實現
5. Redis 緩存設計問題以及性能優化
 5.1 緩存穿透
  5.1.1 布隆過濾器
   5.1.1.1 概念
   5.1.1.2 添加元素
   5.1.1.3 查詢是否包含某元素
   5.1.1.4 現有實現
   5.1.1.5 適用場景
 5.2 緩存失效
 5.3 緩存雪崩
 5.4 熱點緩存 key 重建優化
 5.5 數據庫雙寫一致性解決方案
 5.6 Redis 開發規範與性能優化
  5.6.1 鍵值設計
   5.6.1.1 key 設計
   5.6.1.2 value 設計
  5.6.2 命令使用
  5.6.3 客戶端使用

1. Redis 核心數據結構與高性能原理

1.1 Redis 核心數據結構

Redis 的核心數據結構主要由:string、list、hash、set、zset、bitmap

1.1.1 string

常用操作:

  • set:存入字符串鍵值對

  • get:獲取字符串鍵值對

  • del:刪除字符串鍵值

  • mset:批量存儲字符串鍵值對

  • mget:批量獲取字符串鍵值對

  • expire:設置一個鍵的過期時間

  • incr:將 key 中存儲的值加一(原子加減)

  • decr:將 key 中存儲的值減一(原子加減)

  • incrby:將 key 中存儲的值加指定值(原子加減)

  • decrby:將 key 中存儲的值減指定值(原子加減)

應用場景:

  • 單值緩存

    • set key value
      get key
      
  • 對象緩存

    • 方式一:
      	set user:{id} value(json 格式)
      	get user:{id}
      方式二:推薦方式
      	mset user:{id}:name zhangsan user:{id}:age 20
      	mget user:{id}:name user:{id}:age
      
  • 分佈式鎖

    • setnx {lockkey} 1 # 返回 1 表示獲取鎖成功
      setnx {lockkey} 1 # 返回 0 表示獲取鎖失敗
      # 執行業務操作
      ...
      del {lockkey} # 業務完成刪除鎖
      
      set {lockkey} 1 ex 10 nx # 設置過期時間,防止程序意外終止無法釋放鎖
      
  • 計數器

    • incr {key}
      get {key}
      
  • Web 集羣 session 共享

    • spring session + redis 實現 session 共享
      
  • 分佈式系統 ID

    • incrby {key} 1000 # Redis 批量生成序列號提升性能
      
      # 當然還有其他的分佈式 ID 解決方案,比如雪花算法、zookeeper 生成等等
      
  • 分佈式限流器

1.1.2 hash

常用操作:

  • hset:存儲一個哈希表 key 的一個 field 鍵值
  • hsetnx:存儲一個不存在的哈希表 key 的 field 鍵值
  • hmset:存儲一個哈希表 key 的多個field 鍵值
  • hget:獲取哈希表 key 對應的一個 field 值
  • hmget:獲取哈希表 key 對應的多個 field 值
  • hdel:刪除哈希表 key 中的 field 鍵值
  • hlen:返回哈希表 key 中的 field 的數量
  • hgetall:返回哈希表 key 中的所有的 field 鍵值
  • hincrby:爲哈希表 key 中 field 鍵的值加上增量

應用場景:

  • 對象存儲

    • hmset user {id}:name zhangsan {id}:age 20
      hmget user {id}:name {id}:age
      
  • 電商購物車

    • 購物車的設計
      1. 用戶 id 爲 key
      2. 商品 id 爲 field
      3. 商品數量爲 value # 當然這裏的還可能會存儲一個集合數據,包括商品數量、修改時間、電商 id 等等,按照相應的需求設計
      
      購物車的操作
      1. 添加商品:hset cart:1001 1001 1
      2. 增加數量:hincrby cart:1001 1001 1
      3. 商品總數:hlen cart:1001
      4. 刪除商品:hdel cart:1001 1001
      5. 獲取購物車所有商品:hgetall cart:1001
      

hash 結構優缺點:

優點:

  • 同類數據歸類整合存儲,方便管理數據
  • 相比 string 操作消耗內存與 cpu 更小
  • 相比 string 存儲更節省空間

缺點:

  • 過期功能不能在 field 上使用,只能用在 key 上
  • Redis 集羣架構下不適合大規模使用,因爲 key 是會被劃分到固定的 slot 上,即劃分到固定集羣節點上

1.1.3 list

常用操作:

  • lpush:將一個或多個 value 插入到 key 的表頭(最左邊)
  • rpush:將一個或多個 value 插入到 key 的表尾(最右邊)
  • lpop:移除並返回 key 列表頭元素
  • rpop:移除並返回 key 列表尾元素
  • lrange:返回列表 key 中指定區間內的元素,區間以偏移量 start 和 stop 指定
  • blpop:移除並返回列表 key 的頭元素,如果列表中沒有元素,則阻塞
  • brpop:移除並返回列表 key 的尾元素,如果列表中沒有元素,則阻塞

應用場景:

  • 常用數據結構

    • stack(棧)= lpush + lpop -> FILO 先進後出
      queue(隊列)= lpush + rpop -> FIFO 先進先出
      blocking queue(阻塞隊列)= lpush + brpop
      
  • 微博和微信公衆號消息流

    •   比如在微博上關注了某些博主 A、B
        1. A 發了條微博,消息 id 爲 1001
        	lpush msg:{粉絲 id} 1001
        2. B 發了條微博,消息 id 爲 1002
        	lpush msg:{粉絲 id} 1002
        3. 粉絲查看最新微博消息
        	lrange msg:{粉絲 id} 0 5
      

1.1.4 set

常用操作:

  • sadd:往集合 key 中存入元素,元素存在則忽略,若 key 不存在則添加
  • srem:從集合 key 中刪除元素
  • smembers:獲取集合 key 中的所有元素
  • scard:獲取集合 key 元素的個數
  • sismember:判斷元素是否存在集合 key 中
  • srandmember:從集合 key 中選出 count 個元素,元素不從 key 中刪除
  • spop:從集合 key 中選出 count 個元素,元素從 key 中刪除
  • sinter:交集運算
  • sinterstore:將交集結果存入新的集合 destination 中
  • sunion:並集運算
  • sunionstore:將並集結果存入新的集合 destination 中
  • sdiff:差集運算
  • sdiffstore:將差集結果存入新的集合 destination 中

應用場景:

  • 抽獎

    • 1. 點擊參與抽獎
      	sadd key {userid}
      2. 查看參與抽獎的所有用戶
      	smembers key
      3. 抽取 count 名中獎用戶
      	spop key [count] / srandmember key [count]
      
  • 微信微博點贊、收藏、標籤

    • 1. 點贊
      	sadd like:{消息 id} {用戶 id}
      2. 取消點贊
      	srem like:{消息 id} {用戶 id}
      3. 檢查用戶是否點過贊
      	sismember like:{消息 id} {用戶 id}
      4. 獲取點讚的用戶列表
      	smembers like:{消息 id}
      5. 獲取點贊用戶個數
      	scard like:{消息 id}
      
  • 集合操作

    • set1 -> {a,b,c}
      set2 -> {b,c,d}
      set3 -> {c,d,e}
      
      sinter set1 set2 set3 -> {c}
      sunion set1 set2 set3 -> {a,b,c,d,e}
      sdiff set1 set2 set3 -> {a} # 以 set1 爲基準,查找它與其他集合中不一樣的數據
      
  • 集合操作實現微博微信關注模型

    • A 關注的人:
      	Aset -> {B,C,D}
      B 關注的人:
      	Bset -> {A,C,D,E}
      C 關注的人:
      	Cset -> {A,B,D,E,F}
      
      A 進入 B 的主頁,查看以下信息
      1. A 和 B 共同關注的人:
      	sinter Aset Bset -> {C,D}
      2. A 關注的人也關注他(B):
      	sismember Cset B
      	sismember Dset B
      3. A 可能認識的人:
      	sdiff Bset Aset -> {E}
      
  • 集合操作實現電商商品篩選

    • 比如京東搜索商品頁面,篩選筆記本電腦。支持品牌、操作系統、CPU 品牌、分辨率等等選項進行篩選。那麼在後臺需要維護這些篩項集合。
      
      如果要添加一款手機,那麼可能會這樣做:
      sadd brand:huawei p30
      sadd brand:xiaomi mi5
      sadd brand:iphone iphone13
      sadd os:android p30 mi5
      sadd cpu:brand:intel p30 mi5
      sadd ram:8g p30 mi5 iphone8
      
      那麼選擇出:安卓系統、Intel 的 CPU、內存 8g 的手機:
      sinter os:android cpu:brand:intel ram:8g -> {p30,mi5}
      

1.1.5 zset

常用操作:

  • zadd:往有序集合 key 中添加帶分值的元素
  • zrem:從有序集合 key 中刪除元素
  • zscore:返回有序集合 key 中元素 member 的分值
  • zincrby:爲有序集合 key 中的元素 member 的分值加上 increment
  • zcard:返回有序集合 key 中的個數
  • zrange:正序獲取有序集合 key 從 start 下標到 stop 下標的元素
  • zrevrange:倒序獲取有序集合 key 從 start 下標到 stop 下標的元素
  • zunionstore:並集計算,並存儲到 destkey 集合中
  • zinterstore:交集計算,並存儲到 destkey 集合中

應用場景:

  • 集合操作實現排行榜

    • 微博熱搜、頭條熱榜等等
      
      1. 點擊新聞
      	zincrby hotNews:20201217 1 法國總統馬克龍新冠檢測呈陽性
      2. 展示當日排行前十
      zrevrange hotNews:20201217 0 10 withscores
      3. 七日搜索榜單計算
      	zunionstore hotNews:20201211-20201217 7 hotNews:20201211 .. hotNews:20201217
      4. 展示七日排行前十
      	zrevrange hotNews:20201211-20201217 0 10 withscores
      

1.1.6 bit

常用操作:

  • setbit:設置 key 指定偏移量的值,值只能爲 0 或 1
  • getbit:返回 key 指定偏移量的值
  • bitop:對不同的 key 的位進行位運算操作(and、or、not、xor)
  • bitCount:返回指定 key 中值爲 1 的個數

應用場景:

  • 用戶在線狀態

    • 使用一個名爲 online 的 key 保存數據,用戶的 id 爲 offset,在線狀態用 0 和 1 表示
      setbit online {用戶 id} 1/0
      
      如果用戶很多,那就進行分片。
      
  • 統計活躍用戶

    • 使用時間爲緩存 key,用戶 id 爲 offset,當日活躍就設置 1
      setbit active:20201217 {uid} {status}
      
  • 用戶簽到

    • 每個用戶有自己的簽到 bitmap 作爲 key,設置一個活動開始時間,用當前時間減去活動開始時間的天數作爲 offset,是否簽到用 0 和 1 表示。
      start_date = 2020-12-01
      current_date = 2020-12-17
      currentDays = current_date - start_date;
      
      setbit sign:{uid} currentDays 1
      
      計算活躍天數
      bitcount sign:{uid} 0 -1
      
  • 布隆過濾器

bitmap 的優缺點:

優點:

  • 基於最小單位 bit 進行存儲,節省空間
  • 時間複雜度爲 O(1),操作快,計算快
  • 方便擴容

缺點:

  • Redis 中的 bit 映射被限制在 512MB,最大是 2^32。建議每個 key 的位數都控制下

參考資料:https://blog.csdn.net/u011957758/article/details/74783347

1.1.7 geo

geo 是一種基於 zset 結構存儲的數據結構,用於構建地理位置數據。

常用操作:

  • geoadd:添加一個或多個地址空間位置到 zset
  • geohash:返回一個標準的地理空間的 geohash 字符串
  • geopos:返回地理空間的經緯度
  • geodist:返回兩個地理空間之間的距離
  • georadius:查詢指定半徑內所有的地理空間元素的集合
  • georadiusbymember:查詢指定半徑匹配到最大距離的一個地理空間元素

應用場景:

  • 微信、微博附近的人
  • 微信<搖一搖><搶紅包>
  • 滴滴打車、青桔單車<附近的車>

Redis 更多應用場景:

  • 搜索自動補全
  • 布隆過濾器

1.1.8 其他高級命令

keys:全量遍歷,用來列出所有滿足特性正則表達式的 key,當 redis 數據量比較大的時候,性能很差,要避免使用

scan:漸進式遍歷鍵。像遊標一樣的遍歷匹配條件的 key,集合類的數據結構也有對應的遍歷鍵

info:查看 Redis 服務運行信息

1.2 Redis 高性能核心原理

Redis 單線程爲什麼還能這麼快?

因爲它的所有數據都是在內存中的,所有的運算都是內存級別的運算,而且單線程避免了多線程的上線文切換性能損耗問題。正是因爲 Redis 是單線程的,所有要小心使用 Redis 指令,避免那些耗時的指令(keys)。

Redis 單線程如何處理那麼多的併發客戶端連接?

Redis 採用 IO 多路複用,它利用 epoll 來實現 OP 多路複用,將連接信息和事件放入隊列中,依次放到文件事件分派器,事件分派器將事件分發給事件處理器。有連接應答處理器、命令請求處理器、命令回覆處理器,單線程主要是說它的各個事件處理器是單線程的。

同樣 NGINX 也是採用 IO 多路複用原理解決 C10K 問題。同樣 Java 中的 NIO 也是採用 IO 多路複用。

1.3 管道(pipeline)

客戶端可以一次性發送多個請求而不用等待服務器的響應,待所有的命令都發送完畢之後在一次性讀取服務的相應,這樣可以極大的降低多條命令執行的網絡傳輸開銷,管道執行多條命令的網絡開銷實際上相當於一次命令執行的網絡開銷。

需要注意管道用 pipeline 方式打包命令發送,Redis 必須在處理完所有命令前先緩存其所有命令的處理結果。打包的命令越多,緩存消耗的內存也越多。所以並不是打包的命令越多越好。

pipeline 中發送的每個命令都會被 server 立即執行,如果執行失敗,然後會在此後的響應中得到信息;也就是 pipeline 並不是表達“所有命令都一起執行成功”的語義。管道中前面的命令失敗,不會影響後面的命令執行。

Pipelinepl=jedis.pipelined();
for(inti=0;i<10;i++){
  pl.incr("pipelineKey");
	pl.set("key" + i, "" + i);
}
List<Object>results=pl.syncAndReturnAll();
System.out.println(results);

1.4 Lua 腳本

Redis 2.6 版本推出了腳本功能,允許開發者使用 Lua 語言編寫腳本傳到 Redis 中去執行。

使用腳本的好處:

  1. 減少網絡開銷:本來多次網絡請求,可以用一個請求就完成,減少了網絡往返時延,類似於管道。
  2. 原子操作:將整個腳本作爲一個整體執行,中間不會被其他命令插入。管道不是原子的,不過 Redis 的批量操作命令(比如 mset)是原子的。
  3. 代替 Redis 的事務功能

可以使用 eval 命令來執行 lua 腳本。

注意,不要在 lua 腳本中出現循環和耗時的運算,否則 Redis 會阻塞。將不會接受其他的命令。所以使用時注意不要出現死循環、耗時的計算。

1.5 Redis 的設計與實現

TODO 2020-12-17 20:19:41

2. Redis 持久化、主從與哨兵架構

2.1 Redis 持久化

2.1.1 RDB 快照(snapshot)

默認情況下,Redis 將內存中的數據快照保存到名爲 dump.db 的二進制文件中,簡稱 rdb 文件。

自動保存:可以對 Redis 進行設置,讓它“在 N 秒內數據集至少有 M 個改動”這一條件被滿足時,自動保存數據集,設置爲 save N M 。

手動保存:進入 Redis 客戶端,執行 save 或者 bgsave 命令可以觸發手動保存數據集,這兩個命令的區別是 bgsave 是用後臺生成 rdb 文件。

2.1.2 AOF(append-only file)

快照功能並不耐久,不能實時的保存數據,Redis 提供了一種實時記錄操作指令的持久化方式:AOF,將修改的每一條指令記錄到 appendonly.aof 文件中,簡稱 aof 文件。

AOF 默認是關閉,建議打開。它有三個配置選項:

  • appendfsync always:每次有新的命令就立即同步到 AOF 文件中。
  • appendfsync everysec:每秒同步一次指令,足夠快,Redis 宕機時只會丟失 1 秒的數據(推薦)。
  • appendfsync no:從不同步,而是交給操作系統來處理。更快,但是不安全。

AOF 重寫:aof 文件中可能有太多沒用的指令,比如執行多次 set 操作,但是恢復數據時只會選擇最近的 set 的值,所以 AOF 會定期根據內存的最新數據生成 aof 文件。

可以通過配置 AOF 自動重寫的頻率:

  • auto-aof-rewrite-min-size 64mb:AOF 文件至少要達到 64M 纔會自動重寫
  • auto-aof-rewrite-percentage 100:AOF 文件自上一次重寫後文件大小增長了 100%則再次觸發重寫

當然還可以通過命令手動的重寫 aof 文件,進入客戶端執行 bgrewriteaof,AOF 重寫時 Redis 會 fork 出一個子進程去執行,並不會對 Redis 的正常命令處理有太多的影響。

2.1.3 RDB 與 AOF 的選擇

RDB 與 AOF 該如何選擇呢?

命令 RDB AOF
啓動優先級
體積
恢復速度
數據安全性 容易丟失數據 根據策略決定
場景 冷備 熱備

Redis 啓動時如果 rdb 文件和 aof 文件,則優先選擇 aof 文件恢復數據,因爲 aof 文件一般

2.1.3 混合持久化

重啓 Redis 時,我們很少使用 RDB 來恢復內存狀態,因爲太容易丟失數據了。通常使用 AOF 日誌重放,但是重放 AOF 日誌性能相對 RDB 來說慢很多,這樣會導致 Redis 啓動需要花費比較長的時間。

Redis 4.0 增加混合持久化來解決這個問題,可以通過配置 aof-use-rdb-preamble yes 來開啓該功能。

開啓混合持久化之後,AOF 在重寫時,不是單純的將內存數據轉換爲 RESP 命令寫入 aof 文件,而是會重寫這一刻的內存做 RDB 快照處理,並且將 RDB 快照內存和增量的 AOF 修改內存數據的命令存在一起,都一起寫入 aof 文件。

這樣 Redis 在重啓的時候,可以先加載 RDB 的內容,然後再重放增量的 AOF 日誌,就可以完全代替之前的 AOF 全量文件重放,大大提高重啓效率。

混合持久化的 aof 文件結構:

混合持久化的 aof 文件結構

2.2 Redis 主從架構

主從架構

Redis 主從工作原理

如果爲一個 master 配置了一個 slave,不管這個 slave 是否是第一次連接上 master,都會發送一個 sync 命令,master 收到指令後,會在後臺進行數據持久化通過 bgsave 命令生辰當前最新的快照 rdb 文件,持久化期間,master 會繼續接受客戶端的指令,同時把這些可能修改數據集的請求緩存到內存中。當持久化完畢後,master 會把這個份 rdb 文件發送給 slave,slave 會把接收到的數據進行持久化生成 rdb,然後加載到內存中。然後 master 再將之前緩存在內存中的命令發送給 slave。

當 master 與 slave 之間的連接由於某些原因斷開時,slave 能夠自動重連到 master,如果 master 收到了多個 slave 的併發連接請求,它只會進行一次持久化。

當 master 與 slave 斷開重連後,只進行部分數據複製(2.8 版本後)。

master 會在其內存中創建一個複製數據用的緩存隊列,緩存最近一段時間的數據,master 和它所有的 slave 都會維護了複製的數據下標 offset 和 master 的進程 id,因此,當網絡連接斷開後,slave 會請求 master 繼續進行未完成的複製,從所記錄的數據下標開始。如果 master 的進程 id 變化了,或者 slave 數據下標的 offset 太舊了,已經不存在 master 的緩存隊列中,那麼就將進行一次全量數據複製。

Redis 從 2.8 版本後,改用支持部分數據複製的命令 PSYNC 去 master 同步數據。

主從複製(全量複製)的流程:

主從複製(全量複製)

主從複製(部分複製)的流程:

主從複製(部分複製)

2.3 Redis 哨兵高可用架構

哨兵高可用架構

哨兵 sentinel 是特殊的 Redis 服務,不提供讀寫服務,主要是用來監控 Redis 實例節點。

哨兵架構下客戶端第一次從哨兵找出 Redis 的主節點,然後直接訪問 Redis 的主節點,不會每次都通過 sentinel 代理訪問 Redis 的主節點,當 Redis 的主節點發送變化時,哨兵第一時間感知到,並且將 Redis 主節點通知給客戶端(這裏 Redis 的客戶端一般都實現了訂閱功能,訂閱哨兵發佈的節點變動消息)。

2.3.1 哨兵 leader 選舉流程

當一個 master 服務器被某個 sentinel 視爲客觀下線狀態後,該 sentinel 會與其他的 sentinel 協商選出 sentinel 的 leader,這個 leader 負責故障轉移工作。

每個發現 master 服務器進入客觀下線的 sentinel 都可以要求其他 sentinel 選舉自己爲 leader,選舉是先到先得。

同時每個 sentinel 每次選舉都會自增配置紀元(選舉週期),每個紀元只會選擇一個 sentinel 的 leader。如果有超過一半的 sentinel 選舉某個 sentinel 作爲 leader,之後該 leader 進行故障轉移操作,從存活的 slave 中選舉出新的 master。

哨兵集羣只有一個哨兵節點,Redis 的主從選舉也能正常的進行,如果 master 掛了,那唯一的哪個哨兵就是哨兵 leader,可以正常選舉 master。

不過爲了高可用一般都推薦部署至少三個哨兵節點。因爲過半選舉節省機器資源。

2.3.2 哨兵架構缺點

Redis 哨兵架構有以下缺點:

  • 主從切換的瞬間會存在訪問瞬斷的情況
  • 哨兵模式只有一個主節點對外提供服務,沒法支持很高的併發
  • 單個節點的內存不宜設置過大,否則會導致持久化文件過大,影響數據的恢復和主從同步效率

3. Redis 集羣架構

Redis 3.0 開始支持集羣模式。集羣模式如下圖所示:

集羣架構

Redis 集羣是一個由多個主從節點羣組成的分佈式服務器羣,它具有複製、高可用、分片特性。

Redis 集羣不需要 sentinel 哨兵也能完成節點的移除和故障轉移的功能,需要將每個節點設置成集羣模式,這種集羣模式沒有中心節點,可以水平擴展。可以線性擴展到上萬個節點(推薦不要超過 1000 個節點)。

Redis 集羣的性能和高可用都優於之前版本哨兵模式,且集羣配置非常簡單。

3.1 集羣原理分析

Redis cluster 將所有的數據劃分爲 16384 個 slot(槽位),每個節點負責其中一部分槽位。槽位的信息存儲在每個節點總。

當 Redis cluster 的客戶端來連接集羣時,它也會得到一個集羣的槽位信息並將其緩存到客戶端本地。這樣當客戶端要查找某個 key 時,可以直接定位目標節點。同時因爲槽位的信息可能會存在客戶端與服務器不一致的情況,還需要糾正機制來實現槽位信息的校驗調整。

3.1.1 槽位定位算法

cluster 默認會對 key 值使用 crc16 算法進行 hash 得到一個整數值,然後用這個整數值對 16384 進行取模來得到具體的槽位。

HASH_SLOT = CRC16(key) % 16384

3.1.2 跳轉重定向

當客戶端向一個錯誤的節點發出了指令,該節點會發現指令的 key 所在的槽位並不歸自己管理,這時它會向客戶端發送一個特殊的跳轉指令攜帶目標操作的節點地址,告訴客戶端去連接這個節點獲取數據。

客戶端端收到指令後跳轉到正確的節點上去操作,還會同步更新糾正本地的操作映射表緩存,後續所有 key 將會使用新的槽位映射表。

3.1.3 Redis 集羣節點間的通信機制

Redis cluster 節點間採用 gossip 協議來進行通信。

維護集羣的元數據有兩種方式:

  • 集中式:
    • 優點:元數據的更新和讀取,時效性很好,一旦元數據出現變更立即就會更新到集中式的存儲中,其他節點讀取的時候立即就可以立即感知到
    • 缺點:將所有的元數據的更新壓力全部集中在一個地方,可能會導致元數據的存儲壓力。
  • gossip:是一個無中心化的通信協議,包括:ping、pong、meet、fail 等消息
    • ping:每個節點都會頻繁的給其他節點發送 ping,其中包含自己的狀態還有自己維護的集羣元數據,互相通過 ping 交換元數據;
    • pong:返回 ping 和 meet,包含自己的狀態和其他信息,也可用於信息廣播和更新;
    • fail:某個節點判斷另一個節點 fail 之後,就發送 fail 給其他節點,通知其他節點,指定的節點宕機了;
    • meet:某個節點發送 meet 給新加入的節點,讓新節點加入到集羣中,然後新節點就會開始與其他節點進行通信,不需要發送形成網絡的所需的所有 cluster meet 命令。發送 cluster meet 消息以便每個節點都能夠達到其他每個節點,只需通過一條已知的節點鏈就夠了。由於在心跳包中會交換 gossip 消息,將會創建節點間缺失的連接。
    • 優點:元數據的更新比較分散,不是集中在一個地方,更新請求會陸陸續續,打到所有節點上去更新,有一定的延遲,降低了壓力。
    • 缺點:元數據更新有有延遲,可能會導致集羣的操作有一些滯後

3.1.4 網絡抖動

網絡抖動會導致集羣節點之間連接變得不可用,然後很快又恢復正常。

Redis cluster 提供了一個選項 cluster-node-timeout,表示某個節點持續 timeout的時間失聯時,纔可以認定該節點出現故障,需要進行主從切換。

如果沒有這個選項,網絡抖動會導致主從頻繁切換(數據的重複複製)。

3.2 集羣選舉原理分析

當 slave發現自己的master 變成 fail 狀態時,便嘗試進行 failover,以期望變成 master。由於掛掉的 master 可能會有多個 slave,從而存在多個 slave 競爭成爲 master 節點的過程。其過程如下:

  1. slave 發現 master 變成 fail
  2. 將自己記錄的集羣 currentEpoch 加一,並廣播 FAILOVER_AUTH_REQUEST 消息
  3. 其他節點收到該消息,只有 master 響應,判斷請求的合法性,併發送 FAILOVER_AUTH_ACK,對每一個 epoch 只發送一次 ack
  4. 嘗試 failover 的 slave收集 master返回的 FAILOVER_AUTH_ACK
  5. slave 收到超過半數 master 的 ack 後變成新 master(這裏解釋爲什麼必須要有 3 個主節點,如果只有兩個,當其中一個掛掉,只剩下一個主節點是不能選舉成功的)
  6. 廣播 pong 消息通知其他集羣節點

從節點並不是已進入 fail 狀態就馬上嘗試發起選舉,而是有一定的延遲,一定的延遲確保我們等待 fail 狀態在集羣中傳播,slave 如果立即嘗試選舉,其他 master或許尚未意識到 fail 狀態,可能會拒絕投票。

延遲計算公式:DELAY = 500ms + random( 0 ~ 500ms) + SLAVE_RANK * 1000ms

SLAVE_RANK 表示此 slave 從 master 複製數據的總量 rank。rank 越小代表已複製的數據越新。這種方式下持有最新數據的 slave 將會首先發起選舉(理論上)。

3.2.1 集羣是否完整才能提供服務

當配置了 cluster-require-full-coverage 爲 no 時,表示當負責一個插槽的主庫下線且沒有相應的從庫進行故障恢復時,集羣仍然可用,如果爲 yes 則集羣不可用。

4. Redis 分佈式鎖

4.1 分佈式鎖的適用場景

  • 互聯網秒殺
  • 搶優惠券
  • 接口冪等性校驗

4.2 Redis 分佈式鎖的實現

  • 通過 setnx 命令設置鎖,如果鎖不存在則設置,否則就返回
  • 鎖有過期時間,防止應用程序掛掉之後無法釋放鎖
  • 鎖必須由創建者才能釋放,並且鎖釋放要求使用 lua 腳本實現,包括查看鎖是否存、鎖是否是自己創建、以及刪除鎖操作
  • 鎖支持自動延時(看門狗),防止程序在鎖過期時間之內沒有完成,而導致鎖自動釋放。

4.3 Redis 實現分佈式鎖的問題

  • 無法保證一致性。當在集羣架構中,當一個應用程序獲取了鎖,而在 Redis 主從節點複製數據時,主節點掛掉,鎖的數據還沒有複製到從節點,此時會導致其他應用程序也獲取到鎖,導致無法保證鎖的一致性。

4.4 如何保障一致性問題

  • 使用應用程序加鎖後,同時使用 wait 命令,等待主節點數據同步到從節點,這樣能確保 slave 成功複製到鎖數據才執行。但是如果 master 在同步數據到 slave 時掛了,wait 命令會返回失敗,需要重新獲取鎖。wait 只會阻塞發送它的客戶端,不影響其它客戶端。
  • 使用 Redlock 紅鎖,它是一個要求一個把鎖要被多個 Redis 實例一起獲取,當超過所有 Redis 實例的一半實例都獲取到鎖了,才認爲鎖獲取成功。使用紅鎖的缺點很多,加鎖、解鎖耗時較長,難以在集羣版本中實現,佔用的資源過大,需要創建多個 Redis 實例。
  • 使用 zookeeper 實現分佈式鎖

4.5 高性能的分佈式鎖如何實現

在高併發情況下多個線程搶同一把鎖等待耗時較長,比如秒殺同一個商品,此時我們就可以把一個商品按照庫存來分解爲多個鎖,比如一個商品的庫存是 1000,那麼我們把庫存分成 10 段,每一段是 100 個庫存,這樣鎖也就可以由一個分解爲 10 個鎖,從而緩解了大量請求同時爭搶一把鎖的等待過長的問題。

5. Redis 緩存設計問題以及性能優化

5.1 緩存穿透

緩存穿透是指查詢一個根本不存在的數據,緩存層和存儲層都不會命中,通常出於容錯的考慮,如果從存儲層查不到數據則不寫入緩存層。

緩存穿透將導致不存在的數據每次請求都要到存儲層去查詢,失去了緩存層保護後端的意義。

造成緩存穿透的基本原因有:

  1. 自身業務代碼或數據出現問題
  2. 一些惡意攻擊、爬蟲等造成大量的空命中

緩存穿透的解決方案:

  1. 緩存空對象:當在存儲層查不到對應的數據時,那麼就給緩存層存儲一個特定值的數據。讓該數據被後端生成之後,重新寫入緩存中。
  2. 布隆過濾器:先用布隆過濾器做一次過濾,對於不存在數據布隆過濾器一般都能過過濾掉。當布隆過濾器判斷某個值存在時,這個值可能不存在,當它判斷某個值不存在時,則它肯定不存在。

5.1.1 布隆過濾器

布隆過濾器

概念

布隆過濾器就是一個大型位數組和幾個不一樣的無偏 hash 函數。所謂無偏就是能夠把元素的 hash 值算的比較均勻。

添加元素

向布隆過濾器中添加 key 時,會使用多個 hash 函數對 key 進行 hash 運算得到一個整數索引,然後對位數組長度進行取模運算得到一個位置,每個 hash 函數都會算得一個不同的位置。再把位數組的這幾個位置都置爲 1 就完成了 add 操作。

查詢是否包含某元素

向布隆過濾器查詢 key 時,和 add 操作一樣,也會把 hash 的幾個位置都算出來,看看位數組中這幾個位置是否都爲 1,只要有一個位爲 0,那麼就說明布隆過濾器中這個 key 不存在。如果都是 1,這並不能說明這個 key 就一定存在,只是極有可能存在,因爲這些位被置爲 1 可能是因爲其他 key 存在所導致的。如果這個位數組比較稀疏,這個概率就很大,如果這個位數組比較擁擠,這個概率就會降低。

現有實現

在使用布隆過濾器時,需要對它進行初始化,即要把已存在的數據灌入到布隆過濾器中。

谷歌的 guava 包實現了布隆過濾器,不過它是基於 JVM 進程的,要想實現分佈式布隆過濾器,需要基於 Redis 的 bitmap 來實現。

Redisson 框架已經實現了這些分佈式對象,其中就包括分佈式布隆過濾器。

適用場景

這種方式適用於數據命中不高、數據相對固定、實時性低(通常是大數據集)的應用場景,代碼維護比較複雜,但是緩存空間佔用較少。

5.2 緩存失效

當大批量緩存在同一時間失效可能會導致大量請求同時穿透緩存,直達數據庫,可能會造成數據庫瞬間壓力過大甚至掛掉,對於這種情況我們在批量增加緩存時最好將這些緩存過期時間設置爲一個時間段內不同的時間。

5.3 緩存雪崩

由於緩存層承受着大量請求,有效的保護存儲層,但是如果緩存層由於某些原因不能提供服務(比如超大併發過來,緩存層支撐不住,或者緩存層設計不好,類似大量請求訪問 bigkey,導致緩存支撐的併發急劇下降),於是大量的請求都會到達存儲層,存儲層的調用量會暴增,造成存儲層也級聯宕機的情況。

解決方案:

  1. 保證緩存層服務高可用,比如使用 Redis 集羣架構
  2. 使用服務端使用依賴隔離組件爲後端限流並降級,比如使用 hystrix 組件
  3. 服務端使用 JVM 緩存,即使 Redis 服務掛了也能抗住一部分流量,阻止流量打到存儲層
  4. 提前演練,演練緩存層宕機之後,應用以及後端的負載情況以及可能出現的問題,對這些問題做預案

5.4 熱點緩存 key 重建優化

一般我們都是用“緩存 + 過期時間”的策略既可以加速數據讀寫,有保證數據的定期更新這種模式基本能滿足絕大部分需求。但是兩個問題如果同時出現,可能就會對應用造成致命的危害:

  1. 當 key 是一個熱點 key(例如一個人們的娛樂新聞),併發量非常大
  2. 重建緩存不能在短時間內完成,可能是一個複雜計算,例如複雜的 SQL、多次 I/O。多個依賴等

在緩存失效的瞬間,會大量的請求進來完成重建,造成後端負載增大,甚至可能會讓應用崩潰。

解決這個問題就是要避免大量的請求同時重建緩存,我們可以利用互斥鎖(也就是分佈式鎖)來解決,就是說同一時刻只允許一個請求重建緩存,其他請求等待重建緩存的請求執行完,重新從緩存獲取即可(或者是立即返回服務器繁忙等信息)。

5.5 數據庫雙寫一致性解決方案

當要更新存儲層的數據時,有兩種方式更新緩存,分別是:先更新數據庫,再更新或者刪除緩存;先更新或者刪除緩存,再更新數據庫。

對於上面說的更新或者刪除緩存,我們一般按照按需保存緩存的原則,在更新數據時使用刪除緩存,在重新查詢緩存時再從數據庫查數據緩存起來。

我們對比下兩種方案:

  1. 先更新數據庫,再刪除緩存
    1. 優點:在更新數據庫期間,不會影響到緩存數據,緩存還可以提供數據查詢
    2. 缺點:如果在更新了數據庫操作後,更新緩存操作之前的這個時間段,服務發生了宕機,導致緩存沒有被更新,這樣就數據庫與緩存中的數據不一致
  2. 先刪除緩存,再更新數據庫
    1. 優點:先刪除了緩存,緩存數據立刻失效,即使在更新數據庫時宕機,下次查詢也是會從數據庫查詢數據緩存起來,數據一致
    2. 缺點:可能存在一個線程刪除了緩存,準備更新數據庫;另一個線程進來查詢緩存,發現緩存爲空又去數據庫裏查詢並緩存數據;然後第一個線程開始更新數據庫。這樣也導致了數據不一致

針對先刪除緩存,再更新數據庫方案的數據雙寫一致性問題,解決的思路就是使用互斥鎖,在刪除緩存和更新數據庫的操作那裏加一分佈式鎖,兩個操作執行完畢之後再釋放鎖,,同時再構建緩存的邏輯處也使用相同的分佈式鎖,獲得鎖之後纔可以從數據庫查詢數據並緩存起來,這樣也保證了。

還有一種解決思路,就是將更新數據操作和查詢並構建緩存的操作進行排隊處理,在 JVM 中構建內存隊列,即誰先來的就先處理誰。比如更新緩存操作先來的,那麼就先處理更新緩存的操作,接着在處理構建緩存的操作。那麼如果該服務部署了多個實例的話,那就就需要將藉助分佈式消息隊列,將消息按照一定的規則發送到不同的服務實例,每個服務實例內部會根據消息創建不同的內存隊列以及對應的消費線程進行消費。

TODO 2020-12-18 13:57:20(分佈式消息隊列如何保證消息在消費端的消費順序)

5.6 Redis 開發規範與性能優化

5.6.1 鍵值設計

5.6.1.1 key 設計

  1. 可讀性和可管理性:以業務名(或數據庫名)爲前綴,用冒號分割,比如業務名:表名:id
  2. 簡潔性:保證語義的前提下,控制 key 的長度
  3. 不要包含特殊符號:空格、換行、單雙引號以及其他轉義字符

5.6.1.2 value 設計

  1. 拒絕 bigkey(防止網卡流量、慢查詢):Redis 中,一個字符串最大 512MB,一個二級數據結構(例如 hash、list、set、zset)可以存儲大約 40億個(2^32-1)元素,但實際中如果存在下面的兩種情況,就認爲它是 bigkey:

  2. 字符串類型:它的 big 體現在單個 value 值很大,一般認爲超過 10KB 就是 bigkey

  3. 非字符串類型:hash、list、set、zset,它們 big 體現在元素個數太多,元素個數不要超過 5000 個

  4. 非字符串的 bigkey,不要使用 del 刪除,使用 hscan、sscan、zscan 方式漸進刪除,同時要注意防止 bigkey 過期時間自動刪除問題(例如一個 200 萬的 zset 設計 1 小時過期,會觸發 del 操作,造成阻塞)
    
    bigkey 的危害:
    	1. 導致 Redis 阻塞
    	2. 網絡擁塞:假設一個 bigkey 1MB,客戶端每秒訪問量爲 1000,即每秒產生 1000MB 的流量,對於普通的千兆網卡(按照字節算是 128MB/s)的服務來說是滅頂之災
    	3. 過期刪除:一個 bigkey 設置了過期時間,過期後會被刪除,如果沒有使用 Redis4.0 的過期異步刪除(lazyfree-lazy-expire yes),就會產生阻塞 Redis 的可能性
    
    bigkey 的產生
    bigkey 的產生都是由於程序設計不當,或者對於數據規模預料不清楚造成的。
    	1. 社交類:粉絲列表,如果某些明星或大 V 不精心設計,很定時 bigkey
    	2. 統計類:例如按天存儲某項功能或者網站的用戶集合,除非沒幾個人用,否則筆試 bigkey
    	3. 緩存類:將數據從數據庫 load 出來序列化到 Redis 裏,要注意兩點,一是不是有必要把所有字段都緩存;二有沒有相關關聯數據
    
    如何優化 bigkey
    	1. 拆
    		1. big list: list1、list2、list3 ... listn
    		2. big hash:將數據分段存儲,比如一個存了 100 萬用戶的數據,可以拆分成 200 個 key,每個 key 下面存放 5000 個用戶數據
    	2. 如果 bigkey 不可避免,也要思考下要不要每次把所有的元素都取出來(例如有時候僅僅需要 hmget,而不是 hgetall),刪除也一樣,用優雅的方式來處理
    
  5. 選擇合適的數據類型:例如實體類型,用 hmset user:1 name tom age 20 代替 set user:1:name tom、set user:1:age 20

  6. 控制 key 的生命週期,建議使用 expire 設置過期時間(允許可以打算過期時間,防止集中過期)

5.6.2 命令使用

  1. O(N) 命令關注 N 的數量:在使用 hgetall、lrange、smembers、zrange、sinter 等命令時需要明確N的值。用hscan、sscan、zscan 進行遍歷
  2. 禁用命令:keys、flushall、flushdb
  3. 合理使用 select:不用業務使用不同的 Redis 實例
  4. 使用批量操作提高效率
    1. 原生命令:mget、mset
    2. 非原生命令:pipeline
  5. 使用 lua 代替 Redis 事務

5.6.3 客戶端使用

  1. 避免多個應用使用一個 Redis 實例:不相干的業務拆分,公共數據做服務化

  2. 使用帶有連接池的數據庫

    1. 連接池的優化建議:
      1. maxTotal:最大連接數,需要考慮因素有:希望 Redis 的併發量、客戶端執行命令時間、Redis 資源、資源開銷
         1. 比如一次命令執行時間平均耗時爲 1ms,一個連接的 qps 大約是 1000,
         2. 業務期望的 qps 爲 50_000
         3. 那麼理論上需要的資源池大小 maxTotal 爲 50_000 / 1000 = 50。實際上考慮到要預留一些資源,maxTotal 可以比理論值大一些
      2. maxIdle 和 minIdle:最大最小閒置連接數
      
  3. 高併發下建議客戶端添加熔斷功能(hystrix)

  4. 設置合理的密碼

  5. Redis 對於過期鍵的三種清除策略

    1. 被動刪除:當讀/寫一個已經過期的 key 時,會觸發惰性刪除,直接刪除這個過期的 key

    2. 主動刪除:由於惰性刪除策略無法保證冷數據被及時刪除,所以 Redis 會定期主動淘汰一批已經過期的 key

    3. 當已用內存超過 maxmemory 限定時,觸發主動刪除策略

      1. 第三種策略的情況如下:
        
        當前已用內存超過了 maxmemory 限定時,會觸發主動清除策略
        選號 maxmemory-policy(最大內存淘汰策略)設置好過期時間。如果不設置最大內存,當 Redis 內存超出物理內存限制時,內存的數據會開始和磁盤產生頻繁的交換(swap),讓 Redis 的性能急劇下降。
        
        默認策略是 volatile-lru,即超過最大內存後,在過期鍵中使用 lru 算法進行 key 的刪除,保證不過期數據不被刪除,但是可能出現 OOM 問題。
        
        其他策略:
        - allkeys-lru:根據 lru 算法,不管數據有沒有被超時時間,直到騰出足夠空間爲止
        - allkeys-random:隨即刪除所有鍵
        - volatile-random:隨機刪除過期鍵
        - volatile-ttl:根據鍵值對象的 ttl 屬性,刪除最近將要過期的數據,如果沒有,則回退到 noeviction 策略
        - noevication:不會刪除任何數據,拒絕所有寫入操作並返回客戶端錯誤信息
        
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章