為什么分布式一定要有Redis拔稳?
考慮到絕大部分寫業(yè)務的程序員,在實際開發(fā)中使用 Redis 的時候锹雏,只會 Set Value 和 Get Value 兩個操作巴比,對 Redis 整體缺乏一個認知。
所以我斗膽以 Redis 為題材礁遵,對 Redis 常見問題做一個總結轻绞,希望能夠彌補大家的知識盲點。
本文圍繞以下幾點進行闡述:
- 為什么使用 Redis
- 使用 Redis 有什么缺點
- 單線程的 Redis 為什么這么快
- Redis 的數(shù)據(jù)類型佣耐,以及每種數(shù)據(jù)類型的使用場景
- Redis 的過期策略以及內(nèi)存淘汰機制
- Redis 和數(shù)據(jù)庫雙寫一致性問題
- 如何應對緩存穿透和緩存雪崩問題
- 如何解決 Redis 的并發(fā)競爭 Key 問題
為什么使用 Redis
我覺得在項目中使用 Redis政勃,主要是從兩個角度去考慮:性能和并發(fā)。
當然兼砖,Redis 還具備可以做分布式鎖等其他功能奸远,但是如果只是為了分布式鎖這些其他功能,完全還有其他中間件讽挟,如 ZooKpeer 等代替懒叛,并不是非要使用 Redis。因此耽梅,這個問題主要從性能和并發(fā)兩個角度去答薛窥。
性能
如下圖所示,我們在碰到需要執(zhí)行耗時特別久眼姐,且結果不頻繁變動的 SQL诅迷,就特別適合將運行結果放入緩存。這樣众旗,后面的請求就去緩存中讀取竟贯,使得請求能夠迅速響應。
題外話:忽然想聊一下這個迅速響應的標準逝钥。根據(jù)交互效果的不同屑那,這個響應時間沒有固定標準拱镐。
不過曾經(jīng)有人這么告訴我:"在理想狀態(tài)下,我們的頁面跳轉需要在瞬間解決持际,對于頁內(nèi)操作則需要在剎那間解決沃琅。
另外,超過一彈指的耗時操作要有進度提示蜘欲,并且可以隨時中止或取消益眉,這樣才能給用戶最好的體驗。"
那么瞬間姥份、剎那郭脂、一彈指具體是多少時間呢?
根據(jù)《摩訶僧祗律》記載:
一剎那者為一念澈歉,二十念為一瞬展鸡,二十瞬為一彈指,二十彈指為一羅預埃难,二十羅預為一須臾莹弊,一日一夜有三十須臾。
那么涡尘,經(jīng)過周密的計算忍弛,一瞬間為 0.36 秒、一剎那有 0.018 秒考抄、一彈指長達 7.2 秒细疚。
并發(fā)
如下圖所示,在大并發(fā)的情況下川梅,所有的請求直接訪問數(shù)據(jù)庫惠昔,數(shù)據(jù)庫會出現(xiàn)連接異常。
這個時候挑势,就需要使用 Redis 做一個緩沖操作镇防,讓請求先訪問到 Redis,而不是直接訪問數(shù)據(jù)庫潮饱。
使用 Redis 有什么缺點
大家用 Redis 這么久来氧,這個問題是必須要了解的,基本上使用 Redis 都會碰到一些問題香拉,常見的也就幾個啦扬。
回答主要是四個問題:
緩存和數(shù)據(jù)庫雙寫一致性問題
緩存雪崩問題
緩存擊穿問題
緩存的并發(fā)競爭問題
單線程的 Redis 為什么這么快
這個問題是對 Redis 內(nèi)部機制的一個考察。根據(jù)我的面試經(jīng)驗凫碌,很多人都不知道Redis 是單線程工作模型扑毡。所以,這個問題還是應該要復習一下的盛险。
回答主要是以下三點:
- 純內(nèi)存操作
- 單線程操作瞄摊,避免了頻繁的上下文切換
- 采用了非阻塞 I/O 多路復用機制
題外話:我們現(xiàn)在要仔細的說一說 I/O 多路復用機制勋又,因為這個說法實在是太通俗了,通俗到一般人都不懂是什么意思换帜。
打一個比方:小曲在 S 城開了一家快遞店楔壤,負責同城快送服務。小曲因為資金限制惯驼,雇傭了一批快遞員蹲嚣,然后小曲發(fā)現(xiàn)資金不夠了,只夠買一輛車送快遞祟牲。
經(jīng)營方式一
客戶每送來一份快遞隙畜,小曲就讓一個快遞員盯著,然后快遞員開車去送快遞说贝。
慢慢的小曲就發(fā)現(xiàn)了這種經(jīng)營方式存在下述問題:
- 幾十個快遞員基本上時間都花在了搶車上了议惰,大部分快遞員都處在閑置狀態(tài),誰搶到了車狂丝,誰就能去送快遞换淆。
- 隨著快遞的增多哗总,快遞員也越來越多几颜,小曲發(fā)現(xiàn)快遞店里越來越擠,沒辦法雇傭新的快遞員了讯屈。
- 快遞員之間的協(xié)調(diào)很花時間蛋哭。
綜合上述缺點,小曲痛定思痛涮母,提出了下面的經(jīng)營方式谆趾。
經(jīng)營方式二
小曲只雇傭一個快遞員。然后呢叛本,客戶送來的快遞沪蓬,小曲按送達地點標注好,然后依次放在一個地方来候。
最后跷叉,那個快遞員依次的去取快遞,一次拿一個营搅,然后開著車去送快遞云挟,送好了就回來拿下一個快遞。
上述兩種經(jīng)營方式對比转质,是不是明顯覺得第二種园欣,效率更高,更好呢休蟹?
在上述比喻中:
- 每個快遞員→每個線程
- 每個快遞→每個 Socket(I/O 流)
- 快遞的送達地點→Socket 的不同狀態(tài)
- 客戶送快遞請求→來自客戶端的請求
- 小曲的經(jīng)營方式→服務端運行的代碼
- 一輛車→CPU 的核數(shù)
于是我們有如下結論:
- 經(jīng)營方式一就是傳統(tǒng)的并發(fā)模型沸枯,每個 I/O 流(快遞)都有一個新的線程(快遞員)管理日矫。
- 經(jīng)營方式二就是 I/O 多路復用。只有單個線程(一個快遞員)辉饱,通過跟蹤每個 I/O 流的狀態(tài)(每個快遞的送達地點)搬男,來管理多個 I/O 流。
下面類比到真實的 Redis 線程模型彭沼,如圖所示:
簡單來說缔逛,就是我們的 redis-client 在操作的時候,會產(chǎn)生具有不同事件類型的 Socket姓惑。
在服務端褐奴,有一段 I/O 多路復用程序,將其置入隊列之中于毙。然后敦冬,文件事件分派器,依次去隊列中取唯沮,轉發(fā)到不同的事件處理器中脖旱。
需要說明的是,這個 I/O 多路復用機制介蛉,Redis 還提供了 select萌庆、epoll、evport币旧、kqueue 等多路復用函數(shù)庫践险,大家可以自行去了解。
Redis 的數(shù)據(jù)類型吹菱,以及每種數(shù)據(jù)類型的使用場景
是不是覺得這個問題很基礎巍虫?我也這么覺得。然而根據(jù)面試經(jīng)驗發(fā)現(xiàn)鳍刷,至少百分之八十的人答不上這個問題占遥。
建議,在項目中用到后输瓜,再類比記憶瓦胎,體會更深,不要硬記前痘×菽螅基本上,一個合格的程序員芹缔,五種類型都會用到坯癣。
String
這個沒啥好說的,最常規(guī)的 set/get 操作最欠,Value 可以是 String 也可以是數(shù)字示罗。一般做一些復雜的計數(shù)功能的緩存惩猫。
Hash
這里 Value 存放的是結構化的對象,比較方便的就是操作其中的某個字段蚜点。
我在做單點登錄的時候轧房,就是用這種數(shù)據(jù)結構存儲用戶信息,以 CookieId 作為 Key绍绘,設置 30 分鐘為緩存過期時間奶镶,能很好的模擬出類似 Session 的效果。
List
使用 List 的數(shù)據(jù)結構陪拘,可以做簡單的消息隊列的功能厂镇。另外還有一個就是,可以利用 lrange 命令左刽,做基于 Redis 的分頁功能捺信,性能極佳,用戶體驗好欠痴。
Set
因為 Set 堆放的是一堆不重復值的集合迄靠。所以可以做全局去重的功能。為什么不用 JVM 自帶的 Set 進行去重喇辽?
因為我們的系統(tǒng)一般都是集群部署掌挚,使用 JVM 自帶的 Set,比較麻煩茵臭,難道為了一個做一個全局去重疫诽,再起一個公共服務舅世,太麻煩了旦委。
另外,就是利用交集雏亚、并集缨硝、差集等操作,可以計算共同喜好罢低,全部的喜好查辩,自己獨有的喜好等功能。
Sorted Set
Sorted Set多了一個權重參數(shù) Score网持,集合中的元素能夠按 Score 進行排列宜岛。
可以做排行榜應用,取 TOP N 操作功舀。Sorted Set 可以用來做延時任務萍倡。最后一個應用就是可以做范圍查找。
Redis 的過期策略以及內(nèi)存淘汰機制
這個問題相當重要辟汰,到底 Redis 有沒用到家列敲,這個問題就可以看出來阱佛。
比如你 Redis 只能存 5G 數(shù)據(jù),可是你寫了 10G戴而,那會刪 5G 的數(shù)據(jù)凑术。怎么刪的,這個問題思考過么所意?
還有淮逊,你的數(shù)據(jù)已經(jīng)設置了過期時間,但是時間到了扶踊,內(nèi)存占用率還是比較高壮莹,有思考過原因么?
回答:Redis 采用的是定期刪除+惰性刪除策略。
為什么不用定時刪除策略
定時刪除姻檀,用一個定時器來負責監(jiān)視 Key命满,過期則自動刪除。雖然內(nèi)存及時釋放绣版,但是十分消耗 CPU 資源胶台。
在大并發(fā)請求下,CPU 要將時間應用在處理請求杂抽,而不是刪除 Key诈唬,因此沒有采用這一策略。
定期刪除+惰性刪除是如何工作
定期刪除缩麸,Redis 默認每個 100ms 檢查铸磅,是否有過期的 Key,有過期 Key 則刪除杭朱。
需要說明的是阅仔,Redis 不是每個 100ms 將所有的 Key 檢查一次,而是隨機抽取進行檢查(如果每隔 100ms弧械,全部 Key 進行檢查八酒,Redis 豈不是卡死)。
因此,如果只采用定期刪除策略,會導致很多 Key 到時間沒有刪除玫氢。于是让禀,惰性刪除派上用場。
也就是說在你獲取某個 Key 的時候,Redis 會檢查一下,這個 Key 如果設置了過期時間,那么是否過期了热鞍?如果過期了此時就會刪除。
采用定期刪除+惰性刪除就沒其他問題了么?
不是的,如果定期刪除沒刪除 Key碍现。然后你也沒即時去請求 Key幅疼,也就是說惰性刪除也沒生效。這樣昼接,Redis的內(nèi)存會越來越高爽篷。那么就應該采用內(nèi)存淘汰機制。
在 redis.conf 中有一行配置:
# maxmemory-policy volatile-lru
該配置就是配內(nèi)存淘汰策略的(什么慢睡,你沒配過逐工?好好反省一下自己):
- noeviction:當內(nèi)存不足以容納新寫入數(shù)據(jù)時,新寫入操作會報錯漂辐。應該沒人用吧泪喊。
- allkeys-lru:當內(nèi)存不足以容納新寫入數(shù)據(jù)時,在鍵空間中髓涯,移除最近最少使用的 Key袒啼。推薦使用,目前項目在用這種纬纪。
- allkeys-random:當內(nèi)存不足以容納新寫入數(shù)據(jù)時蚓再,在鍵空間中,隨機移除某個 Key包各。應該也沒人用吧摘仅,你不刪最少使用 Key,去隨機刪问畅。
- volatile-lru:當內(nèi)存不足以容納新寫入數(shù)據(jù)時娃属,在設置了過期時間的鍵空間中,移除最近最少使用的 Key护姆。這種情況一般是把 Redis 既當緩存矾端,又做持久化存儲的時候才用。不推薦签则。
- volatile-random:當內(nèi)存不足以容納新寫入數(shù)據(jù)時须床,在設置了過期時間的鍵空間中铐料,隨機移除某個 Key渐裂。依然不推薦。
- volatile-ttl:當內(nèi)存不足以容納新寫入數(shù)據(jù)時钠惩,在設置了過期時間的鍵空間中柒凉,有更早過期時間的 Key 優(yōu)先移除。不推薦篓跛。
PS:如果沒有設置 expire 的 Key膝捞,不滿足先決條件(prerequisites);那么 volatile-lru愧沟,volatile-random 和 volatile-ttl 策略的行為蔬咬,和 noeviction(不刪除) 基本上一致鲤遥。
Redis 和數(shù)據(jù)庫雙寫一致性問題
一致性問題是分布式常見問題,還可以再分為最終一致性和強一致性林艘。數(shù)據(jù)庫和緩存雙寫盖奈,就必然會存在不一致的問題。
答這個問題狐援,先明白一個前提钢坦。就是如果對數(shù)據(jù)有強一致性要求,不能放緩存啥酱。我們所做的一切爹凹,只能保證最終一致性。
另外镶殷,我們所做的方案從根本上來說禾酱,只能說降低不一致發(fā)生的概率,無法完全避免绘趋。因此宇植,有強一致性要求的數(shù)據(jù),不能放緩存埋心。
回答:首先指郁,采取正確更新策略,先更新數(shù)據(jù)庫拷呆,再刪緩存闲坎。其次,因為可能存在刪除緩存失敗的問題茬斧,提供一個補償措施即可腰懂,例如利用消息隊列。
如何應對緩存穿透和緩存雪崩問題
這兩個問題项秉,說句實在話绣溜,一般中小型傳統(tǒng)軟件企業(yè),很難碰到這個問題娄蔼。如果有大并發(fā)的項目怖喻,流量有幾百萬左右。這兩個問題一定要深刻考慮岁诉。
緩存穿透锚沸,即黑客故意去請求緩存中不存在的數(shù)據(jù),導致所有的請求都懟到數(shù)據(jù)庫上涕癣,從而數(shù)據(jù)庫連接異常哗蜈。
緩存穿透解決方案:
- 利用互斥鎖,緩存失效的時候,先去獲得鎖距潘,得到鎖了炼列,再去請求數(shù)據(jù)庫。沒得到鎖音比,則休眠一段時間重試唯鸭。
- 采用異步更新策略,無論 Key 是否取到值硅确,都直接返回目溉。Value 值中維護一個緩存失效時間,緩存如果過期菱农,異步起一個線程去讀數(shù)據(jù)庫缭付,更新緩存。需要做緩存預熱(項目啟動前循未,先加載緩存)操作陷猫。
- 提供一個能迅速判斷請求是否有效的攔截機制,比如的妖,利用布隆過濾器绣檬,內(nèi)部維護一系列合法有效的 Key。迅速判斷出嫂粟,請求所攜帶的 Key 是否合法有效娇未。如果不合法,則直接返回星虹。
緩存雪崩零抬,即緩存同一時間大面積的失效,這個時候又來了一波請求宽涌,結果請求都懟到數(shù)據(jù)庫上平夜,從而導致數(shù)據(jù)庫連接異常。
緩存雪崩解決方案:
- 給緩存的失效時間卸亮,加上一個隨機值忽妒,避免集體失效。
- 使用互斥鎖兼贸,但是該方案吞吐量明顯下降了段直。
- 雙緩存。我們有兩個緩存寝受,緩存 A 和緩存 B坷牛。緩存 A 的失效時間為 20 分鐘,緩存 B 不設失效時間很澄。自己做緩存預熱操作。
- 然后細分以下幾個小點:從緩存 A 讀數(shù)據(jù)庫,有則直接返回甩苛;A 沒有數(shù)據(jù)蹂楣,直接從 B 讀數(shù)據(jù),直接返回讯蒲,并且異步啟動一個更新線程痊土,更新線程同時更新緩存 A 和緩存 B。
如何解決 Redis 的并發(fā)競爭 Key 問題
這個問題大致就是墨林,同時有多個子系統(tǒng)去 Set 一個 Key赁酝。這個時候大家思考過要注意什么呢?
需要說明一下旭等,我提前百度了一下酌呆,發(fā)現(xiàn)答案基本都是推薦用 Redis 事務機制。
我并不推薦使用 Redis 的事務機制搔耕。因為我們的生產(chǎn)環(huán)境隙袁,基本都是 Redis 集群環(huán)境,做了數(shù)據(jù)分片操作弃榨。
你一個事務中有涉及到多個 Key 操作的時候菩收,這多個 Key 不一定都存儲在同一個 redis-server 上。因此鲸睛,Redis 的事務機制娜饵,十分雞肋。
如果對這個 Key 操作官辈,不要求順序
這種情況下划咐,準備一個分布式鎖,大家去搶鎖钧萍,搶到鎖就做 set 操作即可褐缠,比較簡單。
如果對這個 Key 操作风瘦,要求順序
假設有一個 key1队魏,系統(tǒng) A 需要將 key1 設置為 valueA,系統(tǒng) B 需要將 key1 設置為 valueB万搔,系統(tǒng) C 需要將 key1 設置為 valueC胡桨。
期望按照 key1 的 value 值按照 valueA > valueB > valueC 的順序變化。這種時候我們在數(shù)據(jù)寫入數(shù)據(jù)庫的時候瞬雹,需要保存一個時間戳昧谊。
假設時間戳如下:
系統(tǒng)A key 1 {valueA 3:00}
系統(tǒng)B key 1 {valueB 3:05}
系統(tǒng)C key 1 {valueC 3:10}
那么,假設這會系統(tǒng) B 先搶到鎖酗捌,將 key1 設置為{valueB 3:05}呢诬。接下來系統(tǒng) A 搶到鎖涌哲,發(fā)現(xiàn)自己的 valueA 的時間戳早于緩存中的時間戳,那就不做 set 操作了尚镰,以此類推阀圾。
其他方法,比如利用隊列狗唉,將 set 方法變成串行訪問也可以初烘。總之分俯,靈活變通