絕大部分寫業(yè)務(wù)的程序員协怒,在實(shí)際開發(fā)中使用 Redis 的時候奕删,只會 Set Value 和 Get Value 兩個操作奕坟,對 Redis 整體缺乏一個認(rèn)知。這里對 Redis 常見問題做一個總結(jié)罗侯,解決大家的知識盲點(diǎn)。
1溪猿、為什么使用 Redis
在項(xiàng)目中使用 Redis钩杰,主要考慮兩個角度:性能和并發(fā)。如果只是為了分布式鎖這些其他功能诊县,還有其他中間件 Zookpeer 等代替讲弄,并非一定要使用 Redis。
性能:
如下圖所示依痊,我們在碰到需要執(zhí)行耗時特別久避除,且結(jié)果不頻繁變動的 SQL,就特別適合將運(yùn)行結(jié)果放入緩存胸嘁。這樣瓶摆,后面的請求就去緩存中讀取,使得請求能夠迅速響應(yīng)性宏。
特別是在秒殺系統(tǒng)群井,在同一時間,幾乎所有人都在點(diǎn)毫胜,都在下單书斜。诬辈。。執(zhí)行的是同一操作———向數(shù)據(jù)庫查數(shù)據(jù)荐吉。
根據(jù)交互效果的不同,響應(yīng)時間沒有固定標(biāo)準(zhǔn)样屠。在理想狀態(tài)下穿撮,我們的頁面跳轉(zhuǎn)需要在瞬間解決,對于頁內(nèi)操作則需要在剎那間解決瞧哟。
并發(fā):
如下圖所示混巧,在大并發(fā)的情況下,所有的請求直接訪問數(shù)據(jù)庫勤揩,數(shù)據(jù)庫會出現(xiàn)連接異常咧党。這個時候,就需要使用 Redis 做一個緩沖操作陨亡,讓請求先訪問到 Redis傍衡,而不是直接訪問數(shù)據(jù)庫。
使用 Redis 的常見問題
- 緩存和數(shù)據(jù)庫雙寫一致性問題
- 緩存雪崩問題
- 緩存擊穿問題
- 緩存的并發(fā)競爭問題
2蛙埂、單線程的 Redis 為什么這么快
這個問題是對 Redis 內(nèi)部機(jī)制的一個考察。很多人都不知道 Redis 是單線程工作模型遮糖。
原因主要是以下三點(diǎn):
- 純內(nèi)存操作
- 單線程操作绣的,避免了頻繁的上下文切換
- 采用了非阻塞 I/O 多路復(fù)用機(jī)制
仔細(xì)說一說 I/O 多路復(fù)用機(jī)制,打一個比方:小名在 A 城開了一家快餐店店欲账,負(fù)責(zé)同城快餐服務(wù)屡江。小明因?yàn)橘Y金限制,雇傭了一批配送員赛不,然后小曲發(fā)現(xiàn)資金不夠了惩嘉,只夠買一輛車送快遞。加君羊:874811168即可免費(fèi)領(lǐng)取架構(gòu)資料一份踢故。
經(jīng)營方式一
客戶每下一份訂單文黎,小明就讓一個配送員盯著,然后讓人開車去送殿较。慢慢的小曲就發(fā)現(xiàn)了這種經(jīng)營方式存在下述問題:
- 時間都花在了搶車上了耸峭,大部分配送員都處在閑置狀態(tài),搶到車才能去送斜脂。
- 隨著下單的增多抓艳,配送員也越來越多,小明發(fā)現(xiàn)快遞店里越來越擠,沒辦法雇傭新的配送員了玷或。
- 配送員之間的協(xié)調(diào)很花時間儡首。
綜合上述缺點(diǎn),小明痛定思痛偏友,提出了經(jīng)營方式二蔬胯。
經(jīng)營方式二
小明只雇傭一個配送員。當(dāng)客戶下單位他,小明按送達(dá)地點(diǎn)標(biāo)注好氛濒,依次放在一個地方。最后鹅髓,讓配送員依次開著車去送舞竿,送好了就回來拿下一個。上述兩種經(jīng)營方式對比窿冯,很明顯第二種效率更高骗奖。
在上述比喻中:
- 每個配送員→每個線程
- 每個訂單→每個 Socket(I/O 流)
- 訂單的送達(dá)地點(diǎn)→Socket 的不同狀態(tài)
- 客戶送餐請求→來自客戶端的請求
- 明曲的經(jīng)營方式→服務(wù)端運(yùn)行的代碼
- 一輛車→CPU 的核數(shù)
于是有了如下結(jié)論:
- 經(jīng)營方式一就是傳統(tǒng)的并發(fā)模型,每個 I/O 流(訂單)都有一個新的線程(配送員)管理醒串。
- 經(jīng)營方式二就是 I/O 多路復(fù)用执桌。只有單個線程(一個配送員),通過跟蹤每個 I/O 流的狀態(tài)(每個配送員的送達(dá)地點(diǎn))芜赌,來管理多個 I/O 流仰挣。
下面類比到真實(shí)的 Redis 線程模型,如圖所示:
Redis-client 在操作的時候膘壶,會產(chǎn)生具有不同事件類型的 Socket。在服務(wù)端洲愤,有一段 I/O 多路復(fù)用程序香椎,將其置入隊(duì)列之中。然后禽篱,文件事件分派器,依次去隊(duì)列中取馍惹,轉(zhuǎn)發(fā)到不同的事件處理器中躺率。
3、Redis 的數(shù)據(jù)類型及使用場景
一個合格的程序員万矾,這五種類型都會用到悼吱。
String
最常規(guī)的 set/get 操作,Value 可以是 String 也可以是數(shù)字良狈。一般做一些復(fù)雜的計(jì)數(shù)功能的緩存后添。
Hash
這里 Value 存放的是結(jié)構(gòu)化的對象,比較方便的就是操作其中的某個字段薪丁。我在做單點(diǎn)登錄的時候遇西,就是用這種數(shù)據(jù)結(jié)構(gòu)存儲用戶信息疟丙,以 CookieId 作為 Key赁项,設(shè)置 30 分鐘為緩存過期時間,能很好的模擬出類似 Session 的效果。
List
使用 List 的數(shù)據(jù)結(jié)構(gòu)锰悼,可以做簡單的消息隊(duì)列的功能。另外镜硕,可以利用 lrange 命令闻妓,做基于 Redis 的分頁功能,性能極佳渗常,用戶體驗(yàn)好壮不。
Set
因?yàn)?Set 堆放的是一堆不重復(fù)值的集合。所以可以做全局去重的功能皱碘。我們的系統(tǒng)一般都是集群部署询一,使用 JVM 自帶的 Set 比較麻煩。另外尸执,就是利用交集家凯、并集、差集等操作如失,可以計(jì)算共同喜好绊诲,全部的喜好,自己獨(dú)有的喜好等功能褪贵。
Sorted Set
Sorted Set 多了一個權(quán)重參數(shù) Score掂之,集合中的元素能夠按 Score 進(jìn)行排列〈喽。可以做排行榜應(yīng)用世舰,取 TOP N 操作。Sorted Set 可以用來做延時任務(wù)槽卫。
4跟压、Redis 的過期策略和內(nèi)存淘汰機(jī)制
Redis 是否用到家,從這就能看出來歼培。比如你 Redis 只能存 5G 數(shù)據(jù)震蒋,可是你寫了 10G,那會刪 5G 的數(shù)據(jù)躲庄。怎么刪的查剖,這個問題思考過么?
正解:Redis 采用的是定期刪除+惰性刪除策略噪窘。
為什么不用定時刪除策略
定時刪除笋庄,用一個定時器來負(fù)責(zé)監(jiān)視 Key,過期則自動刪除。雖然內(nèi)存及時釋放直砂,但是十分消耗 CPU 資源菌仁。在大并發(fā)請求下,CPU 要將時間應(yīng)用在處理請求哆键,而不是刪除 Key掘托,因此沒有采用這一策略。
定期刪除+惰性刪除如何工作
定期刪除籍嘹,Redis 默認(rèn)每個 100ms 檢查闪盔,有過期 Key 則刪除。需要說明的是辱士,Redis 不是每個 100ms 將所有的 Key 檢查一次泪掀,而是隨機(jī)抽取進(jìn)行檢查。如果只采用定期刪除策略颂碘,會導(dǎo)致很多 Key 到時間沒有刪除异赫。于是,惰性刪除派上用場头岔。
采用定期刪除+惰性刪除就沒其他問題了么
不是的塔拳,如果定期刪除沒刪除掉 Key。并且你也沒及時去請求 Key峡竣,也就是說惰性刪除也沒生效靠抑。這樣,Redis 的內(nèi)存會越來越高适掰。那么就應(yīng)該采用內(nèi)存淘汰機(jī)制颂碧。
在 redis.conf 中有一行配置:
maxmemory-policy volatile-lru
該配置就是配內(nèi)存淘汰策略的:
- noeviction:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,新寫入操作會報(bào)錯类浪。
- allkeys-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時载城,在鍵空間中,移除最近最少使用的 Key费就。(推薦使用诉瓦,目前項(xiàng)目在用這種)(最近最久使用算法)
- allkeys-random:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,在鍵空間中力细,隨機(jī)移除某個 Key垦搬。(應(yīng)該也沒人用吧,你不刪最少使用 Key艳汽,去隨機(jī)刪)
- volatile-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時,在設(shè)置了過期時間的鍵空間中对雪,移除最近最少使用的 Key河狐。這種情況一般是把 Redis 既當(dāng)緩存,又做持久化存儲的時候才用。(不推薦)
- volatile-random:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時馋艺,在設(shè)置了過期時間的鍵空間中栅干,隨機(jī)移除某個 Key。(依然不推薦)
- volatile-ttl:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時捐祠,在設(shè)置了過期時間的鍵空間中碱鳞,有更早過期時間的 Key 優(yōu)先移除。(不推薦)
5踱蛀、Redis 和數(shù)據(jù)庫雙寫一致性問題
一致性問題還可以再分為最終一致性和強(qiáng)一致性窿给。數(shù)據(jù)庫和緩存雙寫,就必然會存在不一致的問題率拒。前提是如果對數(shù)據(jù)有強(qiáng)一致性要求崩泡,不能放緩存。我們所做的一切猬膨,只能保證最終一致性角撞。
另外,我們所做的方案從根本上來說勃痴,只能降低不一致發(fā)生的概率谒所。因此,有強(qiáng)一致性要求的數(shù)據(jù)沛申,不能放緩存劣领。首先,采取正確更新策略污它,先更新數(shù)據(jù)庫剖踊,再刪緩存。其次衫贬,因?yàn)榭赡艽嬖趧h除緩存失敗的問題德澈,提供一個補(bǔ)償措施即可,例如利用消息隊(duì)列固惯。
6梆造、如何應(yīng)對緩存穿透和緩存雪崩問題
這兩個問題,一般中小型傳統(tǒng)軟件企業(yè)很難碰到葬毫。如果有大并發(fā)的項(xiàng)目镇辉,流量有幾百萬左右,這兩個問題一定要深刻考慮贴捡。緩存穿透忽肛,即黑客故意去請求緩存中不存在的數(shù)據(jù),導(dǎo)致所有的請求都懟到數(shù)據(jù)庫上烂斋,從而數(shù)據(jù)庫連接異常屹逛。
緩存穿透解決方案:
- 利用互斥鎖础废,緩存失效的時候,先去獲得鎖罕模,得到鎖了评腺,再去請求數(shù)據(jù)庫。沒得到鎖淑掌,則休眠一段時間重試蒿讥。
- 采用異步更新策略,無論 Key 是否取到值抛腕,都直接返回芋绸。Value 值中維護(hù)一個緩存失效時間,緩存如果過期兽埃,異步起一個線程去讀數(shù)據(jù)庫侥钳,更新緩存。需要做緩存預(yù)熱(項(xiàng)目啟動前柄错,先加載緩存)操作舷夺。
- 提供一個能迅速判斷請求是否有效的攔截機(jī)制,比如售貌,利用布隆過濾器给猾,內(nèi)部維護(hù)一系列合法有效的 Key。迅速判斷出颂跨,請求所攜帶的 Key 是否合法有效敢伸。如果不合法,則直接返回恒削。
緩存雪崩池颈,即緩存同一時間大面積的失效,這個時候又來了一波請求钓丰,結(jié)果請求都懟到數(shù)據(jù)庫上躯砰,從而導(dǎo)致數(shù)據(jù)庫連接異常。
緩存雪崩解決方案:
- 給緩存的失效時間携丁,加上一個隨機(jī)值琢歇,避免集體失效。
- 使用互斥鎖梦鉴,但是該方案吞吐量明顯下降了李茫。
- 雙緩存。我們有兩個緩存肥橙,緩存 A 和緩存 B魄宏。緩存 A 的失效時間為 20 分鐘,緩存 B 不設(shè)失效時間存筏。自己做緩存預(yù)熱操作娜庇。
- 然后細(xì)分以下幾個小點(diǎn):從緩存 A 讀數(shù)據(jù)庫塔次,有則直接返回;A 沒有數(shù)據(jù)名秀,直接從 B 讀數(shù)據(jù),直接返回藕溅,并且異步啟動一個更新線程匕得,更新線程同時更新緩存 A 和緩存 B。
7巾表、如何解決 Redis 的并發(fā)競爭 Key 問題
這個問題大致就是汁掠,同時有多個子系統(tǒng)去 Set 一個 Key。這個時候要注意什么呢集币?大家基本都是推薦用 Redis 事務(wù)機(jī)制考阱。
但是我并不推薦使用 Redis 的事務(wù)機(jī)制。因?yàn)槲覀兊纳a(chǎn)環(huán)境鞠苟,基本都是 Redis 集群環(huán)境乞榨,做了數(shù)據(jù)分片操作。你一個事務(wù)中有涉及到多個 Key 操作的時候当娱,這多個 Key 不一定都存儲在同一個 redis-server 上吃既。因此,Redis 的事務(wù)機(jī)制跨细,十分雞肋鹦倚。
如果對這個 Key 操作,不要求順序
這種情況下冀惭,準(zhǔn)備一個分布式鎖震叙,大家去搶鎖,搶到鎖就做 set 操作即可散休,比較簡單媒楼。
如果對這個 Key 操作,要求順序
假設(shè)有一個 key1溃槐,系統(tǒng) A 需要將 key1 設(shè)置為 valueA匣砖,系統(tǒng) B 需要將 key1 設(shè)置為 valueB,系統(tǒng) C 需要將 key1 設(shè)置為 valueC昏滴。
期望按照 key1 的 value 值按照 valueA > valueB > valueC 的順序變化猴鲫。這種時候我們在數(shù)據(jù)寫入數(shù)據(jù)庫的時候,需要保存一個時間戳谣殊。
假設(shè)時間戳如下:
系統(tǒng) A key 1 {valueA 3:00}
系統(tǒng) B key 1 {valueB 3:05}
系統(tǒng) C key 1 {valueC 3:10}
那么拂共,假設(shè)系統(tǒng) B 先搶到鎖,將 key1 設(shè)置為{valueB 3:05}姻几。接下來系統(tǒng) A 搶到鎖宜狐,發(fā)現(xiàn)自己的 valueA 的時間戳早于緩存中的時間戳势告,那就不做 set 操作了,以此類推抚恒。其他方法咱台,比如利用隊(duì)列,將 set 方法變成串行訪問也可以俭驮。加君羊:874811168即可免費(fèi)領(lǐng)取架構(gòu)資料一份回溺。
8、總結(jié)
Redis 在國內(nèi)各大公司都能看到其身影混萝,比如我們熟悉的新浪遗遵,阿里,騰訊逸嘀,百度车要,美團(tuán),小米等崭倘。學(xué)習(xí) Redis翼岁,這幾方面尤其重要:Redis 客戶端、Redis 高級功能绳姨、Redis 持久化和開發(fā)運(yùn)維常用問題探討登澜、Redis 復(fù)制的原理和優(yōu)化策略、Redis 分布式解決方案等飘庄。