备战面试日记(6.1) - (缓存相关.Redis全知识点)
本人本科畢業(yè),21屆畢業(yè)生,一年工作經(jīng)驗(yàn),簡歷專業(yè)技能如下,現(xiàn)根據(jù)簡歷,并根據(jù)所學(xué)知識(shí)復(fù)習(xí)準(zhǔn)備面試。
記錄日期:2022.1.15
大部分知識(shí)點(diǎn)只做大致介紹,具體內(nèi)容根據(jù)推薦博文鏈接進(jìn)行詳細(xì)復(fù)習(xí)。
文章目錄
- Redis
- 數(shù)據(jù)結(jié)構(gòu)與對(duì)象
- 數(shù)據(jù)類型分類(對(duì)象)
- 數(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ù)集合升級(jí)
- 升級(jí)的好處
- 整數(shù)集合降級(jí)
- 壓縮列表
- 壓縮列表定義
- 列表節(jié)點(diǎn)構(gòu)成
- previous_entry_length
- encoding
- content
- 連鎖更新
- 編碼轉(zhuǎn)換時(shí)機(jī)
- 字符串
- int
- raw
- embstr
- 為什么raw和embstr的臨界值是44字節(jié)?
- 列表
- ziplist
- linkedlist
- 哈希
- ziplist
- hashtable
- 集合
- intset
- hashtable
- 有序集合
- ziplist
- skiplist
- 持久化
- RDB
- 觸發(fā)方式
- 手動(dòng)觸發(fā)
- 自動(dòng)觸發(fā)
- RDB優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn)
- 缺點(diǎn)
- AOF
- 執(zhí)行流程
- 觸發(fā)方式
- 手動(dòng)觸發(fā)
- 自動(dòng)觸發(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 對(duì) LRU 的實(shí)現(xiàn)
- LFU
- LFU 篩選邏輯
- LFU 的具體實(shí)現(xiàn)
- Redis 對(duì) LFU 的實(shí)現(xiàn)
- LFU 中的 counter 值的衰減機(jī)制
- 使用總結(jié)
- 事務(wù)
- 概念
- 事務(wù)階段
- 事務(wù)錯(cuò)誤處理
- Watch 監(jiān)控
- 引入
- watch 命令
- unwatch 命令
- 總結(jié)說明
- Redis集群
- 主從復(fù)制
- 主從復(fù)制架構(gòu)
- 開啟主從復(fù)制方式
- 命令
- 配置
- 啟動(dòng)命令
- 復(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)對(duì)熱點(diǎn)緩存區(qū)別
- 擴(kuò)容和縮容區(qū)別
- 集群的槽指派
- 指派節(jié)點(diǎn)槽信息
- CLUSTER ADDSLOTS 的命令實(shí)現(xiàn)
- 傳播節(jié)點(diǎn)槽信息
- 記錄集群所有槽的指派信息
- 使用 `clusterState.slots` 和使用 `clusterNode.slots` 保存指派信息相比的好處?
- 集群執(zhí)行命令
- MOVED 錯(cuò)誤
- 節(jié)點(diǎn)數(shù)據(jù)庫的實(shí)現(xiàn)
- 重新分片(比如在線擴(kuò)容)
- ASK 錯(cuò)誤 - (保證集群在線擴(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 分布式鎖
- 引入
- 為什么需要分布式鎖?
- 什么時(shí)候用分布式鎖?
- 分布式鎖需要哪些特性呢?
- 加鎖
- 解鎖
- 續(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)與對(duì)象
數(shù)據(jù)類型分類(對(duì)象)
數(shù)據(jù)類型概述
Redis主要有5種數(shù)據(jù)類型,包括String,List,Set,Zset,Hash,滿足大部分的使用要求。
| STRING | 字符串、整數(shù)或者浮點(diǎn)數(shù) | 對(duì)整個(gè)字符串或者字符串的其中一部分執(zhí)行操作;對(duì)整數(shù)和浮點(diǎn)數(shù)執(zhí)行自增或者自減操作。 | 做簡單的鍵值對(duì)緩存 |
| LIST | 列表 | 從兩端壓入或者彈出元素;對(duì)單個(gè)或者多個(gè)元素進(jìn)行修剪;只保留一個(gè)范圍內(nèi)的元素 | 存儲(chǔ)一些列表型的數(shù)據(jù)結(jié)構(gòu),類似粉絲列表、文章的評(píng)論列表之類的數(shù)據(jù) |
| SET | 無序集合 | 添加、獲取、移除單個(gè)元素;檢查一個(gè)元素是否存在于集合中;計(jì)算交集、并集、差集;從集合里面隨機(jī)獲取元素 | 交集、并集、差集的操作,比如交集,可以把兩個(gè)人的粉絲列表整一個(gè)交集 |
| HASH | 包含鍵值對(duì)的無序散列表 | 添加、獲取、移除單個(gè)鍵值對(duì);獲取所有鍵值對(duì);檢查某個(gè)鍵是否存在 | 結(jié)構(gòu)化的數(shù)據(jù),比如一個(gè)對(duì)象 |
| ZSET | 有序集合 | 添加、獲取、刪除元素;根據(jù)分值范圍或者成員來獲取元素;計(jì)算一個(gè)鍵的排名 | 去重但可以排序,如獲取排名前幾名的用戶 |
另外還有高級(jí)的4種數(shù)據(jù)類型:
- HyperLogLog:通常用于基數(shù)統(tǒng)計(jì)。使用少量固定大小的內(nèi)存,來統(tǒng)計(jì)集合中唯一元素的數(shù)量。統(tǒng)計(jì)結(jié)果不是精確值,而是一個(gè)帶有0.81%標(biāo)準(zhǔn)差(standard error)的近似值。所以,HyperLogLog適用于一些對(duì)于統(tǒng)計(jì)結(jié)果精確度要求不是特別高的場景,例如網(wǎng)站的UV統(tǒng)計(jì)。
- Geo:redis 3.2 版本的新特性。可以將用戶給定的地理位置信息儲(chǔ)存起來, 并對(duì)這些信息進(jìn)行操作:獲取2個(gè)位置的距離、根據(jù)給定地理位置坐標(biāo)獲取指定范圍內(nèi)的地理位置集合。
- Bitmap:位圖。
- Stream:主要用于消息隊(duì)列,類似于 kafka,可以認(rèn)為是 pub/sub 的改進(jìn)版。提供了消息的持久化和主備復(fù)制功能,可以讓任何客戶端訪問任何時(shí)刻的數(shù)據(jù),并且能記住每一個(gè)客戶端的訪問位置,還能保證消息不丟失。
編碼和底層實(shí)現(xiàn)
主要是講述上述五種基本類型的底層編碼實(shí)現(xiàn):
| REDIS_STRING | REDIS_ENCODING_INT | 使用整數(shù)值來實(shí)現(xiàn)的字符串對(duì)象 |
| REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用embstr編碼的簡單動(dòng)態(tài)字符串實(shí)現(xiàn)的字符串對(duì)象 |
| REDIS_STRING | REDIS_ENCODING_RAW | 使用簡單動(dòng)態(tài)字符串實(shí)現(xiàn)的字符串對(duì)象 |
| REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用壓縮列表實(shí)現(xiàn)的列表對(duì)象 |
| REDIS_LIST | REDIS_ENCODING_LINKEDLIST | 使用雙端鏈表實(shí)現(xiàn)的列表對(duì)象 |
| REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用壓縮列表實(shí)現(xiàn)的哈希對(duì)象 |
| REDIS_HASH | REDIS_ENCODING_HT | 使用字典實(shí)現(xiàn)的哈希對(duì)象 |
| REDIS_SET | REDIS_ENCODING_INTSET | 使用整數(shù)集合實(shí)現(xiàn)的集合對(duì)象 |
| REDIS_SET | REDIS_ENCODING_HT | 使用字典實(shí)現(xiàn)的集合對(duì)象 |
| REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用壓縮列表實(shí)現(xiàn)的有序集合對(duì)象 |
| REDIS_ZSET | REDIS_ENCODING_SKIPLIST | 使用跳躍表和字典實(shí)現(xiàn)的有序集合對(duì)象 |
參考《Redis設(shè)計(jì)與實(shí)現(xiàn)》第一部分 數(shù)據(jù)結(jié)構(gòu)與對(duì)象 的 第八章 對(duì)象,p63。
通過上面的整理我們就可以知道他們的具體編碼實(shí)現(xiàn)了,整理如下:
- String:SDS
- list:壓縮列表、雙向鏈表。
- hash:壓縮列表、字典。
- set:整數(shù)集合、字典。
- zset:壓縮列表、跳表。
在Redis中我們可以通過 OBJECT ENCODING命令來查看一個(gè)數(shù)據(jù)庫鍵的值對(duì)象的編碼:
redis> SET msg "hello world" OK redis> OBJECT ENCODING msg "embstr"關(guān)于他們具體在什么時(shí)候使用什么編碼格式,我們?cè)谙挛脑敿?xì)說明!
數(shù)據(jù)結(jié)構(gòu)
主要說明七種對(duì)象:簡單動(dòng)態(tài)字符串、鏈表、字典、跳躍表、整數(shù)集合、壓縮列表。
SDS字符串
簡單動(dòng)態(tài)字符串(SDS),用作Redis的默認(rèn)字符串表示。
SDS定義
每個(gè) sds.h/sdshdr 結(jié)構(gòu)標(biāo)識(shí)一個(gè)SDS值:
struct sdshdr {int len; // 記錄buf數(shù)組中已使用的字節(jié)數(shù)量,等于SDS所保存字符串的長度int free; // 記錄buf數(shù)組中未使用的字節(jié)數(shù)量char buf[]; // 字節(jié)數(shù)組,用于保存字符串 }tip:buf數(shù)組最后一個(gè)字節(jié)會(huì)用來保存’/0’,這也是遵循C字符串以空字符結(jié)尾的慣例,但是這個(gè)字符不會(huì)被計(jì)算在len長度中。
遵循的好處就是它可以直接重用一部分C字符串函數(shù)庫里面的函數(shù)。
SDS 與 C字符串的區(qū)別
如果一張表來說明,即:
| 獲取字符串長度的復(fù)雜度為O(N) | 獲取字符串長度的復(fù)雜度為O(1) |
| API是不安全的,可能會(huì)造成緩沖區(qū)溢出 | API是安全的,不會(huì)造成緩沖區(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í)行時(shí)自動(dòng)完成的。
緩沖區(qū)溢出
因?yàn)镃字符串沒有記錄字符串長度,所以如果使用如下方法:
char *strcat(char *dest, const char *src);當(dāng)開發(fā)者已經(jīng)為 dest 字符串分配了一定的內(nèi)存,此時(shí)如果 src 字符串中內(nèi)容拼接進(jìn)去后的內(nèi)存大于分配的內(nèi)存,則會(huì)造成緩沖區(qū)溢出。
那么SDS字符串是如何解決的呢?
當(dāng) SDS API 需要對(duì) SDS 進(jìn)行修改時(shí),API 會(huì)先檢查 SDS 的空間是否滿足所需的要求,如果不滿足的話,API 會(huì)自動(dòng)將 SDS 的空間擴(kuò)展至執(zhí)行修改所需的大小,然后才執(zhí)行實(shí)際的修改操作,所以使用 SDS 既不需要后動(dòng)修改 SDS 的空間大小,也不會(huì)出現(xiàn)C字符串中的緩沖區(qū)溢出問題。
內(nèi)存重分配次數(shù)
因?yàn)镃字符串的底層實(shí)現(xiàn)總是 N + 1 個(gè)字符串長度的數(shù)組。所以每次執(zhí)行 增長字符串 或是 縮短字符串時(shí),都要先通過重分配擴(kuò)展底層數(shù)組的空間大小 或是 釋放字符串不再使用的空間,來防止緩沖區(qū)溢出 或者 內(nèi)存泄漏。
那么SDS字符串是如何解決的呢?
SDS中使用free屬性記錄未使用空間的字節(jié)數(shù)量。
通過未使用的空間,SDS 實(shí)現(xiàn)了 空間預(yù)分配 和 惰性空間釋放 兩種優(yōu)化策略。
空間預(yù)分配的操作是:當(dāng) SDS 的 API 對(duì)一個(gè) SDS 進(jìn)行修改,并且需要對(duì) SDS 進(jìn)行空間擴(kuò)展的時(shí)候,程序不僅會(huì)為 SDS 分配修改所必須要的空間,還會(huì)為 SDS 分配額外的未使用空間。
這里存在兩種修改情況:
惰性空間釋放的操作是:當(dāng) SDS 的 API 對(duì) 一個(gè) SDS 進(jìn)行修改,并且需要對(duì) SDS 所保存的字符串進(jìn)行縮短時(shí),程序并不立即使用內(nèi)存重分配來回收縮短后多出來的字節(jié),而是使用 free屬性 將這些字節(jié)的數(shù)量記錄起來,并等待將來使用。
當(dāng)然,如果需要真正地釋放 SDS 的未使用空間,會(huì)有 API 去實(shí)現(xiàn),這里不說明。
二進(jìn)制安全
C字符串的字符必須符合某種編碼(比如ASCII),并且除了末尾空字符外,不能包含任何空字符,否則會(huì)被程序誤認(rèn)為是末尾,這使得C字符串只能保存文本數(shù)據(jù),而不能保存二進(jìn)制數(shù)據(jù)。
那么SDS字符串是如何解決的呢?
SDS 的 API 都是二進(jìn)制安全的,所有的 SDS API 都會(huì)以處理二進(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ù)。
鏈表
鏈表定義
每個(gè)鏈表節(jié)點(diǎn)使用一個(gè) adlist.h/listNode 結(jié)構(gòu)來表示:
typedef struct listNode {struct listNode *prev; // 前置指針struct listNode *next; // 后置指針void *value; // 節(jié)點(diǎn)的值 }說明該鏈表是一個(gè)雙向鏈表。
當(dāng)我們使用多個(gè) listNode 組成鏈表,就會(huì)直接使用 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)值對(duì)比函數(shù) }特性總結(jié)
- 雙端:節(jié)點(diǎn)有 prev 和 next 指針,復(fù)雜度為O(1)。
- 無環(huán):對(duì)鏈表的訪問都是以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)表示,每個(gè) dictEntry 結(jié)構(gòu)都保存著一個(gè)kv對(duì):
typedef struct dictEntry {void *key; // 鍵union { // 值void *val;uint64_t u64;uint64_t s64;}struct dictEntry *next; // 指向下個(gè)哈希表節(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)行時(shí),值為1 }哈希沖突
哈希算法
在添加新的鍵值到字典里是,要先進(jìn)行對(duì)key的哈希,根據(jù)哈希值計(jì)算出索引值,根據(jù)索引將新的kv對(duì)放到哈希表數(shù)組的指定索引上。
index = hash&dict -> ht[0].sizemaskRedis 使用 MurmurHash 算法。
解決哈希沖突
Redis 的哈希表使用鏈地址法解決哈希沖突,并且使用的是頭插法。
rehash
hash 對(duì)象在擴(kuò)容時(shí)使用了一種叫 “漸進(jìn)式 rehash” 的方式。
rehash概述
擴(kuò)展和收縮哈希表的工作都是通過執(zhí)行 rehash 來完成的。
reash的步驟如下:
計(jì)算新表(ht[1])的空間大小,取決于舊表(ht[0])當(dāng)前包含的鍵值以及數(shù)量。
將保存在舊表(ht[0])的所有鍵值rehash到新表(ht[1])上。
當(dāng)舊表(ht[0])全部遷移完成后,釋放舊表(ht[0]),將新表設(shè)置為 ht[0] 并在 ht[1]重新創(chuàng)建一張空白哈希表。
這兩個(gè)哈希表的套路是不是有點(diǎn)像jvm運(yùn)行時(shí)數(shù)據(jù)區(qū)的年輕代的幸存者區(qū)?可以引申一下。
rehash條件
當(dāng)下面兩個(gè)條件任意一個(gè)被滿足時(shí),程序就會(huì)自動(dòng)開始對(duì)哈希表進(jìn)行擴(kuò)展操作:
為什么這兩個(gè)命令的是否正在執(zhí)行,和服務(wù)器執(zhí)行擴(kuò)展操作的負(fù)載因子并不相同?
答:是因?yàn)樵趫?zhí)行BGSAVE命令或者BGREWRITEAOF命令的過程中,Redis需要fork子線程,而大多數(shù)os都采用與時(shí)復(fù)制技術(shù)來優(yōu)化子進(jìn)程的使用效率,所以子進(jìn)程存在的期間,服務(wù)器會(huì)提高執(zhí)行擴(kuò)展操作所需的負(fù)載因子,從而盡可能地避免在子進(jìn)程存在期間進(jìn)行哈希擴(kuò)容,可以避免不必要的內(nèi)存寫入操作,節(jié)約內(nèi)存。
與時(shí)復(fù)制:copy-on-write,即不用復(fù)制寫入直接引用父進(jìn)程的物理過程。
BGSAVE命令:fork子進(jìn)程去完成備份持久化。(區(qū)別于SAVE命令,阻塞線程去完成備份持久化)
BGREWRITEAOF命令:異步執(zhí)行AOF重寫,優(yōu)化原文件大小(該命令執(zhí)行失敗不會(huì)丟失數(shù)據(jù),成功才會(huì)真正修改數(shù)據(jù),2.4以后手動(dòng)觸發(fā)該命令)
漸進(jìn)式hash過程
漸進(jìn)式rehash的詳細(xì)步驟:
漸進(jìn)式hash采取 分而治之 的思想,將rehash鍵值對(duì)所需的計(jì)算工作均攤到字典的每個(gè)添加、刪除、查找、更新操作上,避免集中式hash。
漸進(jìn)式hash執(zhí)行期間進(jìn)行哈希表操作
漸進(jìn)式hash的缺點(diǎn)
擴(kuò)容期開始時(shí),會(huì)先給 ht[1] 申請(qǐng)空間,所以在整個(gè)擴(kuò)容期間,會(huì)同時(shí)存在 ht[0] 和 ht[1],會(huì)占用額外的空間。
擴(kuò)容期間同時(shí)存在 ht[0] 和 ht[1],查找、刪除、更新等操作有概率需要操作兩張表,耗時(shí)會(huì)增加。
redis 在內(nèi)存使用接近 maxmemory 并且有設(shè)置驅(qū)逐策略的情況下,出現(xiàn) rehash 會(huì)使得內(nèi)存占用超過 maxmemory,觸發(fā)驅(qū)逐淘汰操作,導(dǎo)致 master/slave 均有有大量的 key 被驅(qū)逐淘汰,從而出現(xiàn) master/slave 主從不一致。
跳躍表
可以把他理解為一個(gè)可以二分查找的鏈表。
它在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選擇了跳躍表而不是紅黑樹?
- 在做范圍查找的時(shí)候,平衡樹比 skiplist 操作要復(fù)雜。
- 在平衡樹上,我們找到指定范圍的小值之后,還需要以中序遍歷的順序繼續(xù)尋找其它不超過大值的節(jié)點(diǎn)。如果不對(duì)平衡樹進(jìn)行一定的改造,這里的中序遍歷并不容易實(shí)現(xiàn)。
- 而在 skiplist 上進(jìn)行范圍查找就非常簡單,只需要在找到小值之后,對(duì)第1層鏈表進(jìn)行若干步的遍歷就可以實(shí)現(xiàn)。
- 平衡樹的插入和刪除操作可能引發(fā)子樹的調(diào)整,邏輯復(fù)雜,而 skiplist 的插入和刪除只需要修改相鄰節(jié)點(diǎn)的指針,操作簡單又快速。
- 從內(nèi)存占用上來說,skiplist比平衡樹更靈活一些。
- 平衡樹每個(gè)節(jié)點(diǎn)包含2個(gè)指針(分別指向左右子樹)。
- skiplist 每個(gè)節(jié)點(diǎn)包含的指針數(shù)目平均為1/(1-p),具體取決于參數(shù)p的大小。如果像Redis里的實(shí)現(xiàn)一樣,取p=1/4,那么平均每個(gè)節(jié)點(diǎn)包含1.33個(gè)指針,比平衡樹更有優(yōu)勢(shì)。
- 查找單個(gè)key,skiplist和平衡樹的時(shí)間復(fù)雜度都為O(log n),大體相當(dāng);而哈希表在保持較低的哈希值沖突概率的前提下,查找時(shí)間復(fù)雜度接近O(1),性能更高一些。所以我們平常使用的各種 Map 或 dictionary 結(jié)構(gòu),大都是基于哈希表實(shí)現(xiàn)的。
- 從算法實(shí)現(xiàn)難度上來比較,skiplist 比平衡樹要簡單得多。
整數(shù)集合
整數(shù)集合定義
每個(gè) intset.h/intset 結(jié)構(gòu)表示一個(gè)整數(shù)集合:
typedef struct intset {uint32_t encoding; // 編碼方式uint32_t length; // 集合包含的元素?cái)?shù)量int8_t contents[]; // 保存元素的數(shù)組 }其中 contents[]就是整數(shù)集合的底層實(shí)現(xiàn):整數(shù)集合的每個(gè)元素都是該數(shù)組的一個(gè)數(shù)組項(xiàng),各個(gè)項(xiàng)在數(shù)組中是從小到大有序排列,并且不重復(fù)。
雖然 contents[] 屬性聲明是 int8_t,但是真正類型取決于 encoding。
整數(shù)集合升級(jí)
整數(shù)升級(jí),即當(dāng)我們將一個(gè)新元素添加到集合中時(shí),新元素的類型比原集合的類型都要長時(shí),整數(shù)集合需要升級(jí),然后才能將新元素添加到集合中。
具體升級(jí)并添加元素的步驟分為三步:
該過程的復(fù)雜度為 O(N)。
升級(jí)的好處
整數(shù)集合降級(jí)
整數(shù)集合不支持降級(jí)操作!
壓縮列表
它的存在意義就是為了節(jié)約內(nèi)存。
壓縮列表定義
壓縮列表就是一個(gè)由一系列特殊編碼的連續(xù)內(nèi)存塊組成的順序型數(shù)據(jù)結(jié)構(gòu)。
壓縮列表的各個(gè)組成部分說明如下表:
| zlbytes | uint32_t | 4字節(jié) | 記錄整個(gè)壓縮鏈表占用的字節(jié)數(shù),在對(duì)壓縮列表進(jìn)行內(nèi)存重分配,或者計(jì)算zlend的位置時(shí)使用。 |
| zltail | uint32_t | 4字節(jié) | 記錄壓縮列表表尾節(jié)點(diǎn)距離壓縮列表起始地址有多少個(gè)字節(jié):通過這個(gè)偏移量,程序無須遍歷整個(gè)壓縮列表就可以確定尾節(jié)點(diǎn)的地址。 |
| zllen | uint16_t | 2字節(jié) | 記錄了壓縮列表包含的字節(jié)數(shù)量,該屬性小于UINT16_MAX(65535)時(shí),該值為壓縮列表包含節(jié)點(diǎn)的數(shù)量;該屬性等于UINT16_MAX(65535)時(shí),節(jié)點(diǎn)的真實(shí)數(shù)量需要遍歷壓縮列表獲得。 |
| entryX | 列表節(jié)點(diǎn) | 不定 | 壓縮列表包含的各個(gè)節(jié)點(diǎn),節(jié)點(diǎn)的長度由節(jié)點(diǎn)保存的內(nèi)容而定。 |
| zlend | uint8_t | 1字節(jié) | 特殊值0xFF(十進(jìn)制255),用于標(biāo)記壓縮列表的末端。 |
列表節(jié)點(diǎn)構(gòu)成
每個(gè)壓縮列表節(jié)點(diǎn)可以保存一個(gè)字節(jié)數(shù)組或者一個(gè)整數(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之間的無符號(hào)整數(shù);
- 1字節(jié)長的有符號(hào);
- 3字節(jié)長的有符號(hào)整數(shù);
- int16_t類型整數(shù);
- int32_t類型整數(shù);
- int64_t類型整數(shù)。
每個(gè)壓縮列表節(jié)點(diǎn)都由 previous_entry_length、encoding、content三個(gè)部分組成:
previous_entry_length
節(jié)點(diǎn)的 previous_entry_length 屬性以字節(jié)為單位,記錄了壓縮列表中前一個(gè)節(jié)點(diǎn)的長度。
previous_entry_length 屬性的長度可以是1字節(jié) 或者 5字節(jié):
- 如果前一節(jié)點(diǎn)的長度小于254字節(jié),那么 previous_entry_length 屬性的長度為1字節(jié):前一節(jié)點(diǎn)的長度就保存在這一個(gè)字節(jié)里面。
- 如果前一節(jié)點(diǎn)的長度大于等于254字節(jié),那么 previous_entry_length 屬性的長度為5字節(jié):其中屬性的第一字節(jié)會(huì)被設(shè)置為0xFE(十進(jìn)制254),而之后的四個(gè)字節(jié)則用于保存前一節(jié)點(diǎn)的長度。
它的好處就是,因?yàn)楣?jié)點(diǎn)的 previous_entry_length 屬性記錄了前一個(gè)節(jié)點(diǎn)的長度,所以程序可以通過指針運(yùn)算,根據(jù)當(dāng)前節(jié)點(diǎn)的起始地址來計(jì)算出前一節(jié)點(diǎn)的起始地址。
壓縮列表的從表尾向表頭遍歷操作就是使用這一原理實(shí)現(xiàn)的,只要我們擁有一個(gè)指向某個(gè)節(jié)點(diǎn)起始地址的指針,那么通過這個(gè)指針以及這個(gè)節(jié)點(diǎn)的 previous_entry_length 屬性,程序就可以一直向前一個(gè)節(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)值可以是一個(gè)字節(jié)數(shù)組或者整數(shù)值,值的類型和長度由節(jié)點(diǎn)的 encoding 屬性決定。
連鎖更新
redis中的壓縮列表在插入數(shù)據(jù)的時(shí)候可能存在連鎖擴(kuò)容的情況。
在壓縮列表中,節(jié)點(diǎn)需要存放上一個(gè)節(jié)點(diǎn)的長度:當(dāng)上一個(gè)entry節(jié)點(diǎn)長度小于254個(gè)字節(jié)的時(shí)候,將會(huì)一個(gè)字節(jié)的大小來存放entry中的數(shù)據(jù);但是當(dāng)上一個(gè)entry節(jié)點(diǎn)長度大于等于254個(gè)字節(jié)的時(shí)候,就會(huì)需要更大的空間來存放數(shù)據(jù)。
在壓縮列表中,會(huì)把大于等于254字節(jié)長度用5個(gè)字節(jié)來存儲(chǔ),第一個(gè)字節(jié)是254,當(dāng)讀到254的時(shí)候,將會(huì)確認(rèn)接下來的4個(gè)字節(jié)大小將是entry的長度數(shù)據(jù)。當(dāng)?shù)谝粋€(gè)字節(jié)為255的時(shí)候,就證明壓縮列表已經(jīng)到達(dá)末端。
由于表示長度的字節(jié)大小不一樣,當(dāng)新節(jié)點(diǎn)的插入可能會(huì)導(dǎo)致下一個(gè)節(jié)點(diǎn)原本存放表示上一節(jié)點(diǎn)的長度的空間大小不夠?qū)е滦枰獢U(kuò)容這一字段。相應(yīng)的該字段將會(huì)由一個(gè)字節(jié)擴(kuò)容到五個(gè)字節(jié),四個(gè)字節(jié)的長度變化,當(dāng)發(fā)生變化的節(jié)點(diǎn)原本長度在250到253之間的時(shí)候,將會(huì)導(dǎo)致下一個(gè)節(jié)點(diǎn)存儲(chǔ)上節(jié)點(diǎn)長度的空間發(fā)生變化,引起一個(gè)連鎖擴(kuò)容的情況,這一情況將會(huì)直到一個(gè)不需要擴(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)的下一個(gè)節(jié)點(diǎn)開始,如果下一個(gè)節(jié)點(diǎn)存放上一個(gè)字節(jié)的空間大小大于或等于當(dāng)前的節(jié)點(diǎn)長度,那么在存放了這一長度數(shù)據(jù)之后,該次連鎖擴(kuò)容直接宣告結(jié)束。如果下一個(gè)節(jié)點(diǎn)存放長度的空間不能容納當(dāng)前節(jié)點(diǎn)的長度,那么就會(huì)將下一個(gè)節(jié)點(diǎn)進(jìn)行擴(kuò)容,并重新申請(qǐng)內(nèi)存大小,并復(fù)制數(shù)據(jù),移動(dòng)指向尾部節(jié)點(diǎn)的指針。最后移動(dòng)到下一個(gè)節(jié)點(diǎn),在下一個(gè)循環(huán)中判斷是否需要繼續(xù)擴(kuò)容。
編碼轉(zhuǎn)換時(shí)機(jī)
Redis中的每個(gè)對(duì)象都由一個(gè) redisObject 結(jié)構(gòu)來表示:
typedef struct redisObject {unsigned type:4; // 類型unsigned encoding:4; // 編碼void *ptr; // 指向底層實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu)的指針 }類型包括基本的五種,編碼指對(duì)應(yīng)類型下的不同編碼實(shí)現(xiàn)。
Redis可以根據(jù)不同的使用場景,來為一個(gè)對(duì)象設(shè)置不同的編碼,從而優(yōu)化對(duì)象在某一場景下的效率。
字符串
字符串的編碼可以是 int 、 raw 或者是 embstr。
int
如果一個(gè)字符串對(duì)象保存的是整數(shù)值,并且這個(gè)整數(shù)值可以用long類型來表示,那么這個(gè)字符串對(duì)象會(huì)將整數(shù)值保存在字符串對(duì)象結(jié)構(gòu)的 ptr 屬性中(將 void* 轉(zhuǎn)換成 long),并將字符串對(duì)象的編碼設(shè)置為int。
raw
如果一個(gè)字符串對(duì)象保存的是一個(gè)字符串值,并且長度大于44字節(jié),那么這個(gè)字符串對(duì)象將使用簡單動(dòng)態(tài)字符串(SDS)來保存,并且編碼設(shè)置為 raw。
embstr
如果一個(gè)字符串對(duì)象保存的是一個(gè)字符串值,并且長度小于等于44字節(jié),那么同上,但是編碼設(shè)置為embstr。
embstr 是專門用于保存短字符串的優(yōu)化編碼方式。它和 raw 的區(qū)別在于,raw編碼會(huì)調(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 的好處:
不過,embstr 編碼沒有任何相應(yīng)的修改程序,它實(shí)際上只是只讀的,當(dāng) embstr 編碼的字符串執(zhí)行修改命令時(shí),總會(huì)變成 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)化,具體解釋如下:
我們知道對(duì)于每個(gè) sds 都有一個(gè) sdshdr,里面的 len 和 free 記錄了這個(gè) sds 的長度和空閑空間,但是這樣的處理十分粗糙,使用的 unsigned int 可以表示很大的范圍,但是對(duì)于很短的 sds 有很多的空間被浪費(fèi)了(兩個(gè)unsigned int 8個(gè)字節(jié))。而這個(gè) commit 則將原來的 sdshdr 改成了 sdshdr16 , sdshdr32 , sdshdr64 ,里面的 unsigned int 變成了 uint8_t ,uint16_t…(還加了一個(gè)char flags)這樣更加優(yōu)化小 sds 的內(nèi)存使用。
本身就是針對(duì)短字符串的 embstr 自然會(huì)使用最小的 sdshdr8 ,而 sdshdr8 與之前的 sdshdr 相比正好減少了5個(gè)字節(jié)(sdsdr8 = uint8_t * 2 + char = 1*2+1 = 3, sdshdr = unsigned int * 2 = 4 * 2 = 8),所以其能容納的字符串長度增加了5個(gè)字節(jié)變成了44。
列表
列表的編碼可以是 ziplist 或者 linkedlist。(壓縮列表 或者 雙向鏈表)
ziplist
如果列表對(duì)象保存的所有字符串元素的長度都小于64字節(jié),并且列表對(duì)象保存的元素?cái)?shù)量小于512個(gè)時(shí),編碼為 ziplist。
linkedlist
上面兩個(gè)條件,只要一個(gè)不滿足,就采取 linkedlist 編碼。
哈希
哈希對(duì)象的編碼可以是 ziplist 或者 hashtable。(壓縮列表 或者 字典)
ziplist
如果哈希對(duì)象保存的所有鍵值對(duì)的鍵和值的字符串長度都小于64字節(jié),并且哈希對(duì)象保存的鍵值對(duì)數(shù)量小于512個(gè)時(shí),編碼為 ziplist。
hashtable
上面兩個(gè)條件,只要一個(gè)不滿足,就采取 hashtable 編碼。
集合
集合對(duì)象的編碼可以是 intset 或者 hashtable。
intset
如果集合對(duì)象保存的所有元素都是整數(shù)值,并且哈希對(duì)象保存的元素?cái)?shù)量小于512個(gè)時(shí),編碼為 intset。
hashtable
上面兩個(gè)條件,只要一個(gè)不滿足,就采取 hashtable 編碼。
有序集合
有序集合的編碼可以是 ziplist 或者 skiplist。
ziplist
如果有序集合對(duì)象保存的所有元素成員的長度都小于64字節(jié),并且有序集合對(duì)象保存的元素?cái)?shù)量小于128個(gè)時(shí),編碼為 ziplist。
skiplist
上面兩個(gè)條件,只要一個(gè)不滿足,就采取 skiplist 編碼。
持久化
詳細(xì)了解參考文章:Redis的兩種持久化RDB和AOF(超詳細(xì))
Redis對(duì)數(shù)據(jù)的操作都是基于內(nèi)存的,當(dāng)遇到了進(jìn)程退出、服務(wù)器宕機(jī)等意外情況,如果沒有持久化機(jī)制,那么Redis中的數(shù)據(jù)將會(huì)丟失無法恢復(fù)。有了持久化機(jī)制,Redis在下次重啟時(shí)可以利用之前持久化的文件進(jìn)行數(shù)據(jù)恢復(fù)。
Redis支持的兩種持久化機(jī)制:
- RDB:把當(dāng)前數(shù)據(jù)生成快照保存在硬盤上。
- AOF:記錄每次對(duì)數(shù)據(jù)的操作到硬盤上。
- 混合持久化:在 redis 4 引入,RDB + AOF 混合使用的方式,RDB 持久化全量數(shù)據(jù),AOF 持久化增量數(shù)據(jù)。
RDB
RDB(Redis DataBase)持久化是把當(dāng)前Redis中全部數(shù)據(jù)生成快照保存在硬盤上。RDB持久化可以手動(dòng)觸發(fā),也可以自動(dòng)觸發(fā)。
觸發(fā)方式
手動(dòng)觸發(fā)
save 和 bgsave 命令都可以手動(dòng)觸發(fā)RDB持久化。
- 執(zhí)行save命令會(huì)手動(dòng)觸發(fā)RDB持久化,但是save命令會(huì)阻塞Redis服務(wù),直到RDB持久化完成。當(dāng)Redis服務(wù)儲(chǔ)存大量數(shù)據(jù)時(shí),會(huì)造成較長時(shí)間的阻塞,不建議使用。
- 執(zhí)行bgsave命令也會(huì)手動(dòng)觸發(fā)RDB持久化,和save命令不同是:Redis服務(wù)一般不會(huì)阻塞。Redis進(jìn)程會(huì)執(zhí)行fork操作創(chuàng)建子進(jìn)程,RDB持久化由子進(jìn)程負(fù)責(zé),不會(huì)阻塞Redis服務(wù)進(jìn)程。Redis服務(wù)的阻塞只發(fā)生在fork階段,一般情況時(shí)間很短。
- 執(zhí)行 bgsave 命令,Redis進(jìn)程先判斷當(dāng)前是否存在正在執(zhí)行的RDB或AOF子線程,如果存在就是直接結(jié)束。
- Redis進(jìn)程執(zhí)行 fork 操作創(chuàng)建子進(jìn)程,在fork操作的過程中Redis進(jìn)程會(huì)被阻塞。
- Redis進(jìn)程 fork 完成后, bgsave 命令就結(jié)束了,自此Redis進(jìn)程不會(huì)被阻塞,可以響應(yīng)其他命令。
- 子進(jìn)程根據(jù)Redis進(jìn)程的內(nèi)存生成快照文件,并替換原有的RDB文件。
- 子進(jìn)程通過信號(hào)量通知Redis進(jìn)程已完成。
簡單說明,save命令會(huì)全程阻塞,bgsave只在創(chuàng)建子線程時(shí)會(huì)阻塞。
自動(dòng)觸發(fā)
在以下幾種場景下,會(huì)自動(dòng)觸發(fā)RDB持久化:
RDB優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
缺點(diǎn)
AOF
執(zhí)行流程
觸發(fā)方式
手動(dòng)觸發(fā)
使用 bgrewriteaof 命令。
自動(dòng)觸發(fā)
根據(jù) auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 配置確定自動(dòng)觸發(fā)的時(shí)機(jī)。
- auto-aof-rewrite-min-size 表示運(yùn)行AOF重寫時(shí)文件大小的最小值,默認(rèn)為64MB。
- auto-aof-rewrite-percentage 表示當(dāng)前AOF文件大小和上一次重寫后AOF文件大小的比值的最小值,默認(rèn)為100。
只用前兩者同時(shí)超過閾值時(shí)才會(huì)自動(dòng)觸發(fā)文件重寫。
AOF文件同步策略
AOF持久化流程中的文件同步有以下幾個(gè)策略:
- 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 每個(gè)一秒將緩存區(qū)內(nèi)容寫入文件 默認(rèn)開啟的寫入方式 appendfsync everysec # 運(yùn)行AOF重寫時(shí)AOF文件大小的增長率的最小值 auto-aof-rewrite-percentage 100 # 運(yùn)行AOF重寫時(shí)文件大小的最小值 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ì)的四個(gè)組成部分的結(jié)構(gòu)如下所示:
它們分別是:
- 套接字
- IO多路復(fù)用程序
- 文件事件分派器
- 事件處理器
處理機(jī)制
文件事件處理器大致可分為三個(gè)處理流程:
拓展
關(guān)于Redis6.0的多線程升級(jí)博客參考鏈接:Redis6 新特性多線程解析
內(nèi)存淘汰機(jī)制
Redis 緩存使用內(nèi)存保存數(shù)據(jù),避免了系統(tǒng)直接從后臺(tái)數(shù)據(jù)庫讀取數(shù)據(jù),提高了響應(yīng)速度。由于緩存容量有限,當(dāng)緩存容量到達(dá)上限,就需要?jiǎng)h除部分?jǐn)?shù)據(jù)挪出空間,這樣新數(shù)據(jù)才可以添加進(jìn)來。Redis 定義了「淘汰機(jī)制」用來解決內(nèi)存被寫滿的問題。
緩存淘汰機(jī)制,也叫緩存替換機(jī)制,它需要解決兩個(gè)問題:
- 決定淘汰哪些數(shù)據(jù)。
- 如何處理那些被淘汰的數(shù)據(jù)。
內(nèi)存淘汰策略
截至在 4.0 之后,Redis定義了「8種內(nèi)存淘汰策略」用來處理 redis 內(nèi)存滿的情況:
- noeviction:不會(huì)淘汰任何數(shù)據(jù),當(dāng)使用的內(nèi)存空間超過 maxmemory 值時(shí),返回錯(cuò)誤。
- volatile-ttl:篩選設(shè)置了過期時(shí)間的鍵值對(duì),越早過期的越先被刪除。
- volatile-random:篩選設(shè)置了過期時(shí)間的鍵值對(duì),隨機(jī)刪除。
- volatile-lru:使用 LRU 算法篩選設(shè)置了過期時(shí)間的鍵值對(duì)。
- volatile-lfu:使用 LFU 算法選擇設(shè)置了過期時(shí)間的鍵值對(duì)。
- allkeys-random:在所有鍵值對(duì)中,隨機(jī)選擇并刪除數(shù)據(jù)。
- allkeys-lru:使用 LRU 算法在所有數(shù)據(jù)中進(jìn)行篩選。
- allkeys-lfu:使用 LFU 算法在所有數(shù)據(jù)中進(jìn)行篩選。
根據(jù)它們的名稱和前綴我們就能如下分類:
- 不淘汰數(shù)據(jù):noeviction。
- 淘汰數(shù)據(jù)
- 在設(shè)置了過期時(shí)間的鍵值對(duì)中進(jìn)行淘汰:volatile-ttl、volatile-random、volatile-lru、volatile-lfu。
- 對(duì)所有數(shù)據(jù)進(jìn)行淘汰:allkeys-random、allkeys-lru、allkeys-lfu。
策略介紹
noeviction
noeviction 策略,也是 Redis 的默認(rèn)策略,它要求 Redis 在使用的內(nèi)存空間超過 maxmemory 值時(shí),也不進(jìn)行數(shù)據(jù)淘汰。一旦緩存被寫滿了,再有寫請(qǐng)求來的時(shí)候,Redis 會(huì)直接返回錯(cuò)誤。
我們實(shí)際項(xiàng)目中,一般不會(huì)使用這種策略。因?yàn)槲覀儤I(yè)務(wù)數(shù)據(jù)量通常會(huì)超過緩存容量的,而這個(gè)策略不淘汰數(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í)候,只會(huì)篩選設(shè)置了過期時(shí)間的鍵值對(duì)上。
比如,我們使用 EXPIRE 命令對(duì)一批鍵值對(duì)設(shè)置了過期時(shí)間,那么會(huì)有兩種情況會(huì)對(duì)這些數(shù)據(jù)進(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í)數(shù)據(jù)篩選的數(shù)據(jù)范圍是所有鍵值對(duì)。
其中allkeys-random的篩選規(guī)則比較簡單,而allkeys-lru,allkeys-lfu分別用到了LRU 和 LFU 算法。
LRU & LFU算法
LRU
LRU 算法全稱 Least Recently Used,一種常見的頁面置換算法。按照「最近最少使用」的原則來篩選數(shù)據(jù),篩選出最不常用的數(shù)據(jù),而最近頻繁使用的數(shù)據(jù)會(huì)留在緩存中。
LRU 篩選邏輯
RU 會(huì)把所有的數(shù)據(jù)組織成一個(gè)鏈表,鏈表的頭和尾分別表示 MRU 端和 LRU 端,分別代表「最近最常使用」的數(shù)據(jù)和「最近最不常用」的數(shù)據(jù)。
每次訪問數(shù)據(jù)時(shí),都會(huì)把剛剛被訪問的數(shù)據(jù)移到 MRU 端,就可以讓它們盡可能地留在緩存中。
如果此時(shí)有新數(shù)據(jù)要寫入時(shí),并且沒有多余的緩存空間,那么該鏈表會(huì)做兩件事情:
簡單說明,即它認(rèn)為剛剛被訪問的數(shù)據(jù),肯定還會(huì)被再次訪問,所以就把它放在 MRU端;LRU 端的數(shù)據(jù)被認(rèn)為是長久不訪問的數(shù)據(jù),在緩存滿時(shí),就優(yōu)先刪除它。
Redis 對(duì) LRU 的實(shí)現(xiàn)
Redis 3.0 前,隨機(jī)選取 N 個(gè)淘汰法。
Redis 默認(rèn)會(huì)記錄每個(gè)數(shù)據(jù)的最近一次訪問的時(shí)間戳(由鍵值對(duì)數(shù)據(jù)結(jié)構(gòu) RedisObject 中的 lru 字段記錄)。
在 Redis 決定淘汰的數(shù)據(jù)時(shí),隨機(jī)選 N(默認(rèn)5) 個(gè) key,把空閑時(shí)間(idle time)最大的那個(gè) key 移除。這邊的 N 可通過 maxmemory-samples 配置項(xiàng)修改:
config set maxmemory-samples 100當(dāng)需要再次淘汰數(shù)據(jù)時(shí),Redis 需要挑選數(shù)據(jù)進(jìn)入「第一次淘汰時(shí)創(chuàng)建的候選集合」。
挑選的標(biāo)準(zhǔn)是:能進(jìn)入候選集合的數(shù)據(jù)的 lru 字段值必須小于「候選集合中最小的 lru 值」。
當(dāng)有新數(shù)據(jù)進(jìn)入備選數(shù)據(jù)集后,如果備選數(shù)據(jù)集中的數(shù)據(jù)個(gè)數(shù)達(dá)到了設(shè)置的閾值時(shí)。Redis 就把備選數(shù)據(jù)集中 lru 字段值最小的數(shù)據(jù)淘汰出去。
Redis3.0后,引入了緩沖池(默認(rèn)容量為16)概念。
當(dāng)每一輪移除 key 時(shí),拿到了 N(默認(rèn)5)個(gè) key 的 idle time,遍歷處理這 N 個(gè) key,如果 key 的 idle time 比 pool 里面的 key 的 idle time 還要大,就把它添加到 pool 里面去。
當(dāng) pool 放滿之后,每次如果有新的 key 需要放入,需要將 pool 中 idle time 最小的一個(gè) key 移除。這樣相當(dāng)于 pool 里面始終維護(hù)著還未被淘汰的 idle time 最大的 16 個(gè) key。
當(dāng)我們每輪要淘汰的時(shí)候,直接從 pool 里面取出 idle time 最大的 key(只取1個(gè)),將之淘汰掉。
整個(gè)流程相當(dāng)于隨機(jī)取 5 個(gè) key 放入 pool,然后淘汰 pool 中空閑時(shí)間最大的 key,然后再隨機(jī)取 5 個(gè) key放入 pool,繼續(xù)淘汰 pool 中空閑時(shí)間最大的 key,一直持續(xù)下去。
在進(jìn)入淘汰前會(huì)計(jì)算出需要釋放的內(nèi)存大小,然后就一直循環(huán)上述流程,直至釋放足夠的內(nèi)存。
LFU
在一些場景下,有些數(shù)據(jù)被訪問的次數(shù)非常少,甚至只會(huì)被訪問一次。當(dāng)這些數(shù)據(jù)服務(wù)完訪問請(qǐng)求后,如果還繼續(xù)留存在緩存中的話,就只會(huì)白白占用內(nèi)存空間。這種情況,就是緩存污染。
為了應(yīng)對(duì)緩存污染問題,Redis 從 4.0 版本開始增加了 LFU 淘汰策略。
LFU 緩存策略是在 LRU 策略基礎(chǔ)上,為每個(gè)數(shù)據(jù)增加了一個(gè)「計(jì)數(shù)器」,來統(tǒng)計(jì)這個(gè)數(shù)據(jù)的訪問次數(shù)。
LFU 篩選邏輯
- 當(dāng)使用 LFU 策略篩選淘汰數(shù)據(jù)時(shí),首先會(huì)根據(jù)數(shù)據(jù)的訪問次數(shù)進(jìn)行篩選,把訪問次數(shù)最低的數(shù)據(jù)淘汰出緩存。
- 如果兩個(gè)數(shù)據(jù)的訪問次數(shù)相同,LFU 策略再比較這兩個(gè)數(shù)據(jù)的訪問時(shí)效性,把距離上一次訪問時(shí)間更久的數(shù)據(jù)淘汰出緩存。
LFU 的具體實(shí)現(xiàn)
我們?cè)谇懊嬲f過,為了避免操作鏈表的開銷,Redis 在實(shí)現(xiàn) LRU 策略時(shí)使用了兩個(gè)近似方法:
- Redis 在 RedisObject 結(jié)構(gòu)中設(shè)置了 lru 字段,用來記錄數(shù)據(jù)的訪問時(shí)間戳。
- Redis 并沒有為所有的數(shù)據(jù)維護(hù)一個(gè)全局的鏈表,而是通過「隨機(jī)采樣」方式,選取一定數(shù)量的數(shù)據(jù)放入備選集合,后續(xù)在備選集合中根據(jù) lru 字段值的大小進(jìn)行篩選刪除。
在此基礎(chǔ)上,Redis 在實(shí)現(xiàn) LFU 策略的時(shí)候,只是把原來 24bit 大小的 lru 字段,又進(jìn)一步拆分成了兩部分:
- ldt 值:lru 字段的前 16bit,表示數(shù)據(jù)的訪問時(shí)間戳。
- counter 值:lru 字段的后 8bit,表示數(shù)據(jù)的訪問次數(shù)。
但是我們會(huì)發(fā)現(xiàn)一個(gè)問題,counter 值的最大記錄值只有255。當(dāng)幾個(gè)緩存數(shù)據(jù)的 counter 值 都達(dá)到255值,就無法正確根據(jù)訪問次數(shù)來決定數(shù)據(jù)的淘汰了。
所以Redis 針對(duì)這個(gè)問題進(jìn)行了優(yōu)化:在實(shí)現(xiàn) LFU 策略時(shí),Redis 并沒有采用數(shù)據(jù)每被訪問一次,就給對(duì)應(yīng)的 counter 值加 1 的計(jì)數(shù)規(guī)則,而是采用了一個(gè)更優(yōu)化的計(jì)數(shù)規(guī)則。
Redis 對(duì) LFU 的實(shí)現(xiàn)
Redis 實(shí)現(xiàn) LFU 策略時(shí)采用計(jì)數(shù)規(guī)則:
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í)際訪問次數(shù)情況下,計(jì)數(shù)器值的變化情況。
| 0 | 104 | 255 | 255 | 255 | 255 |
| 1 | 18 | 49 | 255 | 255 | 255 |
| 10 | 10 | 18 | 142 | 255 | 255 |
| 100 | 8 | 11 | 49 | 143 | 255 |
通過上表的分析:
- 當(dāng) lfu_log_factor 取值為 1 時(shí),實(shí)際訪問次數(shù)為 100K 后,counter 值就達(dá)到 255 了,無法再區(qū)分實(shí)際訪問次數(shù)更多的數(shù)據(jù)了。
- 當(dāng) lfu_log_factor 取值為 100 時(shí),當(dāng)實(shí)際訪問次數(shù)為 10M 時(shí),counter 值才達(dá)到 255。
使用這種非線性遞增的計(jì)數(shù)器方法,即使緩存數(shù)據(jù)的訪問次數(shù)成千上萬,LFU 策略也可以有效的區(qū)分不同的訪問次數(shù),從而合理的進(jìn)行數(shù)據(jù)篩選。
從剛才的表中,我們可以看到,當(dāng) lfu_log_factor 取值為 10 時(shí),百、千、十萬級(jí)別的訪問次數(shù)對(duì)應(yīng)的 counter 值 已經(jīng)有明顯的區(qū)分了。所以,我們?cè)趹?yīng)用 LFU 策略時(shí),一般可以將 lfu_log_factor 取值為 10。
但是對(duì)于一些業(yè)務(wù)場景,上方的設(shè)計(jì)會(huì)存在問題:比如說有些數(shù)據(jù)在「短時(shí)間內(nèi)被大量訪問后就不會(huì)再被訪問了」。
那么再按照訪問次數(shù)來篩選的話,這些數(shù)據(jù)會(huì)被留存在緩存中,但不會(huì)提升緩存命中率。
為此,Redis 在實(shí)現(xiàn) LFU 策略時(shí),還設(shè)計(jì)了一個(gè)「 counter 值的衰減機(jī)制」。
LFU 中的 counter 值的衰減機(jī)制
簡單來說,LFU 策略使用 lfu_decay_time(衰減因子配置項(xiàng)) 來控制訪問次數(shù)的衰減。
通過上方的第二點(diǎn),我們就能知道一個(gè)規(guī)律,lfu_decay_time 值越大,那么相應(yīng)的衰減值會(huì)變小,衰減效果也會(huì)減弱;反之相應(yīng)的衰減值會(huì)變大,衰減效果也會(huì)增強(qiáng)。
所以,如果業(yè)務(wù)應(yīng)用中有短時(shí)高頻訪問的數(shù)據(jù)的話,建議把 lfu_decay_time 值設(shè)置為 1。
使用總結(jié)
事務(wù)
Redis 事務(wù)相對(duì)于Mysql 事務(wù)來說較為簡單,大家可以將二者進(jìn)行對(duì)比,下文也會(huì)整理。
概念
Redis 事務(wù)的本質(zhì)是一組命令的集合。
事務(wù)支持一次執(zhí)行多個(gè)命令,一個(gè)事務(wù)中所有命令都會(huì)被序列化。在事務(wù)執(zhí)行過程,會(huì)按照順序串行化執(zhí)行隊(duì)列中的命令,其他客戶端提交的命令請(qǐng)求不會(huì)插入到事務(wù)執(zhí)行命令序列中。
簡單理解,Redis 中的事務(wù),就是具有一次性、順序性、排他性地在命令序列中執(zhí)行多個(gè)命令。
它的主要作用就是串聯(lián)多個(gè)命令防止別的命令插隊(duì)。
事務(wù)階段
我們可以把Redis 事務(wù)的執(zhí)行分為三個(gè)階段:
從輸入Multi命令開始,輸入的命令都會(huì)依次進(jìn)入命令隊(duì)列中,但不會(huì)執(zhí)行,直到輸入 Exec 后,Redis會(huì)將之前的命令隊(duì)列中的命令依次執(zhí)行。組隊(duì)的過程中可以通過 discard。
事務(wù)錯(cuò)誤處理
事務(wù)的錯(cuò)誤分為兩種情況:
- 如果組隊(duì)中某個(gè)命令報(bào)出了錯(cuò)誤,執(zhí)行時(shí)整個(gè)的所有隊(duì)列都會(huì)被取消。
- 如果執(zhí)行階段某個(gè)命令報(bào)出了錯(cuò)誤,則只有報(bào)錯(cuò)的命令不會(huì)被執(zhí)行,而其他的命令都會(huì)執(zhí)行,不會(huì)回滾。
這說明在 Redis 中,雖然單條命令是原子性執(zhí)行的,但是事務(wù)不保證原子性,且沒有回滾。事務(wù)中任意命令執(zhí)行失敗,其余的命令仍會(huì)被執(zhí)行。
Watch 監(jiān)控
引入
Redis 中的 悲觀鎖 和 樂觀鎖,簡單提及以下:
悲觀鎖(Pessimistic Lock),每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì)block直到它拿到鎖。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫里邊就用到了很多這種鎖機(jī)制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。
樂觀鎖(Optimistic Lock),每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人不會(huì)修改,所以不會(huì)上鎖,但是在更新的時(shí)候會(huì)判斷一下在此期間別人有沒有去更新這個(gè)數(shù)據(jù),可以使用版本號(hào)等機(jī)制。樂觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量。Redis就是利用這種check-and-set機(jī)制實(shí)現(xiàn)事務(wù)的。
watch 命令
在執(zhí)行 multi 之前,先執(zhí)行watch key1 [key2],可以監(jiān)視一個(gè)(或多個(gè)) key ,如果在事務(wù)執(zhí)行之前這個(gè)(或這些) key 被其他命令所改動(dòng),那么事務(wù)將被打斷。
舉例說明:
假如我賬戶上有100元,此時(shí)我們準(zhǔn)備再給賬戶充值50元,準(zhǔn)備買149元的傳說皮膚。
但是此時(shí),以一位糟糕的程序員修改了我們的賬戶,改成了999元。
我很生氣,因?yàn)槲页渲凳×?#xff0c;但是我去賬戶上一看,變成999元了,我馬上給自己一巴掌,“在生氣什么呢?”…
模擬上方情景,這是控制臺(tái)1的操作:
模擬上方情景,這是控制臺(tái)2的操作:
注意:只要執(zhí)行了EXEC,之前加的監(jiān)控鎖都會(huì)被取消!Redis的事務(wù)不保證原子性,一條命令執(zhí)行失敗了,其他的仍然會(huì)執(zhí)行,且不會(huì)回滾。
unwatch 命令
取消 WATCH 命令對(duì)所有 key 的監(jiān)視。
如果在執(zhí)行 WATCH 命令之后,EXEC 命令或 DISCARD 命令先被執(zhí)行了的話,那么就不需要再執(zhí)行 UNWATCH 了。
總結(jié)說明
redis 的事務(wù)不推薦在實(shí)際中使用,如果要使用事務(wù),推薦使用 Lua 腳本,redis 會(huì)保證一個(gè) Lua 腳本里的所有命令的原子性。
Redis集群
除去Redis的單例模式,Redis 的集群模式可以分為三種:主從復(fù)制、哨兵模式、集群模式。
主從復(fù)制
Redis 官方文檔【主從復(fù)制】:REDIS sentinel-old – Redis中國用戶組(CRUG)
主從復(fù)制架構(gòu)
主從復(fù)制,將 Redis 實(shí)例分為兩中角色,一種是被復(fù)制的服務(wù)器稱為主服務(wù)器(master),而對(duì)主服務(wù)器進(jìn)行復(fù)制的服務(wù)器被稱為從服務(wù)器(slave)。
當(dāng)主數(shù)據(jù)庫有數(shù)據(jù)寫入,會(huì)將數(shù)據(jù)同步復(fù)制給從節(jié)點(diǎn),一個(gè)主數(shù)據(jù)庫可以同時(shí)擁有多個(gè)從數(shù)據(jù)庫,而從數(shù)據(jù)庫只能擁有一個(gè)主數(shù)據(jù)庫。值得一提的是,從節(jié)點(diǎn)也可以有從節(jié)點(diǎn),呈現(xiàn)級(jí)聯(lián)結(jié)構(gòu)。
我們可以看到,在主從復(fù)制中,只有一個(gè)是主機(jī),其他的都是從機(jī),并且從機(jī)下面還可以有任意多個(gè)從機(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 #是否在后臺(tái)執(zhí)行,yes:后臺(tái)運(yùn)行;no:不是后臺(tái)運(yùn)行 daemonize yes # 復(fù)制選項(xiàng),slave復(fù)制對(duì)應(yīng)的master。 # replicaof <masterip> <masterport> #如果master設(shè)置了requirepass,那么slave要連上master,需要有master的密碼才行。masterauth就是用來 # 配置master的密碼,這樣可以在連上master后進(jìn)行認(rèn)證。 # masterauth <master-password>在啟動(dòng)節(jié)點(diǎn)時(shí)輸入命令
redis-server master.conf redis-server slave1.conf redis-server slave2.conf不過在docker容器中的Redis鏡像配置存在一些問題,大家自己找一下資料吧。
啟動(dòng)命令
參考博客鏈接:redis啟動(dòng)命令及集群創(chuàng)建
復(fù)制的實(shí)現(xiàn)【重點(diǎn)】
1. 設(shè)置主服務(wù)器的地址和端口
例如客戶端操作從服務(wù)器執(zhí)行如下命令:
127.0.0.1> SLAVEOF 127.0.0.1 6379從服務(wù)器會(huì)將客戶端給定的主服務(wù)器IP地址以及端口號(hào)保存到當(dāng)前從服務(wù)器狀態(tài)的 masterhost 屬性和 masterport 屬性中。
SLAVEOF 命令是一個(gè)異步命令,在完成屬性的設(shè)置工作后,從服務(wù)器會(huì)向客戶端返回"OK",之后開始執(zhí)行真正的復(fù)制工作。
2. 建立套接字連接
從服務(wù)器根據(jù)指定的 IP地址和端口號(hào),創(chuàng)建連向主服務(wù)器套接字(socket)連接。
主服務(wù)器在接受(accept) 從服務(wù)器的套接字連接之后,為該套接字創(chuàng)建相應(yīng)的客戶端狀態(tài)。
這個(gè)時(shí)候可以將從服務(wù)器理解為主服務(wù)器的客戶端。
3. 發(fā)送 PING 命令
從服務(wù)器向主服務(wù)器發(fā)送一個(gè) PING 命令,以檢査套接字的讀寫狀態(tài)是否正常、 主服務(wù)器能否正常處理命令請(qǐng)求。
從服務(wù)器在發(fā)送 PING 命令后,會(huì)遇到三種情況:
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)證階段可能會(huì)遇到以下幾種情況:
5. 發(fā)送端口信息
從服務(wù)器向主服務(wù)器發(fā)送當(dāng)前服務(wù)器的監(jiān)聽端口號(hào), 主服務(wù)器收到后記錄在從服務(wù)器所對(duì)應(yīng)的客戶端狀態(tài)的 slave_listening_port 屬性中。
執(zhí)行命令為 REPLCONF listening-port <port-number> ,port-number 即為端口號(hào)。
目前 slave_listening_port 唯一的作用就是在主服務(wù)器執(zhí)行 INFO replication 命令時(shí)打印從服務(wù)器端口號(hào)。
6. 同步
從服務(wù)器向主服務(wù)器發(fā)送 PSYNC 命令,執(zhí)行同步操作,此時(shí)兩者互為客戶端。
PSYNC 命令有兩種執(zhí)行情況:
主服務(wù)器返回從服務(wù)器也有三種情況:
從上方可知,主要包括全量數(shù)據(jù)同步 和 增量數(shù)據(jù)同步的情況,這跟Redis是否第一次連接和在連接過程中是否離線有關(guān)。
7. 命令傳播
當(dāng)完成了同步之后,就會(huì)進(jìn)入命令傳播階段,這時(shí)主服務(wù)器只要一直將自己執(zhí)行的寫命令發(fā)送給從服務(wù)器,而從服務(wù)器只要一直接收并執(zhí)行主服務(wù)器發(fā)來的寫命令,就可以保證主從一致了。
主從復(fù)制優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
- 同一個(gè)Master可以同步多個(gè)Slaves。
- master能自動(dòng)將數(shù)據(jù)同步到slave,可以進(jìn)行讀寫分離,分擔(dān)master的讀壓力
- master、slave之間的同步是以非阻塞的方式進(jìn)行的,同步期間,客戶端仍然可以提交查詢或更新請(qǐng)求
缺點(diǎn)
- 不具備自動(dòng)容錯(cuò)與恢復(fù)功能,master或slave的宕機(jī)都可能導(dǎo)致客戶端請(qǐng)求失敗,需要等待機(jī)器重啟或手動(dòng)切換客戶端IP才能恢復(fù)
- master宕機(jī),如果宕機(jī)前數(shù)據(jù)沒有同步完,則切換IP后會(huì)存在數(shù)據(jù)不一致的問題
- 難以支持在線擴(kuò)容,Redis的容量受限于單機(jī)配置
總結(jié)
其實(shí)redis的主從模式很簡單,在實(shí)際的生產(chǎn)環(huán)境中很少使用,不建議在實(shí)際的生產(chǎn)環(huán)境中使用主從模式來提供系統(tǒng)的高可用性,之所以不建議使用都是由它的缺點(diǎn)造成的,在數(shù)據(jù)量非常大的情況,或者對(duì)系統(tǒng)的高可用性要求很高的情況下,主從模式也是不穩(wěn)定的。雖然這個(gè)模式很簡單,但是這個(gè)模式是其他模式的基礎(chǔ),所以理解了這個(gè)模式,對(duì)其他模式的學(xué)習(xí)會(huì)很有幫助。
命令傳播階段后的心跳檢測 以及 PSYNC 的實(shí)現(xiàn),具體參照書中,不多解釋了。
哨兵模式
Redis官方文檔【高可用】:REDIS sentinel-old – Redis中國用戶組(CRUG)
參考公眾號(hào)文章:全面分析Redis高可用的奧秘 - Sentinel
哨兵模式架構(gòu)
哨兵(Sentinel) 是 Redis 的高可用性解決方案:由一個(gè)或多個(gè) Sentinel 實(shí)例組成的 Sentinel 系統(tǒng)可以監(jiān)視任意多個(gè)主服務(wù)器,以及這些主服務(wù)器屬下的所有從服務(wù)器。
Sentinel 可以在被監(jiān)視的主服務(wù)器進(jìn)入下線狀態(tài)時(shí),自動(dòng)將下線主服務(wù)器的某個(gè)從服務(wù)器升級(jí)為新的主服務(wù)器,然后由新的主服務(wù)器代替已下線的主服務(wù)器繼續(xù)處理命令請(qǐng)求。
哨兵進(jìn)程
哨兵(Sentinel)其實(shí)也是Redis 實(shí)例,只不過它在啟動(dòng)時(shí)初始化將 Redis 服務(wù)器使用的代碼替換成 Sentinel 專用代碼。
哨兵進(jìn)程的作用
哨兵(Sentinel) 和 一般Redis 的區(qū)別?
哨兵的工作方式
創(chuàng)建連接
這一步是初始化 Sentinel 的最后一步,Sentinel 成為主服務(wù)器的客戶端,可以向主服務(wù)器發(fā)送命令。
每個(gè)sentinel都會(huì)創(chuàng)建兩個(gè)連向主服務(wù)器的異步網(wǎng)絡(luò)連接。
- 命令連接:用于向master服務(wù)發(fā)送命令,并接收命令回復(fù)。
- 訂閱連接:用于訂閱、接收master服務(wù)的 __sentinel__:hello 頻道。
為什么有兩個(gè)連接?
命令連接的原因是:Sentinel 必須向主服務(wù)器發(fā)送命令,以此來與主服務(wù)器通信。
訂閱連接的原因是:目前Redis版本的發(fā)布訂閱功能無法保存被發(fā)送的信息,如果接收信息的客戶端離線,那么這個(gè)客戶端就會(huì)丟失這條信息,為了不丟失 __sentinel__:hello 頻道的任何信息,Sentinel 專門用一個(gè)訂閱連接來接收該頻道的信息。
【簡單理解:不僅需要發(fā)信息,也需要收信息】
獲取主服務(wù)器信息
Sentinel 默認(rèn)會(huì)以10秒一次通過命令連接向被監(jiān)視的主服務(wù)器發(fā)送 INFO 命令,主服務(wù)器收到后回復(fù)自己的run_id、IP、端口、對(duì)應(yīng)的主服務(wù)器信息及主服務(wù)器下的所有從服務(wù)器信息。
Sentinel 根據(jù)返回的主服務(wù)器信息更新自身的 *masters 實(shí)例結(jié)構(gòu);至于主服務(wù)器返回的從服務(wù)器信息用于更新對(duì)應(yīng)的slaves 字典列表。
更新 slaves 字典時(shí)有兩種情況:
獲取從服務(wù)器信息
Sentinel 同樣會(huì)和從服務(wù)器建立異步的命令連接和訂閱連接,并也會(huì)默認(rèn)10秒一次向從服務(wù)器發(fā)送 INFO 命令,從服務(wù)器會(huì)回復(fù)自己的運(yùn)行run_id、角色role、從服務(wù)器復(fù)制偏移量offset、主服務(wù)器的ip和port、主從服務(wù)器連接狀態(tài)、從服務(wù)器優(yōu)先級(jí)等信息,sentinel會(huì)根據(jù)返回信息更新對(duì)應(yīng)的 slave 實(shí)例結(jié)構(gòu)。
向主服務(wù)器和從服務(wù)器發(fā)送信息
Sentinel 默認(rèn)會(huì)以2秒一次通過命令連接向所有被監(jiān)控的主服務(wù)器和從服務(wù)器的_sentinel:hello頻道發(fā)送信息,信息的內(nèi)容包含兩種參數(shù):
參數(shù)列表展示參考:
| s_ip | Sentinel 的 IP地址 |
| s_port | Sentinel 的端口號(hào) |
| s_runid | Sentinel 的運(yùn)行ID |
| s_epoch | Sentinel 當(dāng)前的配置紀(jì)元(configuration epoch) |
| m_name | 主服務(wù)器的名字 |
| m_ip | 主服務(wù)器的IP地址 |
| m_port | 主服務(wù)器的端口號(hào) |
| m_epoch | 主服務(wù)器當(dāng)前的配置紀(jì)元 |
接收來自主服務(wù)器和從服務(wù)器的頻道信息
Sentinel通過訂閱連接向服務(wù)器發(fā)送命令 SUBSCRIBE __sentinel__:hello,保證對(duì)_sentinel_:hello的訂閱一直持續(xù)到 Sentinel 與 服務(wù)器的連接斷開為止。
_sentinel_:hello頻道 與 Sentinel 的關(guān)系是一對(duì)多的關(guān)系,作用在于發(fā)現(xiàn)多個(gè)監(jiān)控同一master的sentinel。
在接收到其他 sentinel 發(fā)送的頻道信息后,會(huì)根據(jù)信息更新 master 對(duì)應(yīng)的 Sentinel 。
與 master 數(shù)據(jù)結(jié)構(gòu)綁定后,會(huì)建立 Sentinel 與 Sentinel 的命令連接,為后續(xù)通訊做準(zhǔn)備。
故障檢測
檢測主觀下線
Sentinel 默認(rèn)會(huì)以1秒一次的頻率向與它建立命令連接的所有實(shí)例(包括master、slave以及發(fā)現(xiàn)的其他sentinel)發(fā)送 PING 命令,對(duì)方接收后返回兩種回復(fù):
- **有效回復(fù):**包括運(yùn)行正常(+PONG)、正在加載(-LOADING)、和主機(jī)下線(-MASTERDOWN)。
- **無效回復(fù):**除有效回復(fù)的三種以外都是無效回復(fù),或者在指定時(shí)限內(nèi)沒有返回任何回復(fù)。
在固定時(shí)間內(nèi),即 down-after-milliseconds(默認(rèn)單位為毫秒) 配置的時(shí)間內(nèi)收到的都是無效回復(fù),Sentinel 就會(huì)標(biāo)記 master 為主觀下線。與此同時(shí),Sentinel 會(huì)將 master 數(shù)據(jù)結(jié)構(gòu)中對(duì)應(yīng)的flags屬性更新為 SRI_S_DOWN 標(biāo)識(shí),表示被監(jiān)控的master在當(dāng)前sentinel中已經(jīng)進(jìn)入主觀下線狀態(tài)。
down-after-milliseconds 的值,不僅是sentinel 用來判斷主服務(wù)器主觀下線狀態(tài),還用來判斷主服務(wù)器下所有從服務(wù)器,以及所有同樣監(jiān)視這個(gè)主服務(wù)器的其他Sentinel的主觀下線狀態(tài)。
簡單說明,即 down-after-millsseconds 配置是作用于當(dāng)前sentinel所監(jiān)控的所有服務(wù)上的,也就是對(duì)應(yīng)master下的slave,以及其他sentinel。另外每個(gè)sentinel可以配置不同down-after-millsenconds,所以判定主觀下線的時(shí)間也就是不同的。
檢測客觀下線
判定 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ù)器的端口號(hào)。
- current_epoch:Sentinel 當(dāng)前的配置紀(jì)元,用于選舉領(lǐng)頭 Sentinel。
- runid:可以是 * 符號(hào) 或 Sentinel 的 run_id,用 * 符號(hào)僅用于檢測主服務(wù)器的客觀下線狀態(tài);用Sentinel 的 run_id 是用于選舉領(lǐng)頭 Sentinel。
其他 Sentinel 接收到 SEBTUBEL is-master-down-by-addr 命令后,會(huì)根據(jù)其中的主服務(wù)器IP和端口號(hào),檢查主服務(wù)器是否已下線,然后向源 Sentinel 返回一條包含三個(gè)參數(shù)的 Multi Bulk 回復(fù):
<down_state> <leader_runid> <leader_epoch>參數(shù)說明:
down_state:返回目標(biāo) Sentinel 對(duì)主服務(wù)器的檢查結(jié)果,1 代表已下線,0 代表為下線。
leader_runid:可以是 * 符號(hào) 或 目標(biāo) Sentinel 的 run_id,用 * 符號(hào)僅用于檢測主服務(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 的值不為 * 時(shí)有效,如果 leader_runid 的值為 *,則 leader_epoch 總為0】
當(dāng) Sentinel 收到從其他 Sentinel 返回的足夠數(shù)量的已下線判斷之后,Sentinel會(huì)將主服務(wù)器實(shí)例結(jié)構(gòu)的 flags 屬性的 SRI_O_DOWN 標(biāo)識(shí)打開,表示主服務(wù)器已經(jīng)進(jìn)入客觀下線狀態(tài)。
足夠數(shù)量的已下線判斷是多少呢?
不同的 Sentinel 判斷客觀下線狀態(tài)的條件是不同的,具體不解釋了,看《Redis設(shè)計(jì)與實(shí)現(xiàn)》P238。
選舉領(lǐng)頭 Sentinel
當(dāng)一個(gè)主服務(wù)器被判斷為客觀下線時(shí),監(jiān)測這個(gè)下線主服務(wù)器的各個(gè) Sentinel 會(huì)進(jìn)行協(xié)商,選舉出一個(gè)領(lǐng)頭 Sentinel,并由領(lǐng)頭 Sentinel 對(duì)下線主服務(wù)器執(zhí)行故障轉(zhuǎn)移操作。
下面盡量直白地介紹選舉領(lǐng)頭 Sentinel 的規(guī)則和方法:
-
每個(gè)在線的 Sentinel 都有被選為領(lǐng)頭 Sentinel 的資格。
-
同一個(gè)配置紀(jì)元內(nèi)(本質(zhì)是計(jì)數(shù)器,在每次選舉后自增一次),每個(gè) Sentinel 都有一次將某個(gè) Sentinel 設(shè)置為局部領(lǐng)頭 Sentinel 的機(jī)會(huì),并且設(shè)置后,在這個(gè)配置紀(jì)元里不能再更改。
-
每個(gè)發(fā)現(xiàn)主服務(wù)器進(jìn)入客觀下線 的Sentinel 都會(huì)要求其他 Sentinel 將自己設(shè)置為局部領(lǐng)頭Sentinel。
-
拉票方式為發(fā)送 SEBTUBEL is-master-down-by-addr 命令,剛才的 *號(hào)替換為源 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,那也得和大家回個(gè)話,它會(huì)為發(fā)送拉票命令的源 Sentinel 回復(fù)命令,記錄了自身選擇的局部領(lǐng)頭 Sentinel的 run_id 和 配置紀(jì)元。
-
如果某個(gè) Sentinel 被半數(shù)以上的 Sentinel 設(shè)置為了局部領(lǐng)頭 Sentinel,那么這個(gè)局部領(lǐng)頭sentinel就變成了領(lǐng)頭sentinel,同一個(gè)配置紀(jì)元內(nèi)可能會(huì)出現(xiàn)多個(gè)局部領(lǐng)頭sentinel,但是領(lǐng)頭sentinel只會(huì)產(chǎn)生一個(gè)。
-
如果在給定的時(shí)限內(nèi),沒有任何一個(gè) Sentinel 被選舉為領(lǐng)頭 Sentinel,那么各個(gè) Sentinel 會(huì)在一段時(shí)間后再次選舉,直到選出領(lǐng)頭 Sentinel 為止。
故障遷移
在選舉出領(lǐng)頭 Sentinel 之后,領(lǐng)頭 Sentinel 會(huì)對(duì)已下線的主服務(wù)器執(zhí)行故障轉(zhuǎn)移操作,可分為三個(gè)步驟:
選出新的主服務(wù)器
(一)、新的主服務(wù)器是從原主服務(wù)器下的從服務(wù)器中選擇的,所以需要選擇狀態(tài)良好、數(shù)據(jù)完整的從服務(wù)器。領(lǐng)頭 Sentinel 的數(shù)據(jù)結(jié)構(gòu)中保存了原master對(duì)應(yīng)的 slave ,Sentinel 會(huì)刪除狀態(tài)較差的slave。過濾執(zhí)行順序如下:
對(duì)應(yīng)第三條,我可以解釋一下,前面提到過,在 down-after-millisecond 設(shè)置的時(shí)長內(nèi)沒有收到有效回復(fù),可以判定當(dāng)前復(fù)制的主服務(wù)器主觀下線。所以,越遲和主服務(wù)器斷開連接的從服務(wù)器,數(shù)據(jù)越新。
(二)、現(xiàn)在過濾出的都是健康的從服務(wù)器了,然后 Sentinel 開始選擇新的主服務(wù)器,有以下三個(gè)優(yōu)先級(jí)順序:
(三)、選出新的主服務(wù)器后,領(lǐng)頭 Sentinel 向被選中的從服務(wù)器發(fā)送 SLAVEOF no one 命令。
在發(fā)送 SLAVEOF no one 命令后,領(lǐng)頭 Sentinel 會(huì)以每秒一次的頻率(平時(shí)是十秒一次)向被選中的從服務(wù)器發(fā)送 INFO 命令,當(dāng)被升級(jí)的服務(wù)器的 role 字段從 slave 變?yōu)?master 時(shí),領(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 會(huì)修改它對(duì)應(yīng)主服務(wù)器下的實(shí)例結(jié)構(gòu)中的設(shè)置。
等舊主服務(wù)器重新上線時(shí),Sentinel 就會(huì)向它發(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ù)都放在一臺(tái)服務(wù)器上,無法較好的進(jìn)行水平擴(kuò)展。
為了解決哨兵模式的痛點(diǎn),集群模式應(yīng)運(yùn)而生。在高可用上,集群基本是直接復(fù)用的哨兵模式的邏輯,并且針對(duì)水平擴(kuò)展進(jìn)行了優(yōu)化。
它具有的特點(diǎn)有:
下面將會(huì)根據(jù)它的特點(diǎn)逐步說明該集群的核心技術(shù)。
集群數(shù)據(jù)結(jié)構(gòu)
使用 clusterNode 結(jié)構(gòu)保存一個(gè)節(jié)點(diǎn)的當(dāng)前狀態(tài),比如創(chuàng)建時(shí)間、名稱、配置紀(jì)元、IP、端口號(hào)等。
每個(gè)節(jié)點(diǎn)都會(huì)為自己和集群中所有其他節(jié)點(diǎn)都創(chuàng)建一個(gè)對(duì)應(yīng)的 clusterNode 結(jié)構(gòu)來記錄各自的節(jié)點(diǎn)狀態(tài)。
struct clusterNode {// 創(chuàng)建節(jié)點(diǎn)的時(shí)間mstime_t ctime;// 節(jié)點(diǎn)的名稱,由40個(gè)十六進(jìn)制字符組成,例如68eef66df23420a5862208ef5...f2ffchar name[REDIS_CLUSTER_NAMELEN];// 節(jié)點(diǎn)標(biāo)識(shí),使用各種不同表示值記錄節(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)的端口號(hào)int port;// 保存連接節(jié)點(diǎn)所需的相關(guān)信息clusterLink *link;// ... };其中的 link 屬性是一個(gè) clusterLink 結(jié)構(gòu),該結(jié)構(gòu)保存連接節(jié)點(diǎn)所需的相關(guān)信息,包括套接字描述符、輸入緩沖區(qū)、輸出緩沖區(qū)。
typedef struct clusterLink {// 連接的創(chuàng)建時(shí)間mestime_t ctime;// TCP 套接字描述符int fd;// 輸出緩沖區(qū),保存著待發(fā)送給其他節(jié)點(diǎn)的信息(message)sds sndbuf;// 輸入緩沖區(qū),保存著從其他節(jié)點(diǎn)接收到的信息sds rcvbuf;// 與這個(gè)連接相關(guān)聯(lián)的節(jié)點(diǎn),如果沒有的話就為 NULLstruct clusterNode *node; }最后一點(diǎn),每個(gè)節(jié)點(diǎn)都保存著一個(gè) clusterState 結(jié)構(gòu),這個(gè)結(jié)構(gòu)記錄了當(dāng)前節(jié)點(diǎn)視角下,所在集群目前所處的狀態(tài)。
例如集群在線或下線狀態(tài)、包含節(jié)點(diǎn)個(gè)數(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)對(duì)應(yīng)的 clusterNode 結(jié)構(gòu)dict *nodes; }集群連接方式
通過發(fā)送 CLUSTER MEET 命令,可以讓目標(biāo)節(jié)點(diǎn)A將另一個(gè)命令攜帶的節(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和節(jié)點(diǎn)B會(huì)通過Gossip 協(xié)議傳播給集群其他的節(jié)點(diǎn),讓他們也和節(jié)點(diǎn)B握手,最終整個(gè)集群達(dá)成共識(shí)。
一般集群元數(shù)據(jù)的維護(hù)有兩種方式:集中式、Gossip 協(xié)議。在Redis集群中采用Gossip 協(xié)議進(jìn)行通信,所以說它是去中心化的集群。
下面說一下這兩種方式的區(qū)別:
集中式:是將集群元數(shù)據(jù)(節(jié)點(diǎn)信息、故障等等)幾種存儲(chǔ)在某個(gè)節(jié)點(diǎn)上。集中式元數(shù)據(jù)集中存儲(chǔ)的一個(gè)典型代表,就是大數(shù)據(jù)領(lǐng)域的 storm。它是分布式的大數(shù)據(jù)實(shí)時(shí)計(jì)算引擎,是集中式的元數(shù)據(jù)存儲(chǔ)的結(jié)構(gòu),底層基于 zookeeper(分布式協(xié)調(diào)的中間件)對(duì)所有元數(shù)據(jù)進(jìn)行存儲(chǔ)維護(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í)效性非常好,一旦元數(shù)據(jù)出現(xiàn)了變更,就立即更新到集中式的存儲(chǔ)中,其它節(jié)點(diǎn)讀取的時(shí)候就可以感知到;不好在于,所有的元數(shù)據(jù)的更新壓力全部集中在一個(gè)地方,可能會(huì)導(dǎo)致元數(shù)據(jù)的存儲(chǔ)有壓力。
gossip 協(xié)議的好處在于,元數(shù)據(jù)的更新比較分散,不是集中在一個(gè)地方,更新請(qǐng)求會(huì)陸陸續(xù)續(xù)打到所有節(jié)點(diǎn)上去更新,降低了壓力;不好在于,元數(shù)據(jù)的更新有延時(shí),可能導(dǎo)致集群中的一些操作會(huì)有一些滯后。
分布式尋址算法【引入】
如果會(huì)的同學(xué)可以跳過,這里只做引申說明。
一般分布式尋址算法有下列幾種:
- hash 算法(大量緩存重建)
- 一致性 hash 算法(自動(dòng)緩存遷移)+ 虛擬節(jié)點(diǎn)(自動(dòng)負(fù)載均衡)
- redis cluster 的 hash slot 算法
hash 算法
來了一個(gè) key,首先計(jì)算 hash 值,然后對(duì)節(jié)點(diǎn)數(shù)取模。然后打在不同的 master 節(jié)點(diǎn)上。一旦某一個(gè) master 節(jié)點(diǎn)宕機(jī),所有請(qǐng)求過來,都會(huì)基于最新的剩余 master 節(jié)點(diǎn)數(shù)去取模,嘗試去取數(shù)據(jù)。這會(huì)導(dǎo)致大部分的請(qǐng)求過來,全部無法拿到有效的緩存,導(dǎo)致大量的流量涌入數(shù)據(jù)庫。
一致性 hash 算法
一致性 hash 算法將整個(gè) hash 值空間組織成一個(gè)虛擬的圓環(huán),整個(gè)空間按順時(shí)針方向組織,下一步將各個(gè) master 節(jié)點(diǎn)(使用服務(wù)器的 ip 或主機(jī)名)進(jìn)行 hash。這樣就能確定每個(gè)節(jié)點(diǎn)在其哈希環(huán)上的位置。
一致性 hash 算法也是使用取模的方法 hash算法的取模法是對(duì)服務(wù)器的數(shù)量進(jìn)行取模,而一致性 hash 算法是對(duì) **2^32 ** 取模:
hash(服務(wù)器A的IP地址) % 2^32 hash(服務(wù)器B的IP地址) % 2^32 hash(服務(wù)器C的IP地址) % 2^32來了一個(gè) key,首先計(jì)算 hash 值,并確定此數(shù)據(jù)在環(huán)上的位置,從此位置沿環(huán)順時(shí)針“行走”,遇到的第一個(gè) master 節(jié)點(diǎn)就是 key 所在位置。
使用 hash 算法時(shí),服務(wù)器數(shù)量發(fā)生改變時(shí),所有服務(wù)器的所有緩存在同一時(shí)間失效了,而使用一致性哈希算法時(shí),服務(wù)器的數(shù)量如果發(fā)生改變,并不是所有緩存都會(huì)失效,而是只有部分緩存會(huì)失效,例如如果一個(gè)節(jié)點(diǎn)掛了,受影響的數(shù)據(jù)僅僅是此節(jié)點(diǎn)到環(huán)空間前一個(gè)節(jié)點(diǎn)(沿著逆時(shí)針方向行走遇到的第一個(gè)節(jié)點(diǎn))之間的數(shù)據(jù),其它不受影響。增加一個(gè)節(jié)點(diǎn)也同理。
hash 環(huán)數(shù)據(jù)傾斜 & 虛擬節(jié)點(diǎn)
然而當(dāng)一致性 hash 算法在節(jié)點(diǎn)太少或是節(jié)點(diǎn)位置分布不均勻時(shí),容易造成大量請(qǐng)求都集中在某一個(gè)節(jié)點(diǎn)上,而造成緩存熱點(diǎn)的問題。如果i此時(shí)該熱點(diǎn)節(jié)點(diǎn)出現(xiàn)故障,那么失效緩存的數(shù)量也將達(dá)到最大值,在極端情況下,有可能引起系統(tǒng)的崩潰,這種情況被稱之為 數(shù)據(jù)傾斜。
為了預(yù)防 數(shù)據(jù)傾斜 的問題,一致性 hash 算法引入了虛擬節(jié)點(diǎn)機(jī)制,即對(duì)每一個(gè)節(jié)點(diǎn)計(jì)算多個(gè) hash,每個(gè)計(jì)算結(jié)果位置都放置一個(gè)虛擬節(jié)點(diǎn)。這樣就實(shí)現(xiàn)了數(shù)據(jù)的均勻分布,負(fù)載均衡。
具體說明,每一個(gè)服務(wù)節(jié)點(diǎn)計(jì)算多個(gè)哈希,每個(gè)計(jì)算結(jié)果位置都放置一個(gè)此服務(wù)節(jié)點(diǎn)。具體做法可以在服務(wù)器ip或主機(jī)名的后面增加編號(hào)來實(shí)現(xiàn)??梢詾槊颗_(tái)服務(wù)器計(jì)算三個(gè)虛擬節(jié)點(diǎn),于是可以分別計(jì)算 “Node1#1”、“Node1#2”、“Node1#3”、“Node2#1”、“Node2#2”、“Node2#3”的哈希值,這樣可以讓hash 環(huán)中存在多個(gè)節(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ù)存儲(chǔ)和數(shù)據(jù)的讀取。
redis 集群中有固定的 16384 個(gè)槽(slot),對(duì)每個(gè) key 計(jì)算 CRC16 值,然后對(duì) 16384 取模,可以獲取 key 對(duì)應(yīng)的 hash slot。
redis 集群中每個(gè) master 都會(huì)被指派部分的槽(slot),假如說當(dāng)前集群中有3個(gè)節(jié)點(diǎn)服務(wù)器,可能是這樣分配的 [0,5000]、[5001,10000]、[10001,16383]。
槽位的實(shí)現(xiàn)其實(shí)就是一個(gè)長度為 16384 的二進(jìn)制數(shù)組,根據(jù)指定索引位上的二進(jìn)制位值來判斷節(jié)點(diǎn)是否處理指定索引的槽位。
所以槽位的遷移非常簡單:
移動(dòng)槽位的成本是非常低的??蛻舳说?api,可以對(duì)指定的數(shù)據(jù),讓他們走同一個(gè)槽位,通過 hash tag 來實(shí)現(xiàn)。
在Redis中通過 CLUSTER ADDSLOTS 命令來指派負(fù)責(zé)的槽位,后面會(huì)詳細(xì)說明。
每個(gè)節(jié)點(diǎn)都會(huì)記錄哪些槽指派給了自己,哪些槽指派給了其他節(jié)點(diǎn)??蛻舳讼蚬?jié)點(diǎn)發(fā)送鍵命令,節(jié)點(diǎn)要計(jì)算這個(gè)鍵屬于哪個(gè)槽。如果是自己負(fù)責(zé)這個(gè)槽,那么直接執(zhí)行命令,如果不是,向客戶端返回一個(gè) MOVED 錯(cuò)誤,指引客戶端轉(zhuǎn)向正確的節(jié)點(diǎn)。
任何一臺(tái)機(jī)器宕機(jī),另外兩個(gè)節(jié)點(diǎn),不影響的。因?yàn)?key 找的是 hash slot,不是機(jī)器。
架構(gòu)圖參照上方《集群模式架構(gòu)》中。
可能有人問,為什么一致性hash算法是65535(2^32)個(gè)位置,而hash slot 算法卻是16384(2^14)個(gè)位置?【翻譯官方回答】
因此,16384個(gè)插槽處于正確的范圍內(nèi),以確保每個(gè)主站有足夠的插槽,最多1000個(gè)節(jié)點(diǎn),但足夠小的數(shù)字可以輕松地將插槽配置傳播為原始位圖。 請(qǐng)注意,在小型集群中,位圖難以壓縮,因?yàn)楫?dāng)N很小時(shí),位圖將設(shè)置插槽/ N位,這是設(shè)置的大部分位。
一致性 hash 算法 和 hash slot 算法的區(qū)別?
定位規(guī)則區(qū)別
它并不是閉合的,key的定位規(guī)則是根據(jù) CRC-16(key) % 16384 的值來判斷屬于哪個(gè)槽區(qū),從而判斷該key屬于哪個(gè)節(jié)點(diǎn),而一致性 hash 算法是根據(jù) hash(key) 的值來順時(shí)針找第一個(gè) hash(ip或主機(jī)名) 的節(jié)點(diǎn),從而確定key存儲(chǔ)在哪個(gè)節(jié)點(diǎn)。
應(yīng)對(duì)熱點(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)有多個(gè)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)會(huì)通過選舉機(jī)制選舉出一個(gè)節(jié)點(diǎn)變成master節(jié)點(diǎn),實(shí)現(xiàn)高可用。但是這里有一點(diǎn)需要考慮,如果master節(jié)點(diǎn)存在熱點(diǎn)緩存,某一個(gè)時(shí)刻某個(gè)key的訪問急劇增高,這時(shí)該mater節(jié)點(diǎn)可能操勞過度而死,隨后從節(jié)點(diǎn)選舉為主節(jié)點(diǎn)后,同樣宕機(jī),一次類推,造成緩存雪崩。(簡單說明就是,都是被大量請(qǐng)求一套秒的,誰上來都一樣QAQ…)
擴(kuò)容和縮容區(qū)別
一致性 hash 算法在新增和刪除節(jié)點(diǎn)后,數(shù)據(jù)會(huì)按照順時(shí)針自動(dòng)來重新分布節(jié)點(diǎn)。
redis 集群的新增和刪除節(jié)點(diǎn)都需要手動(dòng)來分配槽區(qū)。
集群的槽指派
Redis集群通過分片來保存數(shù)據(jù)庫的鍵值對(duì):集群整個(gè)數(shù)據(jù)庫被分為16384個(gè)槽(slot),數(shù)據(jù)庫的每個(gè)鍵都屬于這16384個(gè)槽其中的一個(gè),集群中的每個(gè)節(jié)點(diǎn)可以處理0個(gè)到16384個(gè)槽。
指派節(jié)點(diǎn)槽信息
當(dāng)集群使用 CLUSTER MEET 命令,整個(gè)集群仍處于下線狀態(tài),此時(shí)必須通過它們指派槽,通過發(fā)送 CLUSTER ADDSLOTS 命令給節(jié)點(diǎn),將一個(gè)或多個(gè)槽指派給節(jié)點(diǎn)負(fù)責(zé):
CLUSTER ADDSLOTS <slot> [slot...]比如說將 0 到 5000 個(gè)槽指派給節(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ì)就是一個(gè)二進(jìn)制位數(shù)組,通過對(duì)[0,16383]上的對(duì)應(yīng)索引為標(biāo)記來判斷是否處理該槽位:如果slots數(shù)組上在指定索引位的二進(jìn)制位的值為1,標(biāo)識(shí)節(jié)點(diǎn)負(fù)責(zé)處理該槽,反之同理。
CLUSTER ADDSLOTS 的命令實(shí)現(xiàn)
CLUSTER ADDSLOTS 命令的實(shí)現(xiàn)也比較簡單:
執(zhí)行完畢后,開始廣播通知給集群中的其他節(jié)點(diǎn),自己目前處理的槽位。
傳播節(jié)點(diǎn)槽信息
節(jié)點(diǎn)會(huì)將自己的 slots 數(shù)組通過消息發(fā)送給集群中的其他節(jié)點(diǎn),告知它們自己目前負(fù)責(zé)的槽位。
當(dāng)其他節(jié)點(diǎn)接收到消息,會(huì)更新自己的在 clusterState.nodes 字典中對(duì)應(yīng)節(jié)點(diǎn)的 clusterNode 結(jié)構(gòu)中的 slots 數(shù)組。
記錄集群所有槽的指派信息
在 clusterState 結(jié)構(gòu)中的 slots 數(shù)組記錄了集群中所有 16384 個(gè)槽的指派信息:
typedef struct clusterState {//...clusterNode *slots[16384];//... }slots 數(shù)組包含 16384 個(gè)項(xiàng),每個(gè)數(shù)組項(xiàng)都是一個(gè)指向 clusterNode 的指針:對(duì)應(yīng)指針指向 NULL 時(shí),說明還未分配;指向 clusterNode 結(jié)構(gòu)時(shí),說明已經(jīng)指派給了對(duì)應(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對(duì)應(yīng)的索引位即可,復(fù)雜度為O(1)。
集群執(zhí)行命令
建立集群,并且分配完槽位,此時(shí)集群就會(huì)進(jìn)入上線狀態(tài),這時(shí)候客戶端就可以向集群中的節(jié)點(diǎn)發(fā)送數(shù)據(jù)指令了。
客戶端在向節(jié)點(diǎn)發(fā)送與數(shù)據(jù)庫鍵有關(guān)的命令時(shí),接收命令的節(jié)點(diǎn)就會(huì)計(jì)算出命令要處理的數(shù)據(jù)庫鍵屬于哪個(gè)槽,并檢查這個(gè)槽是否指派個(gè)了自己:
- 如果鍵所在的槽正好指派給當(dāng)前節(jié)點(diǎn),那么節(jié)點(diǎn)就直接執(zhí)行這個(gè)命令。
- 如果鍵所在的槽沒有指派給當(dāng)前節(jié)點(diǎn),那么節(jié)點(diǎn)就會(huì)向客戶端返回 MOVED 錯(cuò)誤,指引客戶端向正確的節(jié)點(diǎn),并再次發(fā)送之前想要執(zhí)行的命令。
節(jié)點(diǎn)會(huì)使用以下算法來給指定 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 的槽號(hào)。
當(dāng)節(jié)點(diǎn)計(jì)算出鍵所屬的槽后,節(jié)點(diǎn)會(huì)檢查自己 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é)的,會(huì)根據(jù) clusterState.slot[i] 指向的 clusterNode 結(jié)構(gòu)中所記錄的 IP 和 端口號(hào),返回客戶端 MOVED 錯(cuò)誤,指引客戶端轉(zhuǎn)向正在處理該槽的節(jié)點(diǎn)。
MOVED 錯(cuò)誤
MOVED 錯(cuò)誤的格式為:
MOVED <slot> <ip>:<port>- slot:鍵所在的槽。
- ip:port:負(fù)責(zé)處理該槽節(jié)點(diǎn)的IP地址和端口號(hào)。
MOVED 錯(cuò)誤一般是不會(huì)打印的,而是根據(jù)該錯(cuò)誤自動(dòng)進(jìn)行節(jié)點(diǎn)轉(zhuǎn)向,并打印轉(zhuǎn)向信息。
如果在單機(jī) redis 的情況下,是會(huì)被客戶端打印出來的。
節(jié)點(diǎn)數(shù)據(jù)庫的實(shí)現(xiàn)
節(jié)點(diǎn)只能使用0號(hào)數(shù)據(jù)庫,而單機(jī)Redis服務(wù)器則沒有限制。
節(jié)點(diǎn)除了將鍵值對(duì)保存在數(shù)據(jù)庫中之外,還會(huì)用 clusterState 結(jié)構(gòu)中的 slots_to_keys跳躍表來保存槽和鍵之間的關(guān)系:
typedef struct clusterState {//...zskiplist *slots_to_keys;//... }slots_to_keys 跳表中每個(gè)節(jié)點(diǎn)的分值(score)都是一個(gè)槽位號(hào);每個(gè)節(jié)點(diǎn)的成員(member)都是一個(gè)數(shù)據(jù)庫鍵。
- 當(dāng)節(jié)點(diǎn)往數(shù)據(jù)庫中添加新的鍵值對(duì)時(shí),節(jié)點(diǎn)會(huì)將鍵的槽位號(hào)以及這個(gè)鍵關(guān)聯(lián)到 slot_to_keys 跳表中。
- 當(dāng)節(jié)點(diǎn)刪除數(shù)據(jù)庫中的某個(gè)鍵值對(duì)時(shí),節(jié)點(diǎn)就會(huì)在 slot_to_keys跳表中解除它們的關(guān)聯(lián)關(guān)系。
重新分片(比如在線擴(kuò)容)
Redis 集群的重新分片操作可以將任意數(shù)量已經(jīng)指派給某個(gè)節(jié)點(diǎn)的槽改為指派給另一個(gè)節(jié)點(diǎn),并且相關(guān)聯(lián)槽位的鍵值對(duì)也會(huì)從源節(jié)點(diǎn)移動(dòng)到目標(biāo)節(jié)點(diǎn)。
重新分片的操作是可以在線進(jìn)行的,保證了高可用。
我們就以在線擴(kuò)容節(jié)點(diǎn)的情況來說吧:比如現(xiàn)在準(zhǔn)備在集群中增加一個(gè)節(jié)點(diǎn),如何將原有分片中的若干個(gè)槽位指派給新添加的節(jié)點(diǎn)?
Redis 集群的重新分片操作是由 Redis 集群管理軟件 redis-trib 負(fù)責(zé)執(zhí)行的:Redis 提供重新分配的所有命令,而 redis-trib 通過向源節(jié)點(diǎn)和目標(biāo)接待你發(fā)送命令來進(jìn)行重新分片操作。
redis-trib 對(duì)集群的單個(gè)槽進(jìn)行重新分片的步驟如下:
如果涉及多個(gè)槽,則給每個(gè)槽重復(fù)執(zhí)行上述本步驟。
ASK 錯(cuò)誤 - (保證集群在線擴(kuò)容的安全性)
在重新分片操作期間,可能會(huì)出現(xiàn)一部分鍵值對(duì)被遷出,一部分鍵值還未被遷出,即在源節(jié)點(diǎn)和目標(biāo)節(jié)點(diǎn)都由對(duì)應(yīng)槽的數(shù)據(jù)。
當(dāng)節(jié)點(diǎn)向源節(jié)點(diǎn)發(fā)送一個(gè)與數(shù)據(jù)庫鍵相關(guān)的命令,并且該鍵的槽位正好處在重新分片的過程中:
ASK 錯(cuò)誤同 MOVED 錯(cuò)誤類似,也是不會(huì)打印的,也會(huì)根據(jù)錯(cuò)誤提供的 IP 和 端口號(hào)自動(dòng)進(jìn)行轉(zhuǎn)向操作。
同理,單機(jī)模式下會(huì)打印錯(cuò)誤。
那 ASK 錯(cuò)誤 和 MOVED 錯(cuò)誤有什么區(qū)別呢?
雖然它們能導(dǎo)致客戶端轉(zhuǎn)向,但是 MOVED 錯(cuò)誤代表槽的負(fù)責(zé)權(quán)已經(jīng)交給另一個(gè)節(jié)點(diǎn)了;而 ASK 錯(cuò)誤只是兩個(gè)節(jié)點(diǎn)在遷移槽的過程中使用的臨時(shí)措施。
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,而是指向一個(gè) clusterNode 結(jié)構(gòu),那么表示當(dāng)前節(jié)點(diǎn)正在從 clusterNode 所代表的節(jié)點(diǎn)導(dǎo)入該槽。
在對(duì)集群重新分片的時(shí)候,向目標(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,而是指向一個(gè) clusterNode 結(jié)構(gòu),那么表示當(dāng)前節(jié)點(diǎn)正在將該槽遷移到 clusterNode 所代表的節(jié)點(diǎn)。
ASKING 命令
當(dāng)客戶端接收到 ASK 錯(cuò)誤并轉(zhuǎn)向正在導(dǎo)入槽的節(jié)點(diǎn)時(shí),客戶端會(huì)先向節(jié)點(diǎn)發(fā)送一個(gè) ASKING 命令,然后才重新發(fā)送要執(zhí)行的命令,這是因?yàn)榭蛻舳巳绻话l(fā)送 ASKING 命令,而直接發(fā)送想要執(zhí)行的命令的話,那么客戶端發(fā)送的命令會(huì)被節(jié)點(diǎn)拒絕執(zhí)行,并返回 MOVED 錯(cuò)誤。
復(fù)制和故障轉(zhuǎn)移
Redis 集群中節(jié)點(diǎn)可分為主節(jié)點(diǎn)(master)和從節(jié)點(diǎn)(slave)。
主節(jié)點(diǎn)用于處理槽;從節(jié)點(diǎn)用于復(fù)制某個(gè)主節(jié)點(diǎn),并在主節(jié)點(diǎn)下線時(shí),代替下線主節(jié)點(diǎn)繼續(xù)處理命令請(qǐng)求。
設(shè)置從節(jié)點(diǎn)方式
向一個(gè)節(jié)點(diǎn)發(fā)送命令:
CLUSTER REPLICATE <node_id>可以讓接收命令的節(jié)點(diǎn)成為 node_id 所指定的節(jié)點(diǎn)的從節(jié)點(diǎn),并開始對(duì)主節(jié)點(diǎn)進(jìn)行復(fù)制操作,具體步驟如下:
故障檢測
集群中每個(gè)節(jié)點(diǎn)都會(huì)定期向其他節(jié)點(diǎn)發(fā)送 PING 信息,以此檢測對(duì)方是否在線,如果接收 PING 信息的節(jié)點(diǎn)沒有在規(guī)定時(shí)間內(nèi)返回 PONG 信息,那么發(fā)送消息的節(jié)點(diǎn)會(huì)將接收消息的節(jié)點(diǎn)標(biāo)記為疑似下線(PFALL)。
如果在集群中,半數(shù)以上負(fù)責(zé)槽的主節(jié)點(diǎn)都將某個(gè)主節(jié)點(diǎn)標(biāo)記為疑似下線,那么這個(gè)主節(jié)點(diǎn)就會(huì)被標(biāo)記為已下線(FALL)。
將該主節(jié)點(diǎn)標(biāo)記為已下線的節(jié)點(diǎn)會(huì)向集群廣播關(guān)于該節(jié)點(diǎn)的 FALL 消息,所有收到這條 FALL 信息的節(jié)點(diǎn)都會(huì)立即將該節(jié)點(diǎn)標(biāo)記為已下線。
故障轉(zhuǎn)移
當(dāng)一個(gè)從節(jié)點(diǎn)發(fā)現(xiàn)自己正在復(fù)制的主節(jié)點(diǎn)進(jìn)入了下線狀態(tài)時(shí),從節(jié)點(diǎn)會(huì)對(duì)下線主節(jié)點(diǎn)進(jìn)行故障轉(zhuǎn)移,按照以下的執(zhí)行步驟:
選舉新的主節(jié)點(diǎn)過程
新的主節(jié)點(diǎn)也是通過選舉產(chǎn)生的,簡單介紹一下它的選舉過程:
類似于領(lǐng)頭 Sentinel 的選舉,可以對(duì)比來看。它們都是基于 Raft 算法的領(lǐng)頭選舉方法來實(shí)現(xiàn)的。
有的小伙伴可能覺得 領(lǐng)頭Sentinel 的選舉不算 Raft,因?yàn)樗詈笫峭ㄟ^領(lǐng)頭 Sentinel 來控制故障遷移的具體過程,這個(gè)就是仁者見仁智者見智了。
Raft 算法的實(shí)現(xiàn)可以參考一下Nacos 源碼中 RaftCore 類的實(shí)現(xiàn),比較通俗易懂。有時(shí)間我會(huì)發(fā)一下 Nacos 源碼中Raft選舉的實(shí)現(xiàn)。
Redis應(yīng)用
Redis 分布式鎖
官方文檔:REDIS distlock – Redis中國用戶組(CRUG)
我最早覺得比較好的實(shí)現(xiàn)分布式鎖思路文章:10分鐘精通Redis分布式鎖中的各種門道
引入
為什么需要分布式鎖?
我們?cè)陂_發(fā)項(xiàng)目時(shí),如果需要在同進(jìn)程內(nèi)的不同線程并發(fā)訪問某項(xiàng)資源,可以使用各種互斥鎖、讀寫鎖。
如果一臺(tái)主機(jī)上的多個(gè)進(jìn)程需要并發(fā)訪問某項(xiàng)資源,則可以使用進(jìn)程間同步的原語,例如信號(hào)量、管道、共享內(nèi)存等。
但如果多臺(tái)主機(jī)需要同時(shí)訪問某項(xiàng)資源,就需要使用一種在全局可見并具有互斥性的鎖了。
這種鎖就是分布式鎖,可以在分布式場景中對(duì)資源加鎖,避免競爭資源引起的邏輯錯(cuò)誤。
什么時(shí)候用分布式鎖?
一般我們使用分布式鎖有兩個(gè)場景:
- 效率:使用分布式鎖可以避免不同節(jié)點(diǎn)重復(fù)相同的工作,這些工作會(huì)浪費(fèi)資源。比如用戶注冊(cè)后調(diào)用發(fā)送郵箱的接口發(fā)送通知,可能不同節(jié)點(diǎn)會(huì)發(fā)出多封郵箱。
- 安全:加分布式鎖同樣可以避免破壞正確性的發(fā)生,如果兩個(gè)節(jié)點(diǎn)在同一條數(shù)據(jù)上面操作,比如多個(gè)節(jié)點(diǎn)機(jī)器對(duì)同一個(gè)訂單操作不同的流程有可能會(huì)導(dǎo)致該筆訂單最后狀態(tài)出現(xiàn)錯(cuò)誤,造成損失。
分布式鎖需要哪些特性呢?
大部分特性其實(shí)都類似于 Java 中的鎖,包括互斥性、可重入、鎖超時(shí)、公平鎖和非公平鎖、一致性。
- 互斥性:在同一時(shí)間點(diǎn),只有一個(gè)客戶端持有鎖。
- 可重入:同一個(gè)節(jié)點(diǎn)上的同一個(gè)線程如果獲取了鎖之后那么也可以再次獲取這個(gè)鎖。
- 鎖超時(shí):在客戶端離線(硬件故障或網(wǎng)絡(luò)異常等問題)時(shí),鎖能夠在一段時(shí)間后自動(dòng)釋放防止死鎖,即超時(shí)自動(dòng)解鎖。
- 公平鎖和非公平鎖:公平鎖即按照請(qǐng)求加鎖的順序獲得鎖,非公平鎖即相反是無序的。
- 一致性:比如說用Redis 實(shí)現(xiàn)分布式鎖時(shí),發(fā)生宕機(jī)情況,此時(shí)會(huì)有主從故障轉(zhuǎn)移的過程中,需要在此過程仍然保持鎖的原狀態(tài)。
- 續(xù)鎖:為了防止死鎖大多數(shù)會(huì)有鎖超時(shí)的設(shè)置,但是如果業(yè)務(wù)的執(zhí)行時(shí)間的不確定性,就需要保證在業(yè)務(wù)仍在執(zhí)行過程中時(shí),客戶端仍要持有鎖。
加鎖
在Redis中加鎖一般都是使用 SET 命令,使用 SET 命令完成 SETNX 和 EXPIRE 操作,并且這是一個(gè)原子操作:
set key value [EX seconds] [PX milliseconds] [NX|XX]上面這條指令是 SET 指令的使用方式,參數(shù)說明如下:
- key、value:鍵值對(duì)。
- EX seconds:設(shè)置失效時(shí)長,單位秒。
- PX milliseconds:設(shè)置失效時(shí)長,單位毫秒。
- NX:key不存在時(shí)設(shè)置value,成功返回OK,失敗返回(nil),SET key value NX 效果等同于 SETNX key value。
- XX:key存在時(shí)設(shè)置value,成功返回OK,失敗返回(nil)。
其中,NX 參數(shù)用于保證在多個(gè)線程并發(fā) set 下,只會(huì)有1個(gè)線程成功,起到了鎖的“唯一”性。
舉例:
// 設(shè)置msg = helloword,失效時(shí)長1000ms,不存在時(shí)設(shè)置 1.1.1.1:6379> set msg helloworld px 1000 nx解鎖
解鎖一般使用 DEL 命令,但是直接刪除鎖可能存在問題。
一般解鎖需要兩步操作:
查詢當(dāng)前“鎖”是否還是我們持有,因?yàn)榇嬖谶^期時(shí)間,所以可能等你想解鎖的時(shí)候,“鎖”已經(jīng)到期,然后被其他線程獲取了,所以我們?cè)诮怄i前需要先判斷自己是否還持有“鎖”。
如果“鎖”還是我們持有,則執(zhí)行解鎖操作,也就是刪除該鍵值對(duì),并返回成功;否則,直接返回失敗。
由于當(dāng)前 Redis 還沒有原子命令直接支持這兩步操作,所以當(dāng)前通常是使用 Lua 腳本來執(zhí)行解鎖操作,Redis 會(huì)保證腳本里的內(nèi)容執(zhí)行是一個(gè)原子操作。
以下是 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]:我們加鎖時(shí)的 value,用于判斷當(dāng)“鎖”是否還是我們持有,如果被其他線程持有了,value 就會(huì)發(fā)生變化。
續(xù)鎖
一般為了防止死鎖,比如服務(wù)器宕機(jī)或斷線的情況下無法手動(dòng)解鎖,此時(shí)就需要給分布式鎖加上過期時(shí)間。
但是假如在我們業(yè)務(wù)執(zhí)行的過程中,Redis 分布式鎖過期了,業(yè)務(wù)還沒處理完怎么辦?
首先,我們?cè)?strong>設(shè)置過期時(shí)間時(shí)要結(jié)合業(yè)務(wù)場景去設(shè)計(jì),盡量設(shè)置一個(gè)比較合理的值,就是理論上正常處理的話,在這個(gè)過期時(shí)間內(nèi)是一定能處理完畢的。
然后我們需要應(yīng)對(duì)一些特殊惡劣情況進(jìn)行設(shè)計(jì)。
目前的解決方案一般有兩種:
同時(shí),需要進(jìn)行告警,人為介入驗(yàn)證數(shù)據(jù)的正確性,然后找出超時(shí)原因,是否需要對(duì)超時(shí)時(shí)間進(jìn)行優(yōu)化等等。
守護(hù)線程“續(xù)命”存在的問題
Redisson 使用看門狗(守護(hù)線程)“續(xù)命”的方案在大多數(shù)場景下是挺不錯(cuò)的,也被廣泛應(yīng)用于生產(chǎn)環(huán)境,但是在極端情況下還是會(huì)存在問題。
問題例子如下:
解決方法:上述問題的根本原因主要是由于 Redis 異步復(fù)制帶來的數(shù)據(jù)不一致問題導(dǎo)致的,因此解決的方向就是保證數(shù)據(jù)的一致。
當(dāng)前比較主流的解法和思路有兩種:
這里我們來說一下第一種 RedLock 的解決思路。
RedLock
紅鎖是Redis作者提出的一致性解決方案。紅鎖的本質(zhì)是一個(gè)概率問題:如果一個(gè)主從架構(gòu)的Redis在高可用切換期間丟失鎖的概率是k%,那么相互獨(dú)立的 N 個(gè) Redis 同時(shí)丟失鎖的概率是多少?如果用紅鎖來實(shí)現(xiàn)分布式鎖,那么丟鎖的概率是(k%)^N。鑒于Redis極高的穩(wěn)定性,此時(shí)的概率已經(jīng)完全能滿足產(chǎn)品的需求。
說明紅鎖的實(shí)現(xiàn)并非這樣嚴(yán)格,一般保證M(1<M=<N)個(gè)同時(shí)鎖上即可,但通常仍舊可以滿足需求。
RedLock 算法
算法很易懂,起 5 個(gè) master 節(jié)點(diǎn),分布在不同的機(jī)房盡量保證可用性。為了獲得鎖,client 會(huì)進(jìn)行如下操作:
失敗重試
如果一個(gè) client 申請(qǐng)鎖失敗了,那么它需要稍等一會(huì)在重試避免多個(gè) client 同時(shí)申請(qǐng)鎖的情況,最好的情況是一個(gè) client 需要幾乎同時(shí)向 5 個(gè) master 發(fā)起鎖申請(qǐng)。另外就是如果 client 申請(qǐng)鎖失敗了它需要盡快在它曾經(jīng)申請(qǐng)到鎖的 master 上執(zhí)行 unlock 操作,便于其他 client 獲得這把鎖,避免這些鎖過期造成的時(shí)間浪費(fèi),當(dāng)然如果這時(shí)候網(wǎng)絡(luò)分區(qū)使得 client 無法聯(lián)系上這些 master,那么這種浪費(fèi)就是不得不付出的代價(jià)了。
RedLock 的問題
- 占用的資源過多,為了實(shí)現(xiàn)紅鎖,需要?jiǎng)?chuàng)建多個(gè)互不相關(guān)的云Redis實(shí)例或者自建Redis,成本較高。
- 嚴(yán)重依賴系統(tǒng)時(shí)鐘。如果線程1從3個(gè)實(shí)例獲取到了鎖,但是這3個(gè)實(shí)例中的某個(gè)實(shí)例的系統(tǒng)時(shí)間走的稍微快一點(diǎn),則它持有的鎖會(huì)提前過期被釋放,當(dāng)他釋放后,此時(shí)又有3個(gè)實(shí)例是空閑的,則線程2也可以獲取到鎖,則可能出現(xiàn)兩個(gè)線程同時(shí)持有鎖了。
- 如果線程1從3個(gè)實(shí)例獲取到了鎖,但是萬一其中有1臺(tái)重啟了,則此時(shí)又有3個(gè)實(shí)例是空閑的,則線程2也可以獲取到鎖,此時(shí)又出現(xiàn)兩個(gè)線程同時(shí)持有鎖了。
總結(jié)
以上是生活随笔為你收集整理的备战面试日记(6.1) - (缓存相关.Redis全知识点)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Vetur can‘t find `ts
- 下一篇: INT102 算法笔记