本系列將和大家分享Redis分散式緩存,本章主要簡單介紹下Redis中的布隆過濾器(Bloom Filter),以及如何破解ServiceStack和如何解決緩存雪崩、緩存穿透、緩存擊穿、緩存預熱問題。 ...
本系列將和大家分享Redis分散式緩存,本章主要簡單介紹下Redis中的布隆過濾器(Bloom Filter),以及如何破解ServiceStack和如何解決緩存雪崩、緩存穿透、緩存擊穿、緩存預熱問題。話不多說,下麵我們直接進入主題。
一、ServiceStack破解
首先我們先來看一下Demo的目錄結構,如下所示:
第一種方式:我們通過 NuGet 安裝 ServiceStack 相關的程式包。
然後在 MyRedis 控制台項目中運行如下測試代碼:
/// <summary> /// 模擬拋出LicenseException異常 /// </summary> public static void ThrowLicenseException() { //模擬1小時內超過6000次請求 Parallel.For(0, 10000, (i) => { using (RedisStringService service = new RedisStringService()) { service.Set("TianYa" + i, i); service.IncrementValue("TianYa" + i); Console.WriteLine(i); } }); }
using System; using MyRedis.Scene; namespace MyRedis { /// <summary> /// Redis:Remote Dictionary Server 遠程字典伺服器 /// 基於記憶體管理(數據存在記憶體),實現了5種數據結構(分別應對各種具體需求),單線程模型的應用程式(單進程單線程),對外提供插入--查詢--固化--集群功能。 /// 正是因為基於記憶體管理所以速度快,可以用來提升性能。但是不能當資料庫,不能作為數據的最終依據。 /// 單線程多進程的模式來提供集群服務。 /// 單線程最大的好處就是原子性操作,就是要麼都成功,要麼都失敗,不會出現中間狀態。Redis每個命令都是原子性(因為單線程),不用考慮併發,不會出現中間狀態。(線程安全) /// Redis就是為開發而生,會為各種開發需求提供對應的解決方案。 /// Redis只是為了提升性能,不做數據標準。任何的數據固化都是由資料庫完成的,Redis不能代替資料庫。 /// Redis實現的5種數據結構:String、Hashtable、Set、ZSet和List。 /// </summary> internal class Program { static void Main(string[] args) { //ServiceStackTest.ShowString(); //OverSellFailedTest.Show(); //OverSellTest.Show(); //ServiceStackTest.ShowHash(); //ServiceStackTest.ShowSet(); //FriendManager.Show(); //ServiceStackTest.ShowZSet(); //RankManager.Show(); //ServiceStackTest.ShowList(); //ServiceStackTest.ShowPublishAndSubscribe(); ServiceStackTest.ThrowLicenseException(); Console.ReadKey(); } } }
運行結果如下所示:
可以發現此時會拋出異常,提示每小時只能進行6000次Redis請求。
第二種方式:使用破解好的 ServiceStack 相關的動態鏈接庫。
首先我們先從 GitHub 上下載最新版本的 ServiceStack 源碼,如下所示:
下載完成後打開對應的解決方案,如下所示:
打開後找到 LicenseUtils.cs 類:
接著找到 LicenseUtils.cs 類中的 ApprovedUsage(...) 方法,修改代碼如下所示:
保存成功後重新生成下 ServiceStack.Text 和 ServiceStack.Redis 這兩個類庫:
重新生成完成後打開 ServiceStack.Redis 類庫的 bin/Debug 目錄,如下所示:
由於Demo中 TianYaSharpCore.ServiceStack 類庫的目標框架為 .NET Standard 2.1 ,故此處要使用 netstandard2.1 文件夾中的動態鏈接庫:
將破解好的這四個動態鏈接庫,複製到Demo的解決方案根目錄下新建的 Lib 文件夾中,如下所示:
然後將Demo中引用 NuGet 的 ServiceStack 相關程式包卸載掉,改成引用本地 Lib 文件夾中的那四個破解好的動態鏈接庫,如下所示:
此外,還需要從 NuGet 上添加 Microsoft.Bcl.Asynclnterfaces 程式包,如下所示:
最後我們重新生成下Demo的解決方案,再次運行 MyRedis 控制台項目,運行結果如下所示:
可以發現,此時不會再拋出異常了,說明我們破解成功了。
二、布隆過濾器
1、場景介紹
1)大數據去重,比如新聞推薦場景,新聞有很多,要不要在app/網站給用戶顯示,看過的顯然不需要顯示,大量的新聞數據面前如何快速去重?
2)註冊用戶,用戶名是否可用?
3)爬蟲程式中大數據量url去重。
4)垃圾郵件/垃圾簡訊去重。
5)用來防止緩存穿透。
2、引子
正式開始介紹布隆過濾器之前,我們先用常規的手段去解決數據去重的問題。
例子1:我們做爬蟲程式,會抓取很多的url地址,我們怎麼知道某個url地址是否爬取過呢?
使用set集合去重,如果數據量大了,有很多的url需要存儲,那麼set集合需要把所有的用過的url地址存下來。假設有1億個url地址,每個url地址有64個字元,那麼存儲這些數據就需要64*8*1億個比特位(這還不考慮本身數據結構需要占用的記憶體),這樣算下來我們就大概需要6個G的記憶體,記憶體占用幾乎是不可忍受的,更別說大型互聯網公司,數據量更大。也就是說用set集合來做大數據判重幾乎不可取。
例子2:使用資料庫判斷某個用戶名是否註冊過?
我們採用MySQL這種帶持久化的資料庫,雖然解決了記憶體問題,但是我們需要跟MySQL伺服器建立連接,MySQL伺服器需要從硬碟讀取判斷,這個時長也是不可忍受的...
總結來說:傳統方式在做大數據去重,要麼有時長的問題,要麼有記憶體占用的問題。那麼我們有沒有一種記憶體占用不那麼大,時長可以忍受的方式呢?布隆過濾器閃亮登場了!
3、布隆過濾器的介紹
布隆過濾器(Bloom Filter)是1970年由布隆提出的。它實際上是一個很長的二進位向量和一系列隨機映射函數。布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都比一般的演算法要好的多,缺點是有一定的誤判率和刪除困難。
4、過濾器原理
從上面的場景中,可以看出我們的需求很簡單隻是要判斷一下,某條數據是否存在,而不關心這條數據的具體內容,也就是說返回一個bool值即可。而我們電腦中二進位天然的滿足了這個特性,0代表false,1代表true。我們可以在記憶體中申請一段bit數組。(又稱bitmap翻譯為點陣圖)
我們先按照簡單的來,我們判斷的是int數值是否存在,比方說1、3、6、7、13已經存在,那我們可以把對應位置的bit位,置為1,表示存在。如圖:
這時候如果我們需要判斷3是否存在的話,判斷該位置的bit位是否為1即可。這樣的話比我們聲明一個int類型的數組,挨個存儲,然後判斷,省去了極大的空間,int值占用4個byte位也就是32個bit位,而採用這種bit數組來做,一下子省去了32倍的記憶體空間。
現在我們知道了,int的儲存和判斷方案,我們回歸實際場景,比如判斷一個url是否使用過。一個url是一個字元串,它不是一個int值,如何確定位置呢? 此時hash函數就派上用場了,我們可以將字元串hash出一個int值。
如圖所示:
當然上圖給出的是理想情況,而實際上我們很難找到一個足夠好hash函數,使得到的hash值在非常小的範圍內,通常一個好的hash函數算出的hash值足夠隨機且均勻,也就是說會隨機的散列到0-integer.max_value的範圍內,這是一個很大的值。
比方說www.baidu.com在java語言自帶的hash函數得出的值為270263191,如果我們只有1000萬個字元串需要判斷,但是隨便一個字元串的hash值有可能會大於1000萬這個數值,所以聲明1000萬大小的bit位是不夠用的,我們需要給出int最大值的bit位,而這會浪費大量的空間,與布隆過濾器的目標相悖。
這時候我們的取餘法,就可以派上用場了,我們把得出的hash值進行對有限bit位空間(bit數組的長度)進行取餘,得出位置。如圖:
上圖我們可以看出,使用取餘法,我們可以做到在有限的空間上存儲數據,達到了壓縮空間的目的,但是大家可以發現這裡會有一個衝突,有可能取餘後我們的不同數據落到同一個位置,甚至我們的hash函數都有可能發生衝突,也就是不同的字元串算出的hash值是一樣的。而且hash衝突幾乎是不可避免的,這也就導致我們的點陣圖上表示存在是有可能誤判的。也就是布隆過濾器告訴你那個位置有數據了但有可能是別人放進去的,你並沒有放進去。這是不可避免的,但是我們可以通過增加hash函數個數的方法來降低衝突率。如圖:
本例中採用3個不同的hash函數將同一個數據分別映射到點陣圖的三個位置上,在判存的時候,只有當映射的這3個位置的bit值都為1時才表示該數據已存在,否則表示該數據不存在。這樣可以在一定程度上降低誤判率。
5、總結
布隆過濾器採用了點陣圖數據結構,大大減少了記憶體占用,採用hash函數將數據映射到點陣圖上,但是Hash函數本身就有衝突,取模節省空間也會導致衝突率的上升。解決的辦法主要就是增加hash函數的個數和點陣圖大小的上限(即加大bit數組的長度)。
至於要使用多少個hash函數合適以及點陣圖的bit位多少合適沒有非常嚴謹的數學證明,我們這裡就不展開了。
市面上有一些現成的工具包已經給我們封裝好布隆過濾器:比如google的guava包就實現了布隆過濾器。又比如redis4.0後通過插件方式提供了布隆過濾器的實現等等。。。
謹記:布隆過濾器有可能會存在誤判的情況,也就是說,不存在的一定不存在,而存在的不一定會存在。
三、緩存雪崩
概念:緩存雪崩是指當緩存中有大量的key在同一時刻過期,或者Redis直接宕機了,導致大量的查詢請求全部到達資料庫,造成資料庫查詢壓力驟增,甚至直接掛掉。
解決方案:
針對第一種大量key同時過期的情況,解決起來比較簡單,只需要將每個key的過期時間打散即可,使它們的失效點儘可能均勻分佈。例如:可以給過期時間加個過期因數(其實就是個隨機數,比如:在1s~1000s 之間隨機取一個),然後我們的實際過期時間=設置的過期時間+過期因數。
針對第二種redis發生故障的情況,部署redis時可以使用redis的幾種高可用方案部署(主從複製模式、哨兵模式、集群模式)。
除了上面兩種解決方式,還可以使用其他策略,比如設置key永不過期、加分散式鎖等。
四、緩存穿透
概念:緩存穿透是指查詢一個緩存中和資料庫中都不存在的數據,導致每次查詢這條數據都會透過緩存,直接查庫,最後返回空。當用戶使用這種不存在的數據瘋狂發起查詢請求的時候,對資料庫造成的壓力就非常大,甚至可能直接掛掉。
可能原因:惡意破壞。
解決方案:
解決緩存穿透的方法一般有兩種,第一種是緩存空對象,第二種是使用布隆過濾器。
第一種方法比較好理解,就是當資料庫中查不到數據的時候,我緩存一個空對象,然後給這個空對象的緩存設置一個過期時間,這樣下次再查詢該數據的時候,就可以直接從緩存中拿到,從而達到了減小資料庫壓力的目的。但這種解決方式有兩個缺點:(1)需要緩存層提供更多的記憶體空間來緩存這些空對象,當這種空對象很多的時候,就會浪費更多的記憶體;(2)會導致緩存層和存儲層的數據不一致,即使在緩存空對象時給它設置了一個很短的過期時間,那也會導致這一段時間內的數據不一致問題。
第二種方案是使用布隆過濾器,這是比較推薦的方法。布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都比一般的演算法要好的多,缺點是有一定的誤判率和刪除困難。在初始化布隆過濾器時,需要將所有的key保存到布隆過濾器中,布隆過濾器就相當於一個位於客戶端與緩存層中間的攔截器一樣,負責判斷key是否在集合中存在。如下圖:
五、緩存擊穿
概念:緩存擊穿是指當緩存中某個熱點數據過期了,在該熱點數據重新載入緩存之前,有大量的查詢請求穿過緩存,直接查詢資料庫。這種情況會導致資料庫壓力瞬間驟增,造成大量請求阻塞,甚至直接掛掉。
解決方案:
解決緩存擊穿的方法也有兩種,第一種是設置key永不過期;第二種是使用分散式鎖,保證同一時刻只能有一個查詢請求重新載入熱點數據到緩存中,這樣,其他的線程只需等待該線程運行完畢,即可重新從Redis中獲取數據。
第一種方式比較簡單,在設置熱點key的時候,不給key設置過期時間即可。不過還有另外一種方式也可以達到讓key不過期的目的,就是給key設置過期時間的時候,也要同時的在後臺啟一個定時任務去定時地更新這個緩存。
第二種方法使用加鎖的方式,鎖的對象就是key,這樣,當大量查詢同一個key的請求併發進來時,只能有一個請求獲取到鎖,獲取到鎖的線程查詢資料庫,然後將結果放入到緩存中,接著釋放鎖,此時,其他處於鎖等待的請求即可繼續執行,由於此時緩存中已經有了數據,所以可以直接從緩存中獲取到數據返回,並不會查詢資料庫。
六、緩存預熱
概念: 當系統上線時,緩存內還沒有數據,如果直接提供給用戶使用,每個請求都會穿過緩存去訪問底層資料庫,如果併發量大的話,很有可能在上線當天就會宕機,這種情況就叫“系統冷啟動”。而緩存預熱就是指系統上線後,提前將相關的緩存數據直接載入到緩存系統中。避免在用戶請求的時候,先查詢資料庫,然後再將數據緩存的問題,用戶直接查詢事先被預熱的緩存數據。通常來講,系統中一些經常使用的數據,一些高併發熱點數據,可以先預熱到緩存中;對於一些請求比較耗時的數據,也可以事先預熱到緩存中。
解決方案:
1、直接寫個緩存刷新頁面,上線時手工操作下;
2、數據量不大,可以在項目啟動的時候自動進行載入;
3、定時刷新緩存,緩存可能會過期,寫一個程式,時不時把數據預熱一下,往Redis裡面寫數據。
部分內容參考文獻:https://baijiahao.baidu.com/s?id=1730541502423010481&wfr=spider&for=pc
Demo源碼:
鏈接:https://pan.baidu.com/s/1RreI5HzAg-tW4WvWstm8ig 提取碼:cm75
此文由博主精心撰寫轉載請保留此原文鏈接:https://www.cnblogs.com/xyh9039/p/17209010.html
版權聲明:如有雷同純屬巧合,如有侵權請及時聯繫本人修改,謝謝!!!