# 前言
### 為什么我要嘗試寫作技術(shù)書籍
- 一個(gè)人年輕時(shí)經(jīng)歷的艱難會(huì)在未來(lái)成為他的財(cái)富
# 第一篇 基礎(chǔ)和應(yīng)用篇
## 1.1 授人以魚(yú)不如授人以漁
Redis:Remote Dictionary Service,遠(yuǎn)程字典服務(wù)
### 1.1.1 由Redis面試想到的
架構(gòu)師的技能水平很高,對(duì)提升團(tuán)隊(duì)研發(fā)效率很有幫助,我們非常欽佩和羨慕,但是普通開(kāi)發(fā)者如果習(xí)慣于在架構(gòu)師封裝好的東西上,只專注于做業(yè)務(wù)開(kāi)發(fā),那么久而久之,在技術(shù)理解和成長(zhǎng)上就會(huì)變得遲鈍甚至麻木。從這個(gè)角度上看,架構(gòu)師可能成為普通開(kāi)發(fā)者的”敵人“,他的強(qiáng)大能力會(huì)讓大家變成”溫室的花朵“,一旦遇到環(huán)境變化就會(huì)不知所措
## 1.2 萬(wàn)丈高樓平地起 — Redis基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)
### 1.2.2 5種基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)
Redis有5種基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),分別為:string(字符串)、list(列表)、hash(字典)、set(集合)、zset(有序集合)
#### string(字符串)
- Redis所有的數(shù)據(jù)結(jié)構(gòu)以唯一的key字符串作為名稱,然后通過(guò)這個(gè)唯一key值來(lái)獲取相應(yīng)的value數(shù)據(jù),不同類型的數(shù)據(jù)結(jié)構(gòu)的差異就在于value的結(jié)構(gòu)不一樣
- Redis的字符串是動(dòng)態(tài)字符串,是可以修改的字符串,內(nèi)部結(jié)構(gòu)的實(shí)現(xiàn)類似于Java的ArrayList,實(shí)際空間capacity一般高于實(shí)際字符串長(zhǎng)度len,1M空間動(dòng)態(tài)擴(kuò)容
- 鍵值對(duì):set name codehole; get name; exits name; del name; get name;
- 批量鍵值對(duì):set name1 codehole; set name2 holycoder; mget name1 name2 name3; mset name1 boy name2 girl name3 unkown; mget name1 name2 name3;
- 過(guò)期和set命令擴(kuò)展:set name codehole; get name; expire name 5; get name; setex name 5 codehole; get name; setnx name codehole; get name; set nax name holycoder; get name;
- 計(jì)數(shù):set age 30; incr age; incrby age 5; incrby age -5; set codehole 9223372036854775807; incr codehole;
#### list(列表)
- Redis的列表相當(dāng)于Java語(yǔ)言里面的LinkedList,注意它是鏈表不是數(shù)組,意味著插入和刪除操作非??欤饕ㄎ缓苈?/p>
- Redis的列表結(jié)構(gòu)常用來(lái)做異步隊(duì)列使用,將需要延后處理的任務(wù)結(jié)構(gòu)體序列化成字符串,塞進(jìn)Redis的列表,另一個(gè)線程從這個(gè)列表中輪訓(xùn)數(shù)據(jù)進(jìn)行處理
- 右邊進(jìn)左邊出:隊(duì)列 rpush books python java golang; llen books; lpop books;
- 右邊進(jìn)右邊出:棧 rpush books python java golang; rpop books;
- ##### 慢操作
- index相當(dāng)于Java鏈表中的get(int index)方法
- ltrim兩個(gè)參數(shù)start_index和end_index定義了一個(gè)區(qū)間,其他的都刪掉
- index可以為負(fù)數(shù),-1位倒數(shù)第一個(gè)元素
- rpush books python java golang; lindex books 1; lrange books 0 -1; ltrim books 1 -1; lrange books 0 -1; ltrim books 1 0; llen books;
- ##### 快速列表
- Redis底層不是簡(jiǎn)單的linkedlist,而是quicklist的一個(gè)結(jié)構(gòu)
- 元素較少的時(shí)候,使用一塊連續(xù)內(nèi)存存儲(chǔ),結(jié)構(gòu)是ziplist,數(shù)據(jù)比較多的時(shí)候改為quicklist,多個(gè)ziplist使用雙向指針串起來(lái)使用
#### hash(字典)
- Redis的字典相當(dāng)于Java語(yǔ)言里面的HashMap,數(shù)組+聯(lián)表二維結(jié)構(gòu),漸進(jìn)式rehash
- hset books java "think in java"; hset books golang "concurrency in go"; hset books python "python cookbook"; hgetall books; hlen books; hget books java; hset books golang "learning go programming"; hget books golang; hmset books java "effective java" ptyhon "learning python";
- hash可對(duì)單個(gè)key進(jìn)行計(jì)數(shù):hset user-laoqian age 29; hincrby user-laoqian age 1
#### set(集合)
- Redis的集合相當(dāng)于Java語(yǔ)言里面的HashSet,鍵值對(duì)無(wú)序的
- sadd books python; sadd books python; sadd books java golang; smembers books; sismember books java; sismember books rust; scard books; spop books;
#### zset(有序列表)
- 類似于Java的SortedSet和HashMap的結(jié)合體,跳躍列表數(shù)據(jù)結(jié)構(gòu)
- zadd books 9.0 "think in java"; zadd books 8.9 "java concurreency"; zadd books 8.6 "java cookbook"; zrange books 0 -1; zrevrange books 0 -1; zcard books; zscore books "java concurrency"; zrank books "java concurrency"; zrangebyscore books 0 8.91; zrangebyscore books -inf 8.91 withscores; zrem books "java concurrency"; zrange books 0 -1;
- 跳躍列表,最下面一層所有的元素都會(huì)串起來(lái)。然后每隔幾個(gè)元素挑選出一個(gè)代表,再將這幾個(gè)代表使用另外一級(jí)指針串起來(lái)。然后在這些代表里面再挑出二級(jí)代表,再串起來(lái)。最終就形成了金字塔結(jié)構(gòu)。跳躍列表采取一個(gè)隨機(jī)策略來(lái)決定新元素可以兼職到第幾層。L0層100%,L1層50%,L2層25%...
### 1.2.3 容器型數(shù)據(jù)結(jié)構(gòu)的通用規(guī)則
list set hash zset都是容器型數(shù)據(jù)結(jié)構(gòu),共享兩條規(guī)則
1. create if not exists,如果容器不存在,那就創(chuàng)建一個(gè),再進(jìn)行操作
2. drop if no elements,如果容器的元素沒(méi)有了,那么立即刪除容器,釋放內(nèi)存
### 1.2.4 過(guò)期時(shí)間
Redis所有的數(shù)據(jù)結(jié)構(gòu)都可以設(shè)置過(guò)期時(shí)間,時(shí)間到了,Redis會(huì)自動(dòng)刪除相應(yīng)對(duì)象
## 1.3 千帆競(jìng)發(fā) — 分布式鎖
原子操作是指不會(huì)被線程調(diào)度機(jī)制打斷的操作。這種操作一旦開(kāi)始,就會(huì)一直運(yùn)行到結(jié)束,中間不會(huì)有任何線程切換
### 1.3.1 分布式鎖的奧義
- 分布式鎖本質(zhì)上要實(shí)現(xiàn)的目標(biāo)就是在Redis里面占一個(gè)”坑“,當(dāng)別的進(jìn)程也要占坑時(shí),發(fā)現(xiàn)那里已經(jīng)有一根”大蘿卜“了,就只好放棄或者稍后再試
- 占坑一般使用setnx(set if not exists)指令,先來(lái)先占,用完了,再調(diào)用del指令釋放”坑“
- setnx lock:codehole true; del lock:codehole;
- 一般拿到鎖之后,再給鎖加一個(gè)過(guò)期時(shí)間,避免占坑后邏輯出現(xiàn)異常,沒(méi)有釋放鎖,導(dǎo)致死鎖
- setnx lock:codehole true; expire lock:codehole 5; del lock:codehole;
- 如果setnx和expire之間服務(wù)器進(jìn)程突然掛掉,還是會(huì)造成死鎖。也不能加事務(wù),事務(wù)的特點(diǎn)是一口氣執(zhí)行,要么全執(zhí)行,要么一個(gè)不執(zhí)行,setnx有可能沒(méi)搶到鎖,expire是不應(yīng)該執(zhí)行的。
- Redis 2.8版本,引入了setnx和expire指令可以一起執(zhí)行
- set lock:codehole true ex 5 nx; del lock:codehole;
### 1.3.2 超時(shí)問(wèn)題
- Redis分布式鎖不要用于較長(zhǎng)時(shí)間的任務(wù)
- 稍微安全的辦法:將set的value參數(shù)設(shè)置為一個(gè)隨機(jī)數(shù),釋放鎖的時(shí)候先匹配隨機(jī)數(shù)是否一致,然后再刪除key。確保當(dāng)前線程占的鎖不會(huì)被其他線程釋放,除非這個(gè)鎖是因?yàn)檫^(guò)期而被服務(wù)器釋放,但匹配和刪除不是一個(gè)院子操作,需要使用Lua腳本處理,保證連續(xù)多個(gè)指令的原子性執(zhí)行。這個(gè)方案只是相對(duì)安全一些,如果真的超時(shí)了,當(dāng)前線程邏輯沒(méi)有處理完,其他線程也會(huì)趁虛而入
### 1.3.3 可重入性
- 如果一個(gè)鎖支持同一個(gè)線程的多次加鎖,那么這個(gè)鎖是可重用的。Redis如果要支持可重入,需要客戶端對(duì)set封裝,使用線程的Threadlocal變量存儲(chǔ)當(dāng)前持有鎖的計(jì)數(shù)
## 1.4 緩兵之計(jì) — 延時(shí)隊(duì)列
### 1.4.1 異步消息隊(duì)列
- Redis的list(列表)數(shù)據(jù)結(jié)構(gòu)常用來(lái)作為異步消息隊(duì)列使用,用rpush和lpush操作入隊(duì)列,用lpop和rpop操作出隊(duì)列
### 1.4.2 隊(duì)列空了怎么辦
- 如果隊(duì)列空了,客戶端就會(huì)陷入pop的死循環(huán),通常我么使用sleep來(lái)解決這個(gè)問(wèn)題
### 1.4.3 阻塞讀
- 睡眠會(huì)導(dǎo)致延遲增大,blpop/brpop,阻塞讀在隊(duì)列沒(méi)有數(shù)據(jù)的時(shí)候,會(huì)立即進(jìn)入休眠狀態(tài),一旦數(shù)據(jù)到來(lái),則立刻醒過(guò)來(lái)。消息的延遲幾乎為0
### 1.4.4 空閑連接自動(dòng)斷開(kāi)
- 如果線程一直阻塞在那里,Redis的客戶端就成了空閑連接,閑置過(guò)久,服務(wù)器一般會(huì)主動(dòng)斷開(kāi)連接,減少閑置資源占用。這個(gè)時(shí)候blpop/brpop會(huì)拋出異常,所以客戶端需要捕獲異常后重試
### 1.4.5 鎖沖突處理
客戶端加鎖沒(méi)成功處理策略
1. 直接拋出異常,通知用戶稍后重試
2. sleep一會(huì)兒,然后重試
3. 將請(qǐng)求轉(zhuǎn)移至延時(shí)隊(duì)列,過(guò)一會(huì)再試
### 1.4.6 延時(shí)隊(duì)列的實(shí)現(xiàn)
- 延時(shí)隊(duì)列可以通過(guò)Redis的zset(有序列表)來(lái)實(shí)現(xiàn),我們將消息序列化成一個(gè)字符串作為set的value,這個(gè)消息的到期處理時(shí)間做為score,然后多個(gè)線程輪訓(xùn)zset獲取到期的任務(wù)進(jìn)行處理。
- Redis的zerm方法可以返回多線程中是否搶到任務(wù)
### 1.4.7 進(jìn)一步優(yōu)化
- 同一個(gè)任務(wù)可能會(huì)被多個(gè)進(jìn)程取到之后再使用zrem進(jìn)行爭(zhēng)搶,沒(méi)搶到的進(jìn)程白取了一次任務(wù),可使用lua scripting來(lái)優(yōu)化,將zrangebyscore和zrem一同挪到服務(wù)器端進(jìn)行原子操作
## 1.5 節(jié)衣縮食 — 位圖
位圖不是特殊的數(shù)據(jù)結(jié)構(gòu),它的內(nèi)容其實(shí)就是普通的字符串,也就是byte數(shù)組,我們可以使用普通的get/set直接獲取和設(shè)置整個(gè)位圖的內(nèi)容,也可以使用位圖操作getbit/setbit等將byte數(shù)組看成”位數(shù)組“來(lái)處理
### 1.5.1 基本用法
零存零取,整存零取
### 1.5.2 統(tǒng)計(jì)和查找
Redis提供了位圖統(tǒng)計(jì)指令bitcount和位圖查找指令bitpos
### 1.5.3 魔術(shù)指令bitfield
- bitfield有三個(gè)子指令,get、set、incrby
- bitfield提供了溢出策略子指令overflow,飽和截?cái)啵〔粓?zhí)行
## 1.6 四兩撥千斤 — HyperLogLog
用來(lái)解決非精確統(tǒng)計(jì)問(wèn)題,UV
### 1.6.1 使用方法
pfadd和set集合的sadd的用法是一樣的,來(lái)一個(gè)用戶ID,就將用戶ID塞進(jìn)去就是。pfcount和scard的用法一樣,直接獲取計(jì)數(shù)值
### 1.6.2 pfadd中的pf是什么意思
HyperLogLog數(shù)據(jù)結(jié)構(gòu)的發(fā)明人Philippe Flajolet,pf是縮寫
### 1.6.3 pfmerge適合的場(chǎng)景
pfmerge,用于將多個(gè)pf計(jì)數(shù)值累加在一起形成一個(gè)新的pf值
### 1.6.4 注意事項(xiàng)
需要占據(jù)12KB的存儲(chǔ)空間,數(shù)據(jù)少的時(shí)候采用稀疏矩陣存儲(chǔ),超過(guò)閾值后,才會(huì)一次性轉(zhuǎn)變?yōu)槌砻芫仃?/p>
### 1.6.5 HyperLogLog實(shí)現(xiàn)原理
給定一系列隨機(jī)數(shù),記錄下低位連續(xù)零位的最大長(zhǎng)度K,可通過(guò)K估算出隨機(jī)數(shù)的數(shù)量N。K和N的對(duì)數(shù)之間存在顯著的線性相關(guān)性
### 1.6.6 pf的內(nèi)存占用為什么是12KB
實(shí)現(xiàn)的時(shí)候使用的是2的14次方桶,每個(gè)桶的maxbits需要6個(gè)bit存儲(chǔ)
## 1.7 層巒疊嶂 — 布隆過(guò)濾器
布隆過(guò)濾器,專門解決去重問(wèn)題
### 1.7.1 布隆過(guò)濾器是什么
是一個(gè)不怎么精確的set結(jié)構(gòu),當(dāng)使用contains方法判斷對(duì)象是否存在,可能產(chǎn)生誤判。它說(shuō)某個(gè)值存在時(shí),可能不存在。它說(shuō)某個(gè)值不存在時(shí),那他肯定不存在
### 1.7.2 Redis中的布隆過(guò)濾器
docker run -p6379:6379 redislabs/rebloom
### 1.7.3 布隆過(guò)濾器的基本用法
- bf.add添加元素,bf.exists查詢?cè)厥欠翊嬖冢砑佣鄠€(gè)元素要用bf.madd,查詢多個(gè)元素是否存在bf.mexists
- bf.reserve指令顯示創(chuàng)建,key,error_rate,initial_size
### 1.7.4 注意事項(xiàng)
initial_size設(shè)置過(guò)大,會(huì)浪費(fèi)存儲(chǔ)空間。error_rate越小,需要存儲(chǔ)空間越大
### 1.7.5 布隆過(guò)濾器的原理
- 數(shù)據(jù)結(jié)構(gòu)里面就是一個(gè)大型的位數(shù)組和幾個(gè)不一樣的無(wú)偏hash函數(shù),無(wú)偏hash就是hash值比較平均
- add的時(shí)候進(jìn)行hash,然后對(duì)長(zhǎng)度取模,相應(yīng)位置1,查詢的時(shí)候,全是1代表極有可能存在,只要有一位是0,那么肯定不存在
- 實(shí)際元素大于初始化數(shù)量,應(yīng)該對(duì)布隆過(guò)濾器進(jìn)行重建,重新分配size更大的過(guò)濾器,并且把歷史元素add進(jìn)去
### 1.7.6 空間占用估計(jì)
- 預(yù)計(jì)元素?cái)?shù)量n,錯(cuò)誤率f=>數(shù)組的長(zhǎng)度l,hash的最佳數(shù)量k
- set中存儲(chǔ)的是每隔元素的內(nèi)容,而布隆過(guò)濾器僅僅存儲(chǔ)元素的指紋
### 1.7.7 實(shí)際元素超出時(shí),誤判率會(huì)怎樣變化
- 錯(cuò)誤率10%,倍數(shù)比為2,錯(cuò)誤率會(huì)到40%
- 錯(cuò)誤率1%,倍數(shù)為2,錯(cuò)誤率會(huì)升到15%
- 錯(cuò)誤率為0.1%,倍數(shù)為2,錯(cuò)誤率會(huì)升到5%
### 1.7.8 用不上Redis 4.0怎么辦
- Redis布隆過(guò)濾器Python庫(kù),pyreBloom
- Redis布隆過(guò)濾器Java庫(kù),orestes-bloomfilter
### 1.7.9 布隆過(guò)濾器的其他應(yīng)用
- 爬蟲(chóng)過(guò)濾爬過(guò)的網(wǎng)站
- NoSQL,查詢某個(gè)row,先通過(guò)內(nèi)存中過(guò)濾器過(guò)濾大量不存在的row
- 垃圾郵件過(guò)濾
## 1.8 短尾求生 — 簡(jiǎn)單限流
除了控制流量,限流還有一個(gè)應(yīng)用目的是控制用戶行為,避免垃圾請(qǐng)求
### 1.8.1 如何使用Redis來(lái)實(shí)現(xiàn)簡(jiǎn)單限流策略
系統(tǒng)要限制用戶的某個(gè)行為在指定的時(shí)間里智能發(fā)生N次
### 1.8.2 解決方案
zset數(shù)據(jù)結(jié)構(gòu)的score值,通過(guò)score來(lái)保留時(shí)間窗口。每一個(gè)行為都會(huì)作為zset中的一個(gè)key保存下來(lái),同一個(gè)用戶的同一種行為用一個(gè)zset記錄
## 1.9 一毛不拔 — 漏斗限流
- 漏斗的剩余空間代表著當(dāng)前行為可以持續(xù)進(jìn)行的數(shù)量,漏嘴的流水率代表著系統(tǒng)允許該行為的最大頻率
- Funnel使用hash,無(wú)法保證原子性,從hash結(jié)構(gòu)中取值,然后內(nèi)存運(yùn)算,再回填到hash。而一旦加鎖,就意味著加鎖失敗可能,選擇重試會(huì)導(dǎo)致性能下降,選擇放棄,影響用戶體驗(yàn),需要Redis-Cell救星
### 1.9.1 Redis-Cell
- Redis 4.0模塊提供Redis-Cell模塊,命令為cl.throttle
- cl.throtttle laoqian:reply 15 30 60 1,laoqian:reply:key laoqian,15:capacity是漏斗容量,30:operations,60 seconds,30/60為漏斗速率,1:need 1 quota,可選,默認(rèn)是1
- 返回值0,15,14,-1,2;0代表允許,1表示拒絕;15:漏斗容量capacity,14:漏斗剩余空間left_quota;-1:如果被拒絕了,需要多長(zhǎng)時(shí)間后再試,單位s;2:多長(zhǎng)時(shí)間后,漏斗完全空出來(lái)
## 1.10 近水樓臺(tái) — GeoHash
### 1.10.1 用數(shù)據(jù)庫(kù)來(lái)算附近的人
一般方法都是通過(guò)指定舉行區(qū)域來(lái)限定元素的數(shù)量,然后對(duì)區(qū)域內(nèi)的元素進(jìn)行全量距離計(jì)算再排序。數(shù)據(jù)庫(kù)表需要把經(jīng)緯度坐標(biāo)加上雙向復(fù)合索引(x, y)
### 1.10.2 GeoHash算法
- GeoHash可以將二維的經(jīng)緯度坐標(biāo)映射到一維的整數(shù)
- 地球看為平面,二分法劃分方格,00,01,10,11,Redis里面經(jīng)緯度用52位整數(shù)進(jìn)行編碼,放進(jìn)zset中,zset的value元素的key,score是GeoHash的52位整數(shù)
### 1.10.3 Geo指令的基本用法
- 增加,geoadd,集合名稱,多個(gè)經(jīng)緯度名稱三元組
- 距離,geodist,集合名稱、兩個(gè)名稱和距離單位
- 獲取元素位置,geopos,集合,元素名稱,獲取的坐標(biāo)是有損的
- 獲取元素的 hash值,base32編碼
- 附近的公司,georadiusbymember,查詢指定元素附近的其他元素;georadius,查詢附近的的元素指令
- 數(shù)據(jù)量過(guò)大,需要對(duì)Geo數(shù)據(jù)進(jìn)行拆分,按照國(guó)家拆分、省、市、區(qū)
## 1.11 大海撈針 — scan
- 如何從海量的key中找出滿足特定前綴的key列表
- keys命令用來(lái)列出所有滿足特定正則字符串規(guī)則的key
- 指令缺點(diǎn):1. 沒(méi)有offset、limit 參數(shù) 2. keys算法是遍歷,復(fù)雜度O(n),這個(gè)指令卡頓,所有讀寫Redis其他指令都會(huì)延后甚至報(bào)錯(cuò),因?yàn)镽edis單線程,引入了scan命令解決
- scan優(yōu)點(diǎn):1. 復(fù)雜度雖然是O(n),但是通過(guò)游標(biāo)分布進(jìn)行的,不會(huì)阻塞線程。2. 提供limit參數(shù) 3. 同keys一樣,也提供模式匹配 4. 服務(wù)器你不需要為游標(biāo)保存狀態(tài),游標(biāo)唯一狀態(tài)就是為客戶端返回游標(biāo)整數(shù) 5. 返回的結(jié)果可能會(huì)有重復(fù),需要客戶端去重 6. 遍歷定的過(guò)程中如果有數(shù)據(jù)修改,改動(dòng)后的數(shù)據(jù)能不能遍歷到是不確定的 7. 單次返回的結(jié)果是空的并不意味著遍歷結(jié)束,而是要看游標(biāo)值是否為0
### 1.11.1 scan基本用法
- scan提供三個(gè)參數(shù),第一個(gè)是cursor整數(shù)值,第二個(gè)是key的正則模式,第三個(gè)是遍歷的limit hint。
- limit不是返回的數(shù)量結(jié)果,是單詞遍歷的字典槽位數(shù)量(約等于)
### 1.11.2 字典的結(jié)構(gòu)
- 在Redis里所有的key都存儲(chǔ)在一個(gè)很大的字典中,類似HashMap,是一維數(shù)組,二維聯(lián)表結(jié)構(gòu)
- scan返回的游標(biāo)就是第一維數(shù)組的位置索引,我們稱之為槽
### 1.11.3 scan遍歷順序
高位進(jìn)位加法來(lái)遍歷,避免擴(kuò)容或縮容時(shí)槽位的遍歷重復(fù)和遺漏
### 1.11.4 字典擴(kuò)容
Java的HashMap擴(kuò)容,重新分配2倍大小數(shù)組,所有元素rehash新的數(shù)組下面,rehash相當(dāng)于元素的hash值對(duì)數(shù)組長(zhǎng)度進(jìn)行取模運(yùn)算,因?yàn)閿?shù)組的長(zhǎng)度是2的n次方,所以等價(jià)于位與操作。7,15,31成為字典的mask值,mask的作用就是保留hash值的低位,高位被置為0
### 1.11.5 對(duì)比擴(kuò)容、縮容前后的遍歷順序
高位進(jìn)位加法的遍歷順序,rehash后的槽位在遍歷順序上是相鄰的。擴(kuò)容可以避免重復(fù)遍歷,縮容會(huì)有重復(fù)遍歷
### 1.11.6 漸進(jìn)式rehash
Java的擴(kuò)容,會(huì)將HashMap一次性rehash,Redis需要使用漸進(jìn)式rehash,先同時(shí)保留舊數(shù)組和新數(shù)組,定時(shí)任務(wù)中以及后續(xù)對(duì)hash指令操作中漸漸地將就數(shù)組中掛接的元素遷移到新數(shù)組
### 1.11.7 更多的scan指令
zscan遍歷zset集合元素,hscan遍歷hash字典的元素,sscan遍歷set集合的元素
### 1.11.8 大key掃描
- 平時(shí)業(yè)務(wù)邏輯,要盡量避免大key產(chǎn)生。會(huì)引起卡頓
- redis-cli -h 127.0.0.1 -p 7001 --bigkeys掃描大key,可以加個(gè)休眠參數(shù) -i 0.1
# 第2篇 原理篇
## 2.1 鞭辟入里 — 線程IO模型
Redis是個(gè)單線程程序
對(duì)于那些O(n)級(jí)別的指令,一定要謹(jǐn)慎使用
### 2.1.1 非阻塞IO
非阻塞IO在套接字對(duì)象上提供了一個(gè)選項(xiàng)Non_Blocking,當(dāng)這個(gè)選項(xiàng)打開(kāi)時(shí),讀寫方法不會(huì)阻塞,而是能讀多少讀多少,能寫多少寫多少。
### 2.1.2 事件輪詢(多路復(fù)用)
最簡(jiǎn)單的事件輪詢API是select函數(shù),它是操作系統(tǒng)他提供給用戶程序的API。輸入是讀寫描述符列表read_fds & writ_fds,輸出是與之對(duì)應(yīng)的可讀可寫時(shí)間。同時(shí)還提供了一個(gè)timeout參數(shù),如果沒(méi)有任何事件到來(lái),那么就最多等待timeout的值的時(shí)間,線程處于阻塞狀態(tài)。一旦期間有任何事件到來(lái),就可以立即返回。時(shí)間過(guò)了之后還是沒(méi)有任何事件到來(lái),也會(huì)立即返回。
### 2.1.3 指令隊(duì)列
Redis會(huì)將每個(gè)客戶端套接字都關(guān)聯(lián)一個(gè)指令隊(duì)列??蛻舳说闹噶钔ㄟ^(guò)隊(duì)列來(lái)排隊(duì)進(jìn)行順序處理,先到先服務(wù)。
### 2.1.4 響應(yīng)隊(duì)列
Redis同樣也會(huì)為每個(gè)客戶端套接字關(guān)聯(lián)一個(gè)響應(yīng)隊(duì)列。Redis服務(wù)器通過(guò)響應(yīng)隊(duì)列來(lái)將指令的返回結(jié)果回復(fù)給客戶端。
### 2.1.5 定時(shí)任務(wù)
Redis的定時(shí)任務(wù)會(huì)記錄在一個(gè)被稱為“最小堆”的數(shù)據(jù)結(jié)構(gòu)中,在這個(gè)堆中,最快要執(zhí)行的任務(wù)排在堆的最上方。
## 2.2 交頭接耳 — 通信協(xié)議
Redis將所有數(shù)據(jù)都放入內(nèi)存中,用一個(gè)單線程對(duì)外提供服務(wù),單個(gè)節(jié)點(diǎn)在跑滿一個(gè)CPU核心的情況下可以達(dá)到了10W/s的超高QPS
### 2.2.1 RESP
RESP是Redis序列化協(xié)議(Redis Serialization Protocol)的縮寫,它是一種直觀的文本協(xié)議,優(yōu)勢(shì)在于實(shí)現(xiàn)過(guò)程異常簡(jiǎn)單,解析性能極好。
Redis協(xié)議將傳輸分為5種最小單元類型,單元結(jié)束時(shí)統(tǒng)一加上回車換行符號(hào)\r\n
1. 單行字符串以”+”符號(hào)開(kāi)頭
2. 多行字符串以“$”符號(hào)開(kāi)頭,后跟字符串長(zhǎng)度
3. 整數(shù)值以“:”符號(hào)開(kāi)頭,后跟整數(shù)的字符串形式
4. 錯(cuò)誤消息以“-”符號(hào)開(kāi)頭
5. 數(shù)組以”*”號(hào)開(kāi)頭,后跟數(shù)組的長(zhǎng)度
### 2.2.2 客戶端 -> 服務(wù)器
客戶端向服務(wù)器發(fā)送的指令只有一種格式,多行字符串?dāng)?shù)組。
### 2.2.3 服務(wù)器 -> 客戶端
服務(wù)器向客戶端回復(fù)的響應(yīng)要支持多種數(shù)據(jù)結(jié)構(gòu),但再?gòu)?fù)雜的消息也不會(huì)超過(guò)以上5種數(shù)據(jù)結(jié)構(gòu)
### 2.2.4 小結(jié)
Redis協(xié)議里雖然有大量冗余的回車換行符,但不影響它成為互聯(lián)網(wǎng)技術(shù)領(lǐng)域非常受歡迎的文本協(xié)議。在技術(shù)領(lǐng)域里,性能并不總是一切,還有簡(jiǎn)單性、易理解性和易實(shí)現(xiàn)性,這些都需要進(jìn)行適當(dāng)權(quán)衡。
## 2.3 未雨綢繆 — 持久化
- Redis持久化機(jī)制有兩種,第一種是快照,第二種是AOF日志??煺帐且淮稳總浞荩珹OF日志是連續(xù)的增量備份。
- 快照是內(nèi)存數(shù)據(jù)的二進(jìn)制序列化形式,在存儲(chǔ)上非常緊湊,而AOF日志記錄的是內(nèi)存數(shù)據(jù)修改的指令記錄文本
- AOF日志在長(zhǎng)期的運(yùn)行過(guò)程中會(huì)變得無(wú)比龐大,數(shù)據(jù)庫(kù)重啟時(shí)需要加載AOF日志進(jìn)行指令重放,這個(gè)時(shí)間就會(huì)無(wú)比漫長(zhǎng),所以需要定期進(jìn)行AOF重寫,給AOF日志進(jìn)行瘦身
### 2.3.1 快照原理
- 為了不阻塞線上的業(yè)務(wù),Redis就需要一邊持久化,一邊響應(yīng)客戶端請(qǐng)求。持久化的同事,內(nèi)存數(shù)據(jù)結(jié)構(gòu)還在改變
- Redis使用操作系統(tǒng)的多進(jìn)程COW(Copy On Write)機(jī)制來(lái)實(shí)現(xiàn)快照持久化。
### 2.3.2 fork(多進(jìn)程)
- Redis在持久化時(shí)會(huì)調(diào)用glibc的函數(shù)fork產(chǎn)生一個(gè)子進(jìn)程,快照持久化完全交給子進(jìn)程來(lái)處理,父進(jìn)程繼續(xù)處理客戶端請(qǐng)求。子進(jìn)程剛產(chǎn)生的時(shí),它和父進(jìn)程共享內(nèi)存里面的代碼段和數(shù)據(jù)段。
- 子進(jìn)程做數(shù)據(jù)持久化,不會(huì)修改現(xiàn)在的內(nèi)存數(shù)據(jù)結(jié)構(gòu),它只是對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行遍歷讀取,然后序列化寫道磁盤中。但是父進(jìn)程不一樣,它必須持續(xù)服務(wù)客戶端請(qǐng)求,然后對(duì)內(nèi)存數(shù)據(jù)結(jié)構(gòu)進(jìn)行不間斷的修改。
- 這個(gè)時(shí)候就會(huì)使用操作系統(tǒng)的COW機(jī)制進(jìn)行數(shù)據(jù)段頁(yè)面的分離。數(shù)據(jù)段是由操作系統(tǒng)的頁(yè)面組合而成,當(dāng)父進(jìn)程對(duì)其中一個(gè)頁(yè)面的數(shù)據(jù)進(jìn)行修改時(shí),會(huì)被共享的頁(yè)面復(fù)制一份分離出來(lái),然后對(duì)這個(gè)復(fù)制的頁(yè)面進(jìn)行修改。這時(shí)子進(jìn)程相應(yīng)的頁(yè)面是沒(méi)有變化的,還是進(jìn)程產(chǎn)生時(shí)那一瞬間的數(shù)據(jù)。
### 2.3.3 AOF原理
Redis會(huì)在收到客戶端修改指令后,進(jìn)行參數(shù)校驗(yàn)、邏輯處理,如果沒(méi)問(wèn)題,就立即將該指令文本存儲(chǔ)到AOF日志中,也就說(shuō),先執(zhí)行指令才將日志存盤。不同于leveldb、hbase等存儲(chǔ)引擎
### 2.3.4 AOF重寫
Redis提供了bgrewriteaof指令用于對(duì)AOF日志進(jìn)行瘦身,其原理就是開(kāi)辟一個(gè)子進(jìn)程對(duì)內(nèi)存進(jìn)行遍歷,轉(zhuǎn)化成一系列Redis的操作指令,序列化到一個(gè)新的AOF日志中。序列化完畢后再將操作期間發(fā)生的增量AOF日志追加到這個(gè)新的AOF日志文件中,追加完畢后就立即替換舊的AOF日志文件了。
### 2.3.5 fsync
- Linux的glibc提供了fsync(int fd)函數(shù)可以將指定文件的內(nèi)容強(qiáng)制從內(nèi)核緩存刷到磁盤。只要Redis進(jìn)程實(shí)時(shí)調(diào)用fsync函數(shù)就可以保證AOF日志不丟失。但是fsync是一個(gè)磁盤IO操作,它很慢!如果Redis執(zhí)行一條指令就fsync一次,那么Redis高性能的低位就不保了。
- 所以在生產(chǎn)環(huán)境的服務(wù)器中,Redis通常是每隔1s左右執(zhí)行一次fsync操作,這個(gè)1s是可以配置的。這是在數(shù)據(jù)安全性和性能之間做的一個(gè)折中,在保持高性能的同時(shí),盡可能使數(shù)據(jù)少丟失。
### 2.3.6 運(yùn)維
Redis的主節(jié)點(diǎn)不會(huì)進(jìn)行持久化操作,持久化操作主要在從節(jié)點(diǎn)進(jìn)行。從節(jié)點(diǎn)是備份節(jié)點(diǎn),沒(méi)有來(lái)自客戶端請(qǐng)求的壓力,它的操作系統(tǒng)資源往往比較充沛
### 2.3.7 Redis 4.0混合持久化
- 重啟Redis時(shí),我們很少使用rdb來(lái)回復(fù)內(nèi)存狀態(tài),因?yàn)闀?huì)丟失大量數(shù)據(jù)。我們通常使用AOF日志重放,但是重放AOF日志相對(duì)于使用rdb要慢的多。
- 于是在Redis重啟的時(shí)候,可以先加載rdb的內(nèi)容,然后再重放增量AOF日志,就可以完全替代之前的AOF全量文件重放,重啟效率因此得到大幅提升。
## 2.4 雷厲風(fēng)行 — 管道
- Redis管道(Pipeline)本身并不是Redis服務(wù)器直接提供的技術(shù),這個(gè)技術(shù)本質(zhì)上是由客戶端提供的,跟服務(wù)器沒(méi)有什么直接關(guān)系。
### 2.4.1 Redis的消息交互
客戶端經(jīng)理寫-讀-寫-讀四個(gè)操作才能完整的執(zhí)行兩條指令,如果我們調(diào)整讀寫順序,改為寫-寫-讀-讀,這兩個(gè)指令同樣可以完成??蛻舳藢?duì)管道中的指令列表改變讀寫順序就可以大幅節(jié)省IO時(shí)間。管道中指令越多,效果越好
### 2.4.2 管道壓力測(cè)試
redis-benchmark,P參數(shù)
### 2.4.5 深入理解管道本質(zhì)
對(duì)于管道來(lái)說(shuō),連續(xù)的write操作根本就沒(méi)有耗時(shí),之后第一個(gè)read操作會(huì)等待一個(gè)網(wǎng)絡(luò)的來(lái)回開(kāi)銷,然后所有的響應(yīng)消息都已經(jīng)送回到內(nèi)核的讀緩沖了,后續(xù)的read操作直接就可以從緩沖中拿到結(jié)果,瞬間就返回了
## 2.5 同舟共濟(jì) — 事務(wù)
### 2.5.1 Redis事務(wù)的基本用法
指令分別是multi、exec、discard。multi表示事務(wù)的開(kāi)始,exec指示事務(wù)的執(zhí)行,discard指示事務(wù)的丟棄
### 2.5.2 原子性
Redis的事務(wù)根本不具備”原子性“,而僅僅是滿足了事務(wù)的”隔離性“中的串行化 — 當(dāng)前執(zhí)行的事務(wù)有著不被其他事務(wù)打斷的權(quán)利
### 2.5.3 discard(丟棄)
在discard之后,隊(duì)列中的所有制令都沒(méi)執(zhí)行
### 2.5.4 優(yōu)化
通常Redis的客戶端在執(zhí)行事務(wù)的都會(huì)結(jié)合pipeline一起使用,這樣可以將多次IO操作壓縮為單次IO操作
### 2.5.5 watch
- 兩個(gè)并發(fā)的客戶端對(duì)賬戶余額進(jìn)行修改操作,需要取出余額在內(nèi)存乘以倍數(shù),將結(jié)果寫回Redis
- 分布式鎖是一種悲觀鎖
- Redis提供的watch機(jī)制,它是一種樂(lè)觀鎖
- watch會(huì)再事務(wù)開(kāi)始之前盯住一個(gè)或者多個(gè)關(guān)鍵變量,當(dāng)事務(wù)執(zhí)行時(shí),也就是服務(wù)器收到了exec指令要順序執(zhí)行緩存的事務(wù)的隊(duì)列時(shí),Redis會(huì)檢查關(guān)鍵變量自watch之后是否被修改了(包括當(dāng)前事務(wù)所在的客戶端)。如果關(guān)鍵變量被人東莞過(guò)了,exec指令就會(huì)返回NULL回復(fù)告知客戶端事務(wù)執(zhí)行失敗,這個(gè)時(shí)候客戶端一般會(huì)選擇重試
### 2.5.6 注意事項(xiàng)
Redis禁止在multi和exec之間執(zhí)行watch指令,必須在multi之前盯住關(guān)鍵變量
## 2.6 小道消息 — PubSub
Redis消息隊(duì)列不足之處,那就是它不支持消息的多播機(jī)制
### 2.6.1 消息多播
消息多播允許生產(chǎn)者只生產(chǎn)一次消息,由中間件負(fù)責(zé)將消息復(fù)制到多個(gè)消息隊(duì)列,每個(gè)消息隊(duì)列由相應(yīng)的消費(fèi)組進(jìn)行消費(fèi)。
### 2.6.2 PubSub
- 為了支持消息多播,它單獨(dú)使用了一個(gè)模塊來(lái)支持消息多播,PubSub(PublisherSubscriber)(發(fā)布者/訂閱者模式)
- PubSub的消費(fèi)者如果使用休眠的方式來(lái)輪詢消息,也會(huì)遭遇消息處理不及時(shí)的問(wèn)題。不過(guò)我們可以使用Lisen阻塞監(jiān)聽(tīng)來(lái)進(jìn)行處理,這點(diǎn)同blpop原理是一樣的。
### 2.6.3 模式訂閱
一次訂閱多個(gè)主題,即使生產(chǎn)者新增加了同模式的主題,消費(fèi)者也可以立即收到消息。psubscribe codehole.*
### 2.6.4 消息結(jié)構(gòu)
- data: 消息的內(nèi)容
- channel:當(dāng)前訂閱的主體名稱
- type:消息類型,普通:message,訂閱指令的反饋:subscribe,模式訂閱反饋:psubscribe,還有unsubscribe和punsubscribe
- pattern:當(dāng)前消息使用哪種模式訂閱到的,如果通過(guò)subscribe則為空
### 2.6.5 PubSub的缺點(diǎn)
Redis消費(fèi)者端斷連,則消息丟失
### 2.6.6 補(bǔ)充
Reids 5.0新增了Steam數(shù)據(jù)結(jié)構(gòu),這個(gè)功能給Redis帶來(lái)了持久化消息隊(duì)列。
## 2.7 開(kāi)源節(jié)流 — 小對(duì)象壓縮
### 2.7.1 32bit VS 64bit
如果Redis使用內(nèi)存不超過(guò)4GB,可以考慮使用32bit進(jìn)行編譯,能夠節(jié)約大量?jī)?nèi)存
### 2.7.2 小對(duì)象壓縮存儲(chǔ)(ziplist)
- 如果Redis內(nèi)部管理的集合數(shù)據(jù)結(jié)構(gòu)很小,它會(huì)使用緊湊存儲(chǔ)形式壓縮存儲(chǔ)
- Redis的ziplist是一個(gè)緊湊的字節(jié)數(shù)組結(jié)構(gòu)。如果它存儲(chǔ)的是hash結(jié)構(gòu),那么key和value會(huì)作為兩個(gè)entry被相鄰存儲(chǔ)。如果它存儲(chǔ)的是zset結(jié)構(gòu),那么value和score會(huì)作為兩個(gè)entry被相鄰存儲(chǔ)。intset是一個(gè)緊湊的證書數(shù)據(jù)結(jié)構(gòu)。如果set里存儲(chǔ)的是字符串,那么sadd立即會(huì)升級(jí)為hashtable機(jī)構(gòu)。
- 當(dāng)集合對(duì)象不斷增加,或者某個(gè)value值過(guò)大,這種小對(duì)象存儲(chǔ)也會(huì)被升級(jí)為標(biāo)準(zhǔn)結(jié)構(gòu)。
### 2.7.3 內(nèi)存回收機(jī)制
- 刪除1GB的key,內(nèi)存并沒(méi)有太大變化。原因操作系統(tǒng)是以頁(yè)回收內(nèi)存,但key分散到了很多頁(yè)
- 如果執(zhí)行了flushdb,內(nèi)存就回收了
- Redis會(huì)重新利用刪除key的內(nèi)存
### 2.7.4 內(nèi)存分配算法
Redis默認(rèn)使用jemalloc(facebook)庫(kù)來(lái)管理內(nèi)存
# 第3篇 集群篇
## 3.1 有備無(wú)患 — 主從同步
### 3.1.1 CAP原理
- CAP原理就好比分布式領(lǐng)域的牛頓定律,它是分布式存儲(chǔ)的理論基石。
- C:Consistent,一致性
- A:Availability,可用性
- P:Partition tolerance,分區(qū)容忍性
- 分布在不同機(jī)器上進(jìn)行網(wǎng)絡(luò)隔離開(kāi)的節(jié)點(diǎn),網(wǎng)絡(luò)斷開(kāi)的時(shí)候成為網(wǎng)絡(luò)分區(qū)
- 當(dāng)網(wǎng)絡(luò)分區(qū)發(fā)生的時(shí)候,一致性和可用性兩難全
### 3.1.2 最終一致
Redis保證最終的一致性,從節(jié)點(diǎn)會(huì)努力追趕主節(jié)點(diǎn),最終從節(jié)點(diǎn)的狀態(tài)會(huì)和主節(jié)點(diǎn)的狀態(tài)保持一致。
### 3.1.3 主從同步與從從同步
從從同步為了減輕主節(jié)點(diǎn)的同步負(fù)擔(dān)后續(xù)版本加的,為了描述簡(jiǎn)單,統(tǒng)一為主從同步
### 3.1.4 增量同步
Redis同步的是指令流,主節(jié)點(diǎn)會(huì)將那些對(duì)自己的狀態(tài)產(chǎn)生修改性影響的指令記錄在本地的內(nèi)存buffer中,然后異步將buffer中的指令同步到從節(jié)點(diǎn),從節(jié)點(diǎn)一遍執(zhí)行同步來(lái)的指令流來(lái)達(dá)到和主節(jié)點(diǎn)一樣的狀態(tài),一遍向主節(jié)點(diǎn)反饋?zhàn)约和降侥睦锪耍ㄆ屏浚?/p>
### 3.1.5 快照同步
需要在主節(jié)點(diǎn)上進(jìn)行一次bgsave,將當(dāng)前內(nèi)存的數(shù)據(jù)全部快照到磁盤文件中,然后再將快照文件的內(nèi)容全部傳送到從節(jié)點(diǎn)。從節(jié)點(diǎn)將快照文件接收完畢后,立即執(zhí)行一次全量加載。如果快照加載的時(shí)間過(guò)長(zhǎng)或者復(fù)制buffer太小,就會(huì)導(dǎo)致快照同步的死循環(huán)。務(wù)必要設(shè)置一個(gè)合適的復(fù)制buffer大小
### 3.1.6 增加從節(jié)點(diǎn)
節(jié)點(diǎn)剛加入到集群中,必須先進(jìn)行一次快照同步,同步完成后再繼續(xù)進(jìn)行增量同步
### 3.1.7 無(wú)盤復(fù)制
2.8.18版本后,Redis支持無(wú)盤復(fù)制,主服務(wù)器通過(guò)套接字將快照內(nèi)容發(fā)送到從節(jié)點(diǎn),生成快照是一個(gè)遍歷的過(guò)程,主節(jié)點(diǎn)會(huì)一邊遍歷內(nèi)存,一邊序列化的內(nèi)容發(fā)送到從節(jié)點(diǎn),從節(jié)點(diǎn)還是跟以前一樣,先接收到的內(nèi)容寫入磁盤,然后進(jìn)行一次性加載
### 3.1.8 wait指令
Redis的復(fù)制是異步的,wait指令可以讓異步復(fù)制變身同步復(fù)制,確保系統(tǒng)的強(qiáng)一致性。
### 3.1.9 小結(jié)
復(fù)制功能也不是必須的,如果只是用Redis做緩存,也就不需要從節(jié)點(diǎn)做備份,掛掉了重啟一下就行。
## 3.2 李代桃僵 — Sentinel
Redis Sentinel集群看成一個(gè)zookeeper集群,它是集群高可用的心臟,一般由3~5個(gè)節(jié)點(diǎn)組成,這樣即使個(gè)別節(jié)點(diǎn)掛了,集群還可以正常運(yùn)轉(zhuǎn)
### 3.2.1 消息丟失
Sentinel不能保證消息完全不丟失,min-slaves-to-write標(biāo)識(shí)主節(jié)點(diǎn)必須至少有這些個(gè)從節(jié)點(diǎn)在進(jìn)行正常復(fù)制,否則就停止對(duì)外寫服務(wù);min-slaves-max-lag表示如果再這些秒內(nèi)沒(méi)有收到從節(jié)點(diǎn)的反饋,就意味著從節(jié)點(diǎn)同步不正常
### 3.2.2 Sentinel基本用法
- 連接池建立新連接時(shí),會(huì)去查詢主節(jié)點(diǎn)地址,然后跟內(nèi)存中的比對(duì),如果變更了,就斷開(kāi)所有連接,重新使用新地址連接,重連的時(shí)候就會(huì)用到新地址
- 處理命令的時(shí)候如果捕獲ReadOnlyError也會(huì)把舊連接關(guān)掉,后續(xù)指令會(huì)重新連接
## 3.3 分而治之 — Codis
- Redis集群方案將眾多小內(nèi)存的Redis實(shí)例整合起來(lái),將分布在多臺(tái)機(jī)器上的眾多CPU核心的計(jì)算能力聚集在一起,完成海量數(shù)據(jù)存取和高并發(fā)讀寫操作
- Codis是集群方案之一,Codis上掛的所有Redis實(shí)例構(gòu)成一個(gè)Redis集群,集群空間不足的時(shí)候,可以通過(guò)動(dòng)態(tài)增加Redis實(shí)例來(lái)實(shí)現(xiàn)擴(kuò)容需求
- Codis只是一個(gè)轉(zhuǎn)發(fā)代理中間件,可以起多個(gè)實(shí)例,顯著增加整體QPS需求,還能起容災(zāi)功能
### 3.3.1 Codis分片原理
- Codis默認(rèn)將所有的key劃分為1024個(gè)槽位,對(duì)客戶端傳來(lái)的key進(jìn)行crc32運(yùn)算計(jì)算hash值,再將hash后的整數(shù)值對(duì)1024整數(shù)取模得到一個(gè)余數(shù),這個(gè)余數(shù)就是對(duì)應(yīng)的槽位
- Codis會(huì)在內(nèi)存維護(hù)槽位和Redis實(shí)例的映射關(guān)系
### 3.3.2 不同的Codis實(shí)例之間槽位關(guān)系如何同步
Codis開(kāi)始使用zookeeper,后來(lái)也支持了etcd,分布式配置存儲(chǔ)數(shù)據(jù)庫(kù)專門用來(lái)持久化槽位關(guān)系
### 3.3.3 擴(kuò)容
- Codis增加SLOTSSCAN指令,可以遍歷指定slot下所有的key。擴(kuò)容的時(shí)候挨個(gè)遷移每個(gè)key到新的Redis節(jié)點(diǎn)
- 當(dāng)Codis接收到位于正在遷移槽位的key后,會(huì)立即強(qiáng)制對(duì)當(dāng)前的單個(gè)key進(jìn)行遷移,遷移完成后,再將請(qǐng)求轉(zhuǎn)發(fā)到新的Redis實(shí)例
### 3.3.4 自動(dòng)均衡
Codis會(huì)在系統(tǒng)比較閑的時(shí)候,觀察每個(gè)Redis實(shí)例對(duì)應(yīng)的slot數(shù)量,如果不平衡,就會(huì)自動(dòng)進(jìn)行遷移
### 3.3.5 Codis的代價(jià)
- 因?yàn)樗械膋ey分散在不同的Redis,就不能再支持事務(wù)了
- 同樣rename操作也很危險(xiǎn),它的參數(shù)是兩個(gè)key,如果這兩個(gè)key在不同的Redis實(shí)例中,rename操作是無(wú)法正確完成的
- 單個(gè)的key對(duì)應(yīng)的value不宜過(guò)大
- Codis因?yàn)樽鳛镻roxy作為中轉(zhuǎn)層,網(wǎng)絡(luò)開(kāi)銷要比單個(gè)Redis大
- Codis的集群配置中心使用zookeeper來(lái)實(shí)現(xiàn),意味著帶來(lái)運(yùn)維代價(jià)
### 3.3.6 Codis的優(yōu)點(diǎn)
相比官方的Redis Cluster集群方案簡(jiǎn)單,分布式的問(wèn)題交給了第三方(zookeeper/etcd)去負(fù)責(zé)
### 3.3.7 mget指令的操作過(guò)程
mget指令獲取多個(gè)key,Codis策略將key按照所分配的實(shí)例打散分組,依次調(diào)用每個(gè)實(shí)例mget,然后結(jié)果匯總給客戶端
### 3.3.8 架構(gòu)變遷
Redis升級(jí),Codis也需要實(shí)時(shí)跟進(jìn)
### 3.3.9 Codis的尷尬
Redis Cluster在業(yè)界逐漸流行,官方升級(jí)不會(huì)考慮第三方
### 3.3.10 Codis的后臺(tái)管理
支持服務(wù)器集群管理功能,可以添加分組、添加節(jié)點(diǎn)、執(zhí)行自動(dòng)均衡命令,可以直接查看slot的狀態(tài),被分配到哪個(gè)實(shí)例
## 3.4 眾志成城 — Cluster
- Redis Cluster是去中心化的,比如三個(gè)節(jié)點(diǎn)的Redis集群,他們是互相連接總成一個(gè)對(duì)等集群對(duì)外服務(wù)
- 將所有數(shù)據(jù)劃分16384個(gè)槽,槽位的信息存儲(chǔ)在每個(gè)節(jié)點(diǎn)中
- 客戶端連接后,也會(huì)得到一份集群的槽位配置信息,客戶端查詢key可以直接定位到節(jié)點(diǎn)
### 3.4.1 槽位定位算法
Redis Cluster對(duì)key使用crc16算法進(jìn)行hash,得到的整數(shù)值對(duì)16384進(jìn)行取模來(lái)獲取槽位
### 3.4.2 跳轉(zhuǎn)
客戶端向錯(cuò)誤的節(jié)點(diǎn)發(fā)出指令后,節(jié)點(diǎn)發(fā)現(xiàn)key所在的槽位不歸自己管理,會(huì)向客戶端發(fā)送一個(gè)特殊的跳轉(zhuǎn)指令攜帶目標(biāo)操作的點(diǎn)的地址
### 3.4.3 遷移
- redis-trib可以讓運(yùn)維人員手動(dòng)調(diào)整槽位的分配情況
- redis遷移的單位是槽,遷移的時(shí)候,槽處于中間過(guò)渡狀態(tài)
- 從源節(jié)點(diǎn)獲取內(nèi)容->存到目標(biāo)節(jié)點(diǎn)->從源節(jié)點(diǎn)刪除內(nèi)容
- 目標(biāo)節(jié)點(diǎn)執(zhí)行restore指令到源節(jié)點(diǎn)刪除key之間,源節(jié)點(diǎn)的主線程會(huì)處于阻塞狀態(tài),知道key被刪除成功
- 集群環(huán)境下,業(yè)務(wù)邏輯要盡量避免產(chǎn)生很大的key
- 訪問(wèn)源節(jié)點(diǎn)的時(shí)候,會(huì)返回客戶端一個(gè)-ASK目標(biāo)節(jié)點(diǎn)的指令,沒(méi)遷移完成的時(shí)候,按理來(lái)說(shuō)不歸新節(jié)點(diǎn)管理,ASKING指令是告訴目標(biāo)節(jié)點(diǎn)下一個(gè)指令不能不理
### 3.4.4 容錯(cuò)
Redis Cluster可以為每個(gè)主節(jié)點(diǎn)設(shè)置從節(jié)點(diǎn),主節(jié)點(diǎn)發(fā)生故障,會(huì)將從節(jié)點(diǎn)提升為主節(jié)點(diǎn)。如果沒(méi)有從節(jié)點(diǎn),也可以設(shè)置require-full-coverage允許部分節(jié)點(diǎn)發(fā)生故障還可對(duì)外訪問(wèn)
### 3.4.5 網(wǎng)絡(luò)抖動(dòng)
- cluster-node-timeout標(biāo)識(shí)當(dāng)某個(gè)節(jié)點(diǎn)持續(xù)timeout的時(shí)間失聯(lián),才可以認(rèn)定該節(jié)點(diǎn)出現(xiàn)故障
- cluster-slave-validity-factor作為系數(shù)放大這個(gè)超時(shí)時(shí)間來(lái)寬松容錯(cuò)緊急程度
### 3.4.6 可能下線(PFail)與確定下線(Fail)
只有當(dāng)大多數(shù)節(jié)點(diǎn)都認(rèn)定某個(gè)節(jié)點(diǎn)失聯(lián)了,集群才認(rèn)為該節(jié)點(diǎn)需要進(jìn)行主從切換來(lái)容錯(cuò)
### 3.4.7 Cluster基本用法
- 構(gòu)造實(shí)例時(shí)候,最好提供多個(gè)節(jié)點(diǎn)去初始化
- Cluster不支持事務(wù),mget幣Redis要慢很多,rename不再是原子的
### 3.4.8 槽位遷移感知
- MOVED是用來(lái)矯正槽位的,客戶端需要更新槽位關(guān)系表
- ASKING是用來(lái)臨時(shí)糾正槽位的,先發(fā)舊槽位,舊槽位有就返回了,客戶端不應(yīng)刷新槽位關(guān)系表
- 重試2次,指令發(fā)送到錯(cuò)誤節(jié)點(diǎn),先MOVED,然后再去另外節(jié)點(diǎn),另外節(jié)點(diǎn)正好遷移操作,ASKING到新的節(jié)點(diǎn),客戶端重試了2次
- 重復(fù)多次,一般客戶端會(huì)設(shè)置一個(gè)重復(fù)次數(shù)
### 3.4.9 集群變更感知
- 目標(biāo)節(jié)點(diǎn)掛掉,客戶端拋ConnectionError,重試的節(jié)點(diǎn)通過(guò)MOVED告知分配的新的節(jié)點(diǎn)
- 運(yùn)維手動(dòng)修改了集群信息,主節(jié)點(diǎn)切換到其他節(jié)點(diǎn),或者移除了集群,打到舊節(jié)點(diǎn)的會(huì)報(bào)ClusterDown,客戶端會(huì)關(guān)閉所有的連接,清空槽位映射關(guān)系表,重新嘗試初始化
# 第4篇 拓展篇
## 4.1 耳聽(tīng)八方 — Stream
- Redis 5.0發(fā)布的,它是一個(gè)強(qiáng)大的支持多播的可持久化消息隊(duì)列。消息鏈表將所有加入的消息都串起來(lái),每個(gè)消息都有一個(gè)唯一的ID和對(duì)應(yīng)的內(nèi)容。消息是持久化的,重啟后,內(nèi)容還在
- 每個(gè)Steam都可掛多個(gè)消費(fèi)組,每個(gè)消費(fèi)組有個(gè)游標(biāo)last_delivered_id在Stream數(shù)組之上往前移動(dòng),表示當(dāng)前消費(fèi)組已經(jīng)消費(fèi)到哪條消息
- 每個(gè)消費(fèi)組的狀態(tài)都是獨(dú)立的,相互不受影響
- 消費(fèi)者內(nèi)部會(huì)有一個(gè)狀態(tài)變量pending_ids,記錄當(dāng)前已被客戶端取走,但還沒(méi)有ack的消息,用來(lái)確??蛻舳酥辽傧M(fèi)了消息一次
### 4.1.1 消息ID
消息ID格式“整數(shù)-整數(shù)”,后面加入的消息的ID必須要大于前面的消息ID
### 4.1.2 消息內(nèi)容
消息內(nèi)容就是鍵值對(duì),形如hash結(jié)構(gòu)的鍵值對(duì),這沒(méi)什么特別之處
### 4.1.3 增刪改查
xadd: 向Stream追加消息。xdel:向Stream上次刪除消息,只是置位,不影響消息總長(zhǎng)度。xrange:獲取Stream中的消息列表,會(huì)自動(dòng)過(guò)濾已刪除的消息。xlen:獲取Stream消息長(zhǎng)度。del:刪除整個(gè)Stream消息列表中的所有消息。
### 4.1.4 獨(dú)立消費(fèi)
不定義消費(fèi)組的情況下對(duì)Stream消息獨(dú)立消費(fèi),xread,完全忽略額消費(fèi)組的存在,就好像Stream是一個(gè)普通的列表一樣
### 4.1.5 創(chuàng)建消費(fèi)組
通過(guò)xgroup create指令創(chuàng)建消費(fèi)組,需要制定其實(shí)消息ID來(lái)初始化last_delivered_id
### 4.1.6 消費(fèi)
xreadgroup指令可進(jìn)行消費(fèi)組的組內(nèi)消費(fèi),需要提供消費(fèi)組名稱,消費(fèi)者起始消息ID。它同xread一樣,也可以阻塞等待新消息。讀到新消息后,對(duì)應(yīng)的消息ID就會(huì)進(jìn)入消費(fèi)者的PEL(正在處理的消息)結(jié)構(gòu)里,客戶端處理完畢后使用xack指令通知服務(wù)器,本條消息已經(jīng)處理完畢,該消息ID就會(huì)從PEL中移除。
### 4.1.7 Steam消息太多怎么辦
Redis提供了一個(gè)定長(zhǎng)Stream功能,在xadd的指令中提供一個(gè)定長(zhǎng)長(zhǎng)度參數(shù)maxlen,就可以將老的消息干掉,確保鏈表不超過(guò)指定長(zhǎng)度。
### 4.1.8 消息如果忘記ack會(huì)怎樣
如果消費(fèi)者處理完消息沒(méi)有ack,就會(huì)導(dǎo)致PEL不斷增大,內(nèi)存就會(huì)放大
### 4.1.9 PEL如何避免消息丟失
如果客戶端斷開(kāi)了連接,待客戶端重新連接之后,可以再次收到PEL中的消息ID列表,此時(shí)xreadgroup的起始消息ID必須是人以有效的消息ID,一般將參數(shù)設(shè)為0-0
### 4.1.10 Stream的高可用
Stream的高可用是建立在主從復(fù)制基礎(chǔ)上的,不過(guò)鑒于Redis的指令復(fù)制是異步的,在failover發(fā)生時(shí),Redis可能丟失極小部分?jǐn)?shù)據(jù)
### 4.1.11 分區(qū)Partition
Redis的服務(wù)器沒(méi)有原生支持分區(qū)能力,如果想要使用分區(qū),那就需要分配多個(gè)Stream,然后在客戶端使用一定的策略來(lái)生產(chǎn)消息到不通的Stream。
### 4.1.12 小結(jié)
Stream的消費(fèi)模型借鑒了Kafka的消費(fèi)分組的概念,彌補(bǔ)了Redis PubSub不能持久化消息的缺陷。
## 4.2 無(wú)所不知 — Info指令
Info指令繁多,分為9大塊:
1. Server:服務(wù)器運(yùn)行的環(huán)境參數(shù)
2. Clients:客戶端相關(guān)信息
3. Memory:服務(wù)器運(yùn)行內(nèi)存統(tǒng)計(jì)數(shù)據(jù)
4. Persistence:持久化信息
5. Stats:通用統(tǒng)計(jì)數(shù)據(jù)
6. Replication:主從復(fù)制相關(guān)信息
7. CPU:CPU使用情況
8. Cluster:集群信息
9. KeySpace:鍵值對(duì)統(tǒng)計(jì)數(shù)量信息
### 4.2.1 Redis每秒執(zhí)行多少指令
info stats | grep ops? 代表客戶端每秒發(fā)送多少指令到服務(wù)器執(zhí)行,redis-cli monitor可以觀察哪些key被頻繁訪問(wèn)
### 4.2.2 Redis連接了多少客戶端
info clients,通過(guò)觀察數(shù)量可以確定是否存在意料之外的連接。如果不對(duì),則可以用過(guò)client list指令列出所有的客戶端連接地址來(lái)確定源頭??蛻舳说臄?shù)量有個(gè)重要的參數(shù)rejected_connections,表示因?yàn)槌鲎畲筮B接而被拒絕的客戶端連接次數(shù),如果過(guò)多,則需要調(diào)整maxclients參數(shù)
### 4.2.3 Redis內(nèi)存占用多大
info memory,如果單個(gè)Redis內(nèi)存占用過(guò)大,并且在業(yè)務(wù)上沒(méi)有太多壓縮的空間的話,可以考慮集群化了。
### 4.2.4 復(fù)制積壓緩沖區(qū)多大
info replication,它嚴(yán)重影響主從復(fù)制的效率。通過(guò)sync_partial_err半同步失敗的次數(shù),來(lái)決定是否需要擴(kuò)大積壓緩沖區(qū)。
## 4.3 拾遺補(bǔ)漏 — 再談分布式鎖
在Sentinel集群中,當(dāng)主節(jié)點(diǎn)掛掉時(shí),原先客戶端在主節(jié)點(diǎn)申請(qǐng)的一把鎖,還沒(méi)即使同步到從節(jié)點(diǎn),從節(jié)點(diǎn)變成主節(jié)點(diǎn)后,另外一個(gè)客戶端請(qǐng)求加鎖,被批準(zhǔn),就導(dǎo)致系統(tǒng)同樣一把鎖被兩個(gè)客戶端同時(shí)持有。
### 4.3.1 Redlock算法
加鎖時(shí),它會(huì)向半數(shù)節(jié)點(diǎn)發(fā)送set(key, value, nx=True, ex=xxx)指令,只要過(guò)半節(jié)點(diǎn)set成功,就認(rèn)為加鎖成功,釋放鎖時(shí),需要向所有節(jié)點(diǎn)發(fā)送del指令。不過(guò)Redlock算法還需要考慮出錯(cuò)重試、時(shí)鐘漂移等很多細(xì)節(jié)問(wèn)題
### 4.3.2 Redlock使用場(chǎng)景
如果你很在乎高可用性,希望即使掛掉一臺(tái)Redis也完全不受影響,就應(yīng)該考慮Redlock,不過(guò)性能也會(huì)下降
### 4.4 朝生暮死 — 過(guò)期策略
Redis內(nèi)部有個(gè)死神,他時(shí)刻盯著所有設(shè)置了過(guò)期時(shí)間的key,壽命一到就立即收割。
### 4.4.1 過(guò)期的key集合
Redis會(huì)將每個(gè)設(shè)置了過(guò)期時(shí)間的key放入到一個(gè)獨(dú)立的字典中,以后會(huì)定時(shí)遍歷這個(gè)字典來(lái)刪除到期的key。除了遍歷外,還會(huì)使用惰性策略來(lái)刪除過(guò)期key,惰性刪除是客戶端訪問(wèn)這個(gè)key的時(shí)候,對(duì)key進(jìn)行過(guò)期檢查,如果過(guò)期了就立即刪除。
### 4.4.2 定時(shí)掃描策略
Redis每秒進(jìn)行10次過(guò)期掃描,采用簡(jiǎn)單的貪心策略:
1. 從過(guò)期字典隨機(jī)選出20個(gè)key
2. 刪除這20個(gè)key中已經(jīng)過(guò)期的key
3. 如果過(guò)期的key的比例超過(guò)1/4,那么就重復(fù)步驟1
為了掃描不會(huì)出現(xiàn)循環(huán)過(guò)度,算法加了掃描的上限,默認(rèn)不超過(guò)25ms
如果客戶端請(qǐng)求到來(lái)時(shí),服務(wù)器正好進(jìn)入過(guò)期掃描狀態(tài),客戶端的請(qǐng)求將會(huì)等待25ms后才會(huì)進(jìn)行處理,如果客戶端將超時(shí)時(shí)間設(shè)置的比較短,10ms,就會(huì)因?yàn)槌瑫r(shí)而關(guān)閉。而且無(wú)法從slowlog中看到慢查詢記錄,因?yàn)槁樵兪沁壿嬏幚頃r(shí)間,不包含等待時(shí)間。所以開(kāi)發(fā)人員一定要注意過(guò)期時(shí)間,如果大批量的key過(guò)期,要給過(guò)期時(shí)間設(shè)置隨機(jī)范圍,而不能同一時(shí)間過(guò)期。
### 4.4.3 從節(jié)點(diǎn)的過(guò)期策略
從節(jié)點(diǎn)不會(huì)進(jìn)行過(guò)期掃描,主節(jié)點(diǎn)在key到期的時(shí)候,會(huì)在AOF文件里增加一條del指令,同步到所有從節(jié)點(diǎn)。
## 4.5 優(yōu)勝劣汰 — LRU
實(shí)際內(nèi)存超出maxmemory時(shí),Redis提供的策略:
1. noeviction:不會(huì)繼續(xù)服務(wù)寫請(qǐng)求(del請(qǐng)求可繼續(xù)服務(wù)),讀請(qǐng)求可以繼續(xù)進(jìn)行,默認(rèn)淘汰策略
2. volatile-lru:嘗試淘汰設(shè)置了過(guò)期時(shí)間的key,最少使用的key優(yōu)先被淘汰
3. volatile-ttl:淘汰策略是key的剩余壽命ttl的值,值越小優(yōu)先被淘汰
4. volatile-random:淘汰key是從過(guò)期key集合中隨機(jī)的key
5. allkeys-lru:全體的key集合進(jìn)行l(wèi)ru
6. allkeys-random:淘汰的key是全體集合進(jìn)行隨機(jī)
### 4.5.1 LRU算法
位于鏈表尾部的元素就是不被重用的元素,所以會(huì)被踢掉。位于表頭的元素就是最近剛剛被人用過(guò)的元素,所以暫時(shí)不會(huì)被踢。
### 4.5.2 近似LRU算法
- Redis使用的是一個(gè)近似LRU算法,它跟LRU算法還不太一樣,之所以不使用LRU算法,是因?yàn)槠湫枰拇罅康念~外內(nèi)存,需要對(duì)現(xiàn)有的數(shù)據(jù)結(jié)構(gòu)進(jìn)行較大的改造。
- Redis為實(shí)現(xiàn)近似LRU算法,給每個(gè)key增加了一個(gè)額外的小字段,這個(gè)字段長(zhǎng)度是24個(gè)bit,也就是最后一次被訪問(wèn)的時(shí)間戳。
- LRU只有惰性刪除,Redis執(zhí)行寫操作的時(shí)候,發(fā)現(xiàn)內(nèi)存超出maxmemory,就會(huì)執(zhí)行一次LRU淘汰算法,隨機(jī)采樣5(可設(shè)置)個(gè)key,然后淘汰掉最舊的key,如果內(nèi)存還超過(guò)maxmemory,就繼續(xù)隨機(jī)采樣淘汰
## 4.6 平波緩進(jìn) — 懶惰刪除
Redis是單線程的,Redis內(nèi)部實(shí)際上并不是只有一個(gè)主線程,還有幾個(gè)異步線程專門處理一些耗時(shí)的操作
### 4.6.1 Redis為什么使用懶惰刪除
如果被刪除的key是一個(gè)非常大的對(duì)象,刪除操作就會(huì)導(dǎo)致單線程卡頓,4.0后引入了unlink指令,能對(duì)刪除操作進(jìn)行懶處理,丟給后臺(tái)線程來(lái)異步回收內(nèi)存。unlink相當(dāng)于減掉樹(shù)枝焚燒
### 4.6.2 flush
flushdb和flushall指令很慢,4.0后提供了后面增加async將整顆樹(shù)根連根拔起,扔給后臺(tái)進(jìn)程焚燒
### 4.6.3 異步隊(duì)列
主線程將對(duì)象的引用從“大樹(shù)”中摘除后,會(huì)將這個(gè)key的內(nèi)存回收操作包裝成一個(gè)任務(wù),塞進(jìn)異步任務(wù)隊(duì)列,后臺(tái)線程會(huì)從這個(gè)異步隊(duì)列中取任務(wù)
### 4.6.4 AOF Sync也很慢
Redis每秒(可設(shè)置)次同步AOF日志到磁盤,執(zhí)行AOF sync操作的線程是一個(gè)獨(dú)立的異步線程,同樣它也有一個(gè)屬于自己的任務(wù)隊(duì)列。
### 4.6.5 更多異步刪除點(diǎn)
還有一種flush操作,發(fā)生正在全量同步的從節(jié)點(diǎn)中,在接收完整的rdb文件后,也需要將當(dāng)前內(nèi)存一次性清空。
## 4.7 妙手仁心 — 優(yōu)雅地使用Jedis
Java程序一般都是多線程的應(yīng)用程序,意味著我們很少直接使用Jedis,而是要Jedis的連接池 — JedisPool,因?yàn)镴edis對(duì)象不是線程安全的,當(dāng)我們使用Jedis對(duì)象時(shí),需要從連接池中拿出一個(gè)Jedis對(duì)象獨(dú)占,使用完畢后再歸還給線程池
### 4.7.1 重試
遇到錯(cuò)誤連接的時(shí)候需要發(fā)送重試指令,也不能無(wú)限次的重試
## 4.8 居安思危 — 保護(hù)Redis
### 4.8.1 指令安全
比如一些指令keys會(huì)導(dǎo)致Redis卡頓,flushdb和flushall會(huì)清空所有數(shù)據(jù),rename-command指令用于將某些危險(xiǎn)的指令修改成特別的名字,用來(lái)避免人為誤操作。
### 4.8.2 端口安全
運(yùn)維人員務(wù)必在Redis的配置文件中指定監(jiān)聽(tīng)的IP地址,增加Redis的密碼訪問(wèn)限制,客戶端必須使用auth指令
### 4.8.3 Lua腳本安全
必須禁止Lua腳本由用戶輸入的內(nèi)容生成,同時(shí)我們應(yīng)該讓Redis以普通用戶的身份啟動(dòng)。
### 4.8.4 SSL代理
Redis并不支持SSL鏈接,如果要用到公網(wǎng)上,可以考慮SSL代理,常見(jiàn)的有ssh,官方推薦spiped工具,同時(shí)SSL代理也可用于主從復(fù)制上
## 4.9 隔墻有耳 — Redis安全通信
### 4.9.1 spiped原理
spiped接收Redis Client發(fā)送過(guò)來(lái)的請(qǐng)求,然后傳到server對(duì)應(yīng)的spiped,然后解密給Redis Server,Server再走一個(gè)反向流程給Client
### 4.9.2 spiped使用入門
Spiped可同時(shí)支持多個(gè)client,但不支持多個(gè)server
# 第5篇 源碼篇
## 5.1 絲分縷析 — 探索“字符串”內(nèi)部
C語(yǔ)言里面的字符串標(biāo)準(zhǔn)以NULL結(jié)束,但獲取長(zhǎng)度的時(shí)候是O(n),Redis承受不起。Redis的字符串叫SDS,它是一個(gè)帶著長(zhǎng)度信息的字節(jié)數(shù)組。capacity表示所分配數(shù)組的長(zhǎng)度,len代表字符串實(shí)際長(zhǎng)度。
### 5.1.1 embstr VS raw
- 長(zhǎng)度特別短的時(shí)候使用embstr,長(zhǎng)度超過(guò)44字節(jié)使用raw形式存儲(chǔ)。
- RedisObject包含類型type(4bit),同一類型的type會(huì)有不同的存儲(chǔ)形式encoding(4bit),為了記錄LRU信息(24bit),每個(gè)對(duì)象有個(gè)引用計(jì)數(shù)refcount(4 bytes),ptr指向真實(shí)對(duì)象的具體存儲(chǔ)位置
### 5.1.2 擴(kuò)容策略
在字符串小于1MB,擴(kuò)容采用加倍策略。超過(guò)1MB以后,只擴(kuò)容1MB
## 5.2 循序漸進(jìn) — 探索”字典”內(nèi)部
### 5.2.1 字典內(nèi)部結(jié)構(gòu)
- 字典內(nèi)部結(jié)構(gòu)包含兩個(gè)hashtable,通常情況下只有一個(gè)有值,當(dāng)擴(kuò)容的時(shí)候,需要漸進(jìn)式移出,移出完成后會(huì)刪除舊的hashtable
- hashtable的結(jié)構(gòu)和Java的HashMap幾乎是一樣的,都是通過(guò)分桶的方式解決hash沖突。第一維是數(shù)組,第二維是鏈表
### 5.2.2 漸進(jìn)式rehash
大字典擴(kuò)容比較耗時(shí),Redis使用漸進(jìn)式rehash小步搬遷。搬遷操作埋伏在當(dāng)前字典的后續(xù)指令中(客戶端的hset、hdel等)Redis還會(huì)再定時(shí)任務(wù)中對(duì)字典主動(dòng)搬遷
### 5.2.3 查找過(guò)程
hash_func可以將key映射到一個(gè)整數(shù),不通的key會(huì)被映射成分布比較均勻散亂的整數(shù)。
### 5.2.4 hash函數(shù)
hashtable的性能好不好完全取決于hash函數(shù)的質(zhì)量,如果hash函數(shù)能夠?qū)ey打散的比較均勻,那么hash函數(shù)就是個(gè)好函數(shù)。Redis字典默認(rèn)hash函數(shù)是siphash
### 5.2.5 hash攻擊
有的hash函數(shù)存在偏向性,會(huì)將查找從O(1)退化到O(n)
### 5.2.6 擴(kuò)容條件
正常情況下,hash元素的個(gè)數(shù)等于第一維數(shù)組長(zhǎng)度,就開(kāi)始擴(kuò)容。擴(kuò)容原數(shù)組的2倍,如果Redis正在做bgsave,Redis盡量不擴(kuò)容減少內(nèi)存頁(yè)過(guò)多分離。但到達(dá)5倍就會(huì)強(qiáng)制擴(kuò)容
### 5.2.7 縮容條件
縮容條件是元素個(gè)數(shù)低于數(shù)組長(zhǎng)度的10%,縮容不考慮是否在bgsave
### 5.2.8 set的結(jié)構(gòu)
set的結(jié)構(gòu)底層也是字典,只不過(guò)value是NULL,其他特征和字典一致
## 5.3 挨肩迭背 — 探索“壓縮列表”內(nèi)部
- 在元素比較少的時(shí)候zset和hash采用壓縮列表進(jìn)行存儲(chǔ),壓縮列表是一塊連續(xù)的內(nèi)存空間,元素之間緊挨著存儲(chǔ),沒(méi)有任何冗余空隙。
- 壓縮列表支持雙向遍歷,所以才會(huì)有ztail_offset字段
### 5.3.1 增加元素
因?yàn)榫o湊存儲(chǔ),意味著每插入一個(gè)新元素就需要調(diào)用realloc擴(kuò)展內(nèi)存。所以不宜存儲(chǔ)大型字符串
### 5.3.2 級(jí)聯(lián)更新
如果ziplist里面的每個(gè)entry恰好存儲(chǔ)了253字節(jié)內(nèi)容,那么第一個(gè)entry內(nèi)容的修改就會(huì)導(dǎo)致后續(xù)所有entry的級(jí)聯(lián)更新
### 5.3.3 intset小整數(shù)集合
當(dāng)set集合容納的元素都是整數(shù)并且元素個(gè)數(shù)較小時(shí),Redis會(huì)使用intset來(lái)存儲(chǔ)集合元素。
### 5.4 風(fēng)馳電掣 — 探索”快速列表”內(nèi)部
quicklist是ziplist和linkedlist的混合體,它將linkedlist按段切分,每一段使用ziplist讓存儲(chǔ)緊湊,多個(gè)ziplist之間使用雙向指針串聯(lián)起來(lái)。
### 5.4.1 每個(gè)ziplist存多少元素
默認(rèn)ziplist長(zhǎng)度為8KB,超過(guò)這個(gè)字節(jié)數(shù)就會(huì)靈氣一個(gè)ziplist,這個(gè)長(zhǎng)度可由list-max-ziplist-size決定
### 5.4.2 壓縮深度
默認(rèn)不壓縮,由list-comporess-depth決定。為了支持快速的push/pop操作,首尾的ziplist不壓縮
## 5.5 凌波微步 — 探索“跳躍列表”內(nèi)部
zset是一個(gè)復(fù)合結(jié)構(gòu),一方面他需要一個(gè)hash存儲(chǔ)value和score的對(duì)應(yīng)關(guān)系,另一方面需要提供按照score排序的功能,還需要能夠指定score范圍來(lái)獲取value列表的功能,就需要“跳躍列表”,內(nèi)部實(shí)現(xiàn)是一個(gè)hash字典加一個(gè)跳躍列表skiplist
### 5.5.1 基本結(jié)構(gòu)
跳躍列表共有64層,每一個(gè)key/value對(duì)應(yīng)的結(jié)構(gòu)是zslnode結(jié)構(gòu),kv之間使用指針串起來(lái)形成雙向鏈表結(jié)構(gòu),他們是有序排列的,從小到大,不同的kv層高可能不一樣,層數(shù)越高kv越少,每一層元素的遍歷都是從kv header出發(fā)。
### 5.5.2 查找過(guò)程
從header的最高層開(kāi)始遍歷找到第一個(gè)節(jié)點(diǎn)(最后一個(gè)比我小的元素),然后從這個(gè)節(jié)點(diǎn)開(kāi)始降一層再遍歷找到第二個(gè)節(jié)點(diǎn)(最后一個(gè)表我小的元素),最后降到最底層進(jìn)行遍歷就找到了期望的節(jié)點(diǎn)。
### 5.5.3 隨機(jī)層數(shù)
對(duì)于每一個(gè)新插入的節(jié)點(diǎn),都需要隨機(jī)算法給它分配一個(gè)合理的層數(shù)。概率逐級(jí)遞減2倍
### 5.5.4 插入過(guò)程
先搜索插入點(diǎn)合適的搜索路徑,創(chuàng)建新節(jié)點(diǎn),分配隨機(jī)層數(shù),再將搜索路徑上的節(jié)點(diǎn)和這個(gè)新節(jié)點(diǎn)通過(guò)前后指針串起來(lái)。同時(shí)需要更新maxLevel
### 5.5.5 刪除過(guò)程
搜索路徑找出來(lái),然后對(duì)于每個(gè)層的相關(guān)節(jié)點(diǎn)進(jìn)行重排前后后向指針,同事需要更新maxLevel
### 5.5.6 更新過(guò)程
zadd如果value不存在,那么插入過(guò)程,如果存在就是更新score的值,不會(huì)帶來(lái)排序的改變,就不需要調(diào)整位置,如果排序位置變了,需要調(diào)整位置。一個(gè)簡(jiǎn)單的策略就是先刪除這個(gè)元素,在插入這個(gè)元素。
### 5.5.7 如果score值都一樣呢
zset不僅只看score,score相同還會(huì)比較value
### 5.5.8 元素排名怎么算出來(lái)的
zset可以獲取元素排名rank,Redis在skiplist的forward指針上進(jìn)行了優(yōu)化,給forward指針增加span跨度屬性,表示從前一個(gè)節(jié)點(diǎn)沿著當(dāng)前層的forward指針跳到當(dāng)前這個(gè)節(jié)點(diǎn)中間會(huì)跳過(guò)多少節(jié)點(diǎn)。
## 5.6 破舊立新 — 探索“緊湊列表”內(nèi)部
listpack跟ziplist的結(jié)構(gòu)幾乎一模一樣,只是少了一個(gè)zltail_offset字段。ziplist通過(guò)這個(gè)字段來(lái)定位出最后一個(gè)元素的位置,用于逆序遍歷。listpack長(zhǎng)度字段放在了元素定的尾部,而且存儲(chǔ)的不是上一個(gè)元素的長(zhǎng)度,是當(dāng)前元素的長(zhǎng)度。所以就可以省去了zltail_offset,最后一個(gè)元素位置可以通過(guò)total_bytes字段和最后一個(gè)元素的長(zhǎng)度字段計(jì)算出來(lái)。
### 5.6.1 級(jí)聯(lián)更新
消滅了ziplist存在的級(jí)聯(lián)更新,元素與元素之間完全獨(dú)立,不會(huì)因?yàn)橐粋€(gè)元素的長(zhǎng)度變長(zhǎng)就導(dǎo)致后續(xù)的元素內(nèi)容收到影響。
### 5.6.2 取代ziplist尚待時(shí)日
ziplist在Redis的數(shù)據(jù)結(jié)構(gòu)中使用太廣泛了,替換起來(lái)復(fù)雜度會(huì)非常高。listpack目前只使用在新增加的Stream數(shù)據(jù)結(jié)構(gòu)中
## 5.7 金枝玉葉 — 探索”基數(shù)樹(shù)”內(nèi)部
rax是一個(gè)有序字典樹(shù)(基數(shù)樹(shù)Radix Tree),按照key的字典排列,支持快速地定位、插入和刪除操作。hash不具備排序功能,zset則是按照score進(jìn)行排序的。rax跟zset不同的是它是按照key排序的。
### 5.7.1 應(yīng)用
一本英文字典看成一顆Radix Tree,有了這棵樹(shù),就可以快速地檢索字典,還可以查詢以某個(gè)前綴開(kāi)頭的單詞有哪些??衫迷诠簿值木用駲n案、時(shí)間序列應(yīng)用、Web服務(wù)器的Router、Stream的消息隊(duì)列(消息ID是時(shí)間戳+序號(hào)),Cluster中用來(lái)記錄槽位和key的對(duì)應(yīng)關(guān)系
### 5.7.2 結(jié)構(gòu)
rax是一顆比較特殊的Radix Tree,結(jié)構(gòu)不是標(biāo)準(zhǔn)的Radix Tree,如果一個(gè)中間節(jié)點(diǎn)有多個(gè)葉節(jié)點(diǎn),那么路由鍵就只是一個(gè)字符;如果只有一個(gè)葉子節(jié)點(diǎn),那么路由鍵就是一個(gè)字符串。
## 5.8 精益求精 — LFU VS LRU
LFU是Least Frequently Used,表示按最近的訪問(wèn)頻率進(jìn)行淘汰,它比LRU更加精確地表示了一個(gè)key被訪問(wèn)的熱度。
### 5.8.1 Redis對(duì)象的熱度
所有的對(duì)象頭結(jié)構(gòu)中都有一個(gè)24bit的字段,這個(gè)字段用來(lái)記錄對(duì)象的熱度
### 5.8.2 LRU模式
在LRU模式下,lru字段存儲(chǔ)的是Redis始終server.lruclock,默認(rèn)是對(duì)2的24次方取模的結(jié)果。
### 5.8.3 LFU模式
在LFU模式下,lru字段24bit用來(lái)存儲(chǔ)兩個(gè)值,分別是ldt(last decrement time)和logc(logistic counter)。logc是8bit,存儲(chǔ)訪問(wèn)頻次的對(duì)數(shù)值,并且值還會(huì)隨著時(shí)間衰減,新建的對(duì)象默認(rèn)為5.ldt是16bit,用來(lái)存儲(chǔ)上一次logc的更新時(shí)間。它取的分鐘時(shí)間戳對(duì)2的16進(jìn)行取模,每45天會(huì)折返。
### 5.8.4 為什么Redis要緩存系統(tǒng)時(shí)間戳
每一次獲取系統(tǒng)時(shí)間戳都是一次系統(tǒng)調(diào)用,系統(tǒng)調(diào)用相對(duì)來(lái)說(shuō)比較費(fèi)時(shí)間,它需要對(duì)時(shí)間進(jìn)行緩存,獲取時(shí)間都是從緩沖直接拿。
### 5.8.5 Redis為什么在獲取lruclock時(shí)使用原子操作
Redis實(shí)際上不是單線程,背后還有幾個(gè)異步線程也在默默工作,llruclock字段是需要支持多線程讀寫的。使用attomic讀寫能保證多線程lruclock數(shù)據(jù)的一致性
### 5.8.6 如何打開(kāi)LFU模式
淘汰策略參數(shù)maxmemory-policy增加了2個(gè)選項(xiàng),volatile-lfu和allkeys-lfu
## 5.9 如履薄冰 — 懶惰刪除的巨大犧牲
一步線程在Redis內(nèi)部有一個(gè)特別的名字:BIO。
### 5.9.1 懶惰刪除的最初實(shí)現(xiàn)不是異步線程
異步線程不用為每種數(shù)據(jù)結(jié)構(gòu)適配一套漸進(jìn)式釋放策略,也不用搞個(gè)自適應(yīng)算法來(lái)仔細(xì)控制回收頻率,只是將對(duì)象從全局字典中摘掉,然后往隊(duì)列一扔,主線程就干別的去了。異步線程從隊(duì)列中取出對(duì)象,直接走正常的同步釋放邏輯就可以了。
### 5.9.2 異步線程方案其實(shí)也相當(dāng)復(fù)雜
Redis內(nèi)部對(duì)象有共享機(jī)制阻礙了異步線程的改造,比如集合的并集操作sunionstore用來(lái)將多個(gè)集合合并成一個(gè)新集合。懶惰刪除相當(dāng)于徹底砍掉某個(gè)樹(shù)枝,將它扔到異步刪除隊(duì)列中,如果底層是共享的,就做不到徹底刪除。為了支持懶惰刪除,Antirez將對(duì)象的共享機(jī)制徹底拋棄。
### 5.9.3 異步刪除的實(shí)現(xiàn)
執(zhí)行懶惰刪除時(shí),Redis將刪除操作的相關(guān)參數(shù)封裝成一個(gè)bio_job結(jié)構(gòu),然后追加到鏈表尾部,異步線程通過(guò)遍歷鏈表摘取job元素來(lái)挨個(gè)執(zhí)行異步任務(wù)。
### 5.9.4 隊(duì)列安全
當(dāng)主線程將任務(wù)追加到隊(duì)列之前需要給它加鎖,追加完畢后,再釋放鎖,還需要喚醒異步線程 — 如果其在休眠的話。異步線程摘取也需要加鎖,摘出來(lái)后解鎖。
### 5.10 跋山涉水 — 深入字典遍歷
### 5.10.1 一邊遍歷一邊修改
Redis對(duì)象樹(shù)的主干是一個(gè)字典,keys命令搜索指定模式key時(shí),會(huì)遍歷整個(gè)主干字典。如果key被找到了,但對(duì)象已經(jīng)過(guò)期,就會(huì)從主干字典中將該key刪除。
### 5.10.2 重復(fù)遍歷的難題
字典在擴(kuò)容的時(shí)候要進(jìn)行漸進(jìn)式遷移,會(huì)存在新舊兩個(gè)hashtable,遍歷完舊的時(shí)候進(jìn)行了rehashStep,遍歷新的就會(huì)重復(fù)遍歷
### 5.10.3 迭代器的結(jié)構(gòu)
Redis為字典提供安全迭代器和不安全迭代器,安全指遍歷過(guò)程可以對(duì)字典進(jìn)行查找和修改,為了保證不重復(fù)就會(huì)禁止rehashStep。不安全是指在遍歷過(guò)程中字典是只讀的,不可以修改,正能調(diào)用dictNext對(duì)字典進(jìn)行持續(xù)遍歷,部的調(diào)用任何可能觸發(fā)過(guò)期判斷的函數(shù)。好處是不影響rehash,代價(jià)就是遍歷的元素可能會(huì)出現(xiàn)重復(fù)。安全迭代器在開(kāi)始遍歷時(shí)候,會(huì)給字典打上一個(gè)標(biāo)記,有了這個(gè)標(biāo)記rehashStep就不會(huì)執(zhí)行,遍歷元素就不會(huì)重復(fù)出現(xiàn)。
### 5.10.4 迭代過(guò)程
值得注意的是,在字典擴(kuò)容時(shí)進(jìn)行rehash,將舊數(shù)組中的鏈表遷移到新的數(shù)組中,某個(gè)具體槽位下的鏈表只可能會(huì)遷移到新數(shù)組的兩個(gè)槽位中
### 5.10.5 迭代器的選擇
- 如果遍歷不允許出現(xiàn)重復(fù),就得使用安全迭代器。比如bgaofrewrite需要遍歷所有對(duì)象,轉(zhuǎn)換成操作指令進(jìn)行持久化,絕對(duì)不允許出現(xiàn)重復(fù)。bgsave也需要遍歷所有對(duì)象持久化,不允許重復(fù)。
- 如果遍歷需要處理元素過(guò)期,需要對(duì)字典進(jìn)行修改,那也不許使用安全迭代器。
- 如果允許遍歷過(guò)程出現(xiàn)元素重復(fù),不進(jìn)行字典結(jié)構(gòu)修改,非安全迭代器