對于讀多寫少的高并發(fā)場景,我們會經常使用緩存來進行優(yōu)化。比如說支付寶的余額展示功能,實際上99%的時候都是查詢,1%的請求是變更,所以,我們在這樣的場景下,可以加入緩存,用戶->余額
1.Redis緩存與數據一致性問題
那么基于上面的這個出發(fā)點,問題就來了,當用戶的余額發(fā)生變化的時候,如何更新緩存中的數據,也就是說。
- 我是先更新緩存中的數據再更新數據庫的數據;
- 還是修改數據庫中的數據再更新緩存中的數據;
數據庫的數據和緩存中的數據如何達到一致性?首先,可以肯定的是,redis中的數據和數據庫中的數據不可能保證事務性達到統(tǒng)一的,這個是毫無疑問的,所以在實際應用中,我們都是基于當前的場景進行權衡降低出現(xiàn)不一致問題的出現(xiàn)概率
更新緩存還是讓緩存失效
更新緩存表示數據不但會寫入到數據庫,還會同步更新緩存; 而讓緩存失效是表示只更新數據庫中的數據,然后刪除緩存中對應的key。那么這兩種方式怎么去選擇?這塊有一個衡量的指標。
- 如果更新緩存的代價很小,那么可以先更新緩存,這個代價很小的意思是我不需要很復雜的計算去獲得最新的余額數字。
- 如果是更新緩存的代價很大,意味著需要通過多個接口調用和數據查詢才能獲得最新的結果,那么可以先淘汰緩存。淘汰緩存以后后續(xù)的請求如果在緩存中找不到,自然去數據庫中檢索
先操作數據庫還是先操作緩存
當客戶端發(fā)起事務類型請求時,假設我們以讓緩存失效作為緩存的的處理方式,那么又會存在兩個情況,
- 先更新數據庫再讓緩存失效
- 先讓緩存失效,再更新數據庫
前面我們講過,更新數據庫和更新緩存這兩個操作,是無法保證原子性的,所以我們需要根據當前業(yè)務的場景的容忍性來選擇。也就是如果出現(xiàn)不一致的情況下,哪一種更新方式對業(yè)務的影響最小,就先執(zhí)行影響最小的方案
- 最終一致性的解決方案
下圖展示了,比如針對緩存失效為例子,第一個嘗試去更新數據庫,第二步嘗試讓緩存失效,如果第二步對個某個key的緩存失效,可以發(fā)送失敗消息到mq中,然后自己消費mq的消息不斷的重試讓緩存失效,mq保證消息持久性和緩存和數據庫的數據最終一致性。

