基于c語言開發(fā)高性能key-value存儲非關系形數(shù)據(jù)庫數(shù)據(jù)庫。
一 基礎知識
1.1 五種類型操作
1.1.1 String
1. 腳本操作:
# 添加
set key value
# 獲取
get key
# 刪除數(shù)據(jù)
del key
# 添加或者修改多個數(shù)據(jù)
mset key1 value1 key2 value2
# 獲取多個數(shù)據(jù)
mget key1 key2
1.1.2 hash
每一個key對應的value,類似HashMap集合存儲數(shù)據(jù)的結構。底層使用哈希表結構實現(xiàn)數(shù)據(jù)存儲。
1. 腳本操作:
# 添加或者修改數(shù)據(jù)
hset key field value
# 獲取數(shù)據(jù)
hget key fileld
hgetall key
# 刪除數(shù)據(jù)
hdel key field1 [field2]
# 添加或者修改數(shù)據(jù)
hmset key field1 value1 field2 value2
#獲取多個數(shù)據(jù)
hmget key field1 field2
1.1.3 List
存儲一個key對應多個數(shù)據(jù),并且數(shù)據(jù)的存入和取出順序是一致的。并且底層使用雙向列表。
1. 腳本操作:
# 添加/修改數(shù)據(jù)
lpush key value1 [value2] ......
rpush key value1 [value2] ......
# 獲取數(shù)據(jù)
lrange key start stop
lindex key index
# 獲取并移除數(shù)據(jù)
lpop key
rpop key
1.1.4 Set
一個key對應多個value值,不能存儲重復元素。存儲大量數(shù)據(jù),查詢效率更高。
1. 腳本操作:
# 添加數(shù)據(jù)
sadd key value1 [value2]
# 獲取全部數(shù)據(jù)
smembers key
# 刪除數(shù)據(jù)
srem key member1 [member2]
1.2 key
1.2.1 key基本操作
key是一個字符串,在redis中通過key能獲取值。
1. 腳本操作:
# 刪除指定key
del key
# 獲取key是否存在
exists key
# 獲取key類型
type key
# 設置key有效時間
expire key seconds
pexpire key milliseconds
# 獲取key有效時間
ttl key
pttl key
二 持久化
reids中數(shù)據(jù)存儲在內(nèi)存中。如果redis一直運行,則數(shù)據(jù)會一直保存在內(nèi)存中,我們隨時都可以讀取。但是在現(xiàn)實中,redis可能可能死機,或者部署redis服務器崩潰了。需要重啟服務器或者redis,redis原先內(nèi)存中數(shù)據(jù)就會丟。所以為了保證redis中數(shù)據(jù)的安全性,redis就設計了持久化。redis存儲數(shù)據(jù)時候,會同時將數(shù)據(jù)存儲到硬盤上。如果重啟redis,將硬盤數(shù)據(jù)恢復到redis中。Redis持久化到硬盤中兩種方式,RDB(日志指令)和AOF(數(shù)據(jù)快照)。
2.1 持久化基本概述
1. 什么是持久化:
將內(nèi)存種數(shù)據(jù)存儲到內(nèi)盤中等永久存儲,在一定的時機再從新恢復數(shù)據(jù)。
2. 持久化兩種方式:
計算機中數(shù)據(jù)是二進制存儲,將這二進制數(shù)據(jù)原封不動的記錄下來,也叫快照存儲(RDB),保存的是某一時刻數(shù)據(jù)。
將改變數(shù)據(jù)的操作命令保存下來,即保存操作過程,稱為日志(AOF)。
2.2 RDB(快照)
2.2.1 save指令
1. 手動執(zhí)行save指令:
save
2. save指令相關配置:
#配置本地數(shù)據(jù)庫文件名,默認dump.rdb,常設置為dump-端口號.rdb
dbfilename filename
# 設置存儲文件名
dir path
# 存儲到本地數(shù)據(jù)庫,是否壓縮
rdbcompression yes|no
# 在讀寫過程是否對RDB格式校驗,節(jié)約10%的時間消耗
rdbchecksum yes|no
備注:
save指令會阻塞當前Redis服務器,知道RDB過程完成位置,會造成長時間阻塞。不建議使用
2.2.2 bgsave
1. 手動執(zhí)行bgsave指令:
bgsave
2. bgsave指令相關配置:
# 后臺出現(xiàn)錯誤,是否停止保存,默認yes
stop-writes-on-bgsave-error yes|no
dbfilename filename
dir path
rdbcompression yes|no
rdbchecksum yes|no
3. 配置bgsave自動執(zhí)行:
監(jiān)控時間key變化量
save second changes
例如:
# 900秒,有一個key發(fā)生變化
save 900 1
# 300秒,有10個key發(fā)生變化
save 300 10
# 60秒,有10000個key發(fā)生變化
save 60 10000
完整配置
save second changes
filename filename
dir path
rdbcompression yes|no
rdbchecksum yes|no
stop-writes-on-bgsave-error yes|no
4. bgsave執(zhí)行原理:

針對save命令進行優(yōu)化,Redis中所有涉及RDB操作都采用bgsava方式。
當客戶端給服務端發(fā)送一個save指令時候,服務端立馬給客戶端返回一個結果,同時創(chuàng)建一個子線程執(zhí)行save操作。
2.2.3 RDB總結
優(yōu)點:
RDB時一個二進制文件,存儲效率很高,比AOF速度快很多。
缺點:
無法做到實時持久化,容易造成數(shù)據(jù)丟失。
因為bgsave都會創(chuàng)建一個子線程,會犧牲一些性能。
Redis眾多版本中,RDB的二進制文件無法統(tǒng)一,各個版本的服務之間數(shù)據(jù)格式無法兼容。
2.3 AOF(日志)
以獨立日志方式,記錄每次讀寫命令。重啟服務時,執(zhí)行AOF文件中命令。主要用于解決數(shù)據(jù)實時性,是Redis持久化的主流方式。
2.3.1 AOF日志配置
# 開啟AOF持久化功能
appendonly yes|no
# AOF持久化名字,默認名字為appendonly.aof
appendfilename filename
# AOF保存路徑,和RDB路徑保持一致即可
dir path
# AOF寫數(shù)據(jù)策略,默認everysec
appendfsync always|everysec|no
備注(AOF寫三種策略):
always(每次):每次寫操作都會同步到AOF文件中,性能低。
everysec(每秒):每秒將緩沖區(qū)命令同步到AOF文件中,系統(tǒng)在突然宕機情況會有一秒鐘數(shù)據(jù)丟失,數(shù)據(jù)準確性較高,性能高,建議使用。
no(系統(tǒng)控制):操作系統(tǒng)控制每次同步到AOF文件周期。
2.3.2 AOF重寫
對同一數(shù)據(jù)若干指令轉(zhuǎn)化為最終結果的對應指令。
1. 重寫的作用:
將多條命令壓縮成一條。降低磁盤占用率,提高恢復效率。
2. 重寫的規(guī)則:
進程中有時效數(shù)據(jù),并且已經(jīng)超時,不寫進AOF文件中。
對于無效指令,直接忽略。
對一條數(shù)據(jù)的多條寫命令合并成一條命令。
3. 重寫配置:
手動執(zhí)行
bgrewriteaof
自動重寫配置
auto-aof-rewrite-min-size size
auto-aof-rewrite-percentage percentage
4. AOF寫和重寫流程:
2.4 RDB和AOF應用場景
1. 對數(shù)據(jù)十分敏感,建議使用AOF持久化方案:
AOF持久化策略使用everysecond,默認一秒鐘同步一次。該策略redis仍然能保持很好的性能,
當機器突然出現(xiàn)故障,也就丟失0-1秒鐘數(shù)據(jù)。
2. 數(shù)據(jù)呈現(xiàn)階段有效,使用RDB持久方案:
良好的保持數(shù)據(jù)階段不丟失,且恢復時間快。
三 數(shù)據(jù)刪除與淘汰策略
3.1 過期數(shù)據(jù)刪除(key已經(jīng)過期)
3.1.1 數(shù)據(jù)狀態(tài)
內(nèi)存中數(shù)據(jù)通過TTL指令獲取狀態(tài)
正數(shù): 數(shù)據(jù)在內(nèi)存中有存活時間。
-1:永久存在。
2:已過期數(shù)據(jù),或者被刪除的數(shù)據(jù)。
3.1.2 Redis中時效性數(shù)據(jù)存儲結構
過期數(shù)據(jù)是一塊獨立的存儲空間,Hash結構。field是value的內(nèi)存地址,value是過期時間。最終進行過期處理時候,對該空間數(shù)據(jù)進行檢測,當時間到期后通過filel找到數(shù)據(jù)地址,進行相關操作。
3.1.3 數(shù)據(jù)刪除策略(過期數(shù)據(jù))
在內(nèi)存占用和cpu占用之間尋找一種平衡,不能顧此失彼造成redis性能下降,引發(fā)服務器宕機和內(nèi)存泄漏。針對過期數(shù)據(jù)刪除策略如下:
定時刪除
惰性刪除
定期刪除
1. 定時刪除:
創(chuàng)建一個定時器,key設置過期時間,當?shù)竭_過期時候,定時器任務立即執(zhí)行對鍵刪除。
總結:
到時就刪除,節(jié)約內(nèi)存。但是造成cpu壓力大,影響服務器響應時間和指令吞吐量。即拿時間換空間。
2. 惰性刪除:
數(shù)據(jù)到達過期時間,不做處理。下次訪問該數(shù)據(jù)時進行刪除。
內(nèi)存壓力大,長期占用內(nèi)存空間。但是節(jié)省CPU性能,發(fā)現(xiàn)時刪除。即拿時間換空間。
3. 定期刪除:
相對前兩種方案的一種折中方案。
刪除過程:
Redis啟動服務器初始化時,讀取配置server.hz的值,默認為10.
每秒執(zhí)行server.hz次serverCron()-------->databasesCron()--------->activeExpireCycle()
activeExpireCycle()對每個expires[]進行逐一監(jiān)測。
對某個expires[]檢測時,隨機挑選W個key檢測
如果key超時,刪除key。
如果一輪中刪除的key的數(shù)量>W*25%,循環(huán)該過程。
如果一輪刪除的key的數(shù)量<W*25%,檢查下一個expires[*]
W取值=ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP屬性值
總結:
周期性輪詢redis庫中時效性數(shù)據(jù),采用隨機抽取數(shù)據(jù),利用過期的比例來控制刪除頻率。
檢測頻率可以自定義設置,內(nèi)存壓力不是很大,長期占用內(nèi)存的冷數(shù)據(jù)會被持續(xù)的刪除。即隨機抽查重點抽查。
3.2 數(shù)據(jù)淘汰(redis中內(nèi)存不足)
在redis中執(zhí)行命令之前會調(diào)用freeMemoryIfNeeded()檢查內(nèi)存是否充足。如內(nèi)存不滿足,則要刪除一些數(shù)據(jù)。清除數(shù)據(jù)策略稱為逐出算法。
3.2.1 策略配置
# 使用最大內(nèi)存,生成環(huán)境上設置為50%以上
maxmemory ?mb
# 每次選取待刪除數(shù)據(jù)個數(shù)
maxmemory-samples count
# 對數(shù)據(jù)進行刪除,選擇策略
maxmemory-policy policy
刪除數(shù)據(jù)策略(三類八種)
第一類:檢測易失數(shù)據(jù)
volatile-lru:挑選最近最少使用的數(shù)據(jù)淘汰
volatile-lfu:挑選最近使用次數(shù)最少的數(shù)據(jù)淘汰
volatile-ttl:挑選將要過期的數(shù)據(jù)淘汰
volatile-random:任意選擇數(shù)據(jù)淘汰
第二類:檢測全庫數(shù)據(jù)
allkeys-lru:挑選最近最少使用的數(shù)據(jù)淘汰
allkeLyRs-lfu::挑選最近使用次數(shù)最少的數(shù)據(jù)淘汰
allkeys-random:任意選擇數(shù)據(jù)淘汰,相當于隨機
第三類:放棄數(shù)據(jù)驅(qū)逐
no-enviction(驅(qū)逐):禁止驅(qū)逐數(shù)據(jù)(redis4.0中默認策略),會引發(fā)OOM(Out Of Memory)
三 原理分析
3.1 基礎概念
1. Redis是單線程還是多線程架構:
redis整體并非是一個線程,redis在處理網(wǎng)絡請求和k/v讀寫操作時候是一個線程。而持久化,異步刪除,集群數(shù)據(jù)同步都是額外線程進行處理。
2. 單線程為啥這么快?:
redis大部分操作是基于內(nèi)存中。
因為只有一個線程,避免多線程上下文切換和競爭。
redis底層采用IO多路復用技術,大量高并發(fā)下,提高系統(tǒng)吞吐量。
3.2 多路復用
3.2.1 前置知識
1. file descriptor(fd):
文件描述符。(Linux中一些皆文件,比如:普通文件、目錄文件、連接文件、設備文件等。)文件描述符是內(nèi)核為了高效管理系統(tǒng)打開文件而產(chǎn)生的一個索引(指針),是一個非負數(shù),所有的io操作的系統(tǒng)調(diào)用都是通過fd來操作。
2. 內(nèi)核空間和用戶空間:
內(nèi)核能管理系統(tǒng)所有資源,磁盤讀寫,網(wǎng)絡IO讀寫,內(nèi)存分配回收、進程管理等;他能訪問受保護資源,能訪問底層硬件設備。應用程序想要操作底層硬件必須通過內(nèi)核來進行訪問。
3.2.2 epoll(IO多路復用分析)
3.2.2.1 阻塞型通信分析
阻塞網(wǎng)絡IO流程圖:
a 啟動TestSocket服務->代碼執(zhí)行流程如下:
b ServerSocket ss = new ServerSocket(8888);--->
內(nèi)核調(diào)用socket()方法,返回一個文件描述符3。
調(diào)用bind(3,8888)方法,將fd為3綁定8888端口。
調(diào)用listen(3,50),創(chuàng)建socket監(jiān)聽對象,fd為3監(jiān)聽8888端口。
c Socket s = ss.accept();
調(diào)用系統(tǒng)函數(shù)accept(3,...) = 5,服務端應用等待著內(nèi)核響應,內(nèi)核等待著客戶端連接,所以應用處理阻塞中。
當有客戶端連接創(chuàng)建一個socket連接對象。
d 獲取客戶端發(fā)過來數(shù)據(jù)。
調(diào)用系統(tǒng)函數(shù)recv(5,...) recvfrom(5,...) recvmsg(5,...)函數(shù),內(nèi)核等到客戶端的數(shù)據(jù)發(fā)送。應用等待內(nèi)核響應。
內(nèi)核接受到數(shù)據(jù),返回給應用。
e 將數(shù)據(jù)返回到客戶端。
應用調(diào)用send(5,...)等系統(tǒng)函數(shù),將數(shù)據(jù)發(fā)給內(nèi)核。內(nèi)核發(fā)送完畢返回給應用。
總結:
在服務端接受客戶端數(shù)據(jù)時候,因為應用通過內(nèi)核調(diào)用系統(tǒng)函數(shù)accept(3,...) 、recv(5,...)、send(5,...),必須等到內(nèi)核響應后,應用才能進行下一步操作。所以等待連接、等待發(fā)送數(shù)據(jù)、發(fā)送數(shù)據(jù)都是阻塞的。
因為應用是一個主線程執(zhí)行,所以當有多個客戶端進行連接。必須等待上一個線程完全執(zhí)行完主線程,才能進行下一個線程處理。
3.2.2.2 非阻塞網(wǎng)絡傳輸分析
異步通信和上面同步通信流程類似。只不過在調(diào)用系統(tǒng)函數(shù)accept(3,...) 、recv(5,...)、send(5,...)等前,都調(diào)用系統(tǒng)函數(shù)fcntl(5,....)
導致應用調(diào)用accept(3,...) 、recv(5,...)、send(5,...)立馬執(zhí)行下一步,不用等待內(nèi)核處理完成響應數(shù)據(jù)。從而導致程序異步。提高
網(wǎng)絡通信的性能。
但是因為應用不等待內(nèi)核處理完畢,所以應用要不斷的輪詢向內(nèi)核獲取數(shù)據(jù)。
3.2.2.3 一個線程處理多個客戶端連接
linux中提供了select/poll/epoll系統(tǒng)函數(shù)支持這一模型。它支持應用程序?qū)⒁粋€或者多個fd交給內(nèi)核,內(nèi)核檢測fd上的狀態(tài)變化(連接、讀、寫等)
流程分析

a 應用程序調(diào)用epoll_create(1024)函數(shù)返回fd為5。
創(chuàng)建epoll對象,創(chuàng)建監(jiān)聽樹,創(chuàng)建隊列
socket函數(shù),創(chuàng)建一個fd6
bind函數(shù),將fd6進行綁定
listen將fd6監(jiān)聽
fcntl函數(shù)讓epoll_ctl變成異步。
b 調(diào)用epoll_ctl函數(shù)
創(chuàng)建一個socket節(jié)點到監(jiān)聽樹上(注冊了節(jié)點的監(jiān)聽事件)。
c epoll_wait 函數(shù)
輪詢的從隊列中取出,然后處理多個事件。保證了一個線程處理多個客戶端連接。
總結:
epoll模型優(yōu)勢:
沒有fd模型限制,不會隨著fd增加導致IO效率降低。
不需要每次將fd拷貝到內(nèi)核空間,只需要一次拷貝,后面復用。
mmap技術加速了用戶空間和內(nèi)核空間數(shù)據(jù)訪問。
3.2.3 Select(IO多路復用)
總結:
select支持跨平臺的。但是需要自己維護fd_set,每次都需要fdSet從用戶空間拷貝到內(nèi)核空間。如果fd較多則是一個耗時操作。如果fdset中只有少數(shù)的fd在活躍,性能不高。
3.2.4 高性能的Redis有哪些慢操作?什么樣操作影響性能?
在這里插入圖片描述
四 Redis高可用之-主從復制
當redis服務不可用時候,導致應用服務不可用。物理故障可能導致數(shù)據(jù)丟失。redis提供了主從復制功能。
4.1 Redis主從復制初認識
4.1.1 主從復制優(yōu)點和弊端
1. 優(yōu)點:
如果主節(jié)點宕機后,從節(jié)點作為主節(jié)點備份頂上來。擴展了主節(jié)點讀能力。
2. 缺點:
a master宕機之后,需要從slave選出一個master。然后其他的slave需要復制新master數(shù)據(jù)。同時還需要通知客戶端新的master數(shù)據(jù)。這個過程就叫做故障轉(zhuǎn)移,但是限制這個過程仍然需要人工參與。
b redis的寫能力依然受到單機的限制。
c redis存儲能力也受單機的限制。
4.1.2 主從復制流程
1. 建立連接:
2. 數(shù)據(jù)同步:
數(shù)據(jù)同步注意問題:
a master數(shù)據(jù)量大,數(shù)據(jù)同步應該避免高峰。
b 復制緩沖區(qū)大小要合理,會導致數(shù)據(jù)溢出?;蛘邚椭浦芷陂L,復制部分數(shù)據(jù),發(fā)現(xiàn)數(shù)據(jù)丟失,進行第二次全量復制,slave陷入死循環(huán)。
c master主機內(nèi)存不要太大,占用百分之50-70。留百分之30-50用于執(zhí)行bgsave和創(chuàng)建復制緩沖區(qū)。
3. 命令轉(zhuǎn)移:
master庫數(shù)據(jù)狀態(tài)被修改后,導致主從服務器狀態(tài)不一致,需要讓主從同步到一致的狀態(tài)。同步叫命令傳播。
復制緩沖器:
是一個隊列,用于存儲服務器執(zhí)行命令。每次傳播命令,master都會將命令記錄下來,并存儲到復制緩沖區(qū)中。
總結:
[站外圖片上傳中...(image-ac6d25-1623382960682)]
4.2 哨兵架構
redis節(jié)點出現(xiàn)故障時,sentinel能夠自動完成故障發(fā)現(xiàn)和轉(zhuǎn)移,并通知應用端,實現(xiàn)真正高可用。
4.2.1 哨兵架構分析
1. 哨兵架構做了那些事情:
監(jiān)控: 每個哨兵節(jié)點會定時檢測redis數(shù)據(jù)節(jié)點以及其他sentinel節(jié)點是否可達。
主節(jié)點故障轉(zhuǎn)移: 當master出現(xiàn)故障時候,從slave節(jié)點中選取一個master,從slave復制新的master的數(shù)據(jù)。并且維持后續(xù)主從關系。
通知: 哨兵在完成故障轉(zhuǎn)移后,會通知連接客戶端故障轉(zhuǎn)移的結果。
配置提供者: 哨兵架構中應用端配置了sentinel的節(jié)點集合,通過sentinel獲取master信息。
2. sentinel配置多個節(jié)點好處?
對主節(jié)點進行故障判斷是由所有的sentinel共同判斷,防止誤判。
sentinel有多個,即使個別的sentinel不可用,但整體還是可以用的。
4.2.2 sentinel三個定時任務
1. 第一個定時任務:
每隔10秒,每個sentinel節(jié)點向master和slave發(fā)送info命令,作用如下:
a 向主節(jié)點發(fā)送info命令可用獲取從節(jié)點信息,(sentinel無需配置從節(jié)點),當有新的從節(jié)點加入,立刻感應維護正確的拓撲結構。
b 根據(jù)info命令的回復動態(tài)更新sentinel中維護主從節(jié)點的完整信息。
2. 第二個定時任務:
[站外圖片上傳中...(image-3be466-1623382960682)]
每隔2s,每個sentinel節(jié)點向sentinel:hello節(jié)點發(fā)布當前sentinel信息和sentinel對主節(jié)點判斷,同時其他sentinel節(jié)點訂閱該頻道,作用如下:
a 發(fā)現(xiàn)其他sentinel節(jié)點。
b sentinel節(jié)點交換主節(jié)點狀態(tài),作為客觀下線和領導者選舉的依據(jù)。
3. 第三個定時任務:
[站外圖片上傳中...(image-7ae324-1623382960682)]
每隔一秒鐘每個sentinel要向其他sentinel節(jié)點和redis節(jié)點發(fā)送ping。判斷這些節(jié)點是否可達,從而檢查節(jié)點健康狀況。
4.2.3 主觀下線和客觀下線
[站外圖片上傳中...(image-39944e-1623382960682)]
主觀下線是某一個sentinel一家之言,存在誤判操作。
如果是從節(jié)點或者其他sentinel節(jié)點主觀下線,沒有后續(xù)操作。如果是主節(jié)點還需進行客觀下線判斷。
4.2.4 故障轉(zhuǎn)移過程
- 沒有足夠的sentinel節(jié)點同意主節(jié)點下線,主節(jié)點主觀下線被移除。當主節(jié)點從新向sentinel發(fā)送ping有效回應時,主節(jié)點主觀下線移除。
- 主節(jié)點下線后,sentinel向主節(jié)點和所有從節(jié)點發(fā)送info命令,由之前10s一次變?yōu)槊棵胍淮巍?/li>
- 接下來進行故障轉(zhuǎn)移前選舉出sentinel的leader
因為故障轉(zhuǎn)移工作僅僅是需要一個sentinel節(jié)點完成。所有sentinel節(jié)點之間要進行選舉,選出一個leader完成故障轉(zhuǎn)移。
每一個sentinel都有可能成為leader。
每個sentinel只有一張票,只能投給sentinel,先到先得。
如果某個sentinel得票數(shù)>=一半,選舉成功。如果選舉失敗,進行下一輪選擇。
[站外圖片上傳中...(image-e38a21-1623382960682)]
- sentinel的leader節(jié)點完成故障轉(zhuǎn)移
從salve節(jié)點中選舉一個從節(jié)點,并升級為主節(jié)點。
從節(jié)點從新復制主節(jié)點數(shù)據(jù)。
已下線主節(jié)點變成從節(jié)點,并復制新的主節(jié)點數(shù)據(jù)。(這個設置由于主節(jié)點已經(jīng)下線,無法立刻通知。只能將該設置放到sentinel中,當下線主節(jié)點上線,sentinel會將設置發(fā)送)
將故障轉(zhuǎn)移結果告訴其他sentinel。(sentinel-leader節(jié)點將主節(jié)點相關信息,通過發(fā)布訂閱方式完成)
[站外圖片上傳中...(image-721261-1623382960682)]
- sentinel將故障轉(zhuǎn)移結果通知客戶端。
[站外圖片上傳中...(image-62f77d-1623382960682)]
sentinel會在相關頻道發(fā)布故障轉(zhuǎn)移相關信息,應用端只需要去自己感興趣頻道訂閱即可。
a 根據(jù)sentinel節(jié)點 調(diào)用sentinelGetMasteAddrByName獲取master相關信息。
b 為每一個sentinel創(chuàng)建一個監(jiān)聽線程,并訂閱“+switch-master”的頻道。(sentinel完成故障轉(zhuǎn)移后會在“+switch-master”頻道發(fā)布新的master信息)
c 客戶端從新初始化,連接master。
五 Redis高可用之-集群
雖然主從復制和哨兵架構能解決高可用問題。但是無法擴展寫能力和存儲能力。大數(shù)據(jù)量和高并發(fā)無法滿足高可用。redis提供了分布式解決方案。
5.1 分布式解決方案
1. 客戶端分區(qū):
[站外圖片上傳中...(image-468e5a-1623382960682)]
2. 代理分區(qū):
[站外圖片上傳中...(image-70cb1c-1623382960682)]
3. 查詢路由分區(qū):
[站外圖片上傳中...(image-dbafd7-1623382960682)]
5.2 redis分區(qū)理論
cluster采用虛擬槽分區(qū)方案。redis定義了16384個槽,編號0-16383。每個master屬于16384哈希槽中一部分。
執(zhí)行GET/SET/DEL根據(jù)key進行操作,Redis通過CRC16算法計算key,得到redis節(jié)點。然后操作指定的redis節(jié)點。
[站外圖片上傳中...(image-f83d7a-1623382960682)]
redis擴容:
新增的redis節(jié)點中沒有卡槽分配,因此需要從新分配卡槽,還需要考慮redis中數(shù)據(jù)遷移。
配置文件:
./redis-cli --cluster reshard 192.168.211.141:7001 --cluster-from c9687b2ebec8b99ee14fcbb885b5c3439c58827f,80a69bb8af3737bce2913b2952b4456430a89eb3,612e4af8ea e48426938ce65d12a7d7376b0b37e3 --cluster-to 443096af2ff8c1e89f1160faed4f6a02235822a7 -cluster-slots 100
#參數(shù)說明
--cluster-from:表示slot目前所在的節(jié)點的node ID,多個ID用逗號分隔
--cluster-to:表示需要新分配節(jié)點的node ID --cluster-slots:分配的slot數(shù)量
1. Cluster請求路由:
[站外圖片上傳中...(image-d2345f-1623382960682)]
六 災難解決
6.1 緩存穿透
用戶查詢緩存沒有查到數(shù)據(jù),然后查詢MySQL數(shù)據(jù)庫也沒有查詢到數(shù)據(jù),然后用戶反復刷新導致反復查詢數(shù)據(jù)庫,這種現(xiàn)象叫做緩存穿透。
1. 解決方案一:
第一次查詢查詢緩存和數(shù)據(jù)庫都沒查到數(shù)據(jù),此時將null做為value存儲到緩存中。下次同樣的請求就直接從緩存中取出null。
[站外圖片上傳中...(image-cf4221-1623382960682)]
2. 第二種解決方案(布隆過濾器):
布隆過濾器是什么?:
用于解決大規(guī)模數(shù)據(jù)情況下不需要精準過濾場景。
布隆過濾器內(nèi)部是一個bit數(shù)組,以及2個hash函數(shù)((f_1,f_2))。布隆過濾器有個誤判率概念,誤判率越高,數(shù)組越短,誤判率越低,數(shù)組越長。
如果有兩個數(shù)字,N_1經(jīng)過函數(shù)f_1,f_2計算出兩個數(shù)字,讓存儲到bit數(shù)組中。N_2經(jīng)過f_1 f_2計算也產(chǎn)生兩個數(shù)字。當兩個數(shù)字和和N_1產(chǎn)生的數(shù)字有一個一樣,則代表N_2在集合中,這就是布隆過濾器的計算原理。
[站外圖片上傳中...(image-1c22ae-1623382960682)]
解決思路:
在查詢緩存時候,先去緩存布隆器中查詢。如果在緩存布隆器中查詢到結果后,則進行緩存查詢。如果沒有查詢到直接返回,不進行查詢。
6.2 緩存擊穿
緩存過期,正在此時大量的請求訪問某個key,大量請求查詢數(shù)據(jù)庫,這種現(xiàn)象叫做緩存擊穿。
定時器:
后臺定義一個定時器,定時主動更新緩存。例如:某個在緩存中的數(shù)據(jù),在一分鐘過期,我每隔30秒去更新下緩存數(shù)據(jù)。
這種方案思路簡單,但是增加系統(tǒng)的復雜性。對于key相對固定的,適合。
多級緩存:
[站外圖片上傳中...(image-6a1ae3-1623382960683)]
我們應用程序?qū)?shù)據(jù)存儲到緩存中,并設置永不過期。用戶進行查詢的時候,先查詢ngnix緩存,緩存不存在,查詢redis緩存中,并將數(shù)據(jù)存儲到Nginx一級緩存,并設置更新時間。不僅防止緩存擊穿,還提升程序抗壓能力。
分布式鎖(解決超賣):
和鎖、同步代碼塊實現(xiàn)功能是一樣的,只是使用的業(yè)務場景不一樣。普通鎖和同步代碼塊只能解決單體服務的,分布式鎖解決分布式集群環(huán)境。
使用Redission實現(xiàn)分布式鎖:
1. 引入依賴包。
2. 創(chuàng)建redis集群配置文件,添加配置。
3. 定義獲取鎖、釋放鎖方法。
4. 創(chuàng)建Redisson工廠。
[站外圖片上傳中...(image-7174ec-1623382960683)]
隊列術:
面對封流時候,可以直接將流量放到隊列中。讓后臺不用同時處理更多請求,讓隊列中請求逐個消費。
Nginx緩存隊列術:
了Nginx的代 理緩存,其中有一個屬性叫 proxy_cache_lock。多個客戶端請求一個緩存中不存在的文件。只允許第一個請求發(fā)送到服務端,其他請求在緩存中取到取到信息。
6.3 雪崩
大量的緩存失效,導致大量請求查詢數(shù)據(jù)庫,這種現(xiàn)象叫做雪崩。
解決方案:
多級緩存
限流
隊列限流
數(shù)據(jù)預熱
6.4 緩存一致性
數(shù)據(jù)庫中數(shù)據(jù)發(fā)生了更改,需要更新緩存中的數(shù)據(jù)。解決方案canal。
6.4.1 緩存一致性的原理
[站外圖片上傳中...(image-ecd675-1623382960683)]
使用Canal監(jiān)聽數(shù)據(jù)庫指定表的增量變化,在Java程序中消費Canal監(jiān)聽到的增量變化,并在Java程序中實現(xiàn)對Redis和Nginx緩存更新。
1. Mysql主從復制原理:
a 將mysql master數(shù)據(jù)變更記錄到二進制文件中。
b MySQL slave將master二進制文件拷貝到自己中繼日志中。
c MySQL slave從放relay log日志,同步變更數(shù)據(jù)。
2. Canal工作原理:
a Canal偽裝成slave,向master發(fā)送drump協(xié)議。
b master接受命令之后,向slave(Canal)發(fā)送binary log。
c Canal開始解析binary log。
6.4.2 認識Canal
Canal用于基于Mysql增量日志解析,并提供增量數(shù)據(jù)訂閱和消費。
1. Canal應用場景:
搜索引擎和緩存的更新。
代替輪詢方式來對數(shù)據(jù)庫表變更進行監(jiān)控,有效緩解輪詢導致數(shù)據(jù)庫資源。
6.4.3 Canal的配置
1. 開啟MySQL的bin-log日志:
cd /etc/mysql/mysql.conf.d
在mysqld.cnf下面添加如下配置
# 開啟
binlog log-bin=/var/lib/mysql/mysql-bin
# 選擇 ROW 模式
binlog-format=ROW
# 配置 MySQL replaction 需要定義,不要和 canal 的 slaveId 重復
server-id=12345
2. Canal安裝:
docker run -p 11111:11111 --name canal -d docker.io/canal/canal-server
3. 配置CanalServer:
配置Canal的id:
/home/admin/canal-server/conf/canal.properties
[站外圖片上傳中...(image-906a36-1623382960683)]
配置數(shù)據(jù)庫監(jiān)聽地址和監(jiān)聽數(shù)據(jù)庫以及表變化:
/home/admin/canal-server/conf/example/instance.properties
[站外圖片上傳中...(image-16f8ce-1623382960683)]
配置regex規(guī)則
a 多個正則用逗號隔開(,),轉(zhuǎn)義符要雙斜杠(\\)
b 所有表:.* or .*\\..*
c canal schema下所有表: canal\\..*
d canal下的以canal打頭的表:canal\\.canal.*
e canal schema下的一張表:canal.test1
f 多個規(guī)則組合使用:canal\\..*,mysql.test1,mysql.test2 (逗號分隔)
重啟canal:
docker restart canal
MySQL創(chuàng)建賬號并授權:
create user canal@'%' IDENTIFIED by 'canal'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%'; FLUSH PRIVILEGES;
6.4.4 同步更新緩存
1. 創(chuàng)建類MoneyLogSync,繼承EntryHandler類,實現(xiàn)方法:
@CanalTable(value = "money_log")
@Component
public class MoneyLogSync implements EntryHandler<MoneyLog> {
@Autowired
private MoneyLogService moneyLogService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 數(shù)據(jù)增加變更
* @param moneyLog
*/
@Override
public void insert(MoneyLog moneyLog) {
//查詢用戶的搶紅包列表
List<MoneyLog> moneyLogs = moneyLogService.list(moneyLog.getUsername());
//將數(shù)據(jù)存入到Redis
redisTemplate.boundHashOps("UserMoneyLog").put(moneyLog.getUsername(), moneyLogs);
//更新nginx一級緩存同樣道理
}
}
配置Canal地址:
#Canal配置
canal:
server: 192.168.211.141:11111
destination: example
七 RESTful站點安全終極解決方案
lua腳本解決基于RESTFul開發(fā)安全風控解決方案:【緩存穿透】、【緩存擊穿】、【緩存雪崩】、【黑白名單】、 【定向日志收集】、【防止攻擊】、【限流】、【熔斷】
[站外圖片上傳中...(image-f07850-1623382960683)]
ngnix和lua腳本結合,用lua腳本對請求進行分析,然后降請求轉(zhuǎn)發(fā)下去。
[站外圖片上傳中...(image-f6762e-1623382960683)]
1. lua執(zhí)行http請求:
--輸出json類型數(shù)據(jù)
ngx.header.content_type="application/json;charset=utf8"
local http = require "resty.http"
local httpc = http.new()
--執(zhí)行請求
local res, err = httpc:request_uri("http://192.168.0.105:18082/api/userinfo/one", {
method = "GET",
body = "a=1&b=2",
headers = {
["Content-Type"] = "application/x-www-form-urlencoded",
},
--當前連接保存時間
keepalive_timeout = 60000,
--連接池數(shù)量
keepalive_pool = 10
})
if not res then ngx.say("failed to request: ", err) return end
ngx.say(res.body)
修改nginx.config
#api
location ~ /api {
content_by_lua_file /usr/local/openresty/nginx/lua/resthttp.lua;
}
八 Nginx緩存學習
Nginx處于Web網(wǎng)站服務最外層,而且支持瀏覽器緩存配置和后端緩存,用它做部分數(shù)據(jù)緩存效率更高。
8.1 實現(xiàn)瀏覽器緩存
8.2 Nginx清理緩存
如果不想采用緩存過期,采用第三方緩存清理模塊,nginx_ngx_cache_purge 。在安裝OpenRestry就已經(jīng)實現(xiàn)了。
1. 配置清理緩存:
#清理緩存
location ~ /purge(/.*) {
#清理緩存
proxy_cache_purge openresty_cache $host$1$is_args$args;
}
2. 查看緩存:
每次請求 key 就可以刪除指定緩存,我們可以先查看緩存文件的可以
[站外圖片上傳中...(image-c942ad-1623382960683)]
訪問路徑:http://ip+port/purge/user/wangwu
8.3 Lua腳本基本語法
百度吧
8.4 多級緩存架構
1. 用Java實現(xiàn)多級緩存:
[站外圖片上傳中...(image-86551f-1623382960683)]
1、用戶請求經(jīng)過Nginx
2、Nginx檢查是否有緩存,如果Nginx有緩存,直接響應用戶數(shù)據(jù)
3、Nginx如果沒有緩存,則將請求路由給后端Java服務
4、Java服務查詢Redis緩存,如果有數(shù)據(jù),則將數(shù)據(jù)直接響應給Nginx,并將數(shù)據(jù)存入緩存,Nginx將數(shù)據(jù)響應給用戶
5、如果Redis沒有緩存,則使用Java程序查詢MySQL,并將數(shù)據(jù)存入到Reids,再將數(shù)據(jù)存入到Nginx中
2. 用lua腳本實現(xiàn)多級緩存:
[站外圖片上傳中...(image-5d44f4-1623382960683)]
nginx+lua多級緩存架構搭建,用lua腳本實現(xiàn)連接redis和mysql。避免了tomcat并發(fā)能力瓶頸。
用戶請求查詢數(shù)據(jù):
a 先查詢nginx緩存,如果緩存存在直接響應,不存在直接用lua腳本查詢redis。
b redis中有數(shù)據(jù)直接響應,并把緩存加載到nginx中。如果沒有查詢到緩存,查詢MySQL。
c 查詢到數(shù)據(jù),響應用戶,然后以次放入到redis和nginx緩存中。
Lua腳本連接MySQL:
--MySQL查詢操作,封裝成一個模塊
--Java操作MySqL
--導入依賴包
local mysql = require "resty.mysql"
--配置數(shù)據(jù)源鏈接
local props = {
host = "192.168.211.141",
port = 3306,
database = "redpackage",
user = "root",
password = "123456"
}
--創(chuàng)建一個對象
local mysqldb = {}
--查詢數(shù)據(jù)庫
function mysqldb.query(sql)
--創(chuàng)建鏈接
local db = mysql:new()
--設置超時時間
db:set_timeout(10000)
db:connect(props)
--配置編碼格式
db:query("SET NAMES utf8")
--查詢數(shù)據(jù)庫 "select * from activity_info where id=1"
local result = db:query(sql)
--關閉鏈接
db:close()
--返回結果集
return result
end
return mysqldb
Lua腳本連接Redis:
--操作Redis集群,封裝成一個模塊
--引入依賴庫
local redis_cluster = require "resty.rediscluster"
--配置Redis集群鏈接信息
local config = {
name = "test",
serv_list = {
{ip="192.168.211.141", port = 7001},
{ip="192.168.211.141", port = 7002},
{ip="192.168.211.141", port = 7003},
{ip="192.168.211.141", port = 7004},
{ip="192.168.211.141", port = 7005},
{ip="192.168.211.141", port = 7006},
},
idle_timeout = 1000,
pool_size = 10000,
}
--定義一個對象
local lredis = {}
--創(chuàng)建set()添加數(shù)據(jù)方法
function lredis.set(key,value)
--1)打開鏈接
local red = redis_cluster:new(config)
red:init_pipeline()
--2)執(zhí)行命令【set】
red:set(key,value)
red:commit_pipeline()
--3)關閉鏈接
red:close()
end
--創(chuàng)建查詢數(shù)據(jù)get()
function lredis.get(key)
--1)打開鏈接
local red = redis_cluster:new(config)
red:init_pipeline()
--2)執(zhí)行命令【set】
red:get(key)
local result = red:commit_pipeline()
--3)關閉鏈接
red:close()
--4)返回結果集
return result
end
return lredis
Lua腳本執(zhí)行業(yè)務:
--多級緩存流程操作
--1)Lua腳本查詢Nginx緩存
--2)Nginx如果沒有緩存
--2.1)Lua腳本查詢Redis
--2.1.1)Redis如果有數(shù)據(jù),則將數(shù)據(jù)存入到Nginx緩存,并響應用戶
--2.1.2)Redis沒有數(shù)據(jù),Lua腳本查詢MySQL
-- MySQL有數(shù)據(jù),則將數(shù)據(jù)存入到Redis、Nginx緩存[需要額外定義],響應用戶
--3)Nginx如果有緩存,則直接將緩存響應給用戶
--響應數(shù)據(jù)為JSON類型
ngx.header.content_type="application/json;charset=utf8"
--引入依賴庫
--cjson:對象轉(zhuǎn)JSON或者JSON轉(zhuǎn)對象
local cjson = require("cjson")
local mysql = require("mysql")
local lrredis = require("redis")
--獲取請求參數(shù)ID http://192.168.211.141/act?id=1
local id = ngx.req.get_uri_args()["id"];
--加載本地緩存
local cache_ngx = ngx.shared.act_cache;
--組裝本地緩存的key,并獲取nginx本地緩存
local ngx_key = 'ngx_act_cache_'..id
local actCache = cache_ngx:get(ngx_key)
--如果nginx中沒有緩存,則查詢Redis集群緩存
if actCache == "" or actCache == nil then
--從Redis集群中加載數(shù)據(jù)
local redis_key = 'redis_act_'..id
local result = lrredis.get(redis_key)
--Redis中數(shù)據(jù)為空,查詢數(shù)據(jù)庫
if result[1]==nil or result[1]==ngx.null then
--組裝SQL語句
local sql = "select * from activity_info where id ="..id
--執(zhí)行查詢
result = mysql.query(sql)
--數(shù)據(jù)不為空,則添加到Redis中
if result[1]==nil or result[1]==ngx.null then
ngx.say("no data")
else
--數(shù)據(jù)添加到Nginx緩存和Redis緩存
lrredis.set(redis_key,cjson.encode(result))
cache_ngx:set(ngx_key, cjson.encode(result), 2*60);
ngx.say(cjson.encode(result))
end
else
--將數(shù)據(jù)添加到Nginx緩存中
cache_ngx:set(ngx_key, result, 2*60);
--直接輸出
ngx.say(result)
end
else
--輸出緩存數(shù)據(jù)
ngx.say(actCache)
end
nginx配置:
#活動查詢
location /act {
content_by_lua_file /usr/local/openresty/nginx/lua/activity.lua;
}
8.5 紅包雨案例
8.5.1 紅包場景概述
1. 搶紅包的特點:
a 并發(fā)量大(搶紅包,白撿的都去搶。所以人很多)
b 按照時間段來發(fā)放(生活中的紅包就是在幾個小時發(fā)幾波)
c 搶的紅包肯定是不能超過預設的總金額
e 搶紅包肯定是先到先得。(搶紅包的公平性)
f 在發(fā)紅包時候,可以追加紅包的數(shù)量和延遲搶紅包時間。
2. 搶紅包策略:
[站外圖片上傳中...(image-c0d688-1623382960683)]
老板規(guī)定發(fā)金額和發(fā)的個數(shù)確定好,通過算法得出每個紅包金額,然后分批次將紅包放入到redis中。每個人搶紅包直接從redis中拿就可以了。因為redis是單線程所有每次只能一個用戶取到,所以避免了一個紅包多個人搶。傳說中解決超賣。
8.5.2 紅包放入緩存隊列中
1. 定時將紅包導入緩存隊列:
初始化讀?。?/strong>
創(chuàng)建容器監(jiān)聽類 ,讓該類實現(xiàn)接口 ApplicationListener ,當容器初始化完成后會調(diào)用onApplicationEvent 方法。然后去去到數(shù)據(jù)到redis隊列中。
@Component
public class MoneyPushTask implements ApplicationListener<ContextRefreshedEvent>{
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
//清空歷史數(shù)據(jù)
//加載新的數(shù)據(jù)
}
}
定時加載:
可以使用定時任務,定時的更新紅包數(shù)量
8.5.3 解決大量的人搶紅包導致服務器崩潰
當有大量人去搶紅包,服務器很有可能會崩潰。采用隊列削峰。
[站外圖片上傳中...(image-fac8bc-1623382960683)]
大量用戶來搶紅包時候,使用Lua腳本將將請求放到緩存隊列中,服務端處理隊列中的請求。在lua腳本中可以導入 lua-resty-jwt模塊,用來安全驗證。
8.6 Nginx限流
我們采用多級緩存的模式,但是當用戶反復刷新頁面沒有必要讓所有請求到達服務器。還有一些惡意的攻擊請求,也要避免請求到達服務器。限流是保護系統(tǒng)的一種方式。
1. 控制速率(控制請求數(shù)量和請求速度):
[站外圖片上傳中...(image-3f59f7-1623382960683)]
水過來先放到桶里,然后勻速的將水流出。當流入桶里水過大,水就直接溢出了。用戶請求類似這樣原理,當請求來了先放到緩沖中然后勻速的到達服務器,
當請求過多,直接拒絕請求。
nginx配置文件:
# 配置限制流緩存空間
limit_req_zone $binary_remote_addr zone=contentRateLimit:10m rate=2r/s;
# 配置限流
limit_req zone=contentRateLimit;
# 上面參數(shù)的解釋
binary_remote_addr 是一種key,表示基于 remote_addr(客戶端IP) 來做限流,binary_ 的目的是壓縮內(nèi)存占用 量。 zone:定義共享內(nèi)存區(qū)來存儲
訪問信息, contentRateLimit:10m 表示一個大小為10M,名字為contentRateLimit的 內(nèi)存區(qū)域。1M能存儲16000 IP地址的訪問信息,10M可以存儲
16W IP地址訪問信息。 rate 用于設置大訪問速率,rate=10r/s 表示每秒多處理10個請求。Nginx 實際上以毫秒為粒度來跟蹤請求信息,因 此 10r/s
實際上是限制:每100毫秒處理一個請求。這意味著,自上一個請求處理完后,若后續(xù)100毫秒內(nèi)又有請求到達,將 拒絕處理該請求.我們這里設置
成2 方便測試。
注意:當設置了限流但是并發(fā)上來了,這樣大部分請求都會被拒絕。
lilimit_req zone=contentRateLimit burst=4 nodelay;
2. 控制并發(fā)連接數(shù)(限制某個ip連接服務器的個數(shù),連接服務器的總數(shù)):
利用limit_conn_zone和limit_conn兩個指令,限制某一個ip連接數(shù)。
nginx參數(shù)配置:
#根據(jù)IP地址來限制,存儲內(nèi)存大小10M
limit_conn_zone $binary_remote_addr zone=addr:1m;
limit_conn addr 2;
# 參數(shù)解釋
limit_conn_zone $binary_remote_addr zone=addr:10m; 表示限制根據(jù)用戶的IP地址來顯示,設置存儲地址為的 內(nèi)存大小10M
limit_conn addr 2; 表示 同一個地址只允許連接2次。
限制某個ip連接數(shù)量。限制連接的總個數(shù)
#IP限流
limit_conn_zone $binary_remote_addr zone=perip:10m;
#根據(jù)server的名字限流
limit_conn_zone $server_name zone=perserver:10m;
#單個客戶端ip與服務器的連接數(shù).
limit_conn perip 10;
#限制與服務器的總連接數(shù)
limit_conn perserver 100;