目錄
一、Redis 集群演變
1.1?Replication+Sentinel*高可用
1.2?Proxy+Replication+Sentinel(僅僅了解)
1.3?Redis Cluster 集群 (重點)
1.3.1?Redis-cluster架構(gòu)圖
1.3.2?工作原理
1.3.3?主從切換
1.3.4 副本漂移
1.3.5 分片漂移
二、Redis版本歷史(增加了解)
三、Redis 5.0 源碼清單 (對源碼感興趣的,看一下)
四、Redis和lua整合
4.1?什么是lua
4.2?Redis中使?lua的好處
4.3?lua的安裝和語法
4.3.1?Redis整合lua腳本
4.4?lua 腳本調(diào)?Redis 命令
4.4.1?redis.call();
4.4.2?redis.pcall();
4.4.3?redis-cli --eval
4.5?Redis+lua 秒殺
五、Redis Stream
5.1?Redis Stream介紹
5.2?Redis Stream使?場景
六、Redis分布式
6.1?業(yè)務(wù)場景
6.2?鎖的處理
6.3?分布式鎖
6.3.1?分布式鎖特點
6.3.2?分布式鎖的實現(xiàn)?式
6.4?Redis?式實現(xiàn)分布式鎖
6.4.1 獲取鎖
6.4.2 釋放鎖
6.4.3?Redis分布式鎖--優(yōu)缺點
6.4.4?本質(zhì)分析
6.5??產(chǎn)環(huán)境中的分布式鎖
6.5.1?加鎖機制
6.5.2?Redisson分布式鎖的使?
七、緩存常見問題
7.1?緩存預(yù)熱
7.2?緩存雪崩
7.3?緩存擊穿
7.4?緩存穿透
7.5?緩存降級
7.6?緩存更新
7.7?緩存數(shù)據(jù)庫雙寫一致性 (重點)
7.7.1?先更新redis再更新db
7.7.2?先更新db再更新redis
7.7.3?先更新DB再刪除redis
7.7.4?先刪除redis再更新DB
7.7.5?延遲雙刪
7.7.6?思考變種
7.7.7?總結(jié)
7.8?多個系統(tǒng)同時操作(并發(fā))Redis帶來的數(shù)據(jù)問題
八、Redis 常見面試問題
8.1?Memcache特點
8.2?Reids 特點
首先是對上一篇文章的補充,接下來開始正題
一、Redis 集群演變
1.1?Replication+Sentinel*高可用
?這套架構(gòu)使用的是社區(qū)版本推出的原生高可用解決方案,其架構(gòu)圖如下
這里Sentinel的作用有三個:
監(jiān)控:Sentinel 會不斷的檢查主服務(wù)器和從服務(wù)器是否正常運行。
通知:當(dāng)被監(jiān)控的某個Redis服務(wù)器出現(xiàn)問題,Sentinel通過API腳本向管理員或者其他的應(yīng)用程序發(fā)送通知。
自動故障轉(zhuǎn)移:當(dāng)主節(jié)點不能正常工作時,Sentinel會開始一次自動的故障轉(zhuǎn)移操作,它會將與失效主節(jié)點是主從關(guān)系的其中一個從節(jié)點升級為新的主節(jié)點,并且將其他的從節(jié)點指向新的主節(jié)點。
工作原理:
當(dāng)Master宕機的時候,Sentinel會選舉出新的Master,并根據(jù)Sentinel中client-reconfig-script腳本配置的內(nèi)容,去動態(tài)修改VIP(虛擬IP),將VIP(虛擬IP)指向新的Master。我們的客戶端就連向指定的VIP即可!故障發(fā)生后的轉(zhuǎn)移情況,可以理解為下圖
??
缺陷:
(1)主從切換的過程中會丟數(shù)據(jù)
(2)Redis只能單點寫,不能水平擴容
常用方案:
內(nèi)網(wǎng)DNS,VIP和 封裝客戶端直連Redis Sentinel 端口
(1)內(nèi)網(wǎng)DNS:
底層是 Redis Sentinel 集群,代理著 Redis 主,Web 端連接內(nèi)網(wǎng) DNS 提供服務(wù)。內(nèi)網(wǎng) DNS 按照一定的規(guī)則分配,比如 xxxx.redis.cache/queue.portxxx.xxx,第一個段表示業(yè)務(wù)簡寫,第二個段表示這是Redis 內(nèi)網(wǎng)域名,第三個段表示 Redis 類型,cache 表示緩存,queue 表示隊列,第四個段表示 Redis端口,第五、第六個段表示內(nèi)網(wǎng)主域名。當(dāng)主節(jié)點發(fā)生故障,比如機器故障、Redis 節(jié)點故障或者網(wǎng)絡(luò)不可達(dá),Sentinel集群會調(diào)用 client-reconfig- 配置的腳本,修改對應(yīng)端口的內(nèi)網(wǎng)域名。對應(yīng)端口的內(nèi)網(wǎng)域名指向新的 Redis 主節(jié)點。
優(yōu)點:秒級切換,10秒之內(nèi) ,腳本自定義,架構(gòu)可控,對應(yīng)用透明,前端不用擔(dān)心后端發(fā)生什么變化
缺點:維護成本高,依賴DNS,存在解析超時,哨兵存在短時間服務(wù)不可用,服務(wù)時通過外網(wǎng)不可采用
(2)VIP:
和第一種方案略有不同,把內(nèi)網(wǎng) DNS 換成了虛擬 IP。底層是 Redis Sentinel集群,代理著 Redis 主從,Web 端通過 VIP 提供服務(wù)。在部署 Redis 主從的時候,需要將虛擬P 綁定到當(dāng)前的 Redis 主節(jié)點。當(dāng)主節(jié)點發(fā)生故障,比如機器故障、Redis 節(jié)點故障或者網(wǎng)絡(luò)不可達(dá),Sentinel集群會調(diào)用 client-reconfig-配置的腳本,將VIP 漂移到新的主節(jié)點上。
優(yōu)點:秒級切換,5秒之內(nèi) ;腳本自定義,架構(gòu)可控,對應(yīng)用透明,前端不用擔(dān)心后端發(fā)生什么變化缺點:維護成本更高,使用VIP增加維護成本,并存在IP混亂風(fēng)險
(3)封裝客戶端直連 Redis Sentinel 端口:
這個主要是因為有些業(yè)務(wù)只能通過外網(wǎng)訪問 Redis,于是衍生出了這種方案。Web 使用客戶端連接其中臺 Redis Sentinel 集群中的一臺機器的某個端口,然后通過這個端口獲取到當(dāng)前的主節(jié)點,然后再連接到真實的 Redis 主節(jié)點進(jìn)行相應(yīng)的業(yè)務(wù)員操作。需要注意的是,Redis Sentinel 端口和 Redis 主節(jié)點均需要開放訪問權(quán)限。前端業(yè)務(wù)使用 lava,有JedisSentinelPool 可以復(fù)用。
優(yōu)點: 服務(wù)探測故障及時,DBA維護成本低
缺點: 依賴客戶端支持Sentinel:Sentinel 服務(wù)器和 Redis 節(jié)點需要開放訪問權(quán)限;再有 對應(yīng)用有侵入性
1.2?Proxy+Replication+Sentinel(僅僅了解)
這里的Proxy有兩種選擇:Codis (豌豆英) 和Twemproxy (推特)????????
這套架構(gòu)的時間為2015年,原因有二:
因為Codis開源的比較晚,考慮到更換組件的成本問題。畢竟本來運行好好的東西,你再去換組件,風(fēng)險是很大的。
Redis Cluster在2015年還是試用版,不保證會遇到什么問題,因此不敢嘗試
所以我沒接觸過Codis,之前一直用的是Twemproxy作為Proxy。這里以Twemproxy為例說明,如下圖所示
工作原理:
1.前端使用Twemproxy+KeepAlived做代理,將其后端的多臺Redis實例分片進(jìn)行統(tǒng)一管理與分配。
2.每一個分片節(jié)點的Slave都是Master的副本且只讀
3.Sentinel持續(xù)不斷的監(jiān)控每個分片節(jié)點的Master,當(dāng)Master出現(xiàn)故障且不可用狀態(tài)時,Sentinel會通知/啟動自動故障轉(zhuǎn)移等動作
4.Sentinel 可以在發(fā)生故障轉(zhuǎn)移動作后觸發(fā)相應(yīng)腳本 (通過 client-reconfig-script 參數(shù)配置),腳本獲取到最新的Master來修改Twemproxy配置
缺陷:
(1)部署結(jié)構(gòu)超級復(fù)雜
(2)可擴展性差,進(jìn)行擴縮容需要手動干預(yù)
(3)運維不方便
1.3?Redis Cluster 集群 (重點)
這一章的其他內(nèi)容請在Redis從基礎(chǔ)到進(jìn)階篇(三)----架構(gòu)原理與集群演變?中閱讀,這里是對第三篇進(jìn)行的補充
1.3.1?Redis-cluster架構(gòu)圖
? ??
1.3.2?工作原理
1.客戶端與Redis節(jié)點直連,不需要中間Proxy層,直接連接任意一個Master節(jié)點根據(jù)公式
2.HASH_SLOT=CRC16(key) mod 16384,計算出映射到哪個分片上,然后Redis會去相應(yīng)的節(jié)點進(jìn)行操作
優(yōu)點:
(1)無需Sentinel哨兵監(jiān)控,如果Master掛了,Redis Cluster內(nèi)部自動將Slave切換Master
(2)可以進(jìn)行水亞擴容
(3)支持自動化遷移,當(dāng)出現(xiàn)某個slave宕機了,那么就只有Master了,這時候的高可用性就無法很好的保證了,萬一Master也宕機了,咋辦呢? 針對這種情況,如果說其他Master有多余的Slave,集群自動把多余的slave遷移到?jīng)]有slave的Master 中
缺點:
(1)批量操作是個坑
(2)資源隔離性較差,容易出現(xiàn)相互影響的情況.
1.3.3?主從切換
????????當(dāng)集群中節(jié)點通過錯誤檢測機制發(fā)現(xiàn)某個節(jié)點處于fail狀態(tài)時,會執(zhí)行主從切換。Redis 還提供了手動切換的方法,即通過執(zhí)行 cluster failover 命令
自動切換:
切換流程如下 (假設(shè)被切換的主節(jié)點為M,執(zhí)行切換的從節(jié)點為S)
1.?s先更新自己的狀態(tài),將聲明自己為主節(jié)點。并且將s從M中移除
2. 由于s需要切換為主節(jié)點,所以將s的同步數(shù)據(jù)相關(guān)信息清除 (即不再從M同步鎖數(shù)據(jù))
3.?將M提供服務(wù)的slot都聲明到s中:.
4. 發(fā)送一個PONG包,通知集群中其他節(jié)點更新狀態(tài)
手動切換:
當(dāng)一個節(jié)點接受到 cluster failove 命令之后,執(zhí)行手動切換,流程如下
1. 該從節(jié)點首先向主節(jié)點發(fā)送一個mfstart包。通知主節(jié)點從節(jié)點開始進(jìn)行手動切換
2. 主節(jié)點會阻塞所有客戶端指令的執(zhí)行。之后主節(jié)點在周期函數(shù)clusterCron中發(fā)送ping 包時會在包頭部分做特殊標(biāo)記
3. 當(dāng)從節(jié)點收到主節(jié)點的ping包并且檢測到特殊標(biāo)記之后,會從包頭中獲取主節(jié)點的復(fù)制偏移量4. 從節(jié)點在周期函數(shù)clusterCron中檢測當(dāng)前處理的復(fù)制偏移量與主節(jié)點復(fù)制偏移量是否相等,當(dāng)相等時開始執(zhí)行切換流程
5. 切換完成后,主節(jié)點會講阻塞的所有客戶端命令通過發(fā)送+MOVED 指令重定向到新的主節(jié)點
通過流程可以看到,手動執(zhí)行主從切換流程時不會丟失任何數(shù)據(jù),也不會丟失任何執(zhí)行命令,只在切換過程中會有暫時的停頓
1.3.4 副本漂移
假設(shè)A發(fā)生故障,主A的A1會執(zhí)行切換,切換完成后A1變?yōu)锳1,此時主A1會出現(xiàn)單點問題
在周期性調(diào)度函數(shù) clusterCron中會定期檢查如下條件
1是否存在單點的主節(jié)點,即主節(jié)點沒有任何一臺可用的從節(jié)點
2是否存在有兩臺及以上可用從節(jié)點的主節(jié)點
如果以上兩個條件都滿足,從有最多可用從節(jié)點中選擇一臺從節(jié)點執(zhí)行副本漂移。選擇標(biāo)準(zhǔn)為按節(jié)點名稱從小到大,選擇最靠前的一臺從節(jié)點執(zhí)行漂移。具體漂移過程
3.從C的記錄中將C1移除
4.將C1所記錄的主節(jié)點更改為A1
5.在A1中添加C1從節(jié)點
6.將C1的數(shù)據(jù)同步源設(shè)置為A1
漂移過程只是更改一些節(jié)點所記錄的信息,之后會通過心跳包將該信息同步到所有的集群節(jié)點。
1.3.5 分片漂移
這點也請查看上一篇文章的6.7小節(jié)添加節(jié)點
二、Redis版本歷史(增加了解)
1.Redis2.6 Redis2.6在2012年正式發(fā)布
2.Redis2.8
? ?Redis2.8在2013年11月22日正式發(fā)布
? ?Redis Sentinel第二版,相比于Redis2.6的Redis Sentinel,此版本已經(jīng)變成生產(chǎn)可用。
3.Redis3.0
? ?Redis3.0在2015年4月1日正式發(fā)布4.Redis3.2
? ?Redis3.2在2016年5月6日正式發(fā)布,集群高可用
5.Redis4.0
? ?Redis 4.0在2017年7月發(fā)布為GA,主要是增加了混合持久化和LFU淘汰策略
6.Redis5.0
? ?Redis5.0 2018年10月18日正式發(fā)布,stream 是重要新增特性
三、Redis 5.0 源碼清單 (對源碼感興趣的,看一下)
1.基本數(shù)據(jù)結(jié)構(gòu)
? ?動態(tài)字符串sds.c
? ?整數(shù)集合intset.c
? ?壓縮列表ziplist.c
? ?快速鏈表quicklist.c
? ?字典dict.c2.Redis數(shù)據(jù)類型的底層實現(xiàn)
? ?Redis對象object.c
? ?字符串t_string.c
? ?列表t list.c
? ?字典t_hash.c
? ?集合及有序集合t set.c和t_zset3.Redis數(shù)據(jù)庫的實現(xiàn)
? ?數(shù)據(jù)庫的底層實現(xiàn)db.c
? ?持久化rdb.c和aof.c4.Redis服務(wù)端和客戶端實現(xiàn)
? ?事件驅(qū)動ae.c和ae_epoll.c
? ?網(wǎng)絡(luò)連接anet.c和networking.c
? ?服務(wù)端程序server.c
? ?客戶端程序redis-cli.c5. 集群相關(guān)
? ? 主從復(fù)制replication.c? ? 哨兵sentinel.c
? ? 集群cluster.c
6.特殊數(shù)據(jù)類型
? ?其他數(shù)據(jù)結(jié)構(gòu),如hyperloglog.c、geo.c
? ?數(shù)據(jù)流t stream.c
? ?streams的底層實現(xiàn)結(jié)構(gòu)listpack.c和rax.c
四、Redis和lua整合
4.1?什么是lua
lua 是?種輕量?巧的 腳本語? ,?標(biāo)準(zhǔn) C 語? 編寫并以源代碼形式開放, 其設(shè)計?的是為了嵌?應(yīng)?程序中,從?為應(yīng)?程序提供靈活的擴展和定制功能。
4.2?Redis中使?lua的好處
1. 減少?絡(luò)開銷 ,在 Lua 腳本中可以把多個命令放在同?個腳本中運?2. 原?操作 , redis 會將整個腳本作為?個整體執(zhí)?,中間不會被其他命令插?。換句話說,編寫腳本 的過程中?需擔(dān)?會出現(xiàn)競態(tài)條件。 隔離性3. 復(fù)?性 ,客戶端發(fā)送的腳本會永遠(yuǎn)存儲在 redis 中,這意味著其他客戶端可以復(fù)?這?腳本來完成同樣的邏輯
4.3?lua的安裝和語法
lua 教程 https://www.runoob.com/lua/lua-tutorial.html
4.3.1?Redis整合lua腳本
從 Redis2.6.0 版本開始,通過 內(nèi)置的 lua 編譯 / 解釋器 ,可以使? EVAL 命令對 lua 腳本進(jìn)?求值。
EVAL命令
EVAL script numkeys key [key ...] arg [arg ...]
命令說明:?
script 參數(shù): 是?段 Lua 腳本程序,它會被運?在 Redis 服務(wù)器上下?中,這段腳本不必 ( 也不應(yīng)該 ) 定義為?個 Lua 函數(shù)。numkeys 參數(shù): ?于指定鍵名參數(shù)的個數(shù)。key [key ...] 參數(shù): 從 EVAL 的第三個參數(shù)開始算起,使?了 numkeys 個鍵( key ),表示在腳本中所?到的那些Redis 鍵 (key) ,這些鍵名參數(shù)可以在 Lua 中通過全局變量 KEYS 數(shù)組,? 1 為基址的形式訪問( KEYS[1] , KEYS[2] ,以此類推 ) 。arg [arg ...] 參數(shù): 可以在 Lua 中通過全局變量 ARGV 數(shù)組訪問,訪問的形式和 KEYS 變量類似 ( ARGV[1] 、 ARGV[2] ,諸如此類 ) 。
./redis-cli
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
4.4?lua 腳本調(diào)?Redis 命令
4.4.1?redis.call();
返回值就是 redis 命令執(zhí)?的返回值如果出錯,返回錯誤信息,不繼續(xù)執(zhí)?
4.4.2?redis.pcall();
返回值就是 redis 命令執(zhí)?的返回值如果出錯了 記錄錯誤信息,繼續(xù)執(zhí)?
在腳本中,使? return 語句將返回值返回給客戶端,如果沒有 return ,則返回 nil示例:127.0.0.1:6379> eval "return redis.call('set',KEYS[1],'bar')" 1 foo OK
4.4.3?redis-cli --eval
可以使? redis-cli --eval 命令指定?個 lua 腳本?件去執(zhí)?。
local num = redis.call('GET', KEYS[1]);
if not num then
return 0;
else
local res = num * ARGV[1];
redis.call('SET',KEYS[1], res);
return res;
end
[root@localhost bin]# ./redis-cli --eval redis.lua lua:incrbyml , 8
(integer) 0
[root@localhost bin]# ./redis-cli incr lua:incrbyml
(integer) 1
[root@localhost bin]# ./redis-cli --eval redis.lua lua:incrbyml , 8
(integer) 8
[root@localhost bin]# ./redis-cli --eval redis.lua lua:incrbyml , 8
(integer) 64
[root@localhost bin]# ./redis-cli --eval redis.lua lua:incrbyml , 2
(integer) 128
[root@localhost bin]# ./redis-cli
--eval :告訴 redis 客戶端去執(zhí)?后?的 lua 腳本redis.lua :具體的 lua 腳本?件名稱lua:incrbymul : lua 腳本中需要的 key8 : lua 腳本中需要的 value
上?命令中 keys 和 values 中間需要使?逗號隔開,并且逗號兩邊都要有空格
4.5?Redis+lua 秒殺
goodId:
{
"total":100,
"released":0;
}
local n = tonumber(ARGV[1])
if not n or n == 0 then
return 0
end
local vals = redis.call("HMGET", KEYS[1], "total", "released");
local total = tonumber(vals[1])
local blocked = tonumber(vals[2])
if not total or not blocked then
return 0
end
if blocked + n <= total then
redis.call("HINCRBY", KEYS[1], "released", n)
return n;
end
return 0
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.0.1.RELEASE</version>
long count = redisHelper.getStrCache().execute(new RedisCallback<Long>() {
@Nullable
@Override
public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
long ret =redisConnection.eval(script.getScriptAsString().getBytes(),ReturnType.INTEGER, 1, key.getBytes(), String.valueOf(count).getBytes());
return ret;
}
});
針對redis到databases的更新,思考了很久,沒有找到較好的解決辦法,先采?定時任務(wù)異步更新。?于數(shù)據(jù)是否丟失的問題,如果redis掛了,重啟后redis會恢復(fù)數(shù)據(jù),等下次定時任務(wù)就可以 將數(shù)據(jù)庫中的數(shù)據(jù)保持?致,缺點是redis掛了秒殺活動會失敗。
redis 存?份相關(guān) hash 鍵名單表,通過讀取名單表來讀取更新通過流式讀取 databases 中的表來讀取更新。
五、Redis Stream
5.1?Redis Stream介紹
Redis 5.0 全新的數(shù)據(jù)類型: streams ,官?把它定義為:以更抽象的?式建模?志的數(shù)據(jù)結(jié)構(gòu)。 Redis 的streams 主要是?個 append only ( AOF )的數(shù)據(jù)結(jié)構(gòu),?少在概念上它是?種在內(nèi)存中表示的抽象 數(shù)據(jù)類型,只不過它們實現(xiàn)了更強?的操作,以克服?志?件本身的限制。
如果你了解 MQ ,那么可以把 streams 當(dāng)做基于內(nèi)存的 MQ 。如果你還了解 kafka ,那么甚?可以把streams當(dāng)做基于內(nèi)存的 kafka 。 listpack 存儲信息, Rax 組織 listpack 消息鏈表listpack 是對 ziplist 的改進(jìn),它? ziplist 少了?個定位最后?個元素的屬性
1.streams ?持多個客戶端(消費者)等待數(shù)據(jù)( Linux 環(huán)境開多個窗?執(zhí)? XREAD 即可模擬),并 且每個客戶端得到的是完全相同的數(shù)據(jù)。2.Pub/Sub 是發(fā)送忘記的?式,并且不存儲任何數(shù)據(jù); ? streams 模式下,所有消息被?限期追加在 streams 中,除??于顯式執(zhí)?刪除( XDEL ) 。 XDEL 只做?個標(biāo)記位 其實信息和?度還在。3.streams 的 Consumer Groups 也是 Pub/Sub ?法實現(xiàn)的控制?式。
streams 數(shù)據(jù)結(jié)構(gòu)本身?常簡單,但是 streams 依然是 Redis 到?前為?最復(fù)雜的類型,其原因是實現(xiàn)的 ?些額外的功能:?系列的阻塞操作允許消費者等待?產(chǎn)者加?到streams 的新數(shù)據(jù)。另外還有?個稱為Consumer Groups 的概念, Consumer Group 概念最先由 kafka 提出, Redis 有?個類似實現(xiàn),和kafka的 Consumer Groups 的?的是?樣的:允許?組客戶端協(xié)調(diào)消費相同的信息流!
127.0.0.1:6379> xadd mystream * message apple
"1589994652300-0"
127.0.0.1:6379> xadd mystream * message orange
"1589994679942-0"
127.0.0.1:6379> xrange mystream - +
1) 1) "1589994652300-0"
2) 1) "message"
2) "apple"
2) 1) "1589994679942-0"
2) 1) "message"
2) "orange"
xread block 0 streams mystream $
127.0.0.1:6379> xadd mystream * message strawberry
127.0.0.1:6379> xgroup create mystream mygroup1 0
OK
127.0.0.1:6379> xgroup create mystream mygroup2 0
OK
127.0.0.1:6379> xreadgroup group mygroup1 zange count 2 streams mystream >
1) 1) "mystream"
2) 1) 1) "1589994652300-0"
2) 1) "message"
2) "apple"
2) 1) "1589994679942-0"
2) 1) "message"
2) "orange"
127.0.0.1:6379> xreadgroup group mugroup1 tuge count 2 streams mystream >
1) 1) "mystream"
2) 1) 1) "1589995171242-0"
2) 1) "message"
2) "strawberry"
127.0.0.1:6379> xreadgroup group mugroup2 tuge count 1 streams mystream >
1) 1) "mystream"
2) 1) 1) "1589995171242-0"
2) 1) "message"
2) "apple"
5.2?Redis Stream使?場景
六、Redis分布式
6.1?業(yè)務(wù)場景
1 、庫存超賣 ?如 5 個筆記本 A 看 準(zhǔn)備買 3 個 B 買 2 個 C 4 個 ?下單 3+2+4 =92 、防??戶重復(fù)下單3 、 MQ 消息去重4 、訂單操作變更
6.2?鎖的處理
synchronize 、 ReentrantLock
分布式鎖是控制分布式系統(tǒng)之間同步訪問共享資源的?種?式。
6.3?分布式鎖
1. 客戶端通過競爭獲取鎖才能對共享資源進(jìn)?操作 ( ①獲取鎖 ) ;2. 當(dāng)持有鎖的客戶端對共享資源進(jìn)?操作時(②占有鎖)3. 其他客戶端都不可以對這個資源進(jìn)?操作(③阻塞)4. 直到持有鎖的客戶端完成操作( ④釋放鎖 ) ;
6.3.1?分布式鎖特點
互斥性????????在任意時刻,只有?個客戶端可以持有鎖(排他性)?可?,具有容錯性????????只要鎖服務(wù)集群中的?部分節(jié)點正常運?,客戶端就可以進(jìn)?加鎖解鎖操作避免死鎖????????具備鎖失效機制,鎖在?段時間之后?定會釋放。(正常釋放或超時釋放)加鎖和解鎖為同?個客戶端?????????個客戶端不能釋放其他客戶端加的鎖了
6.3.2?分布式鎖的實現(xiàn)?式
基于數(shù)據(jù)庫實現(xiàn)分布式鎖基于 zookeeper 時節(jié)點的分布式鎖基于 Redis 的分布式鎖基于 Etcd 的分布式鎖
6.4?Redis?式實現(xiàn)分布式鎖
6.4.1 獲取鎖
6.4.2 釋放鎖
public static boolean releaseLock(String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return
redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey),
Collections.singletonList(requestId));
if (result.equals(1L)) {
return true;
}
return false;
}
6.4.3?Redis分布式鎖--優(yōu)缺點
優(yōu)點:????????Redis是基于內(nèi)存存儲,并發(fā)性能好。缺點:????????1. 需要考慮原?性、超時、誤刪等情形。????????2. 獲鎖失敗時,客戶端只能?旋等待,在?并發(fā)情況下,性能消耗?較?。
redis ?可?最常?的?案就是 主從復(fù)制 (master-slave ),這種模式也給 redis 分布式鎖 挖了?坑。 redis cluster 集群環(huán)境下,假如現(xiàn)在 A 客戶端 想要加鎖,它會根據(jù)路由規(guī)則選擇?臺 master 節(jié)點寫? key mylock ,在加鎖成功后, master 節(jié)點會把 key 異步復(fù)制給對應(yīng)的 slave 節(jié)點。如果此時 redis master 節(jié)點宕機,為保證集群可?性,會進(jìn)? 主備切換 , slave 變?yōu)榱? redis master 。 B 客戶端 在新的 master 節(jié)點上加鎖成功,? A 客戶端 也以為??還是成功加了鎖的。 此時就會導(dǎo)致同?時間內(nèi)多個客戶端對?個分布式鎖完成了加鎖,導(dǎo)致各種臟數(shù)據(jù)的產(chǎn)?。?于解決辦法嘛,?前看還沒有什么根治 的?法, 只能盡量保證機器的穩(wěn)定性 ,減少發(fā)?此事件的概率。
6.4.4?本質(zhì)分析
CAP 模型分析P:容錯A:???可?C:?致性在分布式環(huán)境下不可能滿?三者共存,只能滿?其中的兩者共存,在分布式下 P 不能舍棄 ( 舍棄 P 就是單機了) 。所以只能是 CP (強?致性模型)和 AP( ?可?模型 ) 。分布式鎖是 CP 模型, Redis 集群是 AP 模型。 (base)
與業(yè)務(wù)有關(guān)當(dāng)業(yè)務(wù)不需要數(shù)據(jù)強?致性時,?如:社交場景,就可以使? Redis 實現(xiàn)分布式鎖當(dāng)業(yè)務(wù)必須要數(shù)據(jù)的強?致性,即不允許重復(fù)獲得鎖,?如?融場景(重復(fù)下單,重復(fù)轉(zhuǎn)賬)就不要使?可以使? CP 模型實現(xiàn),?如: zookeeper 和 etcd 。
6.5??產(chǎn)環(huán)境中的分布式鎖
6.5.1?加鎖機制
"if (redis.call('exists',KEYS[1])==0) then "+
"redis.call('hset',KEYS[1],ARGV[2],1) ; "+
"redis.call('pexpire',KEYS[1],ARGV[1]) ; "+
"return nil; end ;" +
"if (redis.call('hexists',KEYS[1],ARGV[2]) ==1 ) then "+
"redis.call('hincrby',KEYS[1],ARGV[2],1) ; "+
"redis.call('pexpire',KEYS[1],ARGV[1]) ; "+
"return nil; end ;" +
"return redis.call('pttl',KEYS[1]) ;"
第?段 if 判斷語句,就是? “exists myLock” 命令判斷?下,如果你要加鎖的那個鎖 key 不存在的話,你就進(jìn)?加鎖。如何加鎖呢?很簡單,?下?的命令:hset myLock8743c9c0-0795-4907-87fd-6c719a6b4586:1 1通過這個命令設(shè)置?個 hash 數(shù)據(jù)結(jié)構(gòu),這?命令執(zhí)?后,會出現(xiàn)?個類似下?的數(shù)據(jù)結(jié)構(gòu):myLock :{"8743c9c0-0795-4907-87fd-6c719a6b4586:1":1 }上述就代表 “8743c9c0-0795-4907-87fd-6c719a6b4586:1” 這個客戶端對 “myLock” 這個鎖 key 完成了加鎖。接著會執(zhí)? “pexpire myLock 30000” 命令,設(shè)置 myLock 這個鎖 key 的?存時間是 30 秒。
(1)?鎖互斥機制
那么在這個時候,如果客戶端 2 來嘗試加鎖,執(zhí)?了同樣的?段 lua 腳本,會咋樣呢?很簡單,第?個 if 判斷會執(zhí)? “exists myLock” ,發(fā)現(xiàn) myLock 這個鎖 key 已經(jīng)存在了。接著第?個 if 判斷,判斷?下, myLock 鎖 key 的 hash 數(shù)據(jù)結(jié)構(gòu)中,是否包含客戶端 2 的 ID ,但是明顯不是的,因為那?包含的是客戶端1 的 ID 。所以,客戶端 2 會獲取到 pttl myLock 返回的?個數(shù)字,這個數(shù)字代表了 myLock 這個鎖 key 的 剩余?存時 間。 ?如還剩 15000 毫秒的?存時間。此時客戶端 2 會進(jìn)??個 while 循環(huán),不停的嘗試加鎖。
(2)??動延時機制
只要客戶端 1 ?旦加鎖成功,就會啟動?個 watch dog 看?狗, 他是?個后臺線程,會每隔 10 秒檢查? 下 ,如果客戶端 1 還持有鎖 key ,那么就會不斷的延?鎖 key 的?存時間。
(3)?可重?鎖機制
第?個 if 判斷肯定不成?, “exists myLock” 會顯示鎖 key 已經(jīng)存在了。第?個 if 判斷會成?,因為 myLock 的 hash 數(shù)據(jù)結(jié)構(gòu)中包含的那個 ID ,就是客戶端 1 的那個 ID ,也就是 “8743c9c0-0795-4907-87fd-6c719a6b4586:1”此時就會執(zhí)?可重?加鎖的邏輯,他會?:incrby myLock8743c9c0-0795-4907-87fd-6c71a6b4586:1 1通過這個命令,對客戶端 1 的加鎖次數(shù),累加 1 。數(shù)據(jù)結(jié)構(gòu)會變成:myLock :{"8743c9c0-0795-4907-87fd-6c719a6b4586:1":2 }
(4)?釋放鎖機制
#如果key已經(jīng)不存在,說明已經(jīng)被解鎖,直接發(fā)布(publish)redis消息
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; " +
"end;" +
# key和field不匹配,說明當(dāng)前客戶端線程沒有持有鎖,不能主動解鎖。
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
# 將value減1
"local counter = redis.call('hincrby', KEYS[1], ARGV[3],-1); " +
# 如果counter>0說明鎖在重?,不能刪除key
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
# 刪除key并且publish 解鎖消息
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
– KEYS[1] :需要加鎖的 key ,這?需要是字符串類型。– KEYS[2] : redis 消息的 ChannelName, ?個分布式鎖對應(yīng)唯?的?個 channelName:“redisson_lock channel {” + getName() + “}”– ARGV[1] : reids 消息體,這?只需要?個字節(jié)的標(biāo)記就可以,主要標(biāo)記 redis 的 key 已經(jīng)解鎖,再結(jié)合redis的 Subscribe ,能喚醒其他訂閱解鎖消息的客戶端線程申請鎖。– ARGV[2] :鎖的超時時間,防?死鎖– ARGV[3] :鎖的唯?標(biāo)識,也就是剛才介紹的 id ( UUID.randomUUID() ) + “:” + threadId如果執(zhí)? lock.unlock() ,就可以釋放分布式鎖,此時的業(yè)務(wù)邏輯也是?常簡單的。其實說?了,就是每次都對 myLock 數(shù)據(jù)結(jié)構(gòu)中的那個加鎖次數(shù)減 1 。如果發(fā)現(xiàn)加鎖次數(shù)是 0 了,說明這個客戶端已經(jīng)不再持有鎖了,此時就會?:“del myLock” 命令,從 redis ?刪除這個 key 。然后呢,另外的客戶端 2 就可以嘗試完成加鎖了。
6.5.2?Redisson分布式鎖的使?
(1)?加?jar包的依賴
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.7.2</version>
</dependency>
(2)?配置Redisson
private static Config config = new Config();
//聲明redisso對象
private static Redisson redisson = null;
//實例化redisson
static{
config.useClusterServers()
// 集群狀態(tài)掃描間隔時間,單位是毫秒
.setScanInterval(2000)
//cluster?式?少6個節(jié)點(3主3從,3主做sharding,3從?來保證主宕機后可以?可?)
.addNodeAddress("redis://127.0.0.1:6379" )
.addNodeAddress("redis://127.0.0.1:6380")
.addNodeAddress("redis://127.0.0.1:6381")
.addNodeAddress("redis://127.0.0.1:6382")
.addNodeAddress("redis://127.0.0.1:6383")
.addNodeAddress("redis://127.0.0.1:6384");
//得到redisson對象
redisson = (Redisson) Redisson.create(config);
}
//獲取redisson對象的?法
public static Redisson getRedisson(){
return redisson;
}
}
(3)?鎖的獲取和釋放?
public class DistributedRedisLock {
//從配置類中獲取redisson對象
private static Redisson redisson = RedissonManager.getRedisson();
private static final String LOCK_TITLE = "redisLock_";
//加鎖
public static boolean acquire(String lockName){
//聲明key對象
String key = LOCK_TITLE + lockName;
//獲取鎖對象
RLock mylock = redisson.getLock(key);
//加鎖,并且設(shè)置鎖過期時間3秒,防?死鎖的產(chǎn)? uuid+threadId
mylock.lock(3,TimeUtil.SECOND);
//加鎖成功
return true;
}
//鎖的釋放
public static void release(String lockName){
//必須是和加鎖時的同?個key
String key = LOCK_TITLE + lockName;
//獲取鎖對象
RLock mylock = redisson.getLock(key);
//釋放鎖(解鎖)
mylock.unlock();
}
}
(4)?業(yè)務(wù)邏輯中使?分布式鎖
public String discount() throws IOException{
String key = "test123";
//加鎖
DistributedRedisLock.acquire(key);
//執(zhí)?具體業(yè)務(wù)邏輯
dosoming
//釋放鎖
DistributedRedisLock.release(key);
//返回結(jié)果
return soming;
}
七、緩存常見問題
7.1?緩存預(yù)熱
1. 直接寫個緩存刷新頁面,上線前手工操作一下2. 數(shù)據(jù)量不大的時候,可以在項目啟動的時候自動加載3. 定時刷新緩存
7.2?緩存雪崩
什么叫緩存雪崩?
當(dāng)緩存服務(wù)器重啟或者大量緩存集中在某一個時間段失效,這樣在失效的時候,也會給后端系統(tǒng) ( 比如 DB)帶來很大壓力。
1 :在緩存失效后,通過加鎖或者隊列來控制讀數(shù)據(jù)庫寫緩存的線程數(shù)量。比如對某個 key 只允許一個線程查詢數(shù)據(jù)和寫緩存,其他線程等待。2 :不同的 key ,設(shè)置不同的過期時間,讓緩存失效的時間點盡量均勻。setRedis ( Key , value , time + Math.random() * 10000 ) 代碼3 :做二級緩存, A1 為原始緩存, A2 為拷貝緩存, A1 失效時,可以訪問 A2 , A1 緩存失效時間設(shè)置為短期,A2設(shè)置為長期(此點為補充)
7.3?緩存擊穿
對于一些設(shè)置了過期時間的 key ,如果這些 key 可能會在某些時間點被超高并發(fā)地訪問,是一種非常 “ 熱點” 的數(shù)據(jù)。這個時候,需要考慮一個問題:緩存被 “ 擊穿 ” 的問題,這個和緩存雪崩的區(qū)別在于這里針對某一key 緩存,前者則是很多 key 。緩存在某個時間點過期的時候,恰好在這個時間點對這個 Key 有大量的并發(fā)請求過來,這些請求發(fā)現(xiàn)緩存過期一般都會從后端DB 加載數(shù)據(jù)并回設(shè)到緩存,這個時候大并發(fā)的請求可能會瞬間把后端 DB 壓垮。
使用 redis 的 setnx 互斥鎖先進(jìn)行判斷,這樣其他線程就處于等待狀態(tài),保證不會有大并發(fā)操作去操作數(shù)據(jù)庫。if(redis.sexnx()==1){????????//先查詢緩存????????//查詢數(shù)據(jù)庫????????//加入緩存}
7.4?緩存穿透
一般的緩存系統(tǒng),都是按照 key 去緩存查詢,如果不存在對應(yīng)的 value ,就應(yīng)該去后端系統(tǒng)查找(比如DB)。如果 key 對應(yīng)的 value 是一定不存在的,并且對該 key 并發(fā)請求量很大,就會對后端系統(tǒng)造成很大的壓力。也就是說,對不存在的 key 進(jìn)行高并發(fā)訪問,導(dǎo)致數(shù)據(jù)庫壓力瞬間增大,這就叫做【緩存穿透】。
1. 在服務(wù)器端,接收參數(shù)時業(yè)務(wù)接口中過濾不合法的值, null ,負(fù)值,和空值進(jìn)行檢測和空值。2.bloom filter :類似于哈希表的一種算法,用所有可能的查詢條件生成一個 bitmap ,在進(jìn)行數(shù)據(jù)庫查詢之前會使用這個bitmap 進(jìn)行過濾,如果不在其中則直接過濾,從而減輕數(shù)據(jù)庫層面的壓力。采用的是一票否決 只要有一個認(rèn)為你不存在 就認(rèn)為你是不存在的3. 空值緩存:一種比較簡單的解決辦法,在第一次查詢完不存在的數(shù)據(jù)后,將該 key 與對應(yīng)的空值也放入緩存中,只不過設(shè)定為較短的失效時間,例如幾分鐘,這樣則可以應(yīng)對短時間的大量的該key 攻擊,設(shè)置為較短的失效時間是因為該值可能業(yè)務(wù)無關(guān),存在意義不大,且該次的查詢也未必是攻擊者發(fā)起,無過久存儲的必要,故可以早點失效
7.5?緩存降級
當(dāng)訪問量出現(xiàn)劇增、服務(wù)出現(xiàn)問題(相應(yīng)時間慢或者不響應(yīng)) 或非核心業(yè)務(wù)影響到核心流程的性能,還需要保證服務(wù)的可用性,即便有損服務(wù)。
一般: ex 有些服務(wù)偶爾網(wǎng)絡(luò)抖動或者服務(wù)正在上線超時,可以自定降級.警告:有些服務(wù)在一端時間內(nèi)有波動( 95%-100% ),可以自定降級或人工降級,還有發(fā)送告警.錯誤:可利用率低于 90% , redis 連接池被打爆了,數(shù)據(jù)庫連接池被打爆,或者訪問量突然猛增到系統(tǒng)能承受的最大閾值,這時候根據(jù)情況自動降級或人工降級.嚴(yán)重錯誤:比如因為特殊原因數(shù)據(jù)錯誤了,需要緊急人工降級。 redis服務(wù)出問題了, 不去查數(shù)據(jù)庫,而是直接返回一個默認(rèn)值(自定義一些隨機值).
7.6?緩存更新
1. 定期去清理過期的緩存2. 當(dāng)有用戶請求過來時,先判斷這個請求用到的緩存是否過期,過期的話就去底層系統(tǒng)得到新數(shù)據(jù)進(jìn)行緩存更新
7.7?緩存數(shù)據(jù)庫雙寫一致性 (重點)
DB KV雙寫 就一定會出現(xiàn)數(shù)據(jù)一致性問題 文章來源:http://www.zghlxwxcb.cn/news/detail-697470.html
一般來說,在讀取緩存方面,我們都是先讀取緩存,再讀取數(shù)據(jù)庫的。但是,在更新緩存方面,我們是需要先更新緩存,再更新數(shù)據(jù)庫?還是先更新數(shù)據(jù)庫,再更新緩存?還 是說有其他的方案?
7.7.1?先更新redis再更新db
A_update_redis
B_update_redis
B_update_db
A_update_db
7.7.2?先更新db再更新redis
A_update_db
B_update_db
B_update_redis
A_update_redis
7.7.3?先更新DB再刪除redis
A_update_db
B_update_db
B_rm_redis
A_rm_redis
A_get_data
redis_cache_miss
A_get_db
B_update_db
B_rm_redis
(此時如果拿db是b值,但是redis沒有值)
A_update_redis
7.7.4?先刪除redis再更新DB
A_rm_redis
B_get_data
B_redis_miss
B_get_db
B_update_redis
A_update_db
7.7.5?延遲雙刪
rm_redis
update_db
sleep xxx ms
rm_redis
7.7.6?思考變種
update_db
sleep xxx ms
rm_redis
7.7.7?總結(jié)
當(dāng)然這些極端情況本身要求同一個 key 是多寫的,這個根據(jù)業(yè)務(wù)需求來看是否需要,比如某些場景本身就是寫少讀多的
7.8?多個系統(tǒng)同時操作(并發(fā))Redis帶來的數(shù)據(jù)問題
系統(tǒng) A 、 B 、 C 三個系統(tǒng),分別去操作 Redis 的同一個 Key ,本來順序是 1 , 2 , 3 是正常的,但是因為系統(tǒng)A網(wǎng)絡(luò)突然抖動了一下, B , C 在他前面操作了 Redis ,這樣數(shù)據(jù)不就錯了么。就好比下單,支付,退款三個順序你變了,你先退款,再下單,再支付,那流程就會失敗,那數(shù)據(jù)不就亂了?你訂單還沒生成你卻支付,退款了?明顯走不通了,這在線上是很恐怖的事情。這種情況怎么解決呢?可以找個管家?guī)臀覀児芾砗脭?shù)據(jù)的嘛!
文章來源地址http://www.zghlxwxcb.cn/news/detail-697470.html
某個時刻,多個系統(tǒng)實例都去更新某個 key ??梢曰? Zookeeper 實現(xiàn)分布式鎖。每個系統(tǒng)通過 Zookeeper 獲取分布式鎖,確保同一時間,只能有一個系統(tǒng)實例在操作某個 Key ,別人都不允許讀和寫。要寫入緩存的數(shù)據(jù),都是從 MySQL 里查出來的,都得寫入 MySQL 中,寫入 MySQL 中的時候必須保 存一個時間戳,從 MySQL 查出來的時候,時間戳也查出來每次要 寫之前,先判斷 一下當(dāng)前這個 Value 的時間戳是否比緩存里的 Value 的時間戳要新。如果是的話,那么可以寫,否則,就不能用舊的數(shù)據(jù)覆蓋新的數(shù)據(jù)
八、Redis 常見面試問題
8.1?Memcache特點
MC 處理請求時使用多線程異步 IO 的方式,可以合理利用 CPU 多核的優(yōu)勢,性能非常優(yōu)秀;MC 功能簡單,使用內(nèi)存存儲數(shù)據(jù)MC 對緩存的數(shù)據(jù)可以設(shè)置失效期,過期后的數(shù)據(jù)會被清除;失效的策略采用延遲失效,就是當(dāng)再次使用數(shù)據(jù)時檢查是否失效;當(dāng)容量存滿時,會對緩存中的數(shù)據(jù)進(jìn)行剔除,剔除時,除了會對過期 key 進(jìn)行清理,還會按 LRU 策略對數(shù)據(jù)進(jìn)行剔除。
key 不能超過 250 個字節(jié) ;value 不能超過 1M 字節(jié) ;key 的最大失效時間是 30 天;只支持 K-V 結(jié)構(gòu),不提供持久化和主從同步功能 。MC 沒有原生的集群,可以依靠客戶端實現(xiàn)往集群中做分片寫入數(shù)據(jù)。
8.2?Reids 特點
與 MC 不同的是, Redis 采用單線程模式處理請求。這樣做的原因有 2 個:一個是因為采用了非阻塞的異步事件處理機制;另一個是緩存數(shù)據(jù)都是內(nèi)存操作 IO 時間不會太長,單線程可以避免線程上下文切換產(chǎn)生的代價。Redis 支持持久化,所以 Redis 不僅僅可以用作緩存,也可以用作 NoSQL 數(shù)據(jù)庫。相比 MC , Redis 還有一個非常大的優(yōu)勢,就是除了 K-V 之外,還支持多種數(shù)據(jù)格式,例如 list 、set、 sorted set 、 hash 等。Redis 提供主從同步機制,以及 Cluster 集群部署能力,能夠提供高可用服務(wù)。
存儲小數(shù)據(jù)時候 Redis 性能是比 MC 性能高100K 以上 ,MC 的性能是高于 Redis 。MC 本身沒有集群功能,可以使用客戶端做分片
到了這里,關(guān)于Redis從基礎(chǔ)到進(jìn)階篇(四)----性能調(diào)優(yōu)、分布式鎖與緩存問題的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!