2.關于緩存雪崩的解決方案
當緩存大規(guī)模滲透在整個架構中以后,那么緩存本身的可用性講決定整個架構的穩(wěn)定性。那么接下來我們來討論下緩存在應用過程中可能會導致的問題。
緩存雪崩
緩存雪崩是指設置緩存時采用了相同的過期時間,導致緩存在某一個時刻同時失效,或者緩存服務器宕機宕機導致緩存全面失效,請求全部轉發(fā)到了DB層面,DB由于瞬間壓力增大而導致崩潰。緩存失效導致的雪崩效應對底層系統(tǒng)的沖擊是很大的。
解決方式
- 對緩存的訪問,如果發(fā)現(xiàn)從緩存中取不到值,那么通過加鎖或者隊列的方式保證緩存的單進程操作,從而避免失效時并發(fā)請求全部落到底層的存儲系統(tǒng)上;但是這種方式會帶來性能上的損耗
- 將緩存失效的時間分散,降低每一個緩存過期時間的重復率
- 如果是因為緩存服務器故障導致的問題,一方面需要保證緩存服務器的高可用、另一方面,應用程序中可以采用多級緩存
緩存穿透
緩存穿透是指查詢一個根本不存在的數據,緩存和數據源都不會命中。出于容錯的考慮,如果從數據層查不到數據則不寫入緩存,即數據源返回值為 null 時,不緩存 null。緩存穿透問題可能會使后端數據源負載加大,由于很多后端數據源不具備高并發(fā)性,甚至可能造成后端數據源宕掉
解決方式
- 如果查詢數據庫也為空,直接設置一個默認值存放到緩存,這樣第二次到緩沖中獲取就有值了,而不會繼續(xù)訪問數據庫,這種辦法最簡單粗暴。比如,”key” , “&&”。
在返回這個&&值的時候,我們的應用就可以認為這是不存在的key,那我們的應用就可以決定是否繼續(xù)等待繼續(xù)訪問,還是放棄掉這次操作。如果繼續(xù)等待訪問,過一個時間輪詢點后,再次請求這個key,如果取到的值不再是&&,則可以認為這時候key有值了,從而避免了透傳到數據庫,從而把大量的類似請求擋在了緩存之中。
- 根據緩存數據Key的設計規(guī)則,將不符合規(guī)則的key進行過濾
采用布隆過濾器,將所有可能存在的數據哈希到一個足夠大的BitSet中,不存在的數據將會被攔截掉,從而避免了對底層存儲系統(tǒng)的查詢壓力
3.布隆過濾器
布隆過濾器是Burton Howard Bloom在1970年提出來的,一種空間效率極高的概率型算法和數據結構,主要用來判斷一個元素是否在集合中存在。因為他是一個概率型的算法,所以會存在一定的誤差,如果傳入一個值去布隆過濾器中檢索,可能會出現(xiàn)檢測存在的結果但是實際上可能是不存在的,但是肯定不會出現(xiàn)實際上不存在然后反饋存在的結果。因此,Bloom Filter不適合那些“零錯誤”的應用場合。而在能容忍低錯誤率的應用場合下,Bloom Filter通過極少的錯誤換取了存儲空間的極大節(jié)省。
- bitmap
所謂的Bit-map就是用一個bit位來標記某個元素對應的Value,通過Bit為單位來存儲數據,可以大大節(jié)省存儲空間.所以我們可以通過一個int型的整數的32比特位來存儲32個10進制的數字,那么這樣所帶來的好處是內存占用少、效率很高(不需要比較和位移)比如我們要存儲5(101)、3(11)四個數字,那么我們申請int型的內存空間,會有32個比特位。這四個數字的二進制分別對應
從右往左開始數,比如第一個數字是5,對應的二進制數據是101, 那么從右往左數到第5位,把對應的二進制數據存儲到32個比特位上。
第一個5就是 00000000000000000000000000101000
輸入3時候 00000000000000000000000000001100
- 布隆過濾器原理
有了對位圖的理解以后,我們對布隆過濾器的原理理解就會更容易了,仍然以前面提到的40億數據為案例,假設這40億數據為某郵件服務器的黑名單數據,郵件服務需要根據郵箱地址來判斷當前郵箱是否屬于垃圾郵件。原理如下
假設集合里面有3個元素{x, y, z},哈希函數的個數為3。首先將位數組進行初始化,將里面每個位都設置位0。對于集合里面的每一個元素,將元素依次通過3個哈希函數進行映射,每次映射都會產生一個哈希值,這個值對應位數組上面的一個點,然后將位數組對應的位置標記為1。查詢W元素是否存在集合中的時候,同樣的方法將W通過哈希映射到位數組上的3個點。如果3個點的其中有一個點不為1,則可以判斷該元素一定不存在集合中。反之,如果3個點都為1,則該元素可能存在集合中

接下來按照該方法處理所有的輸入對象,每個對象都可能把bitMap中一些白位置涂黑,也可能會遇到已經涂黑的位置,遇到已經為黑的讓他繼續(xù)為黑即可。處理完所有的輸入對象之后,在bitMap中可能已經有相當多的位置已經被涂黑。至此,一個布隆過濾器生成完成,這個布隆過濾器代表之前所有輸入對象組成的集合。
如何去判斷一個元素是否存在bit array中呢? 原理是一樣,根據k個哈希函數去得到的結果,如果所有的結果都是1,表示這個元素可能(假設某個元素通過映射對應下標為4,5,6這3個點。雖然這3個點都為1,但是很明顯這3個點是不同元素經過哈希得到的位置,因此這種情況說明元素雖然不在集合中,也可能對應的都是1)存在。 如果一旦發(fā)現(xiàn)其中一個比特位的元素是0,表示這個元素一定不存在
至于k個哈希函數的取值為多少,能夠最大化的降低錯誤率(因為哈希函數越多,映射沖突會越少),這個地方就會涉及到最優(yōu)的哈希函數個數的一個算法邏輯

下面展示布隆過濾器的使用
首先添加maven依賴
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>
代碼如下:
/**
* @Project: redis
* @description: 布隆過濾器
* @author: sunkang
* @create: 2019-01-12 19:22
* @ModificationHistory who when What
**/
public class BloomFilterDemo {
public static void main(String[] args) {
//插入的key為字符串,預計數據量為一百萬,并且容錯率為0.01
BloomFilter bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),
1000000,0.01);
bloomFilter.put("sunkang");
System.out.println(bloomFilter.mightContain("sunkang"));
}
}