圖解Redis,Redis更新策略、緩存一致性問(wèn)題
一、為什么Redis集群的最大槽數(shù)是16384個(gè)?
2^14^=16384、2^16^=65536。
如果槽位是65536個(gè),發(fā)送心跳信息的消息頭是65536/8/1024 = 8k。
如果槽位是16384個(gè),發(fā)送心跳信息的消息頭是16384/8/1024 = 2k。
因?yàn)镽edis每秒都會(huì)發(fā)送一定數(shù)量的心跳包,如果消息頭是8k,未免有些太大了,浪費(fèi)網(wǎng)絡(luò)資源。
上面提過(guò),Redis的集群主節(jié)點(diǎn)數(shù)量一般不會(huì)超過(guò)1000個(gè)。集群中節(jié)點(diǎn)越多,心跳包的消息體內(nèi)的數(shù)據(jù)就越多,如果節(jié)點(diǎn)過(guò)多,也會(huì)造成網(wǎng)絡(luò)擁堵。因此Redis的作者Salvatore Sanfilippo不建議Redis Cluster的節(jié)點(diǎn)超過(guò)1000個(gè),對(duì)于節(jié)點(diǎn)數(shù)在1000個(gè)以內(nèi)的Redis Cluster,16384個(gè)槽位完全夠用。
Redis主節(jié)點(diǎn)的哈希槽信息是通過(guò)bitmap存儲(chǔ)的,在傳輸過(guò)程中,會(huì)對(duì)bitmap進(jìn)行壓縮,bitmap的填充率越低,壓縮率越高。
bitmap 填充率 = slots / N (N表示節(jié)點(diǎn)數(shù))。
也就是說(shuō)slots越小,填充率就會(huì)越小,壓縮率就會(huì)越高,傳輸效率就會(huì)越高。?
二、Redis集群是什么?
由于數(shù)據(jù)量過(guò)大,單個(gè)master復(fù)制集難以承擔(dān),因此需要多個(gè)master進(jìn)行承擔(dān)工作,每個(gè)master存儲(chǔ)部分?jǐn)?shù)據(jù),這就是Redis集群。
Redis集群包含多個(gè)master,一個(gè)master對(duì)應(yīng)多個(gè)slave,由于集群自帶故障轉(zhuǎn)移機(jī)制,因此Redis集群不用再使用哨兵sentinel功能。
Redis Cluster是Redis3.0引入的一種無(wú)中心化的集群,客戶端可以向任何一個(gè)節(jié)點(diǎn)通信,不同節(jié)點(diǎn)間的數(shù)據(jù)不互通,Redis Cluster將數(shù)據(jù)的key通過(guò)將CRC16算法的結(jié)果取模16383后,分給16384個(gè)slot槽,集群的每個(gè)節(jié)點(diǎn)負(fù)責(zé)一部分hash槽,節(jié)點(diǎn)只負(fù)責(zé)管理映射到這個(gè)槽的KV數(shù)據(jù),對(duì)于不是當(dāng)前槽的KV數(shù)據(jù),會(huì)向客戶端發(fā)送一個(gè)MOVED,表示需要客戶端重新重定向到其它節(jié)點(diǎn)。
使用Redis集群時(shí),我們將需要存儲(chǔ)的數(shù)據(jù)分配到多臺(tái)Redis服務(wù)器上,稱為分片。
數(shù)據(jù)如何分片?
對(duì)key進(jìn)行CRC16(key)算法處理并通過(guò)對(duì)總分片數(shù)量取模,然后使用確定性哈希函數(shù),將指定的key多次映射到同一個(gè)分片上。這種模式下,在進(jìn)行服務(wù)器擴(kuò)容的時(shí)候,不會(huì)影響集群的使用狀態(tài)。
Redis集群不保證強(qiáng)一致性,在特定條件下,Redis集群可能會(huì)丟掉一些命令。
三、slot槽位映射的方式
1、哈希取余分區(qū)
哈希取余分區(qū)的優(yōu)點(diǎn)是分配均勻,使用hash(key)/3的形式讓固定的一部分請(qǐng)求存入指定的master,每臺(tái)master處理一部分?jǐn)?shù)據(jù),起到了負(fù)載均衡的效果。
哈希取余分區(qū)最大的缺點(diǎn)就是不方便擴(kuò)容,當(dāng)需要擴(kuò)容時(shí),映射關(guān)系需要進(jìn)行重新計(jì)算。
2、一致性哈希算法
(1)一致性哈希算法是什么?
一致性哈希算法在1997年由麻省理工學(xué)院提出,是一種特殊的哈希算法,目的是解決分布式緩存的問(wèn)題。在移除或者添加一個(gè)服務(wù)器時(shí),能夠盡可能小地改變已存在的服務(wù)請(qǐng)求與處理請(qǐng)求服務(wù)器之間的映射關(guān)系。一致性哈希解決了簡(jiǎn)單哈希算法在分布式哈希表( Distributed Hash Table,DHT) 中存在的動(dòng)態(tài)伸縮等問(wèn)題。
一致性哈希算法將整個(gè)哈希值空間映射成一個(gè)虛擬的圓環(huán),整個(gè)哈??臻g的取值范圍為0~2^32^-1,整個(gè)空間按順時(shí)針?lè)较蚪M織,0~2^32^-1在零點(diǎn)中方向重合。
接下來(lái)使用如下算法對(duì)服務(wù)請(qǐng)求進(jìn)行映射,將服務(wù)請(qǐng)求使用哈希算法算出對(duì)應(yīng)的hash值,然后根據(jù)hash值的位置沿圓環(huán)順時(shí)針查找,第一臺(tái)遇到的服務(wù)器就是所對(duì)應(yīng)的處理請(qǐng)求服務(wù)器。
當(dāng)增加一臺(tái)新的服務(wù)器,受影響的數(shù)據(jù)僅僅是新添加的服務(wù)器到其環(huán)空間中前一臺(tái)的服務(wù)器(也就是順著逆時(shí)針?lè)较蛴龅降牡谝慌_(tái)服務(wù)器)之間的數(shù)據(jù),其他都不會(huì)受到影響。
綜上所述,一致性哈希算法對(duì)于節(jié)點(diǎn)的增減都只需重定位環(huán)空間中的一小部分?jǐn)?shù)據(jù),具有較好的容錯(cuò)性和可擴(kuò)展性。
(2)一致性哈希算法的優(yōu)點(diǎn)
- 可擴(kuò)展性。
- 更好的適應(yīng)數(shù)據(jù)的快速增長(zhǎng),當(dāng)某個(gè)分片存儲(chǔ)數(shù)據(jù)過(guò)多時(shí),可以將其一分為二,不需要對(duì)全部的數(shù)據(jù)進(jìn)行重新hash計(jì)算和劃分。
(3)一致性哈希算法的缺點(diǎn)
當(dāng)服務(wù)節(jié)點(diǎn)太少時(shí),容易造成數(shù)據(jù)傾斜,分配不均。
大量的緩存數(shù)據(jù)集中到了一臺(tái)或者幾臺(tái)服務(wù)節(jié)點(diǎn)上,稱為數(shù)據(jù)傾斜。
四、Redis更新策略
1、如果Redis中有數(shù)據(jù),需要和數(shù)據(jù)庫(kù)中的值相同。
2、如果Redis中無(wú)數(shù)據(jù),數(shù)據(jù)庫(kù)中的最新值要對(duì)Redis進(jìn)行同步更新。
五、Redis讀寫緩存
1、同步直寫策略
寫入數(shù)據(jù)庫(kù)也同步寫Redis緩存,緩存和數(shù)據(jù)庫(kù)中的數(shù)據(jù)一致;對(duì)于讀寫緩存來(lái)說(shuō),要保證緩存和數(shù)據(jù)庫(kù)中的數(shù)據(jù)一致,就要保證同步直寫策略。
2、異步緩寫策略
某些業(yè)務(wù)運(yùn)行中,MySQL數(shù)據(jù)更新之后,允許在一定時(shí)間后再進(jìn)行Redis數(shù)據(jù)同步,比如物流系統(tǒng)。
當(dāng)出現(xiàn)異常情況時(shí),不得不將失敗的動(dòng)作重新修補(bǔ),需要借助rabbitmq或kafka進(jìn)行重寫。
六、雙檢加鎖策略
多個(gè)線程同時(shí)去查詢數(shù)據(jù)庫(kù)的這條數(shù)據(jù),那么我們可以在第一個(gè)查詢數(shù)據(jù)的請(qǐng)求上使用一個(gè) 互斥鎖來(lái)鎖住它。
其他的線程走到這一步拿不到鎖就等著,等第一個(gè)線程查詢到了數(shù)據(jù),然后做緩存。
后面的線程進(jìn)來(lái)發(fā)現(xiàn)已經(jīng)有緩存了,就直接走緩存。
七、數(shù)據(jù)庫(kù)和緩存一致性的更新策略
1、先更新數(shù)據(jù)庫(kù),再更新Redis
按照常理出牌的話,應(yīng)該都是如此吧?那么,這種情況下,會(huì)有啥問(wèn)題呢?
如果更新數(shù)據(jù)庫(kù)成功后,更新Redis之前異常了,會(huì)出現(xiàn)什么情況呢?
數(shù)據(jù)庫(kù)與Redis內(nèi)緩存數(shù)據(jù)不一致。
2、先更新緩存,再更新數(shù)據(jù)庫(kù)
多線程情況下,會(huì)有問(wèn)題。
比如
- 線程1更新redis = 200;
- 線程2更新redis = 100;
- 線程2更新MySQL = 100;
- 線程1更新MySQL = 200;
結(jié)果呢,Redis=100、MySQL=200;我擦!
3、先刪除緩存,再更新數(shù)據(jù)庫(kù)
- 線程1刪除了Redis的緩存數(shù)據(jù),然后去更新MySQL數(shù)據(jù)庫(kù)。
- 還沒(méi)等MySQL更新完畢,線程2殺來(lái),讀取緩存數(shù)據(jù)。
- 但是,此時(shí)MySQL數(shù)據(jù)庫(kù)還沒(méi)更新,線程2讀取了MySQL中的舊值,然后線程2,還會(huì)將舊值寫入Redis作為數(shù)據(jù)緩存。
- 線程1更新完MySQL數(shù)據(jù)后,發(fā)現(xiàn)Redis中已經(jīng)有數(shù)據(jù)了,之前都刪過(guò)了,那我就不更新了。
完蛋了。。
延時(shí)雙刪
延時(shí)雙刪可以解決上面的問(wèn)題,只要sleep的時(shí)間大于線程2讀取數(shù)據(jù)再寫入緩存的時(shí)間就可以了,也就是線程1的二次清緩存操作要在線程2寫入緩存之后,這樣才能保證Redis緩存中的數(shù)據(jù)是最新的。
延遲雙刪最大的問(wèn)題就是sleep,在效率為王的今天,sleep能不用還是不用為好。
你不睡我都嫌你慢,你還睡上了...
4、先更新數(shù)據(jù)庫(kù),再刪除緩存
- 線程1先更新數(shù)據(jù)庫(kù),再刪除Redis緩存。
- 線程2在線程1刪除Redis緩存之前發(fā)起請(qǐng)求,得到了未刪除的Redis緩存。
- 線程1此時(shí)才刪除Redis緩存數(shù)據(jù)。
問(wèn)題還是有,這翻來(lái)覆去的,沒(méi)完沒(méi)了了。
這種情況如何解決呢?
引入消息中間件解決戰(zhàn)斗,再一次詳細(xì)的復(fù)盤一下。
- 更新數(shù)據(jù)庫(kù)。
- 數(shù)據(jù)庫(kù)將操作信息寫入binlog日志。
- 訂閱程序提取出key和數(shù)據(jù)。
- 嘗試刪除緩存操作,發(fā)現(xiàn)刪除失敗。
- 將這些數(shù)據(jù)信息發(fā)送到消息中間件中。
- 從消息中間件中獲取該數(shù)據(jù),重新操作。
5、總結(jié)
哪吒推薦使用第四種方式,先更新數(shù)據(jù)庫(kù),再刪除緩存。
方式①和方式②缺點(diǎn)太過(guò)明顯,不考慮;方式③中的sleep,總是讓人頭疼;方式④是一個(gè)比較全面的方案,但是增加了學(xué)習(xí)成本、維護(hù)成本,因?yàn)樵黾恿讼⒅虚g件。
八、MySQL主從復(fù)制工作原理
1、當(dāng) master 主服務(wù)器上的數(shù)據(jù)發(fā)生改變時(shí),則將其改變寫入二進(jìn)制事件日志文件中。
2、salve 從服務(wù)器會(huì)在一定時(shí)間間隔內(nèi)對(duì) master 主服務(wù)器上的二進(jìn)制日志進(jìn)行探測(cè),探測(cè)其是否發(fā)生過(guò)改變。
如果探測(cè)到 master 主服務(wù)器的二進(jìn)制事件日志發(fā)生了改變,則開始一個(gè) I/O Thread 請(qǐng)求 master 二進(jìn)制事件日志。
3、同時(shí) master 主服務(wù)器為每個(gè) I/O Thread 啟動(dòng)一個(gè)dump Thread,用于向其發(fā)送二進(jìn)制事件日志。
4、slave 從服務(wù)器將接收到的二進(jìn)制事件日志保存至自己本地的中繼日志文件中。
5、salve 從服務(wù)器將啟動(dòng) SQL Thread 從中繼日志中讀取二進(jìn)制日志,在本地重放,使得其數(shù)據(jù)和主服務(wù)器保持一致。
6、最后 I/O Thread 和 SQL Thread 將進(jìn)入睡眠狀態(tài),等待下一次被喚醒。
本文轉(zhuǎn)載自微信公眾號(hào)「哪吒編程」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系哪吒編程公眾號(hào)。