分布式緩存
-- 基于Redis集群解決單機Redis存在的問題
單機的Redis存在四大問題:
0.學習目標
1.Redis持久化
Redis有兩種持久化方案:
- RDB持久化
- AOF持久化
1.1.RDB持久化
RDB全稱Redis Database Backup file(Redis數據備份文件)掰读,也被叫做Redis數據快照泳挥。簡單來說就是把內存中的所有數據都記錄到磁盤中。當Redis實例故障重啟后夭委,從磁盤讀取快照文件,恢復數據。快照文件稱為RDB文件第租,默認是保存在當前運行目錄。
1.1.1.執(zhí)行時機
RDB持久化在四種情況下會執(zhí)行:
- 執(zhí)行save命令
- 執(zhí)行bgsave命令
- Redis停機時
- 觸發(fā)RDB條件時
1)save命令
執(zhí)行下面的命令我纪,可以立即執(zhí)行一次RDB:
save命令會導致主進程執(zhí)行RDB,這個過程中其它所有命令都會被阻塞。只有在數據遷移時可能用到浅悉。
2)bgsave命令
下面的命令可以異步執(zhí)行RDB:
這個命令執(zhí)行后會開啟獨立進程完成RDB趟据,主進程可以持續(xù)處理用戶請求,不受影響术健。
3)停機時
Redis停機時會執(zhí)行一次save命令汹碱,實現RDB持久化。
4)觸發(fā)RDB條件
Redis內部有觸發(fā)RDB的機制荞估,可以在redis.conf文件中找到咳促,格式如下:
# 900秒內,如果至少有1個key被修改勘伺,則執(zhí)行bgsave 跪腹, 如果是save "" 則表示禁用RDB
save 900 1
save 300 10
save 60 10000
RDB的其它配置也可以在redis.conf文件中設置:
# 是否壓縮 ,建議不開啟,壓縮也會消耗cpu飞醉,磁盤的話不值錢
rdbcompression yes
# RDB文件名稱
dbfilename dump.rdb
# 文件保存的路徑目錄
dir ./
1.1.2.RDB原理
bgsave開始時會fork主進程得到子進程冲茸,子進程共享主進程的內存數據。完成fork后讀取內存數據并寫入 RDB 文件缅帘。
fork采用的是copy-on-write技術:
- 當主進程執(zhí)行讀操作時轴术,訪問共享內存;
- 當主進程執(zhí)行寫操作時钦无,則會拷貝一份數據逗栽,執(zhí)行寫操作。
1.1.3.小結
RDB方式bgsave的基本流程失暂?
- fork主進程得到一個子進程彼宠,共享內存空間
- 子進程讀取內存數據并寫入新的RDB文件
- 用新RDB文件替換舊的RDB文件
RDB會在什么時候執(zhí)行?save 60 1000代表什么含義趣席?
- 默認是服務停止時
- 代表60秒內至少執(zhí)行1000次修改則觸發(fā)RDB
RDB的缺點兵志?
- RDB執(zhí)行間隔時間長,兩次RDB之間寫入數據有丟失的風險
- fork子進程宣肚、壓縮想罕、寫出RDB文件都比較耗時
1.2.AOF持久化
1.2.1.AOF原理
AOF全稱為Append Only File(追加文件)。Redis處理的每一個寫命令都會記錄在AOF文件霉涨,可以看做是命令日志文件按价。
1.2.2.AOF配置
AOF默認是關閉的,需要修改redis.conf配置文件來開啟AOF:
# 是否開啟AOF功能笙瑟,默認是no
appendonly yes
# AOF文件的名稱
appendfilename "appendonly.aof"
AOF的命令記錄的頻率也可以通過redis.conf文件來配:
# 表示每執(zhí)行一次寫命令楼镐,立即記錄到AOF文件
appendfsync always
# 寫命令執(zhí)行完先放入AOF緩沖區(qū),然后表示每隔1秒將緩沖區(qū)數據寫到AOF文件往枷,是默認方案
appendfsync everysec
# 寫命令執(zhí)行完先放入AOF緩沖區(qū)框产,由操作系統(tǒng)決定何時將緩沖區(qū)內容寫回磁盤
appendfsync no
三種策略對比:
1.2.3.AOF文件重寫
因為是記錄命令凄杯,AOF文件會比RDB文件大的多。而且AOF會記錄對同一個key的多次寫操作秉宿,但只有最后一次寫操作才有意義戒突。通過執(zhí)行bgrewriteaof命令,可以讓AOF文件執(zhí)行重寫功能描睦,用最少的命令達到相同效果膊存。
如圖,AOF原本有三個命令忱叭,但是set num 123 和 set num 666
都是對num的操作隔崎,第二次會覆蓋第一次的值,因此第一個命令記錄下來沒有意義韵丑。
所以重寫命令后爵卒,AOF文件內容就是:mset name jack num 666
Redis也會在觸發(fā)閾值時自動去重寫AOF文件。閾值也可以在redis.conf中配置:
# AOF文件比上次文件 增長超過多少百分比則觸發(fā)重寫
auto-aof-rewrite-percentage 100
# AOF文件體積最小多大以上才觸發(fā)重寫
auto-aof-rewrite-min-size 64mb
1.3.RDB與AOF對比
RDB和AOF各有自己的優(yōu)缺點埂息,如果對數據安全性要求較高技潘,在實際開發(fā)中往往會結合兩者來使用。
2.Redis主從
2.1.搭建主從架構
單節(jié)點Redis的并發(fā)能力是有上限的千康,要進一步提高Redis的并發(fā)能力享幽,就需要搭建主從集群,實現讀寫分離拾弃。
具體搭建流程參考《Redis集群》:
2.2.主從數據同步原理
2.2.1.全量同步
主從第一次建立連接時值桩,會執(zhí)行全量同步,將master節(jié)點的所有數據都拷貝給slave節(jié)點豪椿,流程:
這里有一個問題奔坟,master如何得知salve是第一次來連接呢?搭盾?
有幾個概念咳秉,可以作為判斷依據:
- Replication Id:簡稱replid,是數據集的標記鸯隅,id一致則說明是同一數據集澜建。每一個master都有唯一的replid,slave則會繼承master節(jié)點的replid
- offset:偏移量蝌以,隨著記錄在repl_baklog中的數據增多而逐漸增大炕舵。slave完成同步時也會記錄當前同步的offset。如果slave的offset小于master的offset跟畅,說明slave數據落后于master咽筋,需要更新。
因此slave做數據同步徊件,必須向master聲明自己的replication id 和offset奸攻,master才可以判斷到底需要同步哪些數據蒜危。
因為slave原本也是一個master,有自己的replid和offset舞箍,當第一次變成slave舰褪,與master建立連接時,發(fā)送的replid和offset是自己的replid和offset疏橄。
master判斷發(fā)現slave發(fā)送來的replid與自己的不一致,說明這是一個全新的slave略就,就知道要做全量同步了捎迫。
master會將自己的replid和offset都發(fā)送給這個slave,slave保存這些信息表牢。以后slave的replid就與master一致了窄绒。
因此,master判斷一個節(jié)點是否是第一次同步的依據,就是看replid是否一致。
如圖:
完整流程描述:
- slave節(jié)點請求增量同步
- master節(jié)點判斷replid儒旬,發(fā)現不一致现喳,拒絕增量同步
- master將完整內存數據生成RDB,發(fā)送RDB到slave
- slave清空本地數據憔涉,加載master的RDB
- master將RDB期間的命令記錄在repl_baklog,并持續(xù)將log中的命令發(fā)送給slave
- slave執(zhí)行接收到的命令,保持與master之間的同步
2.2.2.增量同步
全量同步需要先做RDB掏父,然后將RDB文件通過網絡傳輸個slave,成本太高了秆剪。因此除了第一次做全量同步赊淑,其它大多數時候slave與master都是做增量同步。
什么是增量同步仅讽?就是只更新slave與master存在差異的部分數據陶缺。如圖:
那么master怎么知道slave與自己的數據差異在哪里呢?
2.2.3.repl_backlog原理
master怎么知道slave與自己的數據差異在哪里呢?
這就要說到全量同步時的repl_baklog文件了。
這個文件是一個固定大小的數組洁灵,只不過數組是環(huán)形饱岸,也就是說角標到達數組末尾后,會再次從0開始讀寫处渣,這樣數組頭部的數據就會被覆蓋伶贰。
repl_baklog中會記錄Redis處理過的命令日志及offset,包括master當前的offset罐栈,和slave已經拷貝到的offset:
slave與master的offset之間的差異黍衙,就是salve需要增量拷貝的數據了。
隨著不斷有數據寫入荠诬,master的offset逐漸變大琅翻,slave也不斷的拷貝位仁,追趕master的offset:
直到數組被填滿:
此時,如果有新的數據寫入方椎,就會覆蓋數組中的舊數據聂抢。不過,舊的數據只要是綠色的棠众,說明是已經被同步到slave的數據琳疏,即便被覆蓋了也沒什么影響。因為未同步的僅僅是紅色部分闸拿。
但是空盼,如果slave出現網絡阻塞,導致master的offset遠遠超過了slave的offset:
如果master繼續(xù)寫入新數據新荤,其offset就會覆蓋舊的數據揽趾,直到將slave現在的offset也覆蓋:
棕色框中的紅色部分,就是尚未同步苛骨,但是卻已經被覆蓋的數據篱瞎。此時如果slave恢復,需要同步痒芝,卻發(fā)現自己的offset都沒有了俐筋,無法完成增量同步了。只能做全量同步吼野。
2.3.主從同步優(yōu)化
主從同步可以保證主從數據的一致性校哎,非常重要。
可以從以下幾個方面來優(yōu)化Redis主從就集群:
- 在master中配置repl-diskless-sync yes啟用無磁盤復制瞳步,避免全量同步時的磁盤IO闷哆。
- Redis單節(jié)點上的內存占用不要太大,減少RDB導致的過多磁盤IO
- 適當提高repl_baklog的大小单起,發(fā)現slave宕機時盡快實現故障恢復抱怔,盡可能避免全量同步
- 限制一個master上的slave節(jié)點數量,如果實在是太多slave嘀倒,則可以采用主-從-從鏈式結構屈留,減少master壓力
主從從架構圖:
2.4.小結
簡述全量同步和增量同步區(qū)別?
- 全量同步:master將完整內存數據生成RDB测蘑,發(fā)送RDB到slave灌危。后續(xù)命令則記錄在repl_baklog,逐個發(fā)送給slave碳胳。
- 增量同步:slave提交自己的offset到master勇蝙,master獲取repl_baklog中從offset之后的命令給slave
什么時候執(zhí)行全量同步?
- slave節(jié)點第一次連接master節(jié)點時
- slave節(jié)點斷開時間太久挨约,repl_baklog中的offset已經被覆蓋時
什么時候執(zhí)行增量同步味混?
- slave節(jié)點斷開又恢復产雹,并且在repl_baklog中能找到offset時
3.Redis哨兵
Redis提供了哨兵(Sentinel)機制來實現主從集群的自動故障恢復。
3.1.哨兵原理
3.1.1.集群結構和作用
哨兵的結構如圖:
哨兵的作用如下:
- 監(jiān)控:Sentinel 會不斷檢查您的master和slave是否按預期工作
- 自動故障恢復:如果master故障翁锡,Sentinel會將一個slave提升為master蔓挖。當故障實例恢復后也以新的master為主
- 通知:Sentinel充當Redis客戶端的服務發(fā)現來源,當集群發(fā)生故障轉移時馆衔,會將最新信息推送給Redis的客戶端
3.1.2.集群監(jiān)控原理
Sentinel基于心跳機制監(jiān)測服務狀態(tài)瘟判,每隔1秒向集群的每個實例發(fā)送ping命令:
?主觀下線:如果某sentinel節(jié)點發(fā)現某實例未在規(guī)定時間響應,則認為該實例主觀下線角溃。
?客觀下線:若超過指定數量(quorum)的sentinel都認為該實例主觀下線荒适,則該實例客觀下線。quorum值最好超過Sentinel實例數量的一半开镣。
3.1.3.集群故障恢復原理
一旦發(fā)現master故障,sentinel需要在salve中選擇一個作為新的master咽扇,選擇依據是這樣的:
- 首先會判斷slave節(jié)點與master節(jié)點斷開時間長短邪财,如果超過指定值(down-after-milliseconds * 10)則會排除該slave節(jié)點
- 然后判斷slave節(jié)點的slave-priority值,越小優(yōu)先級越高质欲,如果是0則永不參與選舉
- 如果slave-prority一樣树埠,則判斷slave節(jié)點的offset值,越大說明數據越新嘶伟,優(yōu)先級越高
- 最后是判斷slave節(jié)點的運行id大小怎憋,越小優(yōu)先級越高。
當選出一個新的master后九昧,該如何實現切換呢绊袋?
流程如下:
- sentinel給備選的slave1節(jié)點發(fā)送slaveof no one命令,讓該節(jié)點成為master
- sentinel給所有其它slave發(fā)送slaveof 192.168.150.101 7002 命令铸鹰,讓這些slave成為新master的從節(jié)點癌别,開始從新的master上同步數據。
- 最后蹋笼,sentinel將故障節(jié)點標記為slave展姐,當故障節(jié)點恢復后會自動成為新的master的slave節(jié)點
3.1.4.小結
Sentinel的三個作用是什么?
- 監(jiān)控
- 故障轉移
- 通知
Sentinel如何判斷一個redis實例是否健康剖毯?
- 每隔1秒發(fā)送一次ping命令圾笨,如果超過一定時間沒有相向則認為是主觀下線
- 如果大多數sentinel都認為實例主觀下線,則判定服務下線
故障轉移步驟有哪些逊谋?
- 首先選定一個slave作為新的master擂达,執(zhí)行slaveof no one
- 然后讓所有節(jié)點都執(zhí)行slaveof 新master
- 修改故障節(jié)點配置,添加slaveof 新master
3.2.搭建哨兵集群
具體搭建流程參考課前資料《Redis集群.md》:
3.3.RedisTemplate
在Sentinel集群監(jiān)管下的Redis主從集群涣狗,其節(jié)點會因為自動故障轉移而發(fā)生變化谍婉,Redis的客戶端必須感知這種變化舒憾,及時更新連接信息。Spring的RedisTemplate底層利用lettuce實現了節(jié)點的感知和自動切換穗熬。
下面镀迂,我們通過一個測試來實現RedisTemplate集成哨兵機制。
3.3.1.導入Demo工程
首先唤蔗,我們引入課前資料提供的Demo工程:
3.3.2.引入依賴
在項目的pom文件中引入依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.3.3.配置Redis地址
然后在配置文件application.yml中指定redis的sentinel相關信息:
spring:
redis:
sentinel:
master: mymaster
nodes:
- 192.168.150.101:27001
- 192.168.150.101:27002
- 192.168.150.101:27003
3.3.4.配置讀寫分離
在項目的啟動類中探遵,添加一個新的bean:
@Bean
public LettuceClientConfigurationBuilderCustomizer clientConfigurationBuilderCustomizer(){
return clientConfigurationBuilder -> clientConfigurationBuilder.readFrom(ReadFrom.REPLICA_PREFERRED);
}
這個bean中配置的就是讀寫策略,包括四種:
- MASTER:從主節(jié)點讀取
- MASTER_PREFERRED:優(yōu)先從master節(jié)點讀取妓柜,master不可用才讀取replica
- REPLICA:從slave(replica)節(jié)點讀取
- REPLICA _PREFERRED:優(yōu)先從slave(replica)節(jié)點讀取箱季,所有的slave都不可用才讀取master
4.Redis分片集群
4.1.搭建分片集群
主從和哨兵可以解決高可用、高并發(fā)讀的問題棍掐。但是依然有兩個問題沒有解決:
海量數據存儲問題
高并發(fā)寫的問題
使用分片集群可以解決上述問題藏雏,如圖:
分片集群特征:
集群中有多個master,每個master保存不同數據
每個master都可以有多個slave節(jié)點
master之間通過ping監(jiān)測彼此健康狀態(tài)
客戶端請求可以訪問集群任意節(jié)點作煌,最終都會被轉發(fā)到正確節(jié)點
具體搭建流程參考課前資料《Redis集群.md》:
4.2.散列插槽
4.2.1.插槽原理
Redis會把每一個master節(jié)點映射到0~16383共16384個插槽(hash slot)上掘殴,查看集群信息時就能看到:
數據key不是與節(jié)點綁定,而是與插槽綁定粟誓。redis會根據key的有效部分計算插槽值奏寨,分兩種情況:
- key中包含"{}",且“{}”中至少包含1個字符鹰服,“{}”中的部分是有效部分
- key中不包含“{}”病瞳,整個key都是有效部分
例如:key是num,那么就根據num計算悲酷,如果是{itcast}num套菜,則根據itcast計算。計算方式是利用CRC16算法得到一個hash值舔涎,然后對16384取余笼踩,得到的結果就是slot值。
如圖亡嫌,在7001這個節(jié)點執(zhí)行set a 1時嚎于,對a做hash運算,對16384取余挟冠,得到的結果是15495于购,因此要存儲到103節(jié)點。
到了7003后知染,執(zhí)行get num
時肋僧,對num做hash運算,對16384取余,得到的結果是2765嫌吠,因此需要切換到7001節(jié)點
4.2.1.小結
Redis如何判斷某個key應該在哪個實例止潘?
- 將16384個插槽分配到不同的實例
- 根據key的有效部分計算哈希值,對16384取余
- 余數作為插槽辫诅,尋找插槽所在實例即可
如何將同一類數據固定的保存在同一個Redis實例凭戴?
- 這一類數據使用相同的有效部分,例如key都以{typeId}為前綴
4.3.集群伸縮
redis-cli --cluster提供了很多操作集群的命令炕矮,可以通過下面方式查看:
比如么夫,添加節(jié)點的命令:
4.3.1.需求分析
需求:向集群中添加一個新的master節(jié)點,并向其中存儲 num = 10
- 啟動一個新的redis實例肤视,端口為7004
- 添加7004到之前的集群档痪,并作為一個master節(jié)點
- 給7004節(jié)點分配插槽,使得num這個key可以存儲到7004實例
這里需要兩個新的功能:
- 添加一個節(jié)點到集群中
- 將部分插槽分配到新插槽
4.3.2.創(chuàng)建新的redis實例
創(chuàng)建一個文件夾:
mkdir 7004
拷貝配置文件:
cp redis.conf /7004
修改配置文件:
sed /s/6379/7004/g 7004/redis.conf
啟動
redis-server 7004/redis.conf
4.3.3.添加新節(jié)點到redis
添加節(jié)點的語法如下:
執(zhí)行命令:
redis-cli --cluster add-node 192.168.150.101:7004 192.168.150.101:7001
通過命令查看集群狀態(tài):
redis-cli -p 7001 cluster nodes
如圖邢滑,7004加入了集群腐螟,并且默認是一個master節(jié)點:
但是,可以看到7004節(jié)點的插槽數量為0困后,因此沒有任何數據可以存儲到7004上
4.3.4.轉移插槽
我們要將num存儲到7004節(jié)點遭垛,因此需要先看看num的插槽是多少:
如上圖所示,num的插槽為2765.
我們可以將0~3000的插槽從7001轉移到7004操灿,命令格式如下:
具體命令如下:
建立連接:
得到下面的反饋:
詢問要移動多少個插槽,我們計劃是3000個:
新的問題來了:
那個node來接收這些插槽泵督?趾盐?
顯然是7004,那么7004節(jié)點的id是多少呢小腊?
復制這個id救鲤,然后拷貝到剛才的控制臺后:
這里詢問,你的插槽是從哪里移動過來的秩冈?
- all:代表全部本缠,也就是三個節(jié)點各轉移一部分
- 具體的id:目標節(jié)點的id
- done:沒有了
這里我們要從7001獲取,因此填寫7001的id:
填完后入问,點擊done丹锹,這樣插槽轉移就準備好了:
確認要轉移嗎?輸入yes:
然后芬失,通過命令查看結果:
可以看到:
目的達成楣黍。
4.4.故障轉移
集群初識狀態(tài)是這樣的:
其中7001、7002棱烂、7003都是master租漂,我們計劃讓7002宕機。
4.4.1.自動故障轉移
當集群中有一個master宕機會發(fā)生什么呢?
直接停止一個redis實例哩治,例如7002:
redis-cli -p 7002 shutdown
1)首先是該實例與其它實例失去連接
2)然后是疑似宕機:
3)最后是確定下線秃踩,自動提升一個slave為新的master:
4)當7002再次啟動,就會變?yōu)橐粋€slave節(jié)點了:
4.4.2.手動故障轉移
利用cluster failover命令可以手動讓集群中的某個master宕機业筏,切換到執(zhí)行cluster failover命令的這個slave節(jié)點憔杨,實現無感知的數據遷移。其流程如下:
這種failover命令可以指定三種模式:
- 缺始菘住:默認的流程芍秆,如圖1~6歩
- force:省略了對offset的一致性校驗
- takeover:直接執(zhí)行第5歩,忽略數據一致性翠勉、忽略master狀態(tài)和其它master的意見
案例需求:在7002這個slave節(jié)點執(zhí)行手動故障轉移妖啥,重新奪回master地位
步驟如下:
1)利用redis-cli連接7002這個節(jié)點
2)執(zhí)行cluster failover命令
如圖:
效果:
4.5.RedisTemplate訪問分片集群
RedisTemplate底層同樣基于lettuce實現了分片集群的支持,而使用的步驟與哨兵模式基本一致:
1)引入redis的starter依賴
2)配置分片集群地址
3)配置讀寫分離
與哨兵模式相比对碌,其中只有分片集群的配置方式略有差異荆虱,如下:
spring:
redis:
cluster:
nodes:
- 192.168.150.101:7001
- 192.168.150.101:7002
- 192.168.150.101:7003
- 192.168.150.101:8001
- 192.168.150.101:8002
- 192.168.150.101:8003