系列目錄
本篇內(nèi)容基于 Redis v7.0 的闡述;官網(wǎng):https://redis.io/
本篇計劃用 Docker 容器輔助部署,所以需要了解點 Docker 知識;官網(wǎng):https://www.docker.com
一、分布式解決 Session 的問題
在單站點中,可以將在線用戶信息存儲在Session中,隨時變更獲取信息;在多站點分布式集群如何做到Session共享呢?架設(shè)一個Session服務(wù),供多服務(wù)使用。
頻繁使用的數(shù)據(jù)存在DB端,頻繁的DB連接,頻繁的IO;數(shù)據(jù)存于內(nèi)存中更能減少性能的消耗,更能提高使用效率。
集群化分布式時,為解決以上現(xiàn)象,建立緩存服務(wù)顯得尤為重要。
建立緩存服務(wù)選擇性很多,如:Redis、MongoDB等,以下以 Redis 為例:
作者:[Sol·wang] - 博客園,原文出處:https://www.cnblogs.com/Sol-wang/
二、內(nèi)存數(shù)據(jù)庫 Redis
Remote Dictionary Server;
遠程字典服務(wù),Key/Value 存儲系統(tǒng)、列存儲、文檔型存儲等,NoSQL開源內(nèi)存數(shù)據(jù)庫。最多的使用場景是作為數(shù)據(jù)緩存,存在于應(yīng)用與DB之間,減少對DB的訪問,提高數(shù)據(jù)操作的性能。
下圖展示了緩存服務(wù)在整體架構(gòu)中的位置:
2.1 Redis 特性
高性能 / 高可用 / 持久化 / 集群化,單實例每秒讀寫達10萬次;
豐富的數(shù)據(jù)類型 :String / Hash / Set / Zset / 隊列 / 訂閱 / 發(fā)布;
高性能數(shù)據(jù)結(jié)構(gòu):SDS / Intset / ziplist / listpack / quicklist / skiplist;
支持 ACL:Access Control List;精細化的權(quán)限管理策略;
單線程處理事務(wù):順序執(zhí)行,容易上鎖;
多線程處理輔助功能:連接請求 / 持久化等;
單線程處理事務(wù)的優(yōu)缺點
優(yōu)點:順序執(zhí)行,不存在臟讀臟寫幻讀等情況,不存在死鎖,不存在線程管理的開銷。
缺點:單線程的性能瓶頸,多處理器的資源浪費。
2.2 單線程IO多路復(fù)用
通常情況下,同時連接 Redis 的客戶端有成千上萬的,但 Redis 只有一個主線程處理事務(wù),那如何做到多路連接集中到一個線程處理呢?
當(dāng)多個客戶端同時發(fā)起連接后,這也是需要一個過程的,也是有連接完成的先后,誰連接完成就會告訴 Redis,這里的告訴用的是回調(diào)方式,Redis就會把他的任務(wù)放到單一的隊列中,隊列的另一頭連接著主線程。
在這個過程當(dāng)中,多路的連接匯集到一個有足夠處理能力的隊列中進行傳輸;是不是可以理解為:多路連接重復(fù)利用了單個管道;我想...這里也是體現(xiàn)了 Redis 的多路復(fù)用技術(shù)。當(dāng)然,單單就多路復(fù)用來講,也會是多路集中到一路,然后這一路又分成了多路到各各目標(biāo)。
多路復(fù)用也有不同的算法:select、poll、epoll,Redis用的是epoll算法,所以其中有回調(diào)的動作,目前而言,epoll是最高效的;關(guān)于每個具體的算法,有興趣的同學(xué)可以繼續(xù)研究一下。
2.3 啟動 Redis
用 Docker 啟動 Redis 簡單示例:
1、拉取鏡像docker pull redis
2、運行容器 docker run -d --name=some-redis redis
3、連接到 Redis docker exec -it some-redis redis-cli
Redis Docker版默認是沒有配置文件的,官網(wǎng)說:可以再生成鏡像方式解決...
既然沒有配置文件,那 Redis 啟動完全是按所有配置項的默認值運行的;如下傳參啟動:
# 啟動 redis-server,多參數(shù)配置
docker run -d --name some-redis -p 6379:6379 redis redis-server \
--bind 0.0.0.0 \ # 支持任意的連接
--save 60 1 \ # 每60秒 持久化一次
--protected-mode no \ # 取消保護模式
--requirepass 123456 # 登錄密碼 123456;連接后用 auth {passwd} 方式登錄
傳參也就是覆蓋了配置項的默認值,以下查看覆蓋后的配置項效果:config get *
列出所有配置項
Redis 啟動后,都包含 redis-server / redis-client;
所以任意 Redis 都可用 redis-client 連接到其它 redis-server:docker exec -it some-redis redis-cli -h {目標(biāo)IP} -p {目標(biāo)端口}
2.4 重要配置項
通常配置參數(shù)于配置文件中;比如:/etc/redis/redis.conf
配置項 | 說明 |
---|---|
bind | 可訪問限制,白名單;注釋后不限制 |
port | 對外端口 |
timeout | 連接后,沒有通信任務(wù)的空閑時間,超出此時長后自動斷開 |
daemonize | 后臺運行(容器運行時忽略) |
protected-mode | 只能本地訪問的保護模式 |
tcp-backlog | 網(wǎng)絡(luò)連接隊列最大連接數(shù)(對應(yīng)Linux內(nèi)核參數(shù) net.core.somaxconn,有關(guān)命令sysctl ) |
tcp-keepalive | 網(wǎng)路通信檢測間隔,網(wǎng)絡(luò)是否已斷開(當(dāng)timeout為0才起效吧) |
pidfile | 存放ProcessID編號的文件Pid的存放目錄(后臺運行時才會產(chǎn)生pid文件,容器時是否忽略) |
loglevel | 日志級別 |
logfile | 日志文件目錄 |
database | 數(shù)據(jù)庫個數(shù) |
requirepass | Client 連接密碼 |
maxclients | 客戶端同時最大連接數(shù)(對應(yīng)Linux user openfile limit,必設(shè);有關(guān)命令ulimit ) |
maxmemory | 內(nèi)存最大使用量,推薦70% |
三、數(shù)據(jù)類型 / 常用命令
Key/Value 的存儲系統(tǒng),Key相當(dāng)于區(qū)分的變量名稱,類型區(qū)別在于Value;常用數(shù)據(jù)類型有:
String:字符,基礎(chǔ)類型,過期時長,遞增
-
SET/GET key value
寫入/取值 -
GET key key key
取多個值 -
INCR/DECR key
遞增1/遞減1
List:隊列,先進先出,先進后出
-
LPUSH/RPUSH key value
頭/尾添加 -
LPOP/RPOP key
頭/尾移除 -
LLEN key
隊列長度
Set:無序集合,可查詢 交集、并集、差集等
-
SADD|SREM key member member member
插入/移除 元素 -
SISMEMBER key member
是否存在成員 -
SINTER key1 key2
多集合取交集 -
SCARD key
元素個數(shù)總數(shù)
ZSet:帶排序的Set集合;比Set多出一個專用的score值,又可排出名次列
-
ZADD key score member score member
新增成員及序列數(shù),可覆蓋 -
ZRANGE key start stop
按序列范圍取集合 -
ZRANK key member
取正序排名;從0開始的名次 -
ZREVRANK key member
取倒序排名;從0開始的名次
Hash:可同時設(shè)置/獲取多個屬性值
-
HSET key field value field value
成對設(shè)置多個屬性與值 -
HMGET key field [field field]
取多個列的值 -
HINCRBY key field -5
指定屬性的值,遞增/遞減
四、數(shù)據(jù)結(jié)構(gòu)
為什么 Redis 要有自己的數(shù)據(jù)結(jié)構(gòu)
- 查詢要快:體現(xiàn)在單條Key用了連續(xù)性的內(nèi)存空間
- 占用空間少,節(jié)約內(nèi)存:體現(xiàn)在了集合的數(shù)據(jù)壓縮
Redis 中有這么幾種數(shù)據(jù)結(jié)構(gòu):sds (動態(tài)字符串)、hashtable (字典)、linkedlist (鏈表)、intset (整數(shù)集合)、ziplist (壓縮列表)、listpack (緊湊列表)、quicklist (混合列表)、skiplist (跳表);它們分別應(yīng)用于Redis各數(shù)據(jù)類型中,使得Redis在資源利用和運行效率上有著明顯的效果。
以下列出了數(shù)據(jù)類型與數(shù)據(jù)結(jié)構(gòu)的應(yīng)用關(guān)系圖:
關(guān)于 Listpack;未來作為 Ziplist 的升級替代品,v7.0版本也會有 Ziplist 的存在,后續(xù)版本中逐漸被替代。
數(shù)據(jù)結(jié)構(gòu)的自動切換
每種數(shù)據(jù)類型也會有多種數(shù)據(jù)結(jié)構(gòu),至于何種數(shù)據(jù)類型在什么情況下用何種數(shù)據(jù)結(jié)構(gòu),取決于存儲的數(shù)據(jù);
比如:List 每元素小于8KB時,自動使用 Ziplist,否則自動切換為 Quicklist;
比如:Set 每元素為數(shù)字,元素小于512個時,自動使用 Intset,否則自動切換為 Hashtable;
比如:Hash 元素小于512個,每元素長度小于64字節(jié),自動使用 Ziplist;否則自動切換為 Hashtable。
影響數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換的配置項
# 配置文件中 各數(shù)據(jù)類型中的數(shù)據(jù)結(jié)構(gòu)
# 能承載的最大長度/個數(shù)/容量
# 超出最大限制后,變更為其它數(shù)據(jù)結(jié)構(gòu)
hash-max-listpack-value 64
hash-max-listpack-entries 512
list-max-listpack-size -2(8KB)
set-max-intset-entries 512
set-max-listpack-value 64
set-max-listpack-entries 128
zset-max-listpack-value 64
zset-max-listpack-entries 128
以下主要以 sds / ziplist / listpack / skiplist 為例的闡述,基本涵蓋了重要的數(shù)據(jù)結(jié)構(gòu),一些擴展性的數(shù)據(jù)結(jié)構(gòu)有興趣的同學(xué)再深入了解下。
4.1 動態(tài)字符串 - SDS
很多計算機語言都一樣,Redis 也是基于C語言寫的;對于String類型,當(dāng)給一個變量拼接一個字符串時,都是以一個新對象在內(nèi)存中重新開辟新的更長的連續(xù)空間來存儲;重新開辟/釋放舊空間這樣的內(nèi)存損耗。。。所以為什么開發(fā)人員會避免strname + "xxx"
這樣的拼接方式;字符串長度也是底層每次通過遍歷得出的結(jié)果。。。Redis為了避免這樣的損耗,于是就有了 Simple Dynamic Strings 這樣的解決方案 SDS。
Redis String 會事先分配好比實際字符串更長的內(nèi)存空間,并記錄實際字符串的長度,也記錄存儲后剩余的空間長度。
- 取字符串長度時,直接返回記錄的長度值
- 追加字符串時,直接使用空閑的剩余空間
預(yù)分配空間有多長?總有用完的時候:
- 當(dāng)實際字符串長度<1M時,預(yù)分配實際字符串兩倍的連續(xù)長度,相當(dāng)于本次只會占用一半
- 當(dāng)實際字符串長度>1M時,每次預(yù)分配多出1M的長度空間。便于下次直接存儲
- 當(dāng)字符串減少縮短時,多出的剩余空間保留,便于下次追加,或手動命令清除空閑空間
這種 空間預(yù)分配策略 和 惰性刪除策略 就是SDS的性能優(yōu)勢。
4.2 壓縮列表 - ziplist
一塊連續(xù)性的內(nèi)存空間存放了整個Value,也就是一個ziplist,如何做的呢?一個集合的示例:
bytes:記錄 ziplist 的總長度
tail:記錄最后一個 entry 的偏移量,便于快速定位
len:記錄 entry 的總個數(shù)
entry:列表元素,數(shù)據(jù)存放的元素
end:單個 ziplist 的結(jié)束符
prevlen:記錄上個 entry 的總長度;這里記錄值所占用的空間長度,取決于上個 entry 的長度,所以1-5會自動切換
encoding:記錄 data 的實際數(shù)據(jù)類型 及長度;如:int時占空間小,可直接存到 encoding,就不用 data 了
data:實際數(shù)據(jù);占用空間小的數(shù)據(jù)類型,可直接放到 encoding 中,所以這時候沒有 data 的存在,省空間。
ziplist 的取值:
結(jié)構(gòu)圖中,有總長/個數(shù)/偏移量/結(jié)束符/上個元素長度;所以 ziplist 正序倒序是都可以算出每個 entry 具體位置并取出數(shù)據(jù)。
ziplist 的寫入,插/改/刪 統(tǒng)一為覆蓋方式:
1、為新元素找到被覆蓋元素的位置,位置之前的元素 + 新元素 + 位置之后的元素 = 新的ziplist的完整結(jié)構(gòu)
2、申請新ziplist所需長度的內(nèi)存連續(xù)空間,并存入新空間
3、釋放舊空間
看起來挺不錯的,相比鏈表的非連續(xù)性存儲所帶來的性能提升明顯。。那為什么還會有新的改進版本 listpack 的出現(xiàn)?
ziplist 的弊端
ziplist 的問題出在 prevlen;
也就是上面藍色部分的描述,存了相鄰 entry 的長度;如果 entry 長度過長,相鄰的 prevlen 所占空間長度就會從1變?yōu)?;也就是說 entry 數(shù)據(jù)變更,會影響到相鄰的 entry;最嚴重的情況是很多entry長度恰好都在一個臨界值,會導(dǎo)致相鄰prevlen長度的變化,連鎖反應(yīng)是之后位置上的entry級聯(lián)性的連續(xù)重復(fù)多次變更;
上面提到,變更:就是重新申請新的內(nèi)存連續(xù)空間,釋放舊空間;那么級聯(lián)性的連鎖反應(yīng)呢?一個寫操作,引發(fā)的惡性災(zāi)難事件!??!
4.3 緊湊列表 - listpack
listpack 是 ziplist 的升級版于v7.0中,作為替代品都有什么變化,listpack結(jié)構(gòu)如下圖:
上圖看出,listpack 與 ziplist 的區(qū)別在于:
1、header 取消 tail
2、entry 取消 prevlen
3、當(dāng)前 entry 中增加 encoding + data 的總長度 element-total-len
4、element-total-len 中的首位會標(biāo)識出左側(cè)是否還有值,主要用于逆序讀
element-total-len 編碼
長度 1-5 bytes 是可變的,不固定長度如何讀出整個 element-total-len?這其中0/1標(biāo)識了左側(cè)是否有數(shù)據(jù);
listpack 的讀,假設(shè)逆序讀出每個元素的值,已知end固定長度,跳過 1 bytes 到 element-total-len 中讀固定(7)位數(shù),就會知道左側(cè)是否有值,這樣會把 element-total-len 整個讀下來,也就知道了當(dāng)前 entry 長度,那么也就知道了相鄰 entry 的起始位置;繼續(xù)這樣按序把每個 entry 讀出來就完成逆序讀數(shù)據(jù)。
listpack 的寫入,同樣是基于 ziplist 的方式,新元素替換舊元素組成新的長度,存儲到新申請的內(nèi)存連續(xù)空間,釋放舊空間;由于未影響到其它元素,申請一次新空間后完成寫入操作。
這樣以來,listpack 任何寫入的操作,entry 都是在變更自己,不會牽連到其它 entry,這就是對 ziplist 改善的地方。
4.4 跳躍列表 - skiplist
跳表是在鏈表的基礎(chǔ)上追加了更多指針的存儲;鏈表的指針指向了相鄰元素的地址,但跳表又追加了指向間隔元素的指針;這使得跳表在查詢元素的效率上更快。
下表 Linkedlist 與 Skiplist 的查詢區(qū)別:
上圖:兩種查詢的路徑不同,影響到的元素數(shù)量也不同,鏈表搜了11次才找到指定的元素,而跳表僅搜了5次就找到了指定的元素。
比如:元素1 既存了指向下個元素2 的指針,也存了指向元素4 的指針;所以跳表多存的指針讓其可以跳躍搜索,相對于鏈表減少了搜索次數(shù),這就體現(xiàn)了相比鏈表搜索的高效率。
其它數(shù)據(jù)結(jié)構(gòu)
除以上幾種數(shù)據(jù)結(jié)構(gòu)外,Redis也會有Intset、quicklist、Hashtable等,由于基本原理相識,這里簡單描述:
Intset:與ziplist、listpack相似的連續(xù)性集合,主要區(qū)別在于Intset僅支持數(shù)字型;
quicklist:在 ziplist 基礎(chǔ)上擴展的數(shù)據(jù)結(jié)構(gòu),其中每個成員是一個ziplist,插入元素時:插入到前個元素ziplist中的末尾、后ziplist的開頭、或獨立的ziplist。
五、持久化
5.1 RDB 模式
Redis Database:持久化以指定的時間間隔執(zhí)行數(shù)據(jù)集的時間點的快照整庫備份。
觸發(fā)RDB配置:save 36000 1 600 10 30 100
以上從右往左,成對解釋:
??當(dāng)30秒內(nèi)寫入了100次,觸發(fā)持久化,如果未滿足條件,繼續(xù)下一對;
??當(dāng)600秒內(nèi)寫入了10次,觸發(fā)持久化,如果未滿足條件,繼續(xù)下一對;
??當(dāng)36000秒內(nèi)寫入了1次,觸發(fā)持久化。
持久化過程:內(nèi)存 -> 臨時文件 -> 磁盤。
影響RDB效率的配置項:
??stop-writes-on-bgsave-error yes
當(dāng)持久化失敗后強制停止寫入
??rdbcompression yes
快照數(shù)據(jù)壓縮,損耗CPU
??rdbchecksum yes
是否檢測備份文件,損耗CPU≈10%
關(guān)閉RDB持久化模式:save ""
模式優(yōu)劣
優(yōu)勢:體積小,占用磁盤少。
劣勢:當(dāng)持久化發(fā)生異常時,最后一次的持久化有可能失效,不能確保整體數(shù)據(jù)的絕對完整性。
5.2 AOF 模式
Append Only File:追加記錄服務(wù)器接收到的每個寫入命令,增量保存;如果寫入錯誤,Redis 也會具有自動修復(fù)受損的AOF文件;恢復(fù)時,重新按序執(zhí)行指令,從而重建內(nèi)存庫。
配置開啟AOF模式:appendonly yes
持久化頻率策略配置:appendfysnc always|everysec|no
- 每次命令追加:每次寫命令立刻記錄,太頻繁,太耗性能
- 每秒追加一次:每秒集中記錄一次,依然有可觀的性能表現(xiàn)
文件壓縮 Rewrite
主進程 redis-server 創(chuàng)建出一個子進程 bgrewriteaof 對 AOF文件的重新整理,先整理出一個臨時文件,再覆蓋原AOF文件。
Rewrite 的重寫策略:
- 只針對寫入命令的整理
- 相同數(shù)據(jù)只記錄最后寫入命令
- 過期數(shù)據(jù)不記錄
- 多命令合并記錄
多命令合并示例:如累加命令 Incrby 的累加總和合并成一次累加;如集合命令 rpush 的多次追加合并成一次追加多個。
手動觸發(fā)執(zhí)行重寫命令:bgrewriteaof
。
自動觸發(fā)重寫相關(guān)配置:no-appendfsync-on-rewrite no
重寫開關(guān)auto-aof-rewrite-min-size 64mb
重寫觸發(fā)條件,文件超過指定大小auto-aof-rewrite-percentage 100
重寫觸發(fā)條件,文件超過已使用%
AOF文件64MB是不是顯得太小了,可適當(dāng)增加容量如3GB,以防止過多的觸發(fā)壓縮重寫后影響性能;也不可過大,影響數(shù)據(jù)恢復(fù)效率。
模式優(yōu)劣
優(yōu)勢:丟失率低,數(shù)據(jù)較完整。
劣勢:AOF占用磁盤空間大;恢復(fù)時重新全部執(zhí)行一遍命令,恢復(fù)速度慢了點;持久化失敗時,最后一秒寫入命令可能丟失。
Redis 默認開啟 RDB,可同時開啟 RDB + AOF,恢復(fù)數(shù)據(jù)時以 AOF 為優(yōu)先。
由于是單線程方式,Redis 會創(chuàng)建子線程負責(zé)持久化處理,不管是哪種持久化方式,在創(chuàng)建子進程的瞬間,都會有阻塞的現(xiàn)象。
六、分布式集群
6.1 虛擬插槽
Redis 分布式給出一個 16384 長度的集合,每個元素稱為一個 Slot,將所有 Slots 分段平均映射到各個 Master 節(jié)點上;數(shù)據(jù)通過對 Key 的算法映射到各Slot,也就存到了對應(yīng)的 Master 節(jié)點上,所以每個節(jié)點實例負責(zé)其中一部分的Slot讀寫。
通過控制節(jié)點與槽 Slot 的關(guān)系,決定每個 Master 節(jié)點所承載的數(shù)據(jù)量;這在集群節(jié)點維護的時候非常有用。
6.2 創(chuàng)建集群
集群必須了解的conf配置項:
# 每個節(jié)點必須的配置項
cluster-enabled yes
# 節(jié)點失去連接超時時間
cluster-node-timeout 15000
# 節(jié)點間傳輸效率 默認 no(yes:單次多量發(fā)送/no:單次少量多次發(fā)送)
tcp-nodelay no
# DOCKER/NAT support
# (地址端口可能被轉(zhuǎn)發(fā))靜態(tài)配置公共地址
cluster-announce-ip <外部訪問IP>
cluster-announce-port <外部訪問端口>
cluster-announce-bus-port <節(jié)點互通外部端口>
按 Redis 要求的最低 Master 節(jié)點數(shù)量3實例,多主多從的模式,需要創(chuàng)建6個運行實例:(6371-6376,16371-16376)
# 實例示例1(6371,16371)
docker run -d --name clu-rds-1 \
-p 6371:6371 -p 16371:16371 \ # 容器分別開放 對外連接端口 和 節(jié)點間通信端口
redis \
redis-server \ # 啟動 Redis 服務(wù)命令
--bind 0.0.0.0 \ # 不限連接的客戶端來源
--port 6371 \ # 實例對外連接端口
--protected-mode no \ # 非保護模式
--cluster-enabled yes \ # 開啟集群
--cluster-announce-ip 13.13.1.16 \ # 對外的訪問IP
--cluster-announce-port 6371 \ # 集群對外的連接端口
--cluster-announce-bus-port 16371 # 集群節(jié)點間通訊端口(通常為:10000+Port)
Docker 查看6個容器實例:
Docker 3主3從模式 創(chuàng)建集群:
docker exec -it clu-rds-1 \
# 連接到任意實例
redis-cli -p 6371 \
# 創(chuàng)建集群 并指定 1主幾從
--cluster create --cluster-replicas 1 \
# 要包含的所有(6個)運行實例<ip:port>
13.13.1.16:6371 13.13.1.16:6372 13.13.1.16:6373 \
13.13.1.16:6374 13.13.1.16:6375 13.13.1.16:6376
連接到任意容器節(jié)點,查看集群成員:docker exec -it <container-name> redis-cli -p <container-port> cluster nodes
6.3 節(jié)點管理
# 集群信息
redis-cli -p <port> cluster info
# 查看現(xiàn)有節(jié)點成員
redis-cli -p <port> cluster nodes
# 加入新成員,從節(jié)點
redis-cli --cluster add-node <new-node-ip:port> <cluster-member-ip:port> \
--cluster-slave --cluster-master-id <to-master-id>
# 刪除集群成員節(jié)點
redis-cli --cluster del-node <del-ip:port> <del-node-id>
# 重新分配節(jié)點與插槽映射
redis-cli --cluster reshard <member-ip:port>
# 查看某節(jié)點同步信息
redis-cli -p <port> info replication
# 停止某節(jié)點實例運行
redis-cli -p <port> shutdown
分布式帶來的影響
事務(wù)支持有限;
跨節(jié)點的多Key操作有限;如:SET集合不能計算兩KEY的交集等
6.4 分布式鎖
6.4.1 為什么會需要鎖?
??場景 A:
多用戶對同一產(chǎn)品下單購買,庫存有10個,同一時間進來100個用戶各買一個;
購買前都會看下庫存是否夠買,才會生成訂單,否則提醒無貨;
當(dāng)前10個用戶還沒有下單成功并未扣庫存完成時,后90個用戶看到有庫存,也繼續(xù)生成訂單;
最后賣出100各產(chǎn)品,實際庫存不夠。。。
如何確保有庫存,不超賣。
對庫存加鎖,每個用戶按序進來扣庫存,當(dāng)前用戶扣完庫存后,釋放自己加的鎖,以視完成操作繼續(xù)后續(xù)用戶,下個用戶再進來看庫存是否能購買。
??場景 B:
當(dāng)某個用戶下單扣庫存時,倘若發(fā)送了異常,導(dǎo)致未能釋放自己加上去的鎖,那么。。。沒人負責(zé)釋放此鎖了,后續(xù)不能查扣庫存,誰也不能下單了。
如何確保防止出現(xiàn)死鎖?
對庫存鎖加過期時間,時間超出后,自動強制解鎖。
是的,加鎖、過期自動解鎖,是確保以上場景能夠順利進行的必要條件,共享數(shù)據(jù)被多用戶處理的時候,同一時間點,只能被一個用戶訪問處理,多用戶有序訪問處理,及時更新處理結(jié)果。
共享鎖:數(shù)據(jù)統(tǒng)一放緩存中做業(yè)務(wù)處理,在緩存中加鎖,供所有服務(wù)利用。
6.4.2 SET NX EX 命令
- SET:String 類型的KV操作
- NX:Key唯一,僅當(dāng)沒有Key時才能寫入
- EX:Key 的有效時長,到期后自動刪除
所以完整的命令為:set {business-key} {value} nx ex {過期時長}
返回失敗,表示上個用戶正使用
返回成功,表示自己已開始使用
符合分布式鎖的必要條件
1、當(dāng) get {business-key}
有值時,表示已有用戶在占用,需要等待
2、當(dāng) 占用用戶處理完成后,正常釋放自己加的鎖,del {business-key}
3、當(dāng) 占用用戶處理發(fā)生異常退出時,等ex {過期時長}
后,系統(tǒng)自動解鎖,后續(xù)用戶繼續(xù)購買
命令演示案例:(ttl:剩余過期時間秒)
也可以在 Value 中保存用戶的標(biāo)識,解鎖的時候只能解除自己的鎖,防止某些場景下解鎖錯誤。
分布式Id
由于 Redis Incrment (Incr/Incrby),過期時長等,可以限制用戶短時間內(nèi)的下單量等場景,也可以生成唯一標(biāo)識,用到各服務(wù)中。
6.4.3 Lua 腳本
持續(xù)更新。。。
七、高并發(fā)帶來的問題
7.1 緩存穿透
現(xiàn)象:
DB沒有查詢結(jié)果或為NULL,導(dǎo)致查詢結(jié)果沒有進入到緩存;當(dāng)大批量的這種請求時,就會每次“穿透”緩存“直抵”DB,引發(fā)DB的高并發(fā)查詢導(dǎo)致宕機,這種現(xiàn)象就叫緩存穿透。
方案:
黑名單策略(IP/用戶名),防止惡意行為
對結(jié)果為空的DB查詢,給出默認值于緩存中
7.2 緩存擊穿
現(xiàn)象:
單個訪問量大的 Key (熱門數(shù)據(jù))過期后,緩存需要重建該Key,大量的訪問到DB引發(fā)高并發(fā)查詢,導(dǎo)致DB宕機,這種情況稱為緩存擊穿。
方案:
當(dāng)然也可以使用隊列,就降低了并發(fā)的性能;也可以設(shè)置過期時間為永不過期;以下的重點是緩存重建Key的過程:雙重鎖機制
雙重鎖邏輯闡述:(也常應(yīng)用于單例模式中)
第一把鎖處理首個請求動作是否已有緩存,并擋住了第二或大量的后續(xù)請求動作;
當(dāng)首個請求動作處理完成后結(jié)果于緩存中,并釋放鎖,處于等待的大量的請求動作會陸續(xù)進來,這就不對了,等待的請求動作應(yīng)該直接用首個請求處理的結(jié)果才對;
所以,后續(xù)的請求進來后應(yīng)該再次鎖驗證緩存中是否已有數(shù)據(jù),也就是第二次的鎖是為了解決并發(fā)時被阻塞的請求動作,防止重復(fù)查詢DB更新緩存。
7.3 緩存雪崩
現(xiàn)象:
緩存中的熱門 Key 短時間內(nèi)大批量的同時過期,緩存運行正常,導(dǎo)致DB查詢壓力短時間內(nèi)上升至宕機的現(xiàn)象。
方案:
隨機的過期時間,分散過期時間,避免統(tǒng)一生成過期時間文章來源:http://www.zghlxwxcb.cn/news/detail-417415.html
為熱度追加排名,時時調(diào)整熱度數(shù)據(jù),自動延長過期時間文章來源地址http://www.zghlxwxcb.cn/news/detail-417415.html
到了這里,關(guān)于微服務(wù) - Redis緩存 · 數(shù)據(jù)結(jié)構(gòu) · 持久化 · 分布式 · 高并發(fā)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!