备战面试日记(6.1) - (缓存相关.Redis全知识点)
本人本科畢業(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,滿足大部分的使用要求。
| 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_STRING | REDIS_ENCODING_INT | 使用整數(shù)值來實(shí)現(xiàn)的字符串對象 |
| REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用embstr編碼的簡單動態(tài)字符串實(shí)現(xiàn)的字符串對象 |
| REDIS_STRING | REDIS_ENCODING_RAW | 使用簡單動態(tài)字符串實(shí)現(xiàn)的字符串對象 |
| REDIS_LIST | REDIS_ENCODING_ZIPLIST | 使用壓縮列表實(shí)現(xiàn)的列表對象 |
| REDIS_LIST | REDIS_ENCODING_LINKEDLIST | 使用雙端鏈表實(shí)現(xiàn)的列表對象 |
| REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用壓縮列表實(shí)現(xiàn)的哈希對象 |
| REDIS_HASH | REDIS_ENCODING_HT | 使用字典實(shí)現(xiàn)的哈希對象 |
| REDIS_SET | REDIS_ENCODING_INTSET | 使用整數(shù)集合實(shí)現(xiàn)的集合對象 |
| REDIS_SET | REDIS_ENCODING_HT | 使用字典實(shí)現(xiàn)的集合對象 |
| REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用壓縮列表實(shí)現(xiàn)的有序集合對象 |
| REDIS_ZSET | REDIS_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ū)別
如果一張表來說明,即:
| 獲取字符串長度的復(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 分配額外的未使用空間。
這里存在兩種修改情況:
惰性空間釋放的操作是:當(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].sizemaskRedis 使用 MurmurHash 算法。
解決哈希沖突
Redis 的哈希表使用鏈地址法解決哈希沖突,并且使用的是頭插法。
rehash
hash 對象在擴(kuò)容時使用了一種叫 “漸進(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)建一張空白哈希表。
這兩個哈希表的套路是不是有點(diǎn)像jvm運(yùn)行時數(shù)據(jù)區(qū)的年輕代的幸存者區(qū)?可以引申一下。
rehash條件
當(dāng)下面兩個條件任意一個被滿足時,程序就會自動開始對哈希表進(jìn)行擴(kuò)展操作:
為什么這兩個命令的是否正在執(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ì)步驟:
漸進(jìn)式hash采取 分而治之 的思想,將rehash鍵值對所需的計(jì)算工作均攤到字典的每個添加、刪除、查找、更新操作上,避免集中式hash。
漸進(jìn)式hash執(zhí)行期間進(jìn)行哈希表操作
漸進(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ù)集合需要升級,然后才能將新元素添加到集合中。
具體升級并添加元素的步驟分為三步:
該過程的復(fù)雜度為 O(N)。
升級的好處
整數(shù)集合降級
整數(shù)集合不支持降級操作!
壓縮列表
它的存在意義就是為了節(jié)約內(nèi)存。
壓縮列表定義
壓縮列表就是一個由一系列特殊編碼的連續(xù)內(nèi)存塊組成的順序型數(shù)據(jù)結(jié)構(gòu)。
壓縮列表的各個組成部分說明如下表:
| zlbytes | uint32_t | 4字節(jié) | 記錄整個壓縮鏈表占用的字節(jié)數(shù),在對壓縮列表進(jìn)行內(nèi)存重分配,或者計(jì)算zlend的位置時使用。 |
| zltail | uint32_t | 4字節(jié) | 記錄壓縮列表表尾節(jié)點(diǎn)距離壓縮列表起始地址有多少個字節(jié):通過這個偏移量,程序無須遍歷整個壓縮列表就可以確定尾節(jié)點(diǎn)的地址。 |
| zllen | uint16_t | 2字節(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)容而定。 |
| zlend | uint8_t | 1字節(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 的好處:
不過,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持久化:
RDB優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
缺點(diǎn)
AOF
執(zhí)行流程
觸發(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ī)制
文件事件處理器大致可分為三個處理流程:
拓展
關(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)行清理:
其中 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ù)要寫入時,并且沒有多余的緩存空間,那么該鏈表會做兩件事情:
簡單說明,即它認(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ī)則:
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ù)器值的變化情況。
| 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ù)為 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ù)的衰減。
通過上方的第二點(diǎn),我們就能知道一個規(guī)律,lfu_decay_time 值越大,那么相應(yīng)的衰減值會變小,衰減效果也會減弱;反之相應(yīng)的衰減值會變大,衰減效果也會增強(qiáng)。
所以,如果業(yè)務(wù)應(yīng)用中有短時高頻訪問的數(shù)據(jù)的話,建議把 lfu_decay_time 值設(shè)置為 1。
使用總結(jié)
事務(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í)行分為三個階段:
從輸入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 命令后,會遇到三種情況:
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)證階段可能會遇到以下幾種情況:
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ù)器返回從服務(wù)器也有三種情況:
從上方可知,主要包括全量數(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)程的作用
哨兵(Sentinel) 和 一般Redis 的區(qū)別?
哨兵的工作方式
創(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ù)器信息
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_ip | Sentinel 的 IP地址 |
| s_port | Sentinel 的端口號 |
| s_runid | Sentinel 的運(yùn)行ID |
| s_epoch | Sentinel 當(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ù)器中選擇的,所以需要選擇狀態(tài)良好、數(shù)據(jù)完整的從服務(wù)器。領(lǐng)頭 Sentinel 的數(shù)據(jù)結(jié)構(gòu)中保存了原master對應(yīng)的 slave ,Sentinel 會刪除狀態(tài)較差的slave。過濾執(zhí)行順序如下:
對應(yīng)第三條,我可以解釋一下,前面提到過,在 down-after-millisecond 設(shè)置的時長內(nèi)沒有收到有效回復(fù),可以判定當(dāng)前復(fù)制的主服務(wù)器主觀下線。所以,越遲和主服務(wù)器斷開連接的從服務(wù)器,數(shù)據(jù)越新。
(二)、現(xiàn)在過濾出的都是健康的從服務(wù)器了,然后 Sentinel 開始選擇新的主服務(wù)器,有以下三個優(yōu)先級順序:
(三)、選出新的主服務(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)有:
下面將會根據(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和節(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)是否處理指定索引的槽位。
所以槽位的遷移非常簡單:
移動槽位的成本是非常低的。客戶端的 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)個位置?【翻譯官方回答】
因此,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í)行完畢后,開始廣播通知給集群中的其他節(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)行重新分片的步驟如下:
如果涉及多個槽,則給每個槽重復(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)的命令,并且該鍵的槽位正好處在重新分片的過程中:
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)都會定期向其他節(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)也是通過選舉產(chǎ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ì)。
目前的解決方案一般有兩種:
同時,需要進(jìn)行告警,人為介入驗(yàn)證數(shù)據(jù)的正確性,然后找出超時原因,是否需要對超時時間進(jìn)行優(yōu)化等等。
守護(hù)線程“續(xù)命”存在的問題
Redisson 使用看門狗(守護(hù)線程)“續(xù)命”的方案在大多數(shù)場景下是挺不錯的,也被廣泛應(yīng)用于生產(chǎn)環(huán)境,但是在極端情況下還是會存在問題。
問題例子如下:
解決方法:上述問題的根本原因主要是由于 Redis 異步復(fù)制帶來的數(shù)據(jù)不一致問題導(dǎo)致的,因此解決的方向就是保證數(shù)據(jù)的一致。
當(dāng)前比較主流的解法和思路有兩種:
這里我們來說一下第一種 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)行如下操作:
失敗重試
如果一個 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)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Vetur can‘t find `ts
- 下一篇: php mysql 抽奖_jQuery+