日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

备战面试日记(6.1) - (缓存相关.Redis全知识点)

發(fā)布時間:2024/3/24 数据库 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 备战面试日记(6.1) - (缓存相关.Redis全知识点) 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

本人本科畢業(yè),21屆畢業(yè)生,一年工作經(jīng)驗(yàn),簡歷專業(yè)技能如下,現(xiàn)根據(jù)簡歷,并根據(jù)所學(xué)知識復(fù)習(xí)準(zhǔn)備面試。

記錄日期:2022.1.15

大部分知識點(diǎn)只做大致介紹,具體內(nèi)容根據(jù)推薦博文鏈接進(jìn)行詳細(xì)復(fù)習(xí)。

文章目錄

  • Redis
    • 數(shù)據(jù)結(jié)構(gòu)與對象
      • 數(shù)據(jù)類型分類(對象)
        • 數(shù)據(jù)類型概述
        • 編碼和底層實(shí)現(xiàn)
      • 數(shù)據(jù)結(jié)構(gòu)
        • SDS字符串
          • SDS定義
          • SDS 與 C字符串的區(qū)別
            • 獲取字符串長度
            • 緩沖區(qū)溢出
            • 內(nèi)存重分配次數(shù)
            • 二進(jìn)制安全
            • 兼容< string.h >庫的函數(shù)
        • 鏈表
          • 鏈表定義
          • 特性總結(jié)
        • 字典
          • 字典定義
          • 哈希沖突
            • 哈希算法
            • 解決哈希沖突
          • rehash
            • rehash概述
            • rehash條件
            • 漸進(jìn)式hash過程
            • 漸進(jìn)式hash執(zhí)行期間進(jìn)行哈希表操作
            • 漸進(jìn)式hash的缺點(diǎn)
        • 跳躍表
          • 為什么redis選擇了跳躍表而不是紅黑樹?
        • 整數(shù)集合
          • 整數(shù)集合定義
          • 整數(shù)集合升級
            • 升級的好處
          • 整數(shù)集合降級
        • 壓縮列表
          • 壓縮列表定義
          • 列表節(jié)點(diǎn)構(gòu)成
            • previous_entry_length
            • encoding
            • content
          • 連鎖更新
      • 編碼轉(zhuǎn)換時機(jī)
        • 字符串
          • int
          • raw
          • embstr
          • 為什么raw和embstr的臨界值是44字節(jié)?
        • 列表
          • ziplist
          • linkedlist
        • 哈希
          • ziplist
          • hashtable
        • 集合
          • intset
          • hashtable
        • 有序集合
          • ziplist
          • skiplist
    • 持久化
      • RDB
        • 觸發(fā)方式
          • 手動觸發(fā)
          • 自動觸發(fā)
        • RDB優(yōu)缺點(diǎn)
          • 優(yōu)點(diǎn)
          • 缺點(diǎn)
      • AOF
        • 執(zhí)行流程
        • 觸發(fā)方式
          • 手動觸發(fā)
          • 自動觸發(fā)
        • AOF文件同步策略
        • AOF持久化配置
    • 文件事件處理器
      • 組成部分
      • 處理機(jī)制
      • 拓展
    • 內(nèi)存淘汰機(jī)制
      • 內(nèi)存淘汰策略
        • 策略介紹
          • noeviction
          • volatile-ttl、volatile-random、volatile-lru、volatile-lfu
          • allkeys-random、allkeys-lru、allkeys-lfu
        • LRU & LFU算法
          • LRU
            • LRU 篩選邏輯
            • Redis 對 LRU 的實(shí)現(xiàn)
          • LFU
            • LFU 篩選邏輯
            • LFU 的具體實(shí)現(xiàn)
            • Redis 對 LFU 的實(shí)現(xiàn)
            • LFU 中的 counter 值的衰減機(jī)制
        • 使用總結(jié)
    • 事務(wù)
      • 概念
      • 事務(wù)階段
      • 事務(wù)錯誤處理
      • Watch 監(jiān)控
        • 引入
        • watch 命令
        • unwatch 命令
      • 總結(jié)說明
    • Redis集群
      • 主從復(fù)制
        • 主從復(fù)制架構(gòu)
        • 開啟主從復(fù)制方式
          • 命令
          • 配置
          • 啟動命令
        • 復(fù)制的實(shí)現(xiàn)【重點(diǎn)】
          • 1. 設(shè)置主服務(wù)器的地址和端口
          • 2. 建立套接字連接
          • 3. 發(fā)送 PING 命令
          • 4. 身份驗(yàn)證
          • 5. 發(fā)送端口信息
          • 6. 同步
          • 7. 命令傳播
        • 主從復(fù)制優(yōu)缺點(diǎn)
          • 優(yōu)點(diǎn)
          • 缺點(diǎn)
        • 總結(jié)
      • 哨兵模式
        • 哨兵模式架構(gòu)
        • 哨兵進(jìn)程
          • 哨兵進(jìn)程的作用
        • 哨兵(Sentinel) 和 一般Redis 的區(qū)別?
        • 哨兵的工作方式
          • 創(chuàng)建連接
          • 獲取主服務(wù)器信息
          • 獲取從服務(wù)器信息
          • 向主服務(wù)器和從服務(wù)器發(fā)送信息
          • 接收來自主服務(wù)器和從服務(wù)器的頻道信息
        • 故障檢測
          • 檢測主觀下線
          • 檢測客觀下線
          • 選舉領(lǐng)頭 Sentinel
          • 故障遷移
            • 選出新的主服務(wù)器
            • 修改從服務(wù)器的復(fù)制目標(biāo)
            • 將舊主服務(wù)器變?yōu)閺姆?wù)器
      • 集群模式
        • 集群模式架構(gòu)
        • 集群數(shù)據(jù)結(jié)構(gòu)
        • 集群連接方式
        • 分布式尋址算法【引入】
          • hash 算法
          • 一致性 hash 算法
            • hash 環(huán)數(shù)據(jù)傾斜 & 虛擬節(jié)點(diǎn)
          • hash slot 算法
          • 一致性 hash 算法 和 hash slot 算法的區(qū)別?
            • 定位規(guī)則區(qū)別
            • 應(yīng)對熱點(diǎn)緩存區(qū)別
            • 擴(kuò)容和縮容區(qū)別
        • 集群的槽指派
          • 指派節(jié)點(diǎn)槽信息
            • CLUSTER ADDSLOTS 的命令實(shí)現(xiàn)
          • 傳播節(jié)點(diǎn)槽信息
          • 記錄集群所有槽的指派信息
            • 使用 `clusterState.slots` 和使用 `clusterNode.slots` 保存指派信息相比的好處?
        • 集群執(zhí)行命令
          • MOVED 錯誤
        • 節(jié)點(diǎn)數(shù)據(jù)庫的實(shí)現(xiàn)
        • 重新分片(比如在線擴(kuò)容)
          • ASK 錯誤 - (保證集群在線擴(kuò)容的安全性)
          • CLUSTER SETSLOT IMPORTING 命令的實(shí)現(xiàn)
          • CLUSTER SETSLOT MIGRATING 命令的實(shí)現(xiàn)
          • ASKING 命令
        • 復(fù)制和故障轉(zhuǎn)移
          • 設(shè)置從節(jié)點(diǎn)方式
          • 故障檢測
          • 故障轉(zhuǎn)移
            • 選舉新的主節(jié)點(diǎn)過程
    • Redis應(yīng)用
      • Redis 分布式鎖
        • 引入
          • 為什么需要分布式鎖?
          • 什么時候用分布式鎖?
          • 分布式鎖需要哪些特性呢?
        • 加鎖
        • 解鎖
        • 續(xù)鎖
          • 守護(hù)線程“續(xù)命”存在的問題
          • RedLock
            • RedLock 算法
            • 失敗重試
            • RedLock 的問題

Redis

書籍推薦:《Redis的設(shè)計(jì)與實(shí)現(xiàn)》

博客面試文章推薦:全網(wǎng)最硬核 Redis 高頻面試題解析(2021年最新版)

Redis這篇主要要講解的內(nèi)容包括:數(shù)據(jù)結(jié)構(gòu)、redis持久化(aof、rdb)、文件事務(wù)處理器、redis內(nèi)存淘汰機(jī)制、事務(wù)、redis集群(一致性hash等...)、redis分布式鎖都放在Redis的文章里說明。

還有一部分緩存問題,比如緩存設(shè)計(jì)以及緩存數(shù)據(jù)一致性、解決方案-緩存雪崩緩存穿透緩存擊穿等另起一篇寫。

數(shù)據(jù)結(jié)構(gòu)與對象

數(shù)據(jù)類型分類(對象)

數(shù)據(jù)類型概述

Redis主要有5種數(shù)據(jù)類型,包括String,List,Set,Zset,Hash,滿足大部分的使用要求。

數(shù)據(jù)類型可以存儲的值操作應(yīng)用場景
STRING字符串、整數(shù)或者浮點(diǎn)數(shù)對整個字符串或者字符串的其中一部分執(zhí)行操作;對整數(shù)和浮點(diǎn)數(shù)執(zhí)行自增或者自減操作。做簡單的鍵值對緩存
LIST列表從兩端壓入或者彈出元素;對單個或者多個元素進(jìn)行修剪;只保留一個范圍內(nèi)的元素存儲一些列表型的數(shù)據(jù)結(jié)構(gòu),類似粉絲列表、文章的評論列表之類的數(shù)據(jù)
SET無序集合添加、獲取、移除單個元素;檢查一個元素是否存在于集合中;計(jì)算交集、并集、差集;從集合里面隨機(jī)獲取元素交集、并集、差集的操作,比如交集,可以把兩個人的粉絲列表整一個交集
HASH包含鍵值對的無序散列表添加、獲取、移除單個鍵值對;獲取所有鍵值對;檢查某個鍵是否存在結(jié)構(gòu)化的數(shù)據(jù),比如一個對象
ZSET有序集合添加、獲取、刪除元素;根據(jù)分值范圍或者成員來獲取元素;計(jì)算一個鍵的排名去重但可以排序,如獲取排名前幾名的用戶

另外還有高級的4種數(shù)據(jù)類型:

  • HyperLogLog:通常用于基數(shù)統(tǒng)計(jì)。使用少量固定大小的內(nèi)存,來統(tǒng)計(jì)集合中唯一元素的數(shù)量。統(tǒng)計(jì)結(jié)果不是精確值,而是一個帶有0.81%標(biāo)準(zhǔn)差(standard error)的近似值。所以,HyperLogLog適用于一些對于統(tǒng)計(jì)結(jié)果精確度要求不是特別高的場景,例如網(wǎng)站的UV統(tǒng)計(jì)。
  • Geo:redis 3.2 版本的新特性。可以將用戶給定的地理位置信息儲存起來, 并對這些信息進(jìn)行操作:獲取2個位置的距離、根據(jù)給定地理位置坐標(biāo)獲取指定范圍內(nèi)的地理位置集合。
  • Bitmap:位圖。
  • Stream:主要用于消息隊(duì)列,類似于 kafka,可以認(rèn)為是 pub/sub 的改進(jìn)版。提供了消息的持久化和主備復(fù)制功能,可以讓任何客戶端訪問任何時刻的數(shù)據(jù),并且能記住每一個客戶端的訪問位置,還能保證消息不丟失。

編碼和底層實(shí)現(xiàn)

主要是講述上述五種基本類型的底層編碼實(shí)現(xiàn):

類型編碼對象
REDIS_STRINGREDIS_ENCODING_INT使用整數(shù)值來實(shí)現(xiàn)的字符串對象
REDIS_STRINGREDIS_ENCODING_EMBSTR使用embstr編碼的簡單動態(tài)字符串實(shí)現(xiàn)的字符串對象
REDIS_STRINGREDIS_ENCODING_RAW使用簡單動態(tài)字符串實(shí)現(xiàn)的字符串對象
REDIS_LISTREDIS_ENCODING_ZIPLIST使用壓縮列表實(shí)現(xiàn)的列表對象
REDIS_LISTREDIS_ENCODING_LINKEDLIST使用雙端鏈表實(shí)現(xiàn)的列表對象
REDIS_HASHREDIS_ENCODING_ZIPLIST使用壓縮列表實(shí)現(xiàn)的哈希對象
REDIS_HASHREDIS_ENCODING_HT使用字典實(shí)現(xiàn)的哈希對象
REDIS_SETREDIS_ENCODING_INTSET使用整數(shù)集合實(shí)現(xiàn)的集合對象
REDIS_SETREDIS_ENCODING_HT使用字典實(shí)現(xiàn)的集合對象
REDIS_ZSETREDIS_ENCODING_ZIPLIST使用壓縮列表實(shí)現(xiàn)的有序集合對象
REDIS_ZSETREDIS_ENCODING_SKIPLIST使用跳躍表字典實(shí)現(xiàn)的有序集合對象

參考《Redis設(shè)計(jì)與實(shí)現(xiàn)》第一部分 數(shù)據(jù)結(jié)構(gòu)與對象 的 第八章 對象,p63。

通過上面的整理我們就可以知道他們的具體編碼實(shí)現(xiàn)了,整理如下:

  • String:SDS
  • list:壓縮列表、雙向鏈表。
  • hash:壓縮列表、字典。
  • set:整數(shù)集合、字典。
  • zset:壓縮列表、跳表。

在Redis中我們可以通過 OBJECT ENCODING命令來查看一個數(shù)據(jù)庫鍵的值對象的編碼:

redis> SET msg "hello world" OK redis> OBJECT ENCODING msg "embstr"

關(guān)于他們具體在什么時候使用什么編碼格式,我們在下文詳細(xì)說明!

數(shù)據(jù)結(jié)構(gòu)

主要說明七種對象:簡單動態(tài)字符串、鏈表、字典、跳躍表、整數(shù)集合、壓縮列表。

SDS字符串

簡單動態(tài)字符串(SDS),用作Redis的默認(rèn)字符串表示。

SDS定義

每個 sds.h/sdshdr 結(jié)構(gòu)標(biāo)識一個SDS值:

struct sdshdr {int len; // 記錄buf數(shù)組中已使用的字節(jié)數(shù)量,等于SDS所保存字符串的長度int free; // 記錄buf數(shù)組中未使用的字節(jié)數(shù)量char buf[]; // 字節(jié)數(shù)組,用于保存字符串 }

tip:buf數(shù)組最后一個字節(jié)會用來保存’/0’,這也是遵循C字符串以空字符結(jié)尾的慣例,但是這個字符不會被計(jì)算在len長度中。

遵循的好處就是它可以直接重用一部分C字符串函數(shù)庫里面的函數(shù)。

SDS 與 C字符串的區(qū)別

如果一張表來說明,即:

C字符串SDS
獲取字符串長度的復(fù)雜度為O(N)獲取字符串長度的復(fù)雜度為O(1)
API是不安全的,可能會造成緩沖區(qū)溢出API是安全的,不會造成緩沖區(qū)溢出
修改字符串長度N次必然需要執(zhí)行N次內(nèi)存重分配修改字符串長度N次最多需要執(zhí)行N次內(nèi)存重分配
只能保存文本數(shù)據(jù)可以保存文本數(shù)據(jù)或者二進(jìn)制數(shù)據(jù)
可以使用所有的<string.h>庫中的函數(shù)可以使用一部分<string.h>庫中的函數(shù)

那我們根據(jù)這五點(diǎn)來說明,這五大區(qū)別的產(chǎn)生原因:

獲取字符串長度

原因如下:

  • C字符串必須遍歷字符串直到碰到結(jié)尾的空字符為止復(fù)雜度為O(N)
  • SDS字符串在len屬性中記錄了SDS本身的長度復(fù)雜度為O(1)

其中SDS長度的設(shè)置與更新是由SDS的API執(zhí)行時自動完成的。

緩沖區(qū)溢出

因?yàn)镃字符串沒有記錄字符串長度,所以如果使用如下方法:

char *strcat(char *dest, const char *src);

當(dāng)開發(fā)者已經(jīng)為 dest 字符串分配了一定的內(nèi)存,此時如果 src 字符串中內(nèi)容拼接進(jìn)去后的內(nèi)存大于分配的內(nèi)存,則會造成緩沖區(qū)溢出。

那么SDS字符串是如何解決的呢?

當(dāng) SDS API 需要對 SDS 進(jìn)行修改時,API 會先檢查 SDS 的空間是否滿足所需的要求,如果不滿足的話,API 會自動將 SDS 的空間擴(kuò)展至執(zhí)行修改所需的大小,然后才執(zhí)行實(shí)際的修改操作,所以使用 SDS 既不需要后動修改 SDS 的空間大小,也不會出現(xiàn)C字符串中的緩沖區(qū)溢出問題。

內(nèi)存重分配次數(shù)

因?yàn)镃字符串的底層實(shí)現(xiàn)總是 N + 1 個字符串長度的數(shù)組。所以每次執(zhí)行 增長字符串 或是 縮短字符串時,都要先通過重分配擴(kuò)展底層數(shù)組的空間大小 或是 釋放字符串不再使用的空間,來防止緩沖區(qū)溢出 或者 內(nèi)存泄漏。

那么SDS字符串是如何解決的呢?

SDS中使用free屬性記錄未使用空間的字節(jié)數(shù)量。

通過未使用的空間,SDS 實(shí)現(xiàn)了 空間預(yù)分配惰性空間釋放 兩種優(yōu)化策略。

空間預(yù)分配的操作是:當(dāng) SDS 的 API 對一個 SDS 進(jìn)行修改,并且需要對 SDS 進(jìn)行空間擴(kuò)展的時候,程序不僅會為 SDS 分配修改所必須要的空間,還會為 SDS 分配額外的未使用空間。

這里存在兩種修改情況:

  • 對SDS修改后,SDS長度(即len值)< 1MB:這是 len值 會和 free值 相同。此時 buf數(shù)組 實(shí)際長度是 len + free + 1。
  • 對SDS修改后,SDS長度(即len值)> 1MB:會多分配 1MB 未使用空間,比如 len值 為30MB時,此時 buf數(shù)組 實(shí)際長度是 30MB + 1MB + 1byte。
  • 惰性空間釋放的操作是:當(dāng) SDS 的 API 對 一個 SDS 進(jìn)行修改,并且需要對 SDS 所保存的字符串進(jìn)行縮短時,程序并不立即使用內(nèi)存重分配來回收縮短后多出來的字節(jié),而是使用 free屬性 將這些字節(jié)的數(shù)量記錄起來,并等待將來使用。

    當(dāng)然,如果需要真正地釋放 SDS 的未使用空間,會有 API 去實(shí)現(xiàn),這里不說明。

    二進(jìn)制安全

    C字符串的字符必須符合某種編碼(比如ASCII),并且除了末尾空字符外,不能包含任何空字符,否則會被程序誤認(rèn)為是末尾,這使得C字符串只能保存文本數(shù)據(jù),而不能保存二進(jìn)制數(shù)據(jù)。

    那么SDS字符串是如何解決的呢?

    SDS 的 API 都是二進(jìn)制安全的,所有的 SDS API 都會以處理二進(jìn)制的方式來處理 SDS 存放的 buf數(shù)組 里的數(shù)據(jù)。

    所以SDS 的 buf屬性被稱為字節(jié)數(shù)組,就是因?yàn)樗怯脕肀4嬉幌盗卸M(jìn)制數(shù)據(jù)。

    兼容< string.h >庫的函數(shù)

    上面說過了,SDS 也遵循C字符串以空字符結(jié)尾的慣例,就是為了能讓它使用部分<string.h>庫的函數(shù)。

    鏈表

    鏈表定義

    每個鏈表節(jié)點(diǎn)使用一個 adlist.h/listNode 結(jié)構(gòu)來表示:

    typedef struct listNode {struct listNode *prev; // 前置指針struct listNode *next; // 后置指針void *value; // 節(jié)點(diǎn)的值 }

    說明該鏈表是一個雙向鏈表。

    當(dāng)我們使用多個 listNode 組成鏈表,就會直接使用 adlist.h/list 來持有該鏈表進(jìn)行操作:

    typedef struct list {listNode *head; // 表頭節(jié)點(diǎn)listNode *tail; // 表尾節(jié)點(diǎn)unsigned long len; // 鏈表所包含的節(jié)點(diǎn)數(shù)量void *(*dup) (void *ptr); // 節(jié)點(diǎn)值復(fù)制函數(shù)void *(*free) (void *ptr); // 節(jié)點(diǎn)值釋放函數(shù)int (*match) (void *ptr, void *key); // 節(jié)點(diǎn)值對比函數(shù) }
    特性總結(jié)
    • 雙端:節(jié)點(diǎn)有 prev 和 next 指針,復(fù)雜度為O(1)。
    • 無環(huán):對鏈表的訪問都是以NULL為終點(diǎn)。
    • 帶頭尾指針:list 中有head 和 tail 指針,復(fù)雜度為O(1)。
    • 帶鏈表長度計(jì)數(shù)屬性:len屬性保存節(jié)點(diǎn)數(shù),復(fù)雜度為O(1)。
    • 多態(tài):使用 void*指針保存節(jié)點(diǎn)值,可以保存不同類型的值。

    字典

    即數(shù)組 + 鏈表實(shí)現(xiàn)。

    字典定義

    Redis 字典所使用的哈希表由 dict.h/dictht 結(jié)構(gòu)定義:

    typedef struct dictht {dictEntry **table; // 哈希表數(shù)組unsigned long size; // 哈希表大小unsigned long sizemask; // 哈希表大小掩碼,用于計(jì)算索引值,總是等于 size - 1unsigned long used; // 哈希表已有節(jié)點(diǎn)數(shù)量 }

    哈希表節(jié)點(diǎn)使用 dictEntry 結(jié)構(gòu)表示,每個 dictEntry 結(jié)構(gòu)都保存著一個kv對:

    typedef struct dictEntry {void *key; // 鍵union { // 值void *val;uint64_t u64;uint64_t s64;}struct dictEntry *next; // 指向下個哈希表節(jié)點(diǎn),形成鏈表 }

    Redis 中的字典由 dict.h/dict 結(jié)構(gòu)表示:

    typedef struct dict {dictType *type; // 類型特定函數(shù)void *privdata; // 私有數(shù)據(jù)dictht ht[2]; // 哈希表int trehashidx; // rehash索引,當(dāng)rehash不在進(jìn)行時,值為1 }
    哈希沖突
    哈希算法

    在添加新的鍵值到字典里是,要先進(jìn)行對key的哈希,根據(jù)哈希值計(jì)算出索引值,根據(jù)索引將新的kv對放到哈希表數(shù)組的指定索引上。

    index = hash&dict -> ht[0].sizemask

    Redis 使用 MurmurHash 算法。

    解決哈希沖突

    Redis 的哈希表使用鏈地址法解決哈希沖突,并且使用的是頭插法

    rehash

    hash 對象在擴(kuò)容時使用了一種叫 “漸進(jìn)式 rehash” 的方式。

    rehash概述

    擴(kuò)展收縮哈希表的工作都是通過執(zhí)行 rehash 來完成的。

    reash的步驟如下:

  • 計(jì)算新表(ht[1])的空間大小,取決于舊表(ht[0])當(dāng)前包含的鍵值以及數(shù)量。

  • 如果是擴(kuò)展操作,那么新表(ht[1])的大小為第一個大于等于 ht[0].used * 2 的 2^N。
  • 如果是收縮操作,那么新表(ht[1])的大小為第一個大于等于ht[0].used 的 2^N。
  • 將保存在舊表(ht[0])的所有鍵值rehash到新表(ht[1])上。

  • 當(dāng)舊表(ht[0])全部遷移完成后,釋放舊表(ht[0]),將新表設(shè)置為 ht[0] 并在 ht[1]重新創(chuàng)建一張空白哈希表。

  • 這兩個哈希表的套路是不是有點(diǎn)像jvm運(yùn)行時數(shù)據(jù)區(qū)的年輕代的幸存者區(qū)?可以引申一下。

    rehash條件

    當(dāng)下面兩個條件任意一個被滿足時,程序就會自動開始對哈希表進(jìn)行擴(kuò)展操作:

  • 當(dāng)前服務(wù)器沒有在執(zhí)行 BGSAVE 命令或 BGREWRITEAOF 指令,并且哈希表的負(fù)載因子大于等于1。
  • 當(dāng)前服務(wù)器正在執(zhí)行 BGSAVE 命令或 BGREWRITEAOF 指令,并且哈希表的負(fù)載因子大于等于5。【5是因?yàn)橐驯4婀?jié)點(diǎn)數(shù)量包括沖突節(jié)點(diǎn)】
  • 為什么這兩個命令的是否正在執(zhí)行,和服務(wù)器執(zhí)行擴(kuò)展操作的負(fù)載因子并不相同?

    答:是因?yàn)樵趫?zhí)行BGSAVE命令或者BGREWRITEAOF命令的過程中,Redis需要fork子線程,而大多數(shù)os都采用與時復(fù)制技術(shù)來優(yōu)化子進(jìn)程的使用效率,所以子進(jìn)程存在的期間,服務(wù)器會提高執(zhí)行擴(kuò)展操作所需的負(fù)載因子,從而盡可能地避免在子進(jìn)程存在期間進(jìn)行哈希擴(kuò)容,可以避免不必要的內(nèi)存寫入操作,節(jié)約內(nèi)存。

    與時復(fù)制:copy-on-write,即不用復(fù)制寫入直接引用父進(jìn)程的物理過程。

    BGSAVE命令:fork子進(jìn)程去完成備份持久化。(區(qū)別于SAVE命令,阻塞線程去完成備份持久化)

    BGREWRITEAOF命令:異步執(zhí)行AOF重寫,優(yōu)化原文件大小(該命令執(zhí)行失敗不會丟失數(shù)據(jù),成功才會真正修改數(shù)據(jù),2.4以后手動觸發(fā)該命令)

    漸進(jìn)式hash過程

    漸進(jìn)式rehash的詳細(xì)步驟:

  • 為ht[1]分配空間,讓字典同時持有ht[0]和ht[1]兩個哈希表。
  • 在字典中維持一個索引計(jì)數(shù)器變量rehashidx,并將它的值設(shè)為0,表示rehash工作正式開始。
  • 在rehash進(jìn)行過程中,每次對字典進(jìn)行添加、刪除、查找、更新操作時,除了執(zhí)行指定操作以外,還會順帶將ht[0]在rehashidx索引上的所有鍵值對rehash到ht[1]上,當(dāng)rehash工作完成時,rehashidx屬性值加一。
  • 隨著字典操作的不斷執(zhí)行,最終在某一個時間點(diǎn)上,ht[0]的所有鍵值對都會被rehash到ht[1]上,這是將rehashidx的值設(shè)為-1,表示rehash操作已完成。
  • 漸進(jìn)式hash采取 分而治之 的思想,將rehash鍵值對所需的計(jì)算工作均攤到字典的每個添加、刪除、查找、更新操作上,避免集中式hash。

    漸進(jìn)式hash執(zhí)行期間進(jìn)行哈希表操作
  • 進(jìn)行刪除、查找、更新操作時,都會在兩個哈希表上進(jìn)行。比如說查找操作,現(xiàn)在ht[0]上查找,如果ht[0]上沒有就去ht[1]上查找。
  • 進(jìn)行添加操作時,新的鍵值對直接保存在ht[1]中,而ht[0]不進(jìn)行操作,這樣保證ht[0]只減不增。
  • 漸進(jìn)式hash的缺點(diǎn)
  • 擴(kuò)容期開始時,會先給 ht[1] 申請空間,所以在整個擴(kuò)容期間,會同時存在 ht[0] 和 ht[1],會占用額外的空間。

  • 擴(kuò)容期間同時存在 ht[0] 和 ht[1],查找、刪除、更新等操作有概率需要操作兩張表,耗時會增加。

  • redis 在內(nèi)存使用接近 maxmemory 并且有設(shè)置驅(qū)逐策略的情況下,出現(xiàn) rehash 會使得內(nèi)存占用超過 maxmemory,觸發(fā)驅(qū)逐淘汰操作,導(dǎo)致 master/slave 均有有大量的 key 被驅(qū)逐淘汰,從而出現(xiàn) master/slave 主從不一致。

  • 跳躍表

    可以把他理解為一個可以二分查找的鏈表

    它在Redis中只用到過兩處:一是有序集合zset;二是集群節(jié)點(diǎn)的內(nèi)部數(shù)據(jù)結(jié)構(gòu)。

    這塊的實(shí)現(xiàn)就不整理,看博客 或者 看書吧,《Redis設(shè)計(jì)與實(shí)現(xiàn)》p38。

    參考博客鏈接一:面試準(zhǔn)備 – Redis 跳躍表

    參考博客鏈接二:Redis中的跳躍表

    參考博客鏈接三:跳躍表以及跳躍表在redis中的實(shí)現(xiàn)

    為什么redis選擇了跳躍表而不是紅黑樹?
    • 在做范圍查找的時候,平衡樹比 skiplist 操作要復(fù)雜。
      • 在平衡樹上,我們找到指定范圍的小值之后,還需要以中序遍歷的順序繼續(xù)尋找其它不超過大值的節(jié)點(diǎn)。如果不對平衡樹進(jìn)行一定的改造,這里的中序遍歷并不容易實(shí)現(xiàn)。
      • 而在 skiplist 上進(jìn)行范圍查找就非常簡單,只需要在找到小值之后,對第1層鏈表進(jìn)行若干步的遍歷就可以實(shí)現(xiàn)。
    • 平衡樹的插入和刪除操作可能引發(fā)子樹的調(diào)整,邏輯復(fù)雜,而 skiplist 的插入和刪除只需要修改相鄰節(jié)點(diǎn)的指針,操作簡單又快速。
    • 從內(nèi)存占用上來說,skiplist比平衡樹更靈活一些。
      • 平衡樹每個節(jié)點(diǎn)包含2個指針(分別指向左右子樹)。
      • skiplist 每個節(jié)點(diǎn)包含的指針數(shù)目平均為1/(1-p),具體取決于參數(shù)p的大小。如果像Redis里的實(shí)現(xiàn)一樣,取p=1/4,那么平均每個節(jié)點(diǎn)包含1.33個指針,比平衡樹更有優(yōu)勢。
    • 查找單個key,skiplist和平衡樹的時間復(fù)雜度都為O(log n),大體相當(dāng);而哈希表在保持較低的哈希值沖突概率的前提下,查找時間復(fù)雜度接近O(1),性能更高一些。所以我們平常使用的各種 Map 或 dictionary 結(jié)構(gòu),大都是基于哈希表實(shí)現(xiàn)的。
    • 從算法實(shí)現(xiàn)難度上來比較,skiplist 比平衡樹要簡單得多。

    整數(shù)集合

    整數(shù)集合定義

    每個 intset.h/intset 結(jié)構(gòu)表示一個整數(shù)集合:

    typedef struct intset {uint32_t encoding; // 編碼方式uint32_t length; // 集合包含的元素?cái)?shù)量int8_t contents[]; // 保存元素的數(shù)組 }

    其中 contents[]就是整數(shù)集合的底層實(shí)現(xiàn):整數(shù)集合的每個元素都是該數(shù)組的一個數(shù)組項(xiàng),各個項(xiàng)在數(shù)組中是從小到大有序排列,并且不重復(fù)。

    雖然 contents[] 屬性聲明是 int8_t,但是真正類型取決于 encoding。

    整數(shù)集合升級

    整數(shù)升級,即當(dāng)我們將一個新元素添加到集合中時,新元素的類型比原集合的類型都要長時,整數(shù)集合需要升級,然后才能將新元素添加到集合中。

    具體升級并添加元素的步驟分為三步:

  • 根據(jù)新元素的類型,擴(kuò)展底層數(shù)組的空間大小,并為新元素分配空間。
  • 將底層數(shù)組現(xiàn)有的所有元素都轉(zhuǎn)換成與新元素相同的類型,并將類型轉(zhuǎn)換后的元素放置到正確的位置。該過程中,底層數(shù)組的順序不可變。
  • 將新元素加入數(shù)組。
  • 該過程的復(fù)雜度為 O(N)。

    升級的好處
  • 提升整數(shù)集合的靈活性。
  • 盡可能節(jié)約內(nèi)存。
  • 整數(shù)集合降級

    整數(shù)集合不支持降級操作!

    壓縮列表

    它的存在意義就是為了節(jié)約內(nèi)存

    壓縮列表定義

    壓縮列表就是一個由一系列特殊編碼的連續(xù)內(nèi)存塊組成的順序型數(shù)據(jù)結(jié)構(gòu)。

    壓縮列表的各個組成部分說明如下表:

    屬性類型長度用途
    zlbytesuint32_t4字節(jié)記錄整個壓縮鏈表占用的字節(jié)數(shù),在對壓縮列表進(jìn)行內(nèi)存重分配,或者計(jì)算zlend的位置時使用。
    zltailuint32_t4字節(jié)記錄壓縮列表表尾節(jié)點(diǎn)距離壓縮列表起始地址有多少個字節(jié):通過這個偏移量,程序無須遍歷整個壓縮列表就可以確定尾節(jié)點(diǎn)的地址。
    zllenuint16_t2字節(jié)記錄了壓縮列表包含的字節(jié)數(shù)量,該屬性小于UINT16_MAX(65535)時,該值為壓縮列表包含節(jié)點(diǎn)的數(shù)量;該屬性等于UINT16_MAX(65535)時,節(jié)點(diǎn)的真實(shí)數(shù)量需要遍歷壓縮列表獲得。
    entryX列表節(jié)點(diǎn)不定壓縮列表包含的各個節(jié)點(diǎn),節(jié)點(diǎn)的長度由節(jié)點(diǎn)保存的內(nèi)容而定。
    zlenduint8_t1字節(jié)特殊值0xFF(十進(jìn)制255),用于標(biāo)記壓縮列表的末端。
    列表節(jié)點(diǎn)構(gòu)成

    每個壓縮列表節(jié)點(diǎn)可以保存一個字節(jié)數(shù)組或者一個整數(shù)值。其中,字節(jié)數(shù)組可以是以下三種長度之一:

    • 長度小于等于63(2^6 - 1)字節(jié)的字節(jié)數(shù)組;
    • 長度小于等于16383(2^14 - 1)字節(jié)的字節(jié)數(shù)組;
    • 長度小于等于4294967295(2^32 - 1)字節(jié)的字節(jié)數(shù)組;

    而整數(shù)值則可以是以下六種長度的其中一種:

    • 4位長,介于0至12之間的無符號整數(shù);
    • 1字節(jié)長的有符號;
    • 3字節(jié)長的有符號整數(shù);
    • int16_t類型整數(shù);
    • int32_t類型整數(shù);
    • int64_t類型整數(shù)。

    每個壓縮列表節(jié)點(diǎn)都由 previous_entry_length、encoding、content三個部分組成:

    previous_entry_length

    節(jié)點(diǎn)的 previous_entry_length 屬性以字節(jié)為單位,記錄了壓縮列表中前一個節(jié)點(diǎn)的長度

    previous_entry_length 屬性的長度可以是1字節(jié) 或者 5字節(jié):

    • 如果前一節(jié)點(diǎn)的長度小于254字節(jié),那么 previous_entry_length 屬性的長度為1字節(jié):前一節(jié)點(diǎn)的長度就保存在這一個字節(jié)里面。
    • 如果前一節(jié)點(diǎn)的長度大于等于254字節(jié),那么 previous_entry_length 屬性的長度為5字節(jié):其中屬性的第一字節(jié)會被設(shè)置為0xFE(十進(jìn)制254),而之后的四個字節(jié)則用于保存前一節(jié)點(diǎn)的長度。

    它的好處就是,因?yàn)楣?jié)點(diǎn)的 previous_entry_length 屬性記錄了前一個節(jié)點(diǎn)的長度,所以程序可以通過指針運(yùn)算,根據(jù)當(dāng)前節(jié)點(diǎn)的起始地址來計(jì)算出前一節(jié)點(diǎn)的起始地址。

    壓縮列表的從表尾向表頭遍歷操作就是使用這一原理實(shí)現(xiàn)的,只要我們擁有一個指向某個節(jié)點(diǎn)起始地址的指針,那么通過這個指針以及這個節(jié)點(diǎn)的 previous_entry_length 屬性,程序就可以一直向前一個節(jié)點(diǎn)回溯,最終到達(dá)壓縮列表的表頭節(jié)點(diǎn)。

    encoding

    節(jié)點(diǎn)的 encoding 屬性記錄了節(jié)點(diǎn)的 content 屬性所保存數(shù)據(jù)的類型以及長度

    • 1字節(jié)、2字節(jié)或者5字節(jié)長,值的最高位為00、01或者10的是字節(jié)數(shù)組編碼:這種編碼表示節(jié)點(diǎn)的 content 屬性保存著字節(jié)數(shù)組,數(shù)組的長度由編碼除去最高兩位之后的其他位記錄;
    • 1字節(jié)長,值的最高位以11開頭的是整數(shù)編碼:這種編碼表示節(jié)點(diǎn)的 content 屬性保存著整數(shù)值,整數(shù)值的類型和長度由編碼除去最高兩位之后的其他位記錄。
    content

    節(jié)點(diǎn)的 content 屬性負(fù)責(zé)保存節(jié)點(diǎn)的值,節(jié)點(diǎn)值可以是一個字節(jié)數(shù)組或者整數(shù)值,值的類型和長度由節(jié)點(diǎn)的 encoding 屬性決定。

    連鎖更新

    redis中的壓縮列表在插入數(shù)據(jù)的時候可能存在連鎖擴(kuò)容的情況。

    在壓縮列表中,節(jié)點(diǎn)需要存放上一個節(jié)點(diǎn)的長度:當(dāng)上一個entry節(jié)點(diǎn)長度小于254個字節(jié)的時候,將會一個字節(jié)的大小來存放entry中的數(shù)據(jù);但是當(dāng)上一個entry節(jié)點(diǎn)長度大于等于254個字節(jié)的時候,就會需要更大的空間來存放數(shù)據(jù)。

    在壓縮列表中,會把大于等于254字節(jié)長度用5個字節(jié)來存儲,第一個字節(jié)是254,當(dāng)讀到254的時候,將會確認(rèn)接下來的4個字節(jié)大小將是entry的長度數(shù)據(jù)。當(dāng)?shù)谝粋€字節(jié)為255的時候,就證明壓縮列表已經(jīng)到達(dá)末端。

    由于表示長度的字節(jié)大小不一樣,當(dāng)新節(jié)點(diǎn)的插入可能會導(dǎo)致下一個節(jié)點(diǎn)原本存放表示上一節(jié)點(diǎn)的長度的空間大小不夠?qū)е滦枰獢U(kuò)容這一字段。相應(yīng)的該字段將會由一個字節(jié)擴(kuò)容到五個字節(jié),四個字節(jié)的長度變化,當(dāng)發(fā)生變化的節(jié)點(diǎn)原本長度在250到253之間的時候,將會導(dǎo)致下一個節(jié)點(diǎn)存儲上節(jié)點(diǎn)長度的空間發(fā)生變化,引起一個連鎖擴(kuò)容的情況,這一情況將會直到一個不需要擴(kuò)容的節(jié)點(diǎn)為止。

    擴(kuò)容邏輯代碼如下,可參考:

    while (p[0] != ZIP_END) {zipEntry(p, &cur);rawlen = cur.headersize + cur.len;rawlensize = zipStorePrevEntryLength(NULL,rawlen);/* Abort if there is no next entry. */if (p[rawlen] == ZIP_END) break;zipEntry(p+rawlen, &next);/* Abort when "prevlen" has not changed. */if (next.prevrawlen == rawlen) break;if (next.prevrawlensize < rawlensize) {/* The "prevlen" field of "next" needs more bytes to hold* the raw length of "cur". */offset = p-zl;extra = rawlensize-next.prevrawlensize;zl = ziplistResize(zl,curlen+extra);p = zl+offset;/* Current pointer and offset for next element. */np = p+rawlen;noffset = np-zl;/* Update tail offset when next element is not the tail element. */if ((zl+intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))) != np) {ZIPLIST_TAIL_OFFSET(zl) =intrev32ifbe(intrev32ifbe(ZIPLIST_TAIL_OFFSET(zl))+extra);}/* Move the tail to the back. */memmove(np+rawlensize,np+next.prevrawlensize,curlen-noffset-next.prevrawlensize-1);zipStorePrevEntryLength(np,rawlen);/* Advance the cursor */p += rawlen;curlen += extra;} else {if (next.prevrawlensize > rawlensize) {/* This would result in shrinking, which we want to avoid.* So, set "rawlen" in the available bytes. */zipStorePrevEntryLengthLarge(p+rawlen,rawlen);} else {zipStorePrevEntryLength(p+rawlen,rawlen);}/* Stop here, as the raw length of "next" has not changed. */break;} }

    代碼邏輯是:首先,從新插入的節(jié)點(diǎn)的下一個節(jié)點(diǎn)開始,如果下一個節(jié)點(diǎn)存放上一個字節(jié)的空間大小大于或等于當(dāng)前的節(jié)點(diǎn)長度,那么在存放了這一長度數(shù)據(jù)之后,該次連鎖擴(kuò)容直接宣告結(jié)束。如果下一個節(jié)點(diǎn)存放長度的空間不能容納當(dāng)前節(jié)點(diǎn)的長度,那么就會將下一個節(jié)點(diǎn)進(jìn)行擴(kuò)容,并重新申請內(nèi)存大小,并復(fù)制數(shù)據(jù),移動指向尾部節(jié)點(diǎn)的指針。最后移動到下一個節(jié)點(diǎn),在下一個循環(huán)中判斷是否需要繼續(xù)擴(kuò)容。

    編碼轉(zhuǎn)換時機(jī)

    Redis中的每個對象都由一個 redisObject 結(jié)構(gòu)來表示:

    typedef struct redisObject {unsigned type:4; // 類型unsigned encoding:4; // 編碼void *ptr; // 指向底層實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)的指針 }

    類型包括基本的五種,編碼指對應(yīng)類型下的不同編碼實(shí)現(xiàn)。

    Redis可以根據(jù)不同的使用場景,來為一個對象設(shè)置不同的編碼,從而優(yōu)化對象在某一場景下的效率。

    字符串

    字符串的編碼可以是 int 、 raw 或者是 embstr。

    int

    如果一個字符串對象保存的是整數(shù)值,并且這個整數(shù)值可以用long類型來表示,那么這個字符串對象會將整數(shù)值保存在字符串對象結(jié)構(gòu)的 ptr 屬性中(將 void* 轉(zhuǎn)換成 long),并將字符串對象的編碼設(shè)置為int。

    raw

    如果一個字符串對象保存的是一個字符串值,并且長度大于44字節(jié),那么這個字符串對象將使用簡單動態(tài)字符串(SDS)來保存,并且編碼設(shè)置為 raw。

    embstr

    如果一個字符串對象保存的是一個字符串值,并且長度小于等于44字節(jié),那么同上,但是編碼設(shè)置為embstr。

    embstr 是專門用于保存短字符串的優(yōu)化編碼方式。它和 raw 的區(qū)別在于,raw編碼會調(diào)用兩次內(nèi)存分配函數(shù)來分別創(chuàng)建 redisObject 和 sdshdr 結(jié)構(gòu),而embstr 編碼則通過調(diào)用一次內(nèi)存分配函數(shù)來分配一塊連續(xù)的空間,空間中依次包含 redisObject 和 sdshdr 結(jié)構(gòu)。

    使用 embstr 的好處:

  • 內(nèi)存分配次數(shù)減少一次。
  • 釋放內(nèi)存時的調(diào)用函數(shù)次數(shù)也少一次。
  • embstr 保存在連續(xù)的內(nèi)存中,它可以更好地利用緩存帶來的優(yōu)勢。
  • 不過,embstr 編碼沒有任何相應(yīng)的修改程序,它實(shí)際上只是只讀的,當(dāng) embstr 編碼的字符串執(zhí)行修改命令時,總會變成 raw。

    為什么raw和embstr的臨界值是44字節(jié)?

    如果看過書的同學(xué)有疑問很正常,因?yàn)樵凇禦edis的設(shè)計(jì)與實(shí)現(xiàn)》中,它寫的臨界值是39字節(jié),但是實(shí)際上經(jīng)過查找資料,在3.2版本之后就改成了44字節(jié)了。主要原因是為了內(nèi)存優(yōu)化,具體解釋如下:

    我們知道對于每個 sds 都有一個 sdshdr,里面的 len 和 free 記錄了這個 sds 的長度和空閑空間,但是這樣的處理十分粗糙,使用的 unsigned int 可以表示很大的范圍,但是對于很短的 sds 有很多的空間被浪費(fèi)了(兩個unsigned int 8個字節(jié))。而這個 commit 則將原來的 sdshdr 改成了 sdshdr16 , sdshdr32 , sdshdr64 ,里面的 unsigned int 變成了 uint8_t ,uint16_t…(還加了一個char flags)這樣更加優(yōu)化小 sds 的內(nèi)存使用。

    本身就是針對短字符串的 embstr 自然會使用最小的 sdshdr8 ,而 sdshdr8 與之前的 sdshdr 相比正好減少了5個字節(jié)(sdsdr8 = uint8_t * 2 + char = 1*2+1 = 3, sdshdr = unsigned int * 2 = 4 * 2 = 8),所以其能容納的字符串長度增加了5個字節(jié)變成了44。

    列表

    列表的編碼可以是 ziplist 或者 linkedlist。(壓縮列表 或者 雙向鏈表)

    ziplist

    如果列表對象保存的所有字符串元素的長度都小于64字節(jié),并且列表對象保存的元素?cái)?shù)量小于512個時,編碼為 ziplist。

    linkedlist

    上面兩個條件,只要一個不滿足,就采取 linkedlist 編碼。

    哈希

    哈希對象的編碼可以是 ziplist 或者 hashtable。(壓縮列表 或者 字典)

    ziplist

    如果哈希對象保存的所有鍵值對的鍵和值的字符串長度都小于64字節(jié),并且哈希對象保存的鍵值對數(shù)量小于512個時,編碼為 ziplist。

    hashtable

    上面兩個條件,只要一個不滿足,就采取 hashtable 編碼。

    集合

    集合對象的編碼可以是 intset 或者 hashtable。

    intset

    如果集合對象保存的所有元素都是整數(shù)值,并且哈希對象保存的元素?cái)?shù)量小于512個時,編碼為 intset。

    hashtable

    上面兩個條件,只要一個不滿足,就采取 hashtable 編碼。

    有序集合

    有序集合的編碼可以是 ziplist 或者 skiplist。

    ziplist

    如果有序集合對象保存的所有元素成員的長度都小于64字節(jié),并且有序集合對象保存的元素?cái)?shù)量小于128個時,編碼為 ziplist。

    skiplist

    上面兩個條件,只要一個不滿足,就采取 skiplist 編碼。

    持久化

    詳細(xì)了解參考文章:Redis的兩種持久化RDB和AOF(超詳細(xì))

    Redis對數(shù)據(jù)的操作都是基于內(nèi)存的,當(dāng)遇到了進(jìn)程退出、服務(wù)器宕機(jī)等意外情況,如果沒有持久化機(jī)制,那么Redis中的數(shù)據(jù)將會丟失無法恢復(fù)。有了持久化機(jī)制,Redis在下次重啟時可以利用之前持久化的文件進(jìn)行數(shù)據(jù)恢復(fù)。

    Redis支持的兩種持久化機(jī)制:

    • RDB:把當(dāng)前數(shù)據(jù)生成快照保存在硬盤上。
    • AOF:記錄每次對數(shù)據(jù)的操作到硬盤上。
    • 混合持久化:在 redis 4 引入,RDB + AOF 混合使用的方式,RDB 持久化全量數(shù)據(jù),AOF 持久化增量數(shù)據(jù)。

    RDB

    RDB(Redis DataBase)持久化是把當(dāng)前Redis中全部數(shù)據(jù)生成快照保存在硬盤上。RDB持久化可以手動觸發(fā),也可以自動觸發(fā)。

    觸發(fā)方式

    手動觸發(fā)

    save 和 bgsave 命令都可以手動觸發(fā)RDB持久化。

    • 執(zhí)行save命令會手動觸發(fā)RDB持久化,但是save命令會阻塞Redis服務(wù),直到RDB持久化完成。當(dāng)Redis服務(wù)儲存大量數(shù)據(jù)時,會造成較長時間的阻塞,不建議使用。
    • 執(zhí)行bgsave命令也會手動觸發(fā)RDB持久化,和save命令不同是:Redis服務(wù)一般不會阻塞。Redis進(jìn)程會執(zhí)行fork操作創(chuàng)建子進(jìn)程RDB持久化由子進(jìn)程負(fù)責(zé),不會阻塞Redis服務(wù)進(jìn)程。Redis服務(wù)的阻塞只發(fā)生在fork階段,一般情況時間很短。
    • 執(zhí)行 bgsave 命令,Redis進(jìn)程先判斷當(dāng)前是否存在正在執(zhí)行的RDB或AOF子線程,如果存在就是直接結(jié)束。
    • Redis進(jìn)程執(zhí)行 fork 操作創(chuàng)建子進(jìn)程,在fork操作的過程中Redis進(jìn)程會被阻塞。
    • Redis進(jìn)程 fork 完成后, bgsave 命令就結(jié)束了,自此Redis進(jìn)程不會被阻塞,可以響應(yīng)其他命令。
    • 子進(jìn)程根據(jù)Redis進(jìn)程的內(nèi)存生成快照文件,并替換原有的RDB文件。
    • 子進(jìn)程通過信號量通知Redis進(jìn)程已完成。

    簡單說明,save命令會全程阻塞,bgsave只在創(chuàng)建子線程時會阻塞。

    自動觸發(fā)

    在以下幾種場景下,會自動觸發(fā)RDB持久化:

  • 在配置文件中設(shè)置了 save 的相關(guān)配置,如sava m n,它表示在 m 秒內(nèi)數(shù)據(jù)被修改過 n 次時,自動觸發(fā) bgsave 操作。
  • 當(dāng)從節(jié)點(diǎn)做全量復(fù)制時,主節(jié)點(diǎn)會自動執(zhí)行 bgsave 操作,并且把生成的RDB文件發(fā)送給從節(jié)點(diǎn)。
  • 執(zhí)行 debug reload 命令時,也會自動觸發(fā) bgsave 操作。
  • 執(zhí)行 shutdown 命令時,如果沒有開啟AOF持久化也會自動觸發(fā) bgsave 操作。
  • RDB優(yōu)缺點(diǎn)

    優(yōu)點(diǎn)
  • RDB文件是一個緊湊的二進(jìn)制壓縮文件,是Redis在某個時間點(diǎn)的全部數(shù)據(jù)快照。所以使用RDB恢復(fù)數(shù)據(jù)的速度遠(yuǎn)遠(yuǎn)比AOF的快,非常適合備份、全量復(fù)制、災(zāi)難恢復(fù)等場景。
  • 缺點(diǎn)
  • 如果數(shù)據(jù)集非常巨大,并且 CPU 時間非常緊張的話,那么這種停止時間甚至可能會長達(dá)整整一秒。
  • 每次進(jìn)行bgsave操作都要執(zhí)行fork操作創(chuàng)建子經(jīng)常,屬于重量級操作,頻繁執(zhí)行成本過高,所以無法做到實(shí)時持久化,或者秒級持久化。
  • 由于Redis版本的不斷迭代,存在不同格式的RDB版本,有可能出現(xiàn)低版本的RDB格式無法兼容高版本RDB文件的問題。
  • AOF

    執(zhí)行流程

  • 命令追加(append):所有寫命令都會被追加到AOF緩存區(qū)(aof_buf)中。
  • 文件同步(sync):根據(jù)不同策略將AOF緩存區(qū)同步到AOF文件中。
  • 文件重寫(rewrite):定期對AOF文件進(jìn)行重寫,以達(dá)到壓縮的目的。
  • 數(shù)據(jù)加載(load):當(dāng)需要恢復(fù)數(shù)據(jù)時,重新執(zhí)行AOF文件中的命令。
  • 觸發(fā)方式

    手動觸發(fā)

    使用 bgrewriteaof 命令。

    自動觸發(fā)

    根據(jù) auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 配置確定自動觸發(fā)的時機(jī)。

    • auto-aof-rewrite-min-size 表示運(yùn)行AOF重寫時文件大小的最小值,默認(rèn)為64MB。
    • auto-aof-rewrite-percentage 表示當(dāng)前AOF文件大小和上一次重寫后AOF文件大小的比值的最小值,默認(rèn)為100。

    只用前兩者同時超過閾值時才會自動觸發(fā)文件重寫。

    AOF文件同步策略

    AOF持久化流程中的文件同步有以下幾個策略:

    • always:每次寫入緩存區(qū)都要同步到AOF文件中,硬盤的操作比較慢,限制了Redis高并發(fā),不建議配置。
    • no:每次寫入緩存區(qū)后不進(jìn)行同步,同步到AOF文件的操作由操作系統(tǒng)負(fù)責(zé),每次同步AOF文件的周期不可控,而且增大了每次同步的硬盤的數(shù)據(jù)量。
    • eversec:每次寫入緩存區(qū)后,由專門的線程每秒鐘同步一次,做到了兼顧性能和數(shù)據(jù)安全。是建議的同步策略,也是默認(rèn)的策略。

    AOF持久化配置

    # appendonly改為yes,開啟AOF appendonly yes # AOF文件的名字 appendfilename "appendonly.aof" # AOF文件的寫入方式 # everysec 每個一秒將緩存區(qū)內(nèi)容寫入文件 默認(rèn)開啟的寫入方式 appendfsync everysec # 運(yùn)行AOF重寫時AOF文件大小的增長率的最小值 auto-aof-rewrite-percentage 100 # 運(yùn)行AOF重寫時文件大小的最小值 auto-aof-rewrite-min-size 64mb

    文件事件處理器

    推薦博客文章:Redis全面解析一:redis是單線程結(jié)構(gòu)為何還可以支持高并發(fā)

    我們經(jīng)常說Redis是單線程的,但是為什么這么說呢?

    因?yàn)?Redis 內(nèi)部用的是基于 Reactor 模式開發(fā)的文件事件處理器,文件事件處理器是以單線程方式運(yùn)行的,所以redis才叫單線程模型。

    組成部分

    基于 Reactor 模式設(shè)計(jì)的四個組成部分的結(jié)構(gòu)如下所示:

    它們分別是:

    • 套接字
    • IO多路復(fù)用程序
    • 文件事件分派器
    • 事件處理器

    處理機(jī)制

    文件事件處理器大致可分為三個處理流程:

  • 每一個套接字準(zhǔn)備好執(zhí)行連接應(yīng)答、寫入、讀取、關(guān)閉等操作時,就會產(chǎn)生一個文件事件。一個服務(wù)器會連接多個套接字,多個文件事件并發(fā)的出現(xiàn)。
  • I/O多路復(fù)用程序負(fù)責(zé)監(jiān)聽多個套接字,并向文件事件分派器傳送那些產(chǎn)生的套接字,I/O多路復(fù)用程序會將所有產(chǎn)生事件的套接字都放到一個隊(duì)列里面,然后通過這個隊(duì)列,以有序、同步、每次一個套接字的方式向文件事件分派器傳送套接字。當(dāng)上一個套接字處理完畢,接受下一個套接字。
  • 文件事件分派器接收I/O多路復(fù)用程序傳來的套接字,并根據(jù)套接字產(chǎn)生的事件的類型,調(diào)用相應(yīng)的事件處理器。(執(zhí)行不同任務(wù)的套接字關(guān)聯(lián)不同的事件處理器)。
  • 拓展

    關(guān)于Redis6.0的多線程升級博客參考鏈接:Redis6 新特性多線程解析

    內(nèi)存淘汰機(jī)制

    Redis 緩存使用內(nèi)存保存數(shù)據(jù),避免了系統(tǒng)直接從后臺數(shù)據(jù)庫讀取數(shù)據(jù),提高了響應(yīng)速度。由于緩存容量有限,當(dāng)緩存容量到達(dá)上限,就需要刪除部分?jǐn)?shù)據(jù)挪出空間,這樣新數(shù)據(jù)才可以添加進(jìn)來。Redis 定義了「淘汰機(jī)制」用來解決內(nèi)存被寫滿的問題。

    緩存淘汰機(jī)制,也叫緩存替換機(jī)制,它需要解決兩個問題:

    • 決定淘汰哪些數(shù)據(jù)。
    • 如何處理那些被淘汰的數(shù)據(jù)。

    內(nèi)存淘汰策略

    截至在 4.0 之后,Redis定義了「8種內(nèi)存淘汰策略」用來處理 redis 內(nèi)存滿的情況:

    • noeviction:不會淘汰任何數(shù)據(jù),當(dāng)使用的內(nèi)存空間超過 maxmemory 值時,返回錯誤。
    • volatile-ttl:篩選設(shè)置了過期時間的鍵值對,越早過期的越先被刪除。
    • volatile-random:篩選設(shè)置了過期時間的鍵值對,隨機(jī)刪除。
    • volatile-lru:使用 LRU 算法篩選設(shè)置了過期時間的鍵值對。
    • volatile-lfu:使用 LFU 算法選擇設(shè)置了過期時間的鍵值對。
    • allkeys-random:在所有鍵值對中,隨機(jī)選擇并刪除數(shù)據(jù)。
    • allkeys-lru:使用 LRU 算法在所有數(shù)據(jù)中進(jìn)行篩選。
    • allkeys-lfu:使用 LFU 算法在所有數(shù)據(jù)中進(jìn)行篩選。

    根據(jù)它們的名稱和前綴我們就能如下分類:

    • 不淘汰數(shù)據(jù):noeviction。
    • 淘汰數(shù)據(jù)
      • 設(shè)置了過期時間的鍵值對中進(jìn)行淘汰:volatile-ttl、volatile-random、volatile-lru、volatile-lfu。
      • 所有數(shù)據(jù)進(jìn)行淘汰:allkeys-random、allkeys-lru、allkeys-lfu。

    策略介紹

    noeviction

    noeviction 策略,也是 Redis 的默認(rèn)策略,它要求 Redis 在使用的內(nèi)存空間超過 maxmemory 值時,也不進(jìn)行數(shù)據(jù)淘汰。一旦緩存被寫滿了,再有寫請求來的時候,Redis 會直接返回錯誤。

    我們實(shí)際項(xiàng)目中,一般不會使用這種策略。因?yàn)槲覀儤I(yè)務(wù)數(shù)據(jù)量通常會超過緩存容量的,而這個策略不淘汰數(shù)據(jù),導(dǎo)致有些熱點(diǎn)數(shù)據(jù)保存不到緩存中,失去了使用緩存的初衷。

    volatile-ttl、volatile-random、volatile-lru、volatile-lfu

    volatile-random、volatile-ttl、volatile-lru、volatile-lfu 這四種淘汰策略。它們淘汰數(shù)據(jù)的時候,只會篩選設(shè)置了過期時間的鍵值對上。

    比如,我們使用 EXPIRE 命令對一批鍵值對設(shè)置了過期時間,那么會有兩種情況會對這些數(shù)據(jù)進(jìn)行清理:

  • 第一種情況是過期時間到期了,會被刪除。
  • 第二種情況是 Redis 的內(nèi)存使用量達(dá)到了 maxmemory 閾值,Redis 會根據(jù) volatile-random、volatile-ttl、volatile-lru、volatile-lfu 這四種淘汰策略,具體的規(guī)則進(jìn)行淘汰;這也就是說,如果一個鍵值對被刪除策略選中了,即使它的過期時間還沒到,也需要被刪除。
  • 其中 volatile-ttl、volatile-random的篩選規(guī)則比較簡單,而volatile-lru、volatile-lfu分別用到了 LRU 和 LFU 算法。

    allkeys-random、allkeys-lru、allkeys-lfu

    allkeys-random,allkeys-lru,allkeys-lfu 這三種策略跟上述四種策略的區(qū)別是:淘汰時數(shù)據(jù)篩選的數(shù)據(jù)范圍是所有鍵值對。

    其中allkeys-random的篩選規(guī)則比較簡單,而allkeys-lru,allkeys-lfu分別用到了LRU 和 LFU 算法。

    LRU & LFU算法

    LRU

    LRU 算法全稱 Least Recently Used,一種常見的頁面置換算法。按照「最近最少使用」的原則來篩選數(shù)據(jù),篩選出最不常用的數(shù)據(jù),而最近頻繁使用的數(shù)據(jù)會留在緩存中。

    LRU 篩選邏輯

    RU 會把所有的數(shù)據(jù)組織成一個鏈表,鏈表的頭和尾分別表示 MRU 端和 LRU 端,分別代表「最近最常使用」的數(shù)據(jù)和「最近最不常用」的數(shù)據(jù)。

    每次訪問數(shù)據(jù)時,都會把剛剛被訪問的數(shù)據(jù)移到 MRU 端,就可以讓它們盡可能地留在緩存中。

    如果此時有新數(shù)據(jù)要寫入時,并且沒有多余的緩存空間,那么該鏈表會做兩件事情:

  • 將新數(shù)據(jù)放到MRU端。
  • 將LRU端的數(shù)據(jù)刪除。
  • 簡單說明,即它認(rèn)為剛剛被訪問的數(shù)據(jù),肯定還會被再次訪問,所以就把它放在 MRU端;LRU 端的數(shù)據(jù)被認(rèn)為是長久不訪問的數(shù)據(jù),在緩存滿時,就優(yōu)先刪除它。

    Redis 對 LRU 的實(shí)現(xiàn)

    Redis 3.0 前,隨機(jī)選取 N 個淘汰法。

    Redis 默認(rèn)會記錄每個數(shù)據(jù)的最近一次訪問的時間戳(由鍵值對數(shù)據(jù)結(jié)構(gòu) RedisObject 中的 lru 字段記錄)。

    在 Redis 決定淘汰的數(shù)據(jù)時,隨機(jī)選 N(默認(rèn)5) 個 key,把空閑時間(idle time)最大的那個 key 移除。這邊的 N 可通過 maxmemory-samples 配置項(xiàng)修改:

    config set maxmemory-samples 100

    當(dāng)需要再次淘汰數(shù)據(jù)時,Redis 需要挑選數(shù)據(jù)進(jìn)入「第一次淘汰時創(chuàng)建的候選集合」。

    挑選的標(biāo)準(zhǔn)是:能進(jìn)入候選集合的數(shù)據(jù)的 lru 字段值必須小于「候選集合中最小的 lru 值」。

    當(dāng)有新數(shù)據(jù)進(jìn)入備選數(shù)據(jù)集后,如果備選數(shù)據(jù)集中的數(shù)據(jù)個數(shù)達(dá)到了設(shè)置的閾值時。Redis 就把備選數(shù)據(jù)集中 lru 字段值最小的數(shù)據(jù)淘汰出去

    Redis3.0后,引入了緩沖池(默認(rèn)容量為16)概念。

    當(dāng)每一輪移除 key 時,拿到了 N(默認(rèn)5)個 key 的 idle time,遍歷處理這 N 個 key,如果 key 的 idle time 比 pool 里面的 key 的 idle time 還要大,就把它添加到 pool 里面去。

    當(dāng) pool 放滿之后,每次如果有新的 key 需要放入,需要將 pool 中 idle time 最小的一個 key 移除。這樣相當(dāng)于 pool 里面始終維護(hù)著還未被淘汰的 idle time 最大的 16 個 key。

    當(dāng)我們每輪要淘汰的時候,直接從 pool 里面取出 idle time 最大的 key(只取1個),將之淘汰掉。

    整個流程相當(dāng)于隨機(jī)取 5 個 key 放入 pool,然后淘汰 pool 中空閑時間最大的 key,然后再隨機(jī)取 5 個 key放入 pool,繼續(xù)淘汰 pool 中空閑時間最大的 key,一直持續(xù)下去。

    在進(jìn)入淘汰前會計(jì)算出需要釋放的內(nèi)存大小,然后就一直循環(huán)上述流程,直至釋放足夠的內(nèi)存。

    LFU

    在一些場景下,有些數(shù)據(jù)被訪問的次數(shù)非常少,甚至只會被訪問一次。當(dāng)這些數(shù)據(jù)服務(wù)完訪問請求后,如果還繼續(xù)留存在緩存中的話,就只會白白占用內(nèi)存空間。這種情況,就是緩存污染。

    為了應(yīng)對緩存污染問題,Redis 從 4.0 版本開始增加了 LFU 淘汰策略。

    LFU 緩存策略是在 LRU 策略基礎(chǔ)上,為每個數(shù)據(jù)增加了一個「計(jì)數(shù)器」,來統(tǒng)計(jì)這個數(shù)據(jù)的訪問次數(shù)。

    LFU 篩選邏輯
    • 當(dāng)使用 LFU 策略篩選淘汰數(shù)據(jù)時,首先會根據(jù)數(shù)據(jù)的訪問次數(shù)進(jìn)行篩選,把訪問次數(shù)最低的數(shù)據(jù)淘汰出緩存。
    • 如果兩個數(shù)據(jù)的訪問次數(shù)相同,LFU 策略再比較這兩個數(shù)據(jù)的訪問時效性,把距離上一次訪問時間更久的數(shù)據(jù)淘汰出緩存。
    LFU 的具體實(shí)現(xiàn)

    我們在前面說過,為了避免操作鏈表的開銷,Redis 在實(shí)現(xiàn) LRU 策略時使用了兩個近似方法:

    • Redis 在 RedisObject 結(jié)構(gòu)中設(shè)置了 lru 字段,用來記錄數(shù)據(jù)的訪問時間戳。
    • Redis 并沒有為所有的數(shù)據(jù)維護(hù)一個全局的鏈表,而是通過「隨機(jī)采樣」方式,選取一定數(shù)量的數(shù)據(jù)放入備選集合,后續(xù)在備選集合中根據(jù) lru 字段值的大小進(jìn)行篩選刪除。

    在此基礎(chǔ)上,Redis 在實(shí)現(xiàn) LFU 策略的時候,只是把原來 24bit 大小的 lru 字段,又進(jìn)一步拆分成了兩部分:

    • ldt 值:lru 字段的前 16bit,表示數(shù)據(jù)的訪問時間戳。
    • counter 值:lru 字段的后 8bit,表示數(shù)據(jù)的訪問次數(shù)。

    但是我們會發(fā)現(xiàn)一個問題,counter 值的最大記錄值只有255。當(dāng)幾個緩存數(shù)據(jù)的 counter 值 都達(dá)到255值,就無法正確根據(jù)訪問次數(shù)來決定數(shù)據(jù)的淘汰了。

    所以Redis 針對這個問題進(jìn)行了優(yōu)化:在實(shí)現(xiàn) LFU 策略時,Redis 并沒有采用數(shù)據(jù)每被訪問一次,就給對應(yīng)的 counter 值加 1 的計(jì)數(shù)規(guī)則,而是采用了一個更優(yōu)化的計(jì)數(shù)規(guī)則。

    Redis 對 LFU 的實(shí)現(xiàn)

    Redis 實(shí)現(xiàn) LFU 策略時采用計(jì)數(shù)規(guī)則:

  • 每當(dāng)數(shù)據(jù)被訪問一次時,先用「計(jì)數(shù)器當(dāng)前的值」乘以「配置項(xiàng) 」lfu_log_factor ,再加 1;取其倒數(shù),得到一個 p 值。
  • 然后,把這個 p 值和一個取值范圍在(0,1)間的隨機(jī)數(shù) r 值比大小,只有 p 值大于 r 值時,計(jì)數(shù)器才加 1。
  • Redis的部分源碼實(shí)現(xiàn)如下:

    double r = (double)rand() / RAND_MAX; // 隨機(jī)數(shù) r 值 // ...... // baseval 是計(jì)數(shù)器當(dāng)前的值,初始值默認(rèn)是 5,是由代碼中的 LFU_INIT_VAL 常量設(shè)置 double p = 1.0 / (baseval * server.lfu_log_factor + 1); // ((計(jì)數(shù)器當(dāng)前值 * 配置項(xiàng)參數(shù)) + 1 )的倒數(shù) if (r < p) counter++;

    為什么 baseval 的初始值是5,而不是0?是因?yàn)檫@樣可以避免數(shù)據(jù)剛被寫入緩存,就因?yàn)樵L問次數(shù)少而被立即淘汰。

    使用了這種計(jì)算規(guī)則后,我們可以通過設(shè)置不同的 lfu_log_factor 配置項(xiàng),來控制計(jì)數(shù)器值增加的速度,避免 counter 值很快就到 255 了。

    這張表是根據(jù)Redis官網(wǎng)獲得的,進(jìn)一步說明 LFU 策略計(jì)數(shù)器遞增的效果。
    它記錄了當(dāng) lfu_log_factor 取不同值時,在不同的實(shí)際訪問次數(shù)情況下,計(jì)數(shù)器值的變化情況。

    lfu_log_factor100 hits1000 hits100K hits1M hits10M hits
    0104255255255255
    11849255255255
    101018142255255
    10081149143255

    通過上表的分析:

    • 當(dāng) lfu_log_factor 取值為 1 時,實(shí)際訪問次數(shù)為 100K 后,counter 值就達(dá)到 255 了,無法再區(qū)分實(shí)際訪問次數(shù)更多的數(shù)據(jù)了。
    • 當(dāng) lfu_log_factor 取值為 100 時,當(dāng)實(shí)際訪問次數(shù)為 10M 時,counter 值才達(dá)到 255。

    使用這種非線性遞增的計(jì)數(shù)器方法,即使緩存數(shù)據(jù)的訪問次數(shù)成千上萬,LFU 策略也可以有效的區(qū)分不同的訪問次數(shù),從而合理的進(jìn)行數(shù)據(jù)篩選。

    從剛才的表中,我們可以看到,當(dāng) lfu_log_factor 取值為 10 時,百、千、十萬級別的訪問次數(shù)對應(yīng)的 counter 值 已經(jīng)有明顯的區(qū)分了。所以,我們在應(yīng)用 LFU 策略時,一般可以將 lfu_log_factor 取值為 10。

    但是對于一些業(yè)務(wù)場景,上方的設(shè)計(jì)會存在問題:比如說有些數(shù)據(jù)在「短時間內(nèi)被大量訪問后就不會再被訪問了」。

    那么再按照訪問次數(shù)來篩選的話,這些數(shù)據(jù)會被留存在緩存中,但不會提升緩存命中率。

    為此,Redis 在實(shí)現(xiàn) LFU 策略時,還設(shè)計(jì)了一個「 counter 值的衰減機(jī)制」。

    LFU 中的 counter 值的衰減機(jī)制

    簡單來說,LFU 策略使用 lfu_decay_time(衰減因子配置項(xiàng)) 來控制訪問次數(shù)的衰減。

  • LFU 策略會計(jì)算當(dāng)前時間和數(shù)據(jù)最近一次訪問時間的差值,并把這個差值換算成以分鐘為單位。
  • 然后,LFU 策略再把這個差值除以 lfu_decay_time 值,所得的結(jié)果就是數(shù)據(jù) counter 要衰減的值。
  • 通過上方的第二點(diǎn),我們就能知道一個規(guī)律,lfu_decay_time 值越大,那么相應(yīng)的衰減值會變小,衰減效果也會減弱;反之相應(yīng)的衰減值會變大,衰減效果也會增強(qiáng)。

    所以,如果業(yè)務(wù)應(yīng)用中有短時高頻訪問的數(shù)據(jù)的話,建議把 lfu_decay_time 值設(shè)置為 1。

    使用總結(jié)

  • 如果業(yè)務(wù)數(shù)據(jù)中「有明顯的冷熱數(shù)據(jù)區(qū)分」,建議使用 allkeys-lru 策略。這樣,可以充分利用 LRU 算法的優(yōu)勢,把最近最常訪問的數(shù)據(jù)留在緩存中,提升應(yīng)用的訪問性能。
  • 如果業(yè)務(wù)應(yīng)用中的「數(shù)據(jù)訪問頻率相差不大」,沒有明顯的冷熱數(shù)據(jù)區(qū)分,建議使用 allkeys-random 策略,隨機(jī)選擇淘汰的數(shù)據(jù)。
  • 如果業(yè)務(wù)中有「置頂」的需求,比如置頂新聞、置頂視頻,那么,可以使用 volatile-lru 策略,同時不給這些置頂數(shù)據(jù)設(shè)置過期時間。這樣一來,這些需要置頂?shù)臄?shù)據(jù)一直不會被刪除,而其他數(shù)據(jù)會在過期時根據(jù) LRU 規(guī)則進(jìn)行篩選。
  • 事務(wù)

    Redis 事務(wù)相對于Mysql 事務(wù)來說較為簡單,大家可以將二者進(jìn)行對比,下文也會整理。

    概念

    Redis 事務(wù)的本質(zhì)是一組命令的集合。

    事務(wù)支持一次執(zhí)行多個命令,一個事務(wù)中所有命令都會被序列化。在事務(wù)執(zhí)行過程,會按照順序串行化執(zhí)行隊(duì)列中的命令,其他客戶端提交的命令請求不會插入到事務(wù)執(zhí)行命令序列中

    簡單理解,Redis 中的事務(wù),就是具有一次性、順序性、排他性地在命令序列中執(zhí)行多個命令。

    它的主要作用就是串聯(lián)多個命令防止別的命令插隊(duì)。

    事務(wù)階段

    我們可以把Redis 事務(wù)的執(zhí)行分為三個階段:

  • 開始事務(wù)
  • 命令入隊(duì)
  • 執(zhí)行事務(wù)
  • 從輸入Multi命令開始,輸入的命令都會依次進(jìn)入命令隊(duì)列中,但不會執(zhí)行,直到輸入 Exec 后,Redis會將之前的命令隊(duì)列中的命令依次執(zhí)行。組隊(duì)的過程中可以通過 discard。

    事務(wù)錯誤處理

    事務(wù)的錯誤分為兩種情況:

    • 如果組隊(duì)中某個命令報(bào)出了錯誤,執(zhí)行時整個的所有隊(duì)列都會被取消
    • 如果執(zhí)行階段某個命令報(bào)出了錯誤,則只有報(bào)錯的命令不會被執(zhí)行,而其他的命令都會執(zhí)行不會回滾

    這說明在 Redis 中,雖然單條命令是原子性執(zhí)行的,但是事務(wù)不保證原子性,且沒有回滾。事務(wù)中任意命令執(zhí)行失敗,其余的命令仍會被執(zhí)行。

    Watch 監(jiān)控

    引入

    Redis 中的 悲觀鎖 和 樂觀鎖,簡單提及以下:

    悲觀鎖(Pessimistic Lock),每次去拿數(shù)據(jù)的時候都認(rèn)為別人會修改,所以每次在拿數(shù)據(jù)的時候都會上鎖,這樣別人想拿這個數(shù)據(jù)就會block直到它拿到鎖。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫里邊就用到了很多這種鎖機(jī)制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。

    樂觀鎖(Optimistic Lock),每次去拿數(shù)據(jù)的時候都認(rèn)為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù),可以使用版本號等機(jī)制。樂觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量。Redis就是利用這種check-and-set機(jī)制實(shí)現(xiàn)事務(wù)的。

    watch 命令

    在執(zhí)行 multi 之前,先執(zhí)行watch key1 [key2],可以監(jiān)視一個(或多個) key ,如果在事務(wù)執(zhí)行之前這個(或這些) key 被其他命令所改動,那么事務(wù)將被打斷

    舉例說明:

    假如我賬戶上有100元,此時我們準(zhǔn)備再給賬戶充值50元,準(zhǔn)備買149元的傳說皮膚。

    但是此時,以一位糟糕的程序員修改了我們的賬戶,改成了999元。

    我很生氣,因?yàn)槲页渲凳×?#xff0c;但是我去賬戶上一看,變成999元了,我馬上給自己一巴掌,“在生氣什么呢?”…

    模擬上方情景,這是控制臺1的操作:

    模擬上方情景,這是控制臺2的操作:

    注意:只要執(zhí)行了EXEC,之前加的監(jiān)控鎖都會被取消!Redis的事務(wù)不保證原子性,一條命令執(zhí)行失敗了,其他的仍然會執(zhí)行,且不會回滾。

    unwatch 命令

    取消 WATCH 命令對所有 key 的監(jiān)視。

    如果在執(zhí)行 WATCH 命令之后,EXEC 命令或 DISCARD 命令先被執(zhí)行了的話,那么就不需要再執(zhí)行 UNWATCH 了。

    總結(jié)說明

    redis 的事務(wù)不推薦在實(shí)際中使用,如果要使用事務(wù),推薦使用 Lua 腳本,redis 會保證一個 Lua 腳本里的所有命令的原子性。

    Redis集群

    除去Redis的單例模式,Redis 的集群模式可以分為三種:主從復(fù)制、哨兵模式、集群模式。

    主從復(fù)制

    Redis 官方文檔【主從復(fù)制】:REDIS sentinel-old – Redis中國用戶組(CRUG)

    主從復(fù)制架構(gòu)

    主從復(fù)制,將 Redis 實(shí)例分為兩中角色,一種是被復(fù)制的服務(wù)器稱為主服務(wù)器(master),而對主服務(wù)器進(jìn)行復(fù)制的服務(wù)器被稱為從服務(wù)器(slave)。

    當(dāng)主數(shù)據(jù)庫有數(shù)據(jù)寫入,會將數(shù)據(jù)同步復(fù)制給從節(jié)點(diǎn),一個主數(shù)據(jù)庫可以同時擁有多個從數(shù)據(jù)庫,而從數(shù)據(jù)庫只能擁有一個主數(shù)據(jù)庫。值得一提的是,從節(jié)點(diǎn)也可以有從節(jié)點(diǎn),呈現(xiàn)級聯(lián)結(jié)構(gòu)。

    我們可以看到,在主從復(fù)制中,只有一個是主機(jī),其他的都是從機(jī),并且從機(jī)下面還可以有任意多個從機(jī)。

    主數(shù)據(jù)庫可以進(jìn)行讀寫操作,從數(shù)據(jù)庫只能有讀操作(并不一定,只是推薦這么做)。

    開啟主從復(fù)制方式

    命令

    通過slaveof 命令,將 127.0.0.1:6380 的redis實(shí)例成為 127.0.0.1:6379 的redis實(shí)例的從服務(wù)器:

    slaveof 127.0.0.1 6379

    測試如下:


    配置

    通過編寫配置文件,例如先為主配置文件命名為 master.conf 進(jìn)行編寫配置:

    # 通用配置 # bind 127.0.0.1 # 綁定監(jiān)聽的網(wǎng)卡IP,注釋掉或配置成0.0.0.0可使任意IP均可訪問 port 6379 # 設(shè)置監(jiān)聽端口 #是否開啟保護(hù)模式,默認(rèn)開啟。 # 設(shè)置為no之后最好設(shè)置一下密碼 protected-mode no #是否在后臺執(zhí)行,yes:后臺運(yùn)行;no:不是后臺運(yùn)行 daemonize yes # 復(fù)制選項(xiàng),slave復(fù)制對應(yīng)的master。 # replicaof <masterip> <masterport> #如果master設(shè)置了requirepass,那么slave要連上master,需要有master的密碼才行。masterauth就是用來 # 配置master的密碼,這樣可以在連上master后進(jìn)行認(rèn)證。 # masterauth <master-password>

    在啟動節(jié)點(diǎn)時輸入命令

    redis-server master.conf redis-server slave1.conf redis-server slave2.conf

    不過在docker容器中的Redis鏡像配置存在一些問題,大家自己找一下資料吧。

    啟動命令

    參考博客鏈接:redis啟動命令及集群創(chuàng)建

    復(fù)制的實(shí)現(xiàn)【重點(diǎn)】

    1. 設(shè)置主服務(wù)器的地址和端口

    例如客戶端操作從服務(wù)器執(zhí)行如下命令:

    127.0.0.1> SLAVEOF 127.0.0.1 6379

    從服務(wù)器會將客戶端給定的主服務(wù)器IP地址以及端口號保存到當(dāng)前從服務(wù)器狀態(tài)的 masterhost 屬性和 masterport 屬性中。

    SLAVEOF 命令是一個異步命令,在完成屬性的設(shè)置工作后,從服務(wù)器會向客戶端返回"OK",之后開始執(zhí)行真正的復(fù)制工作。

    2. 建立套接字連接

    從服務(wù)器根據(jù)指定的 IP地址和端口號,創(chuàng)建連向主服務(wù)器套接字(socket)連接。

    主服務(wù)器在接受(accept) 從服務(wù)器的套接字連接之后,為該套接字創(chuàng)建相應(yīng)的客戶端狀態(tài)。

    這個時候可以將從服務(wù)器理解為主服務(wù)器的客戶端。

    3. 發(fā)送 PING 命令

    從服務(wù)器主服務(wù)器發(fā)送一個 PING 命令,以檢査套接字的讀寫狀態(tài)是否正常、 主服務(wù)器能否正常處理命令請求。

    從服務(wù)器在發(fā)送 PING 命令后,會遇到三種情況:

  • 主服務(wù)器響應(yīng)超時,表示當(dāng)前兩者之間網(wǎng)絡(luò)連接狀態(tài)不佳,從服務(wù)器重新創(chuàng)建連向主服務(wù)器的套接字。
  • 主服務(wù)器返回錯誤,表示主服務(wù)器暫時無法處理從服務(wù)器的命令請求,從服務(wù)器重新創(chuàng)建連向主服務(wù)器的套接字。
  • 主服務(wù)器返回 "PONG",表示主從之間網(wǎng)絡(luò)連接狀態(tài)正常,主服務(wù)器可以正常處理從服務(wù)器的命令請求。
  • 4. 身份驗(yàn)證

    存在這一步的前提是:從服務(wù)器設(shè)置了 masterauth 選項(xiàng),那么就要進(jìn)行這一步的身份驗(yàn)證,否則跳過。

    從服務(wù)器將 masterauth 選項(xiàng)的值封裝成AUTH password 命令并向主服務(wù)器發(fā)送來進(jìn)行身份驗(yàn)證。

    從服務(wù)器在身份驗(yàn)證階段可能會遇到以下幾種情況:

  • 主服務(wù)器沒有設(shè)置 requirepass 選項(xiàng),并且從服務(wù)器也沒有設(shè)置 masterauth 選項(xiàng),那么繼續(xù)執(zhí)行復(fù)制工作。
  • 如果從服務(wù)器的 AUTH 命令發(fā)送的密碼和主服務(wù)器 requirepass 選項(xiàng)的值相同,那么繼續(xù)執(zhí)行復(fù)制工作;反之,主服務(wù)器返回 invalid password 錯誤。
  • 主服務(wù)器設(shè)置 requirepass 選項(xiàng),但是從服務(wù)器沒有設(shè)置 masterauth 選項(xiàng),那么主服務(wù)器返回 NOAUTH 錯誤;如果主服務(wù)器沒有設(shè)置 requirepass 選項(xiàng),但是從服務(wù)器設(shè)置 masterauth 選項(xiàng),那么主服務(wù)器返回 no password is set錯誤。
  • 5. 發(fā)送端口信息

    從服務(wù)器主服務(wù)器發(fā)送當(dāng)前服務(wù)器的監(jiān)聽端口號, 主服務(wù)器收到后記錄在從服務(wù)器所對應(yīng)的客戶端狀態(tài)的 slave_listening_port 屬性中。

    執(zhí)行命令為 REPLCONF listening-port <port-number> ,port-number 即為端口號。

    目前 slave_listening_port 唯一的作用就是在主服務(wù)器執(zhí)行 INFO replication 命令時打印從服務(wù)器端口號。

    6. 同步

    從服務(wù)器主服務(wù)器發(fā)送 PSYNC 命令,執(zhí)行同步操作,此時兩者互為客戶端。

    PSYNC 命令有兩種執(zhí)行情況:

  • 如果從服務(wù)器以前沒有復(fù)制過或者執(zhí)行過 slaveof no one 命令,那么從服務(wù)器在開始一次新的復(fù)制時,會給主服務(wù)器發(fā)送 PSYNC ? -1 命令。主動請求進(jìn)行完整重同步
  • 相反,如果已經(jīng)復(fù)制過,那么從服務(wù)器在開始一次新的復(fù)制時,將向主服務(wù)器發(fā)送 PSYNC <runid > <offset> 命令,runid 是上次主服務(wù)器的運(yùn)行ID,offset是從服務(wù)器的復(fù)制偏移量。
  • 主服務(wù)器返回從服務(wù)器也有三種情況:

  • 如果主服務(wù)器返回 +FULLRESYNC <runid> <offset> 回復(fù),表示主服務(wù)器執(zhí)行完整重同步操作,runid 為主服務(wù)器的ID,從服務(wù)器會將其保存,offset 是主服務(wù)器的復(fù)制偏移量,從服務(wù)器會將其當(dāng)作自己的起始復(fù)制偏移量。
  • 如果主服務(wù)器返回的是 +CONTINUE回復(fù),表示主服務(wù)器執(zhí)行部分重同步操作,從服務(wù)器只要等待主服務(wù)器發(fā)送缺少的那部分?jǐn)?shù)據(jù)過來即可。
  • 如果主服務(wù)器返回的是 +ERR 回復(fù),那么表示 Redis 版本低于2.8,識別不了 PSYNC 命令,那么從服務(wù)器向主服務(wù)器發(fā)送 SYNC 命令,并與之執(zhí)行完整同步操作。
  • 從上方可知,主要包括全量數(shù)據(jù)同步增量數(shù)據(jù)同步的情況,這跟Redis是否第一次連接和在連接過程中是否離線有關(guān)。

    7. 命令傳播

    當(dāng)完成了同步之后,就會進(jìn)入命令傳播階段,這時主服務(wù)器只要一直將自己執(zhí)行的寫命令發(fā)送給從服務(wù)器,而從服務(wù)器只要一直接收并執(zhí)行主服務(wù)器發(fā)來的寫命令,就可以保證主從一致了。

    主從復(fù)制優(yōu)缺點(diǎn)

    優(yōu)點(diǎn)
    • 同一個Master可以同步多個Slaves。
    • master能自動將數(shù)據(jù)同步到slave,可以進(jìn)行讀寫分離,分擔(dān)master的讀壓力
    • master、slave之間的同步是以非阻塞的方式進(jìn)行的,同步期間,客戶端仍然可以提交查詢或更新請求
    缺點(diǎn)
    • 不具備自動容錯與恢復(fù)功能,master或slave的宕機(jī)都可能導(dǎo)致客戶端請求失敗,需要等待機(jī)器重啟或手動切換客戶端IP才能恢復(fù)
    • master宕機(jī),如果宕機(jī)前數(shù)據(jù)沒有同步完,則切換IP后會存在數(shù)據(jù)不一致的問題
    • 難以支持在線擴(kuò)容,Redis的容量受限于單機(jī)配置

    總結(jié)

    其實(shí)redis的主從模式很簡單,在實(shí)際的生產(chǎn)環(huán)境中很少使用,不建議在實(shí)際的生產(chǎn)環(huán)境中使用主從模式來提供系統(tǒng)的高可用性,之所以不建議使用都是由它的缺點(diǎn)造成的,在數(shù)據(jù)量非常大的情況,或者對系統(tǒng)的高可用性要求很高的情況下,主從模式也是不穩(wěn)定的。雖然這個模式很簡單,但是這個模式是其他模式的基礎(chǔ),所以理解了這個模式,對其他模式的學(xué)習(xí)會很有幫助。

    命令傳播階段后的心跳檢測 以及 PSYNC 的實(shí)現(xiàn),具體參照書中,不多解釋了。

    哨兵模式

    Redis官方文檔【高可用】:REDIS sentinel-old – Redis中國用戶組(CRUG)

    參考公眾號文章:全面分析Redis高可用的奧秘 - Sentinel

    哨兵模式架構(gòu)

    哨兵(Sentinel) 是 Redis 的高可用性解決方案:由一個或多個 Sentinel 實(shí)例組成的 Sentinel 系統(tǒng)可以監(jiān)視任意多個主服務(wù)器,以及這些主服務(wù)器屬下的所有從服務(wù)器。

    Sentinel 可以在被監(jiān)視的主服務(wù)器進(jìn)入下線狀態(tài)時,自動將下線主服務(wù)器的某個從服務(wù)器升級為新的主服務(wù)器,然后由新的主服務(wù)器代替已下線的主服務(wù)器繼續(xù)處理命令請求。

    哨兵進(jìn)程

    哨兵(Sentinel)其實(shí)也是Redis 實(shí)例,只不過它在啟動時初始化將 Redis 服務(wù)器使用的代碼替換成 Sentinel 專用代碼。

    哨兵進(jìn)程的作用
  • 監(jiān)控(Monitoring): 哨兵(sentinel) 會不斷地檢查你的Master和Slave是否運(yùn)作正常。
  • 提醒(Notification):當(dāng)被監(jiān)控的某個Redis節(jié)點(diǎn)出現(xiàn)問題時, 哨兵(sentinel) 可以通過 API 向管理員或者其他應(yīng)用程序發(fā)送通知。
  • 自動故障遷移(Automatic failover):當(dāng)一個Master不能正常工作時,哨兵(sentinel) 會開始一次自動故障遷移操作。
  • 哨兵(Sentinel) 和 一般Redis 的區(qū)別?

  • Sentinel 的本質(zhì)只是一個運(yùn)行在特殊模式下的 Redis 服務(wù)器。
  • 一般Redis 初始化時加載RDB 或者 AOF 文件還原數(shù)據(jù)庫狀態(tài),而Sentinel 不加載是因?yàn)樗皇褂脭?shù)據(jù)庫。
  • Sentinel 使用的代碼是 Sentinel專用代碼。
  • Sentinel 會初始化一個 sentinel.c/sentinelState 結(jié)構(gòu),用于保存所有和 Sentinel 功能相關(guān)的狀態(tài),比如其中的 masters字典記錄了所有被 Sentinel 監(jiān)視的主服務(wù)器相關(guān)信息。
  • 哨兵的工作方式

    創(chuàng)建連接

    這一步是初始化 Sentinel 的最后一步,Sentinel 成為主服務(wù)器的客戶端,可以向主服務(wù)器發(fā)送命令。

    每個sentinel都會創(chuàng)建兩個連向主服務(wù)器的異步網(wǎng)絡(luò)連接

    • 命令連接:用于向master服務(wù)發(fā)送命令,并接收命令回復(fù)。
    • 訂閱連接:用于訂閱、接收master服務(wù)的 __sentinel__:hello 頻道。

    為什么有兩個連接?

    命令連接的原因是:Sentinel 必須向主服務(wù)器發(fā)送命令,以此來與主服務(wù)器通信。

    訂閱連接的原因是:目前Redis版本的發(fā)布訂閱功能無法保存被發(fā)送的信息,如果接收信息的客戶端離線,那么這個客戶端就會丟失這條信息,為了不丟失 __sentinel__:hello 頻道的任何信息,Sentinel 專門用一個訂閱連接來接收該頻道的信息。

    【簡單理解:不僅需要發(fā)信息,也需要收信息】

    獲取主服務(wù)器信息

    Sentinel 默認(rèn)會以10秒一次通過命令連接向被監(jiān)視的主服務(wù)器發(fā)送 INFO 命令,主服務(wù)器收到后回復(fù)自己的run_id、IP、端口、對應(yīng)的主服務(wù)器信息及主服務(wù)器下的所有從服務(wù)器信息。

    Sentinel 根據(jù)返回的主服務(wù)器信息更新自身的 *masters 實(shí)例結(jié)構(gòu);至于主服務(wù)器返回的從服務(wù)器信息用于更新對應(yīng)的slaves 字典列表。

    更新 slaves 字典時有兩種情況:

  • 如果存在從服務(wù)器對應(yīng)的實(shí)例結(jié)構(gòu),那么Sentinel會對該實(shí)例結(jié)構(gòu)進(jìn)行更新。
  • 如果不存在從服務(wù)器對應(yīng)的實(shí)例結(jié)構(gòu),會為這個從服務(wù)器新創(chuàng)建一個實(shí)例結(jié)構(gòu)。
  • 獲取從服務(wù)器信息

    Sentinel 同樣會和從服務(wù)器建立異步的命令連接和訂閱連接,并也會默認(rèn)10秒一次從服務(wù)器發(fā)送 INFO 命令,從服務(wù)器會回復(fù)自己的運(yùn)行run_id、角色role、從服務(wù)器復(fù)制偏移量offset、主服務(wù)器的ip和port、主從服務(wù)器連接狀態(tài)、從服務(wù)器優(yōu)先級等信息,sentinel會根據(jù)返回信息更新對應(yīng)的 slave 實(shí)例結(jié)構(gòu)。

    向主服務(wù)器和從服務(wù)器發(fā)送信息

    Sentinel 默認(rèn)會以2秒一次通過命令連接向所有被監(jiān)控的主服務(wù)器從服務(wù)器的_sentinel:hello頻道發(fā)送信息,信息的內(nèi)容包含兩種參數(shù):

  • 一種參數(shù)是以 s_ 開頭的參數(shù),代表 Sentinel 自身的信息。
  • 另一種參數(shù)是以 m_ 開頭的參數(shù),代表主服務(wù)器的信息。
  • 如果發(fā)送的對象是主服務(wù)器,那么這些參數(shù)就是主服務(wù)器的信息。
  • 如果發(fā)送的對象是從服務(wù)器,那么這些參數(shù)就是從服務(wù)器正在復(fù)制的主服務(wù)器信息。
  • 參數(shù)列表展示參考:

    參數(shù)意義
    s_ipSentinel 的 IP地址
    s_portSentinel 的端口號
    s_runidSentinel 的運(yùn)行ID
    s_epochSentinel 當(dāng)前的配置紀(jì)元(configuration epoch)
    m_name主服務(wù)器的名字
    m_ip主服務(wù)器的IP地址
    m_port主服務(wù)器的端口號
    m_epoch主服務(wù)器當(dāng)前的配置紀(jì)元
    接收來自主服務(wù)器和從服務(wù)器的頻道信息

    Sentinel通過訂閱連接向服務(wù)器發(fā)送命令 SUBSCRIBE __sentinel__:hello,保證對_sentinel_:hello的訂閱一直持續(xù)到 Sentinel 與 服務(wù)器的連接斷開為止。

    _sentinel_:hello頻道 與 Sentinel 的關(guān)系是一對多的關(guān)系,作用在于發(fā)現(xiàn)多個監(jiān)控同一master的sentinel

    在接收到其他 sentinel 發(fā)送的頻道信息后,會根據(jù)信息更新 master 對應(yīng)的 Sentinel 。

    與 master 數(shù)據(jù)結(jié)構(gòu)綁定后,會建立 Sentinel 與 Sentinel 的命令連接,為后續(xù)通訊做準(zhǔn)備。

    故障檢測

    檢測主觀下線

    Sentinel 默認(rèn)會以1秒一次的頻率向與它建立命令連接的所有實(shí)例(包括master、slave以及發(fā)現(xiàn)的其他sentinel)發(fā)送 PING 命令,對方接收后返回兩種回復(fù):

    • **有效回復(fù):**包括運(yùn)行正常(+PONG)、正在加載(-LOADING)、和主機(jī)下線(-MASTERDOWN)。
    • **無效回復(fù):**除有效回復(fù)的三種以外都是無效回復(fù),或者在指定時限內(nèi)沒有返回任何回復(fù)。

    在固定時間內(nèi),即 down-after-milliseconds(默認(rèn)單位為毫秒) 配置的時間內(nèi)收到的都是無效回復(fù),Sentinel 就會標(biāo)記 master 為主觀下線。與此同時,Sentinel 會將 master 數(shù)據(jù)結(jié)構(gòu)中對應(yīng)的flags屬性更新為 SRI_S_DOWN 標(biāo)識,表示被監(jiān)控的master在當(dāng)前sentinel中已經(jīng)進(jìn)入主觀下線狀態(tài)。

    down-after-milliseconds 的值,不僅是sentinel 用來判斷主服務(wù)器主觀下線狀態(tài),還用來判斷主服務(wù)器下所有從服務(wù)器,以及所有同樣監(jiān)視這個主服務(wù)器的其他Sentinel的主觀下線狀態(tài)。

    簡單說明,即 down-after-millsseconds 配置是作用于當(dāng)前sentinel所監(jiān)控的所有服務(wù)上的,也就是對應(yīng)master下的slave,以及其他sentinel。另外每個sentinel可以配置不同down-after-millsenconds,所以判定主觀下線的時間也就是不同的。

    檢測客觀下線

    判定 master 為主觀下線狀態(tài)的 Sentinel,通過命令詢問其他同樣監(jiān)控這一主服務(wù)器的 Sentinel,看它們是否認(rèn)為該 master 真的進(jìn)入了下線狀態(tài)。

    Sentinel 發(fā)送給其他 Sentinel 的命令為:

    SEBTUBEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>

    參數(shù)說明:

    • Ip:被 Sentinel 判斷為主觀下線的主服務(wù)器的IP地址。
    • port:被 Sentinel 判斷為主觀下線的主服務(wù)器的端口號。
    • current_epoch:Sentinel 當(dāng)前的配置紀(jì)元,用于選舉領(lǐng)頭 Sentinel。
    • runid:可以是 * 符號 或 Sentinel 的 run_id,用 * 符號僅用于檢測主服務(wù)器的客觀下線狀態(tài);用Sentinel 的 run_id 是用于選舉領(lǐng)頭 Sentinel。

    其他 Sentinel 接收到 SEBTUBEL is-master-down-by-addr 命令后,會根據(jù)其中的主服務(wù)器IP和端口號,檢查主服務(wù)器是否已下線,然后向源 Sentinel 返回一條包含三個參數(shù)的 Multi Bulk 回復(fù):

    <down_state> <leader_runid> <leader_epoch>

    參數(shù)說明:

    down_state:返回目標(biāo) Sentinel 對主服務(wù)器的檢查結(jié)果,1 代表已下線,0 代表為下線。

    leader_runid:可以是 * 符號 或 目標(biāo) Sentinel 的 run_id,用 * 符號僅用于檢測主服務(wù)器的下線狀態(tài);用局部領(lǐng)頭 Sentinel 的 run_id 是用于選舉局部領(lǐng)頭 Sentinel。

    leader_epoch:目標(biāo) Sentinel 的局部領(lǐng)頭 Sentinel 的配置紀(jì)元,用于選舉領(lǐng)頭 Sentinel。【僅在 leader_runid 的值不為 * 時有效,如果 leader_runid 的值為 *,則 leader_epoch 總為0】

    當(dāng) Sentinel 收到從其他 Sentinel 返回的足夠數(shù)量的已下線判斷之后,Sentinel會將主服務(wù)器實(shí)例結(jié)構(gòu)的 flags 屬性的 SRI_O_DOWN 標(biāo)識打開,表示主服務(wù)器已經(jīng)進(jìn)入客觀下線狀態(tài)

    足夠數(shù)量的已下線判斷是多少呢?

    不同的 Sentinel 判斷客觀下線狀態(tài)的條件是不同的,具體不解釋了,看《Redis設(shè)計(jì)與實(shí)現(xiàn)》P238。

    選舉領(lǐng)頭 Sentinel

    當(dāng)一個主服務(wù)器被判斷為客觀下線時,監(jiān)測這個下線主服務(wù)器的各個 Sentinel 會進(jìn)行協(xié)商,選舉出一個領(lǐng)頭 Sentinel,并由領(lǐng)頭 Sentinel 對下線主服務(wù)器執(zhí)行故障轉(zhuǎn)移操作。

    下面盡量直白地介紹選舉領(lǐng)頭 Sentinel 的規(guī)則和方法:

    • 每個在線的 Sentinel 都有被選為領(lǐng)頭 Sentinel 的資格。

    • 同一個配置紀(jì)元內(nèi)(本質(zhì)是計(jì)數(shù)器,在每次選舉后自增一次),每個 Sentinel 都有一次將某個 Sentinel 設(shè)置為局部領(lǐng)頭 Sentinel 的機(jī)會,并且設(shè)置后,在這個配置紀(jì)元里不能再更改。

    • 每個發(fā)現(xiàn)主服務(wù)器進(jìn)入客觀下線 的Sentinel 都會要求其他 Sentinel 將自己設(shè)置為局部領(lǐng)頭Sentinel。

    • 拉票方式為發(fā)送 SEBTUBEL is-master-down-by-addr 命令,剛才的 *號替換為源 Sentinel的 run_id,表示希望目標(biāo) Sentinel 設(shè)置自己為它的局部領(lǐng)頭 Sentinel。

    • 接收拉票命令的目標(biāo) Sentinel 可是非常單純,誰的命令先發(fā)給它,它就選誰當(dāng)自己的局部領(lǐng)頭 Sentinel,之后的拉票全部拒絕。

    • 當(dāng)然,既然目標(biāo) Sentinel根據(jù)先到先得確定了局部領(lǐng)頭 Sentinel,那也得和大家回個話,它會為發(fā)送拉票命令的源 Sentinel 回復(fù)命令,記錄了自身選擇的局部領(lǐng)頭 Sentinel的 run_id 和 配置紀(jì)元。

    • 如果某個 Sentinel 被半數(shù)以上的 Sentinel 設(shè)置為了局部領(lǐng)頭 Sentinel,那么這個局部領(lǐng)頭sentinel就變成了領(lǐng)頭sentinel,同一個配置紀(jì)元內(nèi)可能會出現(xiàn)多個局部領(lǐng)頭sentinel,但是領(lǐng)頭sentinel只會產(chǎn)生一個。

    • 如果在給定的時限內(nèi),沒有任何一個 Sentinel 被選舉為領(lǐng)頭 Sentinel,那么各個 Sentinel 會在一段時間后再次選舉,直到選出領(lǐng)頭 Sentinel 為止。

    故障遷移

    在選舉出領(lǐng)頭 Sentinel 之后,領(lǐng)頭 Sentinel 會對已下線的主服務(wù)器執(zhí)行故障轉(zhuǎn)移操作,可分為三個步驟:

  • 在已下線的主服務(wù)器下的所有從服務(wù)器中,挑選一個從服務(wù)器作為新的主服務(wù)器。
  • 讓已下線的主服務(wù)器下的所有從服務(wù)器改為復(fù)制新的主服務(wù)器。
  • 將已下線的主服務(wù)器設(shè)置為新的主服務(wù)器的從服務(wù)器,當(dāng)它重新上線時會成為新的主服務(wù)器的從服務(wù)器。
  • 選出新的主服務(wù)器

    (一)、新的主服務(wù)器是從原主服務(wù)器下的從服務(wù)器中選擇的,所以需要選擇狀態(tài)良好、數(shù)據(jù)完整的從服務(wù)器。領(lǐng)頭 Sentinel 的數(shù)據(jù)結(jié)構(gòu)中保存了原master對應(yīng)的 slave ,Sentinel 會刪除狀態(tài)較差的slave。過濾執(zhí)行順序如下:

  • 刪除斷線或者下線的從服務(wù)器。
  • 刪除最近 5 秒內(nèi)沒有回復(fù)過領(lǐng)頭 Sentinel 的 INFO 命令的從服務(wù)器。
  • 刪除與原 master 斷開超過down-after-millisecond * 10 毫秒的從服務(wù)器,這樣可以排除從服務(wù)器與原主服務(wù)器過早斷開連接,保證備選從服務(wù)器的數(shù)據(jù)都是比較新的。
  • 對應(yīng)第三條,我可以解釋一下,前面提到過,在 down-after-millisecond 設(shè)置的時長內(nèi)沒有收到有效回復(fù),可以判定當(dāng)前復(fù)制的主服務(wù)器主觀下線。所以,越遲和主服務(wù)器斷開連接的從服務(wù)器,數(shù)據(jù)越新

    (二)、現(xiàn)在過濾出的都是健康的從服務(wù)器了,然后 Sentinel 開始選擇新的主服務(wù)器,有以下三個優(yōu)先級順序:

  • 然后根據(jù)從服務(wù)器的優(yōu)先級進(jìn)行排序,選出優(yōu)先級最高的服務(wù)器。
  • 如果有多個相同最高優(yōu)先級的從服務(wù)器,那么則根據(jù)它們的復(fù)制偏移量來進(jìn)行排序。
  • 如果有多個優(yōu)先級和復(fù)制偏移量相同的從服務(wù)器,那么選擇 run_id 最小的從服務(wù)器。
  • (三)、選出新的主服務(wù)器后,領(lǐng)頭 Sentinel 向被選中的從服務(wù)器發(fā)送 SLAVEOF no one 命令。

    在發(fā)送 SLAVEOF no one 命令后,領(lǐng)頭 Sentinel 會以每秒一次的頻率(平時是十秒一次)向被選中的從服務(wù)器發(fā)送 INFO 命令,當(dāng)被升級的服務(wù)器的 role 字段從 slave 變?yōu)?master 時,領(lǐng)頭 Sentinel 就知道它已經(jīng)順利成為新主服務(wù)器了。

    修改從服務(wù)器的復(fù)制目標(biāo)

    領(lǐng)頭 Sentinel 給已下線主服務(wù)器下的所有從服務(wù)器發(fā)送 SLAVEOF 命令,讓它們?nèi)?fù)制新的主服務(wù)器。

    將舊主服務(wù)器變?yōu)閺姆?wù)器

    因?yàn)榕f主服務(wù)器下線,領(lǐng)頭Sentinel 會修改它對應(yīng)主服務(wù)器下的實(shí)例結(jié)構(gòu)中的設(shè)置。

    等舊主服務(wù)器重新上線時,Sentinel 就會向它發(fā)送 SLAVEOF 命令,讓他成為新的主服務(wù)器的從服務(wù)器。

    集群模式

    《Redis設(shè)計(jì)與實(shí)現(xiàn)》第十七章 集群 p245;

    官方文檔【集群教程】:REDIS cluster-tutorial – Redis中文資料站 – Redis中國用戶組(CRUG)

    官方文檔【集群規(guī)范】:REDIS cluster-spec – Redis中文資料站 – Redis中國用戶組(CRUG)

    官方文檔【分區(qū)】:REDIS 分區(qū) – Redis中國用戶組(CRUG)

    集群模式架構(gòu)

    哨兵模式最大的缺點(diǎn)就是所有的數(shù)據(jù)都放在一臺服務(wù)器上,無法較好的進(jìn)行水平擴(kuò)展。

    為了解決哨兵模式的痛點(diǎn),集群模式應(yīng)運(yùn)而生。在高可用上,集群基本是直接復(fù)用的哨兵模式的邏輯,并且針對水平擴(kuò)展進(jìn)行了優(yōu)化。

    它具有的特點(diǎn)有:

  • 一個 Redis 集群通常由多個節(jié)點(diǎn)(Node)組成。
  • 采取去中心化的集群模式,將數(shù)據(jù)按槽存儲分布在多個 Redis 節(jié)點(diǎn)上。集群共有 16384 個槽,每個節(jié)點(diǎn)負(fù)責(zé)處理部分槽。
  • 使用 CRC16 算法來計(jì)算 key 所屬的槽:crc16(key,keylen) & 16383。
  • 所有的 Redis 節(jié)點(diǎn)彼此互聯(lián),通過 PING-PONG 機(jī)制來進(jìn)行節(jié)點(diǎn)間的心跳檢測。
  • 分片內(nèi)采用一主多從保證高可用,并提供復(fù)制和故障恢復(fù)功能。在實(shí)際應(yīng)用場景下,通常會將主從分布在不同服務(wù)器,避免單個服務(wù)器出現(xiàn)故障導(dǎo)致整個分片出問題,下圖的 內(nèi)網(wǎng)IP 代表不同的服務(wù)器。
  • 客戶端與 Redis 節(jié)點(diǎn)直連,不需要中間代理層(proxy)。客戶端不需要連接集群所有節(jié)點(diǎn),連接集群中任何一個可用節(jié)點(diǎn)即可。
  • 下面將會根據(jù)它的特點(diǎn)逐步說明該集群的核心技術(shù)。

    集群數(shù)據(jù)結(jié)構(gòu)

    使用 clusterNode 結(jié)構(gòu)保存一個節(jié)點(diǎn)的當(dāng)前狀態(tài),比如創(chuàng)建時間、名稱、配置紀(jì)元、IP、端口號等。

    每個節(jié)點(diǎn)都會為自己和集群中所有其他節(jié)點(diǎn)都創(chuàng)建一個對應(yīng)的 clusterNode 結(jié)構(gòu)來記錄各自的節(jié)點(diǎn)狀態(tài)。

    struct clusterNode {// 創(chuàng)建節(jié)點(diǎn)的時間mstime_t ctime;// 節(jié)點(diǎn)的名稱,由40個十六進(jìn)制字符組成,例如68eef66df23420a5862208ef5...f2ffchar name[REDIS_CLUSTER_NAMELEN];// 節(jié)點(diǎn)標(biāo)識,使用各種不同表示值記錄節(jié)點(diǎn)的角色(主節(jié)點(diǎn)或從節(jié)點(diǎn));以及節(jié)點(diǎn)目前的狀態(tài)(在線或下線)int flags;// 節(jié)點(diǎn)當(dāng)前的配置紀(jì)元,用于實(shí)現(xiàn)故障轉(zhuǎn)移uint64_t configEpoch;// 節(jié)點(diǎn)的IP地址char ip[REDIS_IP_STR_LEN];// 節(jié)點(diǎn)的端口號int port;// 保存連接節(jié)點(diǎn)所需的相關(guān)信息clusterLink *link;// ... };

    其中的 link 屬性是一個 clusterLink 結(jié)構(gòu),該結(jié)構(gòu)保存連接節(jié)點(diǎn)所需的相關(guān)信息,包括套接字描述符、輸入緩沖區(qū)、輸出緩沖區(qū)。

    typedef struct clusterLink {// 連接的創(chuàng)建時間mestime_t ctime;// TCP 套接字描述符int fd;// 輸出緩沖區(qū),保存著待發(fā)送給其他節(jié)點(diǎn)的信息(message)sds sndbuf;// 輸入緩沖區(qū),保存著從其他節(jié)點(diǎn)接收到的信息sds rcvbuf;// 與這個連接相關(guān)聯(lián)的節(jié)點(diǎn),如果沒有的話就為 NULLstruct clusterNode *node; }

    最后一點(diǎn),每個節(jié)點(diǎn)都保存著一個 clusterState 結(jié)構(gòu),這個結(jié)構(gòu)記錄了當(dāng)前節(jié)點(diǎn)視角下,所在集群目前所處的狀態(tài)。

    例如集群在線或下線狀態(tài)、包含節(jié)點(diǎn)個數(shù)、集群當(dāng)前的配置紀(jì)元等信息。

    typedef struct clsterState {// 指向當(dāng)前節(jié)點(diǎn)的指針clusterNode *myself;// 集群當(dāng)前的配置紀(jì)元,用于實(shí)現(xiàn)故障轉(zhuǎn)移uint64_t currentEpoch;// 集群當(dāng)前的狀態(tài),是在線還是下線int state;// 集群節(jié)點(diǎn)名單(包含myself節(jié)點(diǎn))// 字典的key是節(jié)點(diǎn)的名字,value是節(jié)點(diǎn)對應(yīng)的 clusterNode 結(jié)構(gòu)dict *nodes; }

    集群連接方式

    通過發(fā)送 CLUSTER MEET 命令,可以讓目標(biāo)節(jié)點(diǎn)A將另一個命令攜帶的節(jié)點(diǎn)B添加到目標(biāo)節(jié)點(diǎn)A當(dāng)前所在的集群中。

    CLUSTER MEET <ip> <port>

    收到命令后開始進(jìn)行節(jié)點(diǎn)A節(jié)點(diǎn)B握手階段,以此來確認(rèn)彼此的存在,為后面的通信打好基礎(chǔ),該過程簡單說明:

  • 客戶端向節(jié)點(diǎn)A發(fā)送 CLUSTER MEET 命令后,節(jié)點(diǎn)A向節(jié)點(diǎn)B發(fā)送 MEET 信息,給節(jié)點(diǎn)B創(chuàng)建 clusterNode 結(jié)構(gòu),并更新自己的 clusterState 結(jié)構(gòu)。
  • 節(jié)點(diǎn)B返回節(jié)點(diǎn)A PONG 信息。
  • 節(jié)點(diǎn)A返回節(jié)點(diǎn)B PING 信息。
  • 之后,節(jié)點(diǎn)A和節(jié)點(diǎn)B會通過Gossip 協(xié)議傳播給集群其他的節(jié)點(diǎn),讓他們也和節(jié)點(diǎn)B握手,最終整個集群達(dá)成共識。

    一般集群元數(shù)據(jù)的維護(hù)有兩種方式:集中式、Gossip 協(xié)議。在Redis集群中采用Gossip 協(xié)議進(jìn)行通信,所以說它是去中心化的集群。

    下面說一下這兩種方式的區(qū)別:

    集中式:是將集群元數(shù)據(jù)(節(jié)點(diǎn)信息、故障等等)幾種存儲在某個節(jié)點(diǎn)上。集中式元數(shù)據(jù)集中存儲的一個典型代表,就是大數(shù)據(jù)領(lǐng)域的 storm。它是分布式的大數(shù)據(jù)實(shí)時計(jì)算引擎,是集中式的元數(shù)據(jù)存儲的結(jié)構(gòu),底層基于 zookeeper(分布式協(xié)調(diào)的中間件)對所有元數(shù)據(jù)進(jìn)行存儲維護(hù)。

    gossip 協(xié)議所有節(jié)點(diǎn)都持有一份元數(shù)據(jù),不同的節(jié)點(diǎn)如果出現(xiàn)了元數(shù)據(jù)的變更,就不斷將元數(shù)據(jù)發(fā)送給其它的節(jié)點(diǎn),讓其它節(jié)點(diǎn)也進(jìn)行元數(shù)據(jù)的變更。

    集中式好處在于,元數(shù)據(jù)的讀取和更新,時效性非常好,一旦元數(shù)據(jù)出現(xiàn)了變更,就立即更新到集中式的存儲中,其它節(jié)點(diǎn)讀取的時候就可以感知到;不好在于,所有的元數(shù)據(jù)的更新壓力全部集中在一個地方,可能會導(dǎo)致元數(shù)據(jù)的存儲有壓力。

    gossip 協(xié)議好處在于,元數(shù)據(jù)的更新比較分散,不是集中在一個地方,更新請求會陸陸續(xù)續(xù)打到所有節(jié)點(diǎn)上去更新,降低了壓力;不好在于,元數(shù)據(jù)的更新有延時,可能導(dǎo)致集群中的一些操作會有一些滯后。

    分布式尋址算法【引入】

    如果會的同學(xué)可以跳過,這里只做引申說明。

    一般分布式尋址算法有下列幾種:

    • hash 算法(大量緩存重建)
    • 一致性 hash 算法(自動緩存遷移)+ 虛擬節(jié)點(diǎn)(自動負(fù)載均衡)
    • redis cluster 的 hash slot 算法
    hash 算法

    來了一個 key,首先計(jì)算 hash 值,然后對節(jié)點(diǎn)數(shù)取模。然后打在不同的 master 節(jié)點(diǎn)上。一旦某一個 master 節(jié)點(diǎn)宕機(jī),所有請求過來,都會基于最新的剩余 master 節(jié)點(diǎn)數(shù)去取模,嘗試去取數(shù)據(jù)。這會導(dǎo)致大部分的請求過來,全部無法拿到有效的緩存,導(dǎo)致大量的流量涌入數(shù)據(jù)庫。


    一致性 hash 算法

    一致性 hash 算法將整個 hash 值空間組織成一個虛擬的圓環(huán),整個空間按順時針方向組織,下一步將各個 master 節(jié)點(diǎn)(使用服務(wù)器的 ip 或主機(jī)名)進(jìn)行 hash。這樣就能確定每個節(jié)點(diǎn)在其哈希環(huán)上的位置

    一致性 hash 算法也是使用取模的方法 hash算法的取模法是對服務(wù)器的數(shù)量進(jìn)行取模,而一致性 hash 算法是對 **2^32 ** 取模:

    hash(服務(wù)器A的IP地址) % 2^32 hash(服務(wù)器B的IP地址) % 2^32 hash(服務(wù)器C的IP地址) % 2^32

    來了一個 key,首先計(jì)算 hash 值,并確定此數(shù)據(jù)在環(huán)上的位置,從此位置沿環(huán)順時針“行走”,遇到的第一個 master 節(jié)點(diǎn)就是 key 所在位置。

    使用 hash 算法時,服務(wù)器數(shù)量發(fā)生改變時,所有服務(wù)器的所有緩存在同一時間失效了,而使用一致性哈希算法時,服務(wù)器的數(shù)量如果發(fā)生改變,并不是所有緩存都會失效,而是只有部分緩存會失效,例如如果一個節(jié)點(diǎn)掛了,受影響的數(shù)據(jù)僅僅是此節(jié)點(diǎn)到環(huán)空間前一個節(jié)點(diǎn)(沿著逆時針方向行走遇到的第一個節(jié)點(diǎn))之間的數(shù)據(jù),其它不受影響。增加一個節(jié)點(diǎn)也同理。

    hash 環(huán)數(shù)據(jù)傾斜 & 虛擬節(jié)點(diǎn)

    然而當(dāng)一致性 hash 算法在節(jié)點(diǎn)太少或是節(jié)點(diǎn)位置分布不均勻時,容易造成大量請求都集中在某一個節(jié)點(diǎn)上,而造成緩存熱點(diǎn)的問題。如果i此時該熱點(diǎn)節(jié)點(diǎn)出現(xiàn)故障,那么失效緩存的數(shù)量也將達(dá)到最大值,在極端情況下,有可能引起系統(tǒng)的崩潰,這種情況被稱之為 數(shù)據(jù)傾斜。

    為了預(yù)防 數(shù)據(jù)傾斜 的問題,一致性 hash 算法引入了虛擬節(jié)點(diǎn)機(jī)制,即對每一個節(jié)點(diǎn)計(jì)算多個 hash,每個計(jì)算結(jié)果位置都放置一個虛擬節(jié)點(diǎn)。這樣就實(shí)現(xiàn)了數(shù)據(jù)的均勻分布,負(fù)載均衡。

    具體說明,每一個服務(wù)節(jié)點(diǎn)計(jì)算多個哈希,每個計(jì)算結(jié)果位置都放置一個此服務(wù)節(jié)點(diǎn)。具體做法可以在服務(wù)器ip或主機(jī)名的后面增加編號來實(shí)現(xiàn)。可以為每臺服務(wù)器計(jì)算三個虛擬節(jié)點(diǎn),于是可以分別計(jì)算 “Node1#1”、“Node1#2”、“Node1#3”、“Node2#1”、“Node2#2”、“Node2#3”的哈希值,這樣可以讓hash 環(huán)中存在多個節(jié)點(diǎn),使節(jié)點(diǎn)的分布更均勻,當(dāng)然可以虛擬出更多的虛擬節(jié)點(diǎn),以便減小hash環(huán)偏斜所帶來的影響,虛擬節(jié)點(diǎn)越多,hash環(huán)上的節(jié)點(diǎn)就越多,緩存被均勻分布的概率就越大。

    圖就不畫了…理解理解TAT

    hash slot 算法

    redis 集群采用數(shù)據(jù)分片的哈希槽來進(jìn)行數(shù)據(jù)存儲和數(shù)據(jù)的讀取。

    redis 集群中有固定的 16384 個槽(slot),對每個 key 計(jì)算 CRC16 值,然后對 16384 取模,可以獲取 key 對應(yīng)的 hash slot。

    redis 集群中每個 master 都會被指派部分的槽(slot),假如說當(dāng)前集群中有3個節(jié)點(diǎn)服務(wù)器,可能是這樣分配的 [0,5000]、[5001,10000]、[10001,16383]。

    槽位的實(shí)現(xiàn)其實(shí)就是一個長度為 16384 的二進(jìn)制數(shù)組,根據(jù)指定索引位上的二進(jìn)制位值來判斷節(jié)點(diǎn)是否處理指定索引的槽位

    所以槽位的遷移非常簡單:

  • 增加一個 master,就將其他 master 的槽位移動部分過去。
  • 減少一個 master,就將它的槽位移動到其他 master 上去。
  • 移動槽位的成本是非常低的。客戶端的 api,可以對指定的數(shù)據(jù),讓他們走同一個槽位,通過 hash tag 來實(shí)現(xiàn)。

    在Redis中通過 CLUSTER ADDSLOTS 命令來指派負(fù)責(zé)的槽位,后面會詳細(xì)說明。

    每個節(jié)點(diǎn)都會記錄哪些槽指派給了自己,哪些槽指派給了其他節(jié)點(diǎn)。客戶端向節(jié)點(diǎn)發(fā)送鍵命令,節(jié)點(diǎn)要計(jì)算這個鍵屬于哪個槽。如果是自己負(fù)責(zé)這個槽,那么直接執(zhí)行命令,如果不是,向客戶端返回一個 MOVED 錯誤,指引客戶端轉(zhuǎn)向正確的節(jié)點(diǎn)。

    任何一臺機(jī)器宕機(jī),另外兩個節(jié)點(diǎn),不影響的。因?yàn)?key 找的是 hash slot,不是機(jī)器。

    架構(gòu)圖參照上方《集群模式架構(gòu)》中。

    可能有人問,為什么一致性hash算法是65535(2^32)個位置,而hash slot 算法卻是16384(2^14)個位置?【翻譯官方回答】

  • 正常的心跳包攜帶節(jié)點(diǎn)的完整配置,可以用冪等方式替換舊節(jié)點(diǎn)以更新舊配置。 這意味著它們包含原始形式的節(jié)點(diǎn)的插槽配置,它使用 16384 個插槽只占用 2k 空間,但使用 65535 個插槽時將占用高達(dá)8k 的空間
  • 同時,由于其他設(shè)計(jì)權(quán)衡,Redis Cluster不太可能擴(kuò)展到超過1000個主節(jié)點(diǎn)
  • 因此,16384個插槽處于正確的范圍內(nèi),以確保每個主站有足夠的插槽,最多1000個節(jié)點(diǎn),但足夠小的數(shù)字可以輕松地將插槽配置傳播為原始位圖。 請注意,在小型集群中,位圖難以壓縮,因?yàn)楫?dāng)N很小時,位圖將設(shè)置插槽/ N位,這是設(shè)置的大部分位。

    一致性 hash 算法 和 hash slot 算法的區(qū)別?
    定位規(guī)則區(qū)別

    它并不是閉合的,key的定位規(guī)則是根據(jù) CRC-16(key) % 16384 的值來判斷屬于哪個槽區(qū),從而判斷該key屬于哪個節(jié)點(diǎn),而一致性 hash 算法是根據(jù) hash(key) 的值來順時針找第一個 hash(ip或主機(jī)名) 的節(jié)點(diǎn),從而確定key存儲在哪個節(jié)點(diǎn)。

    應(yīng)對熱點(diǎn)緩存區(qū)別

    一致性 hash 算法是創(chuàng)建虛擬節(jié)點(diǎn)來實(shí)現(xiàn)節(jié)點(diǎn)宕機(jī)后的數(shù)據(jù)轉(zhuǎn)移并保證數(shù)據(jù)的安全性和集群的可用性的。

    redis 集群是采用master節(jié)點(diǎn)有多個slave節(jié)點(diǎn)機(jī)制來保證數(shù)據(jù)的完整性的。master節(jié)點(diǎn)寫入數(shù)據(jù),slave節(jié)點(diǎn)同步數(shù)據(jù)。當(dāng)master節(jié)點(diǎn)掛機(jī)后,slave節(jié)點(diǎn)會通過選舉機(jī)制選舉出一個節(jié)點(diǎn)變成master節(jié)點(diǎn),實(shí)現(xiàn)高可用。但是這里有一點(diǎn)需要考慮,如果master節(jié)點(diǎn)存在熱點(diǎn)緩存,某一個時刻某個key的訪問急劇增高,這時該mater節(jié)點(diǎn)可能操勞過度而死,隨后從節(jié)點(diǎn)選舉為主節(jié)點(diǎn)后,同樣宕機(jī),一次類推,造成緩存雪崩。(簡單說明就是,都是被大量請求一套秒的,誰上來都一樣QAQ…)

    擴(kuò)容和縮容區(qū)別

    一致性 hash 算法在新增和刪除節(jié)點(diǎn)后,數(shù)據(jù)會按照順時針自動來重新分布節(jié)點(diǎn)

    redis 集群的新增和刪除節(jié)點(diǎn)都需要手動來分配槽區(qū)

    集群的槽指派

    Redis集群通過分片來保存數(shù)據(jù)庫的鍵值對:集群整個數(shù)據(jù)庫被分為16384個槽(slot),數(shù)據(jù)庫的每個鍵都屬于這16384個槽其中的一個,集群中的每個節(jié)點(diǎn)可以處理0個到16384個槽。

    指派節(jié)點(diǎn)槽信息

    當(dāng)集群使用 CLUSTER MEET 命令,整個集群仍處于下線狀態(tài),此時必須通過它們指派槽,通過發(fā)送 CLUSTER ADDSLOTS 命令給節(jié)點(diǎn),將一個或多個槽指派給節(jié)點(diǎn)負(fù)責(zé):

    CLUSTER ADDSLOTS <slot> [slot...]

    比如說將 0 到 5000 個槽指派給節(jié)點(diǎn)7000負(fù)責(zé):

    CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000

    然后以此類推給其他節(jié)點(diǎn)指派槽。

    槽位是在 clusterNode 結(jié)構(gòu)中的 slots 屬性和 numslot 屬性記錄的,記錄當(dāng)前節(jié)點(diǎn)負(fù)責(zé)處理哪些槽:

    struct clusterNode {//...// 二進(jìn)制位數(shù)組unsigned char slots[16384/8];// 記錄節(jié)點(diǎn)負(fù)責(zé)處理的槽的數(shù)量,即slots數(shù)組中值為1的二進(jìn)制位的數(shù)量int numslots; }

    在上面小節(jié)《分布式尋址算法》的《hash slot 算法》中說過,槽的本質(zhì)就是一個二進(jìn)制位數(shù)組,通過對[0,16383]上的對應(yīng)索引為標(biāo)記來判斷是否處理該槽位:如果slots數(shù)組上在指定索引位的二進(jìn)制位的值為1,標(biāo)識節(jié)點(diǎn)負(fù)責(zé)處理該槽,反之同理。

    CLUSTER ADDSLOTS 的命令實(shí)現(xiàn)

    CLUSTER ADDSLOTS 命令的實(shí)現(xiàn)也比較簡單:

  • 遍歷所有輸入槽,檢查它們是否被指派。
  • 只要有一個被指派,那么就返回錯誤并且終止命令執(zhí)行。
  • 如果都沒有被指派,那么就再次遍歷一遍,將它們指派給當(dāng)前節(jié)點(diǎn)。
  • 設(shè)置 clusterState.slot[i]索引位的指針指向 clusterState.myself。(如果不了解它先看下面再回來)
  • 將數(shù)組在指定索引位上的二進(jìn)制設(shè)置為1。
  • 執(zhí)行完畢后,開始廣播通知給集群中的其他節(jié)點(diǎn),自己目前處理的槽位。

    傳播節(jié)點(diǎn)槽信息

    節(jié)點(diǎn)會將自己的 slots 數(shù)組通過消息發(fā)送給集群中的其他節(jié)點(diǎn),告知它們自己目前負(fù)責(zé)的槽位。

    當(dāng)其他節(jié)點(diǎn)接收到消息,會更新自己的在 clusterState.nodes 字典中對應(yīng)節(jié)點(diǎn)的 clusterNode 結(jié)構(gòu)中的 slots 數(shù)組。

    記錄集群所有槽的指派信息

    在 clusterState 結(jié)構(gòu)中的 slots 數(shù)組記錄了集群中所有 16384 個槽的指派信息:

    typedef struct clusterState {//...clusterNode *slots[16384];//... }

    slots 數(shù)組包含 16384 個項(xiàng),每個數(shù)組項(xiàng)都是一個指向 clusterNode 的指針:對應(yīng)指針指向 NULL 時,說明還未分配;指向 clusterNode 結(jié)構(gòu)時,說明已經(jīng)指派給了對應(yīng)結(jié)構(gòu)所代表的節(jié)點(diǎn)。

    使用 clusterState.slots 和使用 clusterNode.slots 保存指派信息相比的好處?

    使用clusterState.slots 比使用 clusterNode.slots 能夠更高效地解決問題。

    • 如果只使用 clusterNode.slots來記錄,每次都需要遍歷所有 clusterNode 結(jié)構(gòu),復(fù)雜度為O(N)。
    • 但如果使用 clusterState.slots 來記錄,只需要訪問 clusterState.slots對應(yīng)的索引位即可,復(fù)雜度為O(1)。

    集群執(zhí)行命令

    建立集群,并且分配完槽位,此時集群就會進(jìn)入上線狀態(tài),這時候客戶端就可以向集群中的節(jié)點(diǎn)發(fā)送數(shù)據(jù)指令了。

    客戶端在向節(jié)點(diǎn)發(fā)送與數(shù)據(jù)庫鍵有關(guān)的命令時,接收命令的節(jié)點(diǎn)就會計(jì)算出命令要處理的數(shù)據(jù)庫鍵屬于哪個槽,并檢查這個槽是否指派個了自己:

    • 如果鍵所在的槽正好指派給當(dāng)前節(jié)點(diǎn),那么節(jié)點(diǎn)就直接執(zhí)行這個命令
    • 如果鍵所在的槽沒有指派給當(dāng)前節(jié)點(diǎn),那么節(jié)點(diǎn)就會向客戶端返回 MOVED 錯誤指引客戶端向正確的節(jié)點(diǎn),并再次發(fā)送之前想要執(zhí)行的命令。

    節(jié)點(diǎn)會使用以下算法來給指定 key 進(jìn)行計(jì)算:

    def slot_number(key):return CRC16(key) & 16383
    • CRC16(key):計(jì)算鍵 key 的 CRC-16 校驗(yàn)和。
    • & 16383:計(jì)算出介于0至16383之間的整數(shù)作為鍵 key 的槽號。

    當(dāng)節(jié)點(diǎn)計(jì)算出鍵所屬的槽后,節(jié)點(diǎn)會檢查自己 clusterState.slots 數(shù)組中的指定槽位,判斷是否由自己負(fù)責(zé):

    • 如果 clusterState.slot[i] 等于 clusterState.myself,說明是由當(dāng)前節(jié)點(diǎn)負(fù)責(zé)的。
    • 如果 clusterState.slot[i] 不等于 clusterState.myself,說明不是由當(dāng)前節(jié)點(diǎn)負(fù)責(zé)的,會根據(jù) clusterState.slot[i] 指向的 clusterNode 結(jié)構(gòu)中所記錄的 IP 和 端口號,返回客戶端 MOVED 錯誤,指引客戶端轉(zhuǎn)向正在處理該槽的節(jié)點(diǎn)。
    MOVED 錯誤

    MOVED 錯誤的格式為:

    MOVED <slot> <ip>:<port>
    • slot:鍵所在的槽。
    • ip:port:負(fù)責(zé)處理該槽節(jié)點(diǎn)的IP地址和端口號。

    MOVED 錯誤一般是不會打印的,而是根據(jù)該錯誤自動進(jìn)行節(jié)點(diǎn)轉(zhuǎn)向,并打印轉(zhuǎn)向信息。

    如果在單機(jī) redis 的情況下,是會被客戶端打印出來的。

    節(jié)點(diǎn)數(shù)據(jù)庫的實(shí)現(xiàn)

    節(jié)點(diǎn)只能使用0號數(shù)據(jù)庫,而單機(jī)Redis服務(wù)器則沒有限制

    節(jié)點(diǎn)除了將鍵值對保存在數(shù)據(jù)庫中之外,還會用 clusterState 結(jié)構(gòu)中的 slots_to_keys跳躍表來保存槽和鍵之間的關(guān)系:

    typedef struct clusterState {//...zskiplist *slots_to_keys;//... }

    slots_to_keys 跳表中每個節(jié)點(diǎn)的分值(score)都是一個槽位號;每個節(jié)點(diǎn)的成員(member)都是一個數(shù)據(jù)庫鍵。

    • 當(dāng)節(jié)點(diǎn)往數(shù)據(jù)庫中添加新的鍵值對時,節(jié)點(diǎn)會將鍵的槽位號以及這個鍵關(guān)聯(lián)到 slot_to_keys 跳表中。
    • 當(dāng)節(jié)點(diǎn)刪除數(shù)據(jù)庫中的某個鍵值對時,節(jié)點(diǎn)就會在 slot_to_keys跳表中解除它們的關(guān)聯(lián)關(guān)系。

    重新分片(比如在線擴(kuò)容)

    Redis 集群的重新分片操作可以將任意數(shù)量已經(jīng)指派給某個節(jié)點(diǎn)的槽改為指派給另一個節(jié)點(diǎn),并且相關(guān)聯(lián)槽位的鍵值對也會從源節(jié)點(diǎn)移動到目標(biāo)節(jié)點(diǎn)。

    重新分片的操作是可以在線進(jìn)行的,保證了高可用

    我們就以在線擴(kuò)容節(jié)點(diǎn)的情況來說吧:比如現(xiàn)在準(zhǔn)備在集群中增加一個節(jié)點(diǎn),如何將原有分片中的若干個槽位指派給新添加的節(jié)點(diǎn)?

    Redis 集群的重新分片操作是由 Redis 集群管理軟件 redis-trib 負(fù)責(zé)執(zhí)行的:Redis 提供重新分配的所有命令,而 redis-trib 通過向源節(jié)點(diǎn)和目標(biāo)接待你發(fā)送命令來進(jìn)行重新分片操作。

    redis-trib 對集群的單個槽進(jìn)行重新分片的步驟如下:

  • redis-trib給目標(biāo)節(jié)點(diǎn)發(fā)送 CLUSTER SETSLOT <slot> IMPORTING <source_id> 命令,讓目標(biāo)節(jié)點(diǎn)準(zhǔn)備好從源節(jié)點(diǎn)導(dǎo)入對應(yīng)槽位的鍵值對
  • redis-trib 對源節(jié)點(diǎn)發(fā)送 CLUSTER SETSLOT <slot> MIGRATING <target_id>命令,讓源節(jié)點(diǎn)準(zhǔn)備好將對應(yīng)槽位的鍵值對遷移到目標(biāo)節(jié)點(diǎn)
  • redis-trib 向源節(jié)點(diǎn)發(fā)送CLUSTER GETKEYSINSLOT <slot> <count> 命令,獲取最多 count 個對應(yīng)槽的鍵值對的鍵名稱
  • 根據(jù)第三步中所獲得的鍵名,redis-trib 都向源節(jié)點(diǎn)發(fā)送 MIGRATE <target_ip> <target_port> <key_name> 0 <timeout> 命令,將被選中的鍵原子性地遷移到目標(biāo)節(jié)點(diǎn)
  • 重復(fù)第三步和第四步,直到源節(jié)點(diǎn)中所有對應(yīng)槽位的鍵值對都遷移到目標(biāo)節(jié)點(diǎn)為止。
  • redis-trib 向集群中的任意一個節(jié)點(diǎn)發(fā)送 CLUSTER SETSLOT <slot> NODE <target_id> 命令,將對應(yīng)槽指派給了目標(biāo)節(jié)點(diǎn),這個信息會被廣播發(fā)給整個集群,最終整個集群都知道了對應(yīng)槽被指派給了目標(biāo)節(jié)點(diǎn)。
  • 如果涉及多個槽,則給每個槽重復(fù)執(zhí)行上述本步驟。

    ASK 錯誤 - (保證集群在線擴(kuò)容的安全性)

    在重新分片操作期間,可能會出現(xiàn)一部分鍵值對被遷出,一部分鍵值還未被遷出,即在源節(jié)點(diǎn)和目標(biāo)節(jié)點(diǎn)都由對應(yīng)槽的數(shù)據(jù)

    當(dāng)節(jié)點(diǎn)向源節(jié)點(diǎn)發(fā)送一個與數(shù)據(jù)庫鍵相關(guān)的命令,并且該鍵的槽位正好處在重新分片的過程中:

  • 源節(jié)點(diǎn)現(xiàn)在自己的庫中找指定鍵。
  • 找到的話,直接執(zhí)行客戶端發(fā)送的命令。
  • 沒找到的話,判斷當(dāng)前源節(jié)點(diǎn)是否正在遷移對應(yīng)數(shù)據(jù)庫鍵所在的槽位。
  • 如果沒有在遷移,說明鍵不存在,正常執(zhí)行命令。
  • 如果在遷移,說明鍵有可能在目標(biāo)節(jié)點(diǎn),返回 ASK 錯誤。
  • ASK 錯誤同 MOVED 錯誤類似,也是不會打印的,也會根據(jù)錯誤提供的 IP 和 端口號自動進(jìn)行轉(zhuǎn)向操作。

    同理,單機(jī)模式下會打印錯誤。

    那 ASK 錯誤 和 MOVED 錯誤有什么區(qū)別呢?

    雖然它們能導(dǎo)致客戶端轉(zhuǎn)向,但是 MOVED 錯誤代表槽的負(fù)責(zé)權(quán)已經(jīng)交給另一個節(jié)點(diǎn)了;而 ASK 錯誤只是兩個節(jié)點(diǎn)在遷移槽的過程中使用的臨時措施。

    CLUSTER SETSLOT IMPORTING 命令的實(shí)現(xiàn)

    clusterState 結(jié)構(gòu)的 importing_slots_from 數(shù)組記錄了當(dāng)前節(jié)點(diǎn)正在從其他節(jié)點(diǎn)導(dǎo)入的槽

    typedef struct clusterState {//...clusterNode *importing_slots_from[16384];//... }

    如果 importing_slots_from[i] 的值不為 NULL,而是指向一個 clusterNode 結(jié)構(gòu),那么表示當(dāng)前節(jié)點(diǎn)正在從 clusterNode 所代表的節(jié)點(diǎn)導(dǎo)入該槽。

    在對集群重新分片的時候,向目標(biāo)節(jié)點(diǎn)發(fā)送 CLUSTER SETSLOT IMPORTING 命令:

    CLUSTER SETSLOT <slot> IMPORTING <source_id>

    可以將目標(biāo)節(jié)點(diǎn) clusterState.importing_slots_from[i] 的值設(shè)置為 source_id所代表的節(jié)點(diǎn)的 clusterNode 結(jié)構(gòu)。

    CLUSTER SETSLOT MIGRATING 命令的實(shí)現(xiàn)

    clusterState 結(jié)構(gòu)的 migrating_slots_to 數(shù)組記錄了當(dāng)前節(jié)點(diǎn)正在遷移至其他節(jié)點(diǎn)的槽

    typedef struct clusterState {//...clusterNode *migrating_slots_to[16384];//... }

    如果 migrating_slots_to[i] 的值不為 NULL,而是指向一個 clusterNode 結(jié)構(gòu),那么表示當(dāng)前節(jié)點(diǎn)正在將該槽遷移到 clusterNode 所代表的節(jié)點(diǎn)。

    ASKING 命令

    當(dāng)客戶端接收到 ASK 錯誤并轉(zhuǎn)向正在導(dǎo)入槽的節(jié)點(diǎn)時,客戶端會先向節(jié)點(diǎn)發(fā)送一個 ASKING 命令,然后才重新發(fā)送要執(zhí)行的命令,這是因?yàn)榭蛻舳巳绻话l(fā)送 ASKING 命令,而直接發(fā)送想要執(zhí)行的命令的話,那么客戶端發(fā)送的命令會被節(jié)點(diǎn)拒絕執(zhí)行,并返回 MOVED 錯誤。

    復(fù)制和故障轉(zhuǎn)移

    Redis 集群中節(jié)點(diǎn)可分為主節(jié)點(diǎn)(master)和從節(jié)點(diǎn)(slave)。

    主節(jié)點(diǎn)用于處理槽;從節(jié)點(diǎn)用于復(fù)制某個主節(jié)點(diǎn),并在主節(jié)點(diǎn)下線時,代替下線主節(jié)點(diǎn)繼續(xù)處理命令請求。

    設(shè)置從節(jié)點(diǎn)方式

    向一個節(jié)點(diǎn)發(fā)送命令:

    CLUSTER REPLICATE <node_id>

    可以讓接收命令的節(jié)點(diǎn)成為 node_id 所指定的節(jié)點(diǎn)的從節(jié)點(diǎn),并開始對主節(jié)點(diǎn)進(jìn)行復(fù)制操作,具體步驟如下:

  • 接收命令的節(jié)點(diǎn)首先找到 clusterState.nodes 字典中對應(yīng) node_id 所對應(yīng)節(jié)點(diǎn)的 clusterNode 結(jié)構(gòu),并將自身的 clusterState.myself.slaveof 指針指向這個結(jié)構(gòu),來記錄正在復(fù)制的主節(jié)點(diǎn)。
  • 修改自身 clusterState,myself.flags 屬性,關(guān)閉原來的 REDIS_NODE_MASTER 標(biāo)識,打開 REDIS_NODE_SLAVE 標(biāo)識,表明該節(jié)點(diǎn)已經(jīng)從主節(jié)點(diǎn)變成從節(jié)點(diǎn)。
  • 最后,節(jié)點(diǎn)會調(diào)用復(fù)制代碼對主節(jié)點(diǎn)進(jìn)行復(fù)制,相當(dāng)于向從節(jié)點(diǎn)發(fā)送 SLAVEOF 命令。
  • 故障檢測

    集群中每個節(jié)點(diǎn)都會定期向其他節(jié)點(diǎn)發(fā)送 PING 信息,以此檢測對方是否在線,如果接收 PING 信息的節(jié)點(diǎn)沒有在規(guī)定時間內(nèi)返回 PONG 信息,那么發(fā)送消息的節(jié)點(diǎn)會將接收消息的節(jié)點(diǎn)標(biāo)記為疑似下線(PFALL)。

    如果在集群中,半數(shù)以上負(fù)責(zé)槽的主節(jié)點(diǎn)都將某個主節(jié)點(diǎn)標(biāo)記為疑似下線,那么這個主節(jié)點(diǎn)就會被標(biāo)記為已下線(FALL)。

    將該主節(jié)點(diǎn)標(biāo)記為已下線的節(jié)點(diǎn)會向集群廣播關(guān)于該節(jié)點(diǎn)的 FALL 消息,所有收到這條 FALL 信息的節(jié)點(diǎn)都會立即將該節(jié)點(diǎn)標(biāo)記為已下線

    故障轉(zhuǎn)移

    當(dāng)一個從節(jié)點(diǎn)發(fā)現(xiàn)自己正在復(fù)制的主節(jié)點(diǎn)進(jìn)入了下線狀態(tài)時,從節(jié)點(diǎn)會對下線主節(jié)點(diǎn)進(jìn)行故障轉(zhuǎn)移,按照以下的執(zhí)行步驟:

  • 從下線主節(jié)點(diǎn)的所有從節(jié)點(diǎn)中選出一個從節(jié)點(diǎn),讓被選中的從節(jié)點(diǎn)執(zhí)行 SLAVE no one 命令,成為新的主節(jié)點(diǎn)。
  • 新的主節(jié)點(diǎn)會撤銷所有已下線主節(jié)點(diǎn)的槽指派,并將這些槽全部指派給自己
  • 新的主節(jié)點(diǎn)向集群廣播 PONG 信息,這條信息可以啊讓其他主節(jié)點(diǎn)直到這個節(jié)點(diǎn)已經(jīng)成為主節(jié)點(diǎn),并且接管了所有已下線的主節(jié)點(diǎn)負(fù)責(zé)處理的槽。
  • 新的主節(jié)點(diǎn)開始接收自己負(fù)責(zé)處理的槽相關(guān)的命令請求,故障轉(zhuǎn)移完成。
  • 選舉新的主節(jié)點(diǎn)過程

    新的主節(jié)點(diǎn)也是通過選舉產(chǎn)生的,簡單介紹一下它的選舉過程:

  • 每一次開始故障轉(zhuǎn)移操作時,集群的配置紀(jì)元(自增計(jì)數(shù)器,初始值為0)會自增加一。
  • 在每個配置紀(jì)元中,集群中每個負(fù)責(zé)處理槽的主節(jié)點(diǎn)都有一次投票機(jī)會,而第一個來發(fā)送拉票請求的從節(jié)點(diǎn)將獲得它的投票。
  • 當(dāng)從節(jié)點(diǎn)發(fā)現(xiàn)自己正在復(fù)制的主節(jié)點(diǎn)已下線時,會向集群廣播 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 信息,要求所有收到信息,并且有投票權(quán)的主節(jié)點(diǎn)給它投票。
  • 如果一個負(fù)責(zé)處理槽的主節(jié)點(diǎn)尚未投票,在接收到該拉票的 REQUEST 信息時,會返回 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 信息,表示它支持該從節(jié)點(diǎn)。
  • 每個參與選舉的從節(jié)點(diǎn)都會接收這條 ACK 信息,并且統(tǒng)計(jì)自己獲得的支持?jǐn)?shù)。
  • 當(dāng)一個從節(jié)點(diǎn)收集到 N /2 + 1(具有投票權(quán)的節(jié)點(diǎn)的一半數(shù)量加一)時,這個從節(jié)點(diǎn)成為新節(jié)點(diǎn)。【在一個配置紀(jì)元中,只有一個從節(jié)點(diǎn)能達(dá)到這個數(shù)目,確保了主節(jié)點(diǎn)只有一個】
  • 如果在這個配置紀(jì)元中沒有任何從節(jié)點(diǎn)收集到足夠多的支持票,那么會進(jìn)入下一個配置紀(jì)元,并再次進(jìn)行選舉,直到選出新的主節(jié)點(diǎn)為止。
  • 類似于領(lǐng)頭 Sentinel 的選舉,可以對比來看。它們都是基于 Raft 算法的領(lǐng)頭選舉方法來實(shí)現(xiàn)的。

    有的小伙伴可能覺得 領(lǐng)頭Sentinel 的選舉不算 Raft,因?yàn)樗詈笫峭ㄟ^領(lǐng)頭 Sentinel 來控制故障遷移的具體過程,這個就是仁者見仁智者見智了。

    Raft 算法的實(shí)現(xiàn)可以參考一下Nacos 源碼中 RaftCore 類的實(shí)現(xiàn),比較通俗易懂。有時間我會發(fā)一下 Nacos 源碼中Raft選舉的實(shí)現(xiàn)。

    Redis應(yīng)用

    Redis 分布式鎖

    官方文檔:REDIS distlock – Redis中國用戶組(CRUG)

    我最早覺得比較好的實(shí)現(xiàn)分布式鎖思路文章:10分鐘精通Redis分布式鎖中的各種門道

    引入

    為什么需要分布式鎖?

    我們在開發(fā)項(xiàng)目時,如果需要在同進(jìn)程內(nèi)的不同線程并發(fā)訪問某項(xiàng)資源,可以使用各種互斥鎖、讀寫鎖

    如果一臺主機(jī)上的多個進(jìn)程需要并發(fā)訪問某項(xiàng)資源,則可以使用進(jìn)程間同步的原語,例如信號量、管道、共享內(nèi)存等。

    但如果多臺主機(jī)需要同時訪問某項(xiàng)資源,就需要使用一種在全局可見并具有互斥性的鎖了。

    這種鎖就是分布式鎖,可以在分布式場景中對資源加鎖,避免競爭資源引起的邏輯錯誤。

    什么時候用分布式鎖?

    一般我們使用分布式鎖有兩個場景:

    • 效率:使用分布式鎖可以避免不同節(jié)點(diǎn)重復(fù)相同的工作,這些工作會浪費(fèi)資源。比如用戶注冊后調(diào)用發(fā)送郵箱的接口發(fā)送通知,可能不同節(jié)點(diǎn)會發(fā)出多封郵箱
    • 安全:加分布式鎖同樣可以避免破壞正確性的發(fā)生,如果兩個節(jié)點(diǎn)在同一條數(shù)據(jù)上面操作,比如多個節(jié)點(diǎn)機(jī)器對同一個訂單操作不同的流程有可能會導(dǎo)致該筆訂單最后狀態(tài)出現(xiàn)錯誤,造成損失。
    分布式鎖需要哪些特性呢?

    大部分特性其實(shí)都類似于 Java 中的鎖,包括互斥性、可重入、鎖超時、公平鎖和非公平鎖、一致性。

    • 互斥性:在同一時間點(diǎn),只有一個客戶端持有鎖。
    • 可重入:同一個節(jié)點(diǎn)上的同一個線程如果獲取了鎖之后那么也可以再次獲取這個鎖。
    • 鎖超時:在客戶端離線(硬件故障或網(wǎng)絡(luò)異常等問題)時,鎖能夠在一段時間后自動釋放防止死鎖,即超時自動解鎖。
    • 公平鎖和非公平鎖:公平鎖即按照請求加鎖的順序獲得鎖,非公平鎖即相反是無序的。
    • 一致性:比如說用Redis 實(shí)現(xiàn)分布式鎖時,發(fā)生宕機(jī)情況,此時會有主從故障轉(zhuǎn)移的過程中,需要在此過程仍然保持鎖的原狀態(tài)。
    • 續(xù)鎖:為了防止死鎖大多數(shù)會有鎖超時的設(shè)置,但是如果業(yè)務(wù)的執(zhí)行時間的不確定性,就需要保證在業(yè)務(wù)仍在執(zhí)行過程中時,客戶端仍要持有鎖。

    加鎖

    在Redis中加鎖一般都是使用 SET 命令,使用 SET 命令完成 SETNX 和 EXPIRE 操作,并且這是一個原子操作

    set key value [EX seconds] [PX milliseconds] [NX|XX]

    上面這條指令是 SET 指令的使用方式,參數(shù)說明如下:

    • key、value:鍵值對。
    • EX seconds:設(shè)置失效時長,單位秒。
    • PX milliseconds:設(shè)置失效時長,單位毫秒。
    • NX:key不存在時設(shè)置value,成功返回OK,失敗返回(nil),SET key value NX 效果等同于 SETNX key value。
    • XX:key存在時設(shè)置value,成功返回OK,失敗返回(nil)。

    其中,NX 參數(shù)用于保證在多個線程并發(fā) set 下,只會有1個線程成功,起到了鎖的“唯一”性。

    舉例:

    // 設(shè)置msg = helloword,失效時長1000ms,不存在時設(shè)置 1.1.1.1:6379> set msg helloworld px 1000 nx

    解鎖

    解鎖一般使用 DEL 命令,但是直接刪除鎖可能存在問題。

    一般解鎖需要兩步操作:

  • 查詢當(dāng)前“鎖”是否還是我們持有,因?yàn)榇嬖谶^期時間,所以可能等你想解鎖的時候,“鎖”已經(jīng)到期,然后被其他線程獲取了,所以我們在解鎖前需要先判斷自己是否還持有“鎖”。

  • 如果“鎖”還是我們持有,則執(zhí)行解鎖操作,也就是刪除該鍵值對,并返回成功;否則,直接返回失敗。

  • 由于當(dāng)前 Redis 還沒有原子命令直接支持這兩步操作,所以當(dāng)前通常是使用 Lua 腳本來執(zhí)行解鎖操作,Redis 會保證腳本里的內(nèi)容執(zhí)行是一個原子操作

    以下是 Redis 官方給出的 Lua 腳本:

    if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1]) elsereturn 0 end

    參數(shù)說明如下:

    • KEYS[1]:我們要解鎖的 key。
    • ARGV[1]:我們加鎖時的 value,用于判斷當(dāng)“鎖”是否還是我們持有,如果被其他線程持有了,value 就會發(fā)生變化。

    續(xù)鎖

    一般為了防止死鎖,比如服務(wù)器宕機(jī)或斷線的情況下無法手動解鎖,此時就需要給分布式鎖加上過期時間

    但是假如在我們業(yè)務(wù)執(zhí)行的過程中,Redis 分布式鎖過期了,業(yè)務(wù)還沒處理完怎么辦?

    首先,我們在設(shè)置過期時間時要結(jié)合業(yè)務(wù)場景去設(shè)計(jì),盡量設(shè)置一個比較合理的值,就是理論上正常處理的話,在這個過期時間內(nèi)是一定能處理完畢的。

    然后我們需要應(yīng)對一些特殊惡劣情況進(jìn)行設(shè)計(jì)。

    目前的解決方案一般有兩種:

  • 守護(hù)線程“續(xù)命”:額外起一個線程,定期檢查線程是否還持有鎖,如果有則延長過期時間。Redisson 里面就實(shí)現(xiàn)了這個方案,使用“看門狗”定期檢查(每1/3的鎖時間檢查1次),如果線程還持有鎖,則刷新過期時間。
  • 超時回滾:當(dāng)我們解鎖時發(fā)現(xiàn)鎖已經(jīng)被其他線程獲取了,說明此時我們執(zhí)行的操作已經(jīng)是“不安全”的了,此時需要進(jìn)行事務(wù)回滾,并返回失敗。
  • 同時,需要進(jìn)行告警,人為介入驗(yàn)證數(shù)據(jù)的正確性,然后找出超時原因,是否需要對超時時間進(jìn)行優(yōu)化等等。

    守護(hù)線程“續(xù)命”存在的問題

    Redisson 使用看門狗(守護(hù)線程)“續(xù)命”的方案在大多數(shù)場景下是挺不錯的,也被廣泛應(yīng)用于生產(chǎn)環(huán)境,但是在極端情況下還是會存在問題。

    問題例子如下:

  • 線程A首先獲取鎖成功,將鍵值對寫入 Redis 的 master 節(jié)點(diǎn)。
  • 在 Redis 將該鍵值對同步到 Slave 節(jié)點(diǎn)之前,Master 發(fā)生了故障。
  • Redis 觸發(fā)故障轉(zhuǎn)移,其中一個 Slave 升級為新的 master。
  • 此時新的 Master 并不包含線程A寫入的鍵值對,因此線程B嘗試獲取鎖也可以成功拿到鎖。
  • 此時相當(dāng)于有兩個線程獲取到了鎖,可能會導(dǎo)致各種預(yù)期之外的情況發(fā)生,例如最常見的臟數(shù)據(jù)。
  • 解決方法:上述問題的根本原因主要是由于 Redis 異步復(fù)制帶來的數(shù)據(jù)不一致問題導(dǎo)致的,因此解決的方向就是保證數(shù)據(jù)的一致。

    當(dāng)前比較主流的解法和思路有兩種:

  • Redis 作者提出的 RedLock。
  • Zookeeper 實(shí)現(xiàn)的分布式鎖。
  • 這里我們來說一下第一種 RedLock 的解決思路。

    RedLock

    紅鎖是Redis作者提出的一致性解決方案。紅鎖的本質(zhì)是一個概率問題:如果一個主從架構(gòu)的Redis在高可用切換期間丟失鎖的概率是k%,那么相互獨(dú)立的 N 個 Redis 同時丟失鎖的概率是多少?如果用紅鎖來實(shí)現(xiàn)分布式鎖,那么丟鎖的概率是(k%)^N。鑒于Redis極高的穩(wěn)定性,此時的概率已經(jīng)完全能滿足產(chǎn)品的需求。

    說明紅鎖的實(shí)現(xiàn)并非這樣嚴(yán)格,一般保證M(1<M=<N)個同時鎖上即可,但通常仍舊可以滿足需求。

    RedLock 算法

    算法很易懂,起 5 個 master 節(jié)點(diǎn),分布在不同的機(jī)房盡量保證可用性。為了獲得鎖,client 會進(jìn)行如下操作:

  • 得到當(dāng)前的時間,微秒單位。
  • 嘗試順序地在 5 個實(shí)例上申請鎖,當(dāng)然需要使用相同的 key 和 random value,這里一個 client 需要合理設(shè)置與 master 節(jié)點(diǎn)溝通的 timeout 大小,避免長時間和一個 fail 了的節(jié)點(diǎn)浪費(fèi)時間。
  • 當(dāng) client 在大于等于 3 個 master 上成功申請到鎖的時候,且它會計(jì)算申請鎖消耗了多少時間,這部分消耗的時間采用獲得鎖的當(dāng)下時間減去第一步獲得的時間戳得到,如果鎖的持續(xù)時長(lock validity time)比流逝的時間多的話,那么鎖就真正獲取到了。
  • 如果鎖申請到了,那么鎖真正的 lock validity time 應(yīng)該是 origin(lock validity time) - 申請鎖期間流逝的時間。
  • 如果 client 申請鎖失敗了,那么它就會在少部分申請成功鎖的 master 節(jié)點(diǎn)上執(zhí)行釋放鎖的操作,重置狀態(tài)。
  • 失敗重試

    如果一個 client 申請鎖失敗了,那么它需要稍等一會在重試避免多個 client 同時申請鎖的情況,最好的情況是一個 client 需要幾乎同時向 5 個 master 發(fā)起鎖申請。另外就是如果 client 申請鎖失敗了它需要盡快在它曾經(jīng)申請到鎖的 master 上執(zhí)行 unlock 操作,便于其他 client 獲得這把鎖,避免這些鎖過期造成的時間浪費(fèi),當(dāng)然如果這時候網(wǎng)絡(luò)分區(qū)使得 client 無法聯(lián)系上這些 master,那么這種浪費(fèi)就是不得不付出的代價(jià)了。

    RedLock 的問題
    • 占用的資源過多,為了實(shí)現(xiàn)紅鎖,需要創(chuàng)建多個互不相關(guān)的云Redis實(shí)例或者自建Redis,成本較高。
    • 嚴(yán)重依賴系統(tǒng)時鐘。如果線程1從3個實(shí)例獲取到了鎖,但是這3個實(shí)例中的某個實(shí)例的系統(tǒng)時間走的稍微快一點(diǎn),則它持有的鎖會提前過期被釋放,當(dāng)他釋放后,此時又有3個實(shí)例是空閑的,則線程2也可以獲取到鎖,則可能出現(xiàn)兩個線程同時持有鎖了。
    • 如果線程1從3個實(shí)例獲取到了鎖,但是萬一其中有1臺重啟了,則此時又有3個實(shí)例是空閑的,則線程2也可以獲取到鎖,此時又出現(xiàn)兩個線程同時持有鎖了。

    總結(jié)

    以上是生活随笔為你收集整理的备战面试日记(6.1) - (缓存相关.Redis全知识点)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。