一文解析:Redis快取穿透、快取雪崩、Redis併發問題

歡迎關注專欄:Java架構技術進階。裡面有大量batj面試題集錦,還有各種技術分享,如有好文章也歡迎投稿哦。
把redis作為快取使用已經是司空見慣,但是使用redis後也可能會碰到一系列的問題,尤其是資料量很大的時候,經典的幾個問題如下:
(一)快取和資料庫間資料一致性問題
分散式環境下(單機就不用說了)非常容易出現快取和資料庫間的資料一致性問題,針對這一點的話,只能說,如果你的專案對快取的要求是強一致性的,那麼請不要使用快取。我們只能採取合適的策略來降低快取和資料庫間資料不一致的概率,而無法保證兩者間的強一致性。合適的策略包括 合適的快取更新策略,更新資料庫後要及時更新快取、快取失敗時增加重試機制,例如MQ模式的訊息佇列。
(二)快取擊穿問題
快取擊穿表示惡意使用者模擬請求很多快取中不存在的資料,由於快取中都沒有,導致這些請求短時間內直接落在了資料庫上,導致資料庫異常。這個我們在實際專案就遇到了,有些搶購活動、秒殺活動的介面API被大量的惡意使用者刷,導致短時間內資料庫宕機了,好在資料庫是多主多從的,hold住了。
解決方案的話:
1、使用互斥鎖排隊
業界比價普遍的一種做法,即根據key獲取value值為空時,鎖上,從資料庫中load資料後再釋放鎖。若其它執行緒獲取鎖失敗,則等待一段時間後重試。這裡要注意,分散式環境中要使用分散式鎖,單機的話用普通的鎖(synchronized、Lock)就夠了。
public String getWithLock( String key, Jedis jedis, String lockKey, String uniqueId, long expireTime ) { /* 通過key獲取value */ String value = redisService.get( key ); if ( StringUtil.isEmpty( value ) ) { /* * 分散式鎖,詳細可以參考https://blog.csdn.net/fanrenxiang/article/details/79803037 * 封裝的tryDistributedLock包括setnx和expire兩個功能,在低版本的redis中不支援 */ try { boolean locked = redisService.tryDistributedLock( jedis, lockKey, uniqueId, expireTime ); if ( locked ) { value = userService.getById( key ); redisService.set( key, value ); redisService.del( lockKey ); return(value); } else { /* 其它執行緒進來了沒獲取到鎖便等待50ms後重試 */ Thread.sleep( 50 ); getWithLock( key, jedis, lockKey, uniqueId, expireTime ); } } catch ( Exception e ) { log.error( "getWithLock exception=" + e ); return(value); } finally { redisService.releaseDistributedLock( jedis, lockKey, uniqueId ); } } return(value); }
這樣做思路比較清晰,也從一定程度上減輕資料庫壓力,但是鎖機制使得邏輯的複雜度增加,吞吐量也降低了,有點治標不治本。
2、布隆過濾器(推薦)
bloomfilter就類似於一個hash set,用於快速判某個元素是否存在於集合中,其典型的應用場景就是快速判斷一個key是否存在於某容器,不存在就直接返回。布隆過濾器的關鍵就在於hash演算法和容器大小,下面先來簡單的實現下看看效果,我這裡用guava實現的布隆過濾器:
<dependencies > < dependency > < groupId > com.google.guava</ groupId> < artifactId > guava</ artifactId> < version > 23.0 < / version > < / dependency > < / dependencies > public class BloomFilterTest { private static final int capacity= 1000000; private static final int key= 999998; private static BloomFilter<Integer> bloomFilter = BloomFilter.create( Funnels.integerFunnel(), capacity ); static { for ( int i = 0; i < capacity; i++ ) { bloomFilter.put( i ); } } public static void main( String[] args ) { /*返回計算機最精確的時間,單位微妙*/ long start = System.nanoTime(); if ( bloomFilter.mightContain( key ) ) { System.out.println( "成功過濾到" + key ); } long end = System.nanoTime(); System.out.println( "布隆過濾器消耗時間:" + (end - start) ); int sum = 0; for ( int i = capacity + 20000; i < capacity + 30000; i++ ) { if ( bloomFilter.mightContain( i ) ) { sum = sum + 1; } } System.out.println( "錯判率為:" + sum ); } } 成 功過濾到999998 布 隆過濾器消 耗 時間 : 215518 錯 判率 為 : 318
可以看到,100w個數據中只消耗了約0.2毫秒就匹配到了key,速度足夠快。然後模擬了1w個不存在於布隆過濾器中的key,匹配錯誤率為318/10000,也就是說,出錯率大概為3%,跟蹤下BloomFilter的原始碼發現預設的容錯率就是0.03:
public static < T > BloomFilter<T> create( Funnel<T> funnel, int expectedInsertions /* n */ ) { return(create( funnel, expectedInsertions, 0.03 ) ); /* FYI, for 3%, we always get 5 hash functions */ }
我們可呼叫BloomFilter的這個方法顯式的指定誤判率:

private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity,0.01);
我們斷點跟蹤下,誤判率為0.02和預設的0.03時候的區別:


對比兩個出錯率可以發現,誤判率為0.02時陣列大小為8142363,0.03時為7298440,誤判率降低了0.01,BloomFilter維護的陣列大小也減少了843923,可見BloomFilter預設的誤判率0.03是設計者權衡系統性能後得出的值。要注意的是,布隆過濾器不支援刪除操作。用在這邊解決快取穿透問題就是:
public String getByKey( String key ) { /* 通過key獲取value */ String value = redisService.get( key ); if ( StringUtil.isEmpty( value ) ) { if ( bloomFilter.mightContain( key ) ) { value = userService.getById( key ); redisService.set( key, value ); return(value); } else { return(null); } } return(value); }
(三)快取雪崩問題
快取在同一時間內大量鍵過期(失效),接著來的一大波請求瞬間都落在了資料庫中導致連線異常。
解決方案:
1、也是像解決快取穿透一樣加鎖排隊,實現同上;
2、建立備份快取,快取A和快取B,A設定超時時間,B不設值超時時間,先從A讀快取,A沒有讀B,並且更新A快取和B快取;
public String getByKey( String keyA, String keyB ) { String value = redisService.get( keyA ); if ( StringUtil.isEmpty( value ) ) { value = redisService.get( keyB ); String newValue = getFromDbById(); redisService.set( keyA, newValue, 31, TimeUnit.DAYS ); redisService.set( keyB, newValue ); } return(value); }
(四)快取併發問題
這裡的併發指的是多個redis的client同時set key引起的併發問題。比較有效的解決方案就是把redis.set操作放在佇列中使其序列化,必須的一個一個執行,具體的程式碼就不上了,當然加鎖也是可以的,至於為什麼不用redis中的事務,留給各位看官自己思考探究。
歡迎關注專欄:Java架構技術進階。裡面有大量batj面試題集錦,還有各種技術分享,如有好文章也歡迎投稿哦。
歡迎加入程式設計師交流學習群:908676731,群裡有分享的視訊,面試指導,架構資料,還有思維導圖、群裡有視訊,都是乾貨的,你可以下載來看。主要分享分散式架構、高可擴充套件、高效能、高併發、效能優化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分散式專案實戰學習架構師視訊。合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!