首頁 > 軟體

淺談Redis高並行快取架構效能優化實戰

2022-05-13 21:50:41

場景1: 中小型公司Redis快取架構以及線上問題實戰

執行緒A在master獲取鎖之後,master在同步資料到slave時,master突然宕機(此時資料還沒有同步到slave),然後slave會自動選舉成為新的master,此時執行緒B獲取鎖,結果成功了,這樣會造成多個執行緒獲取同一把鎖

解決方案

  • 網上說RedLock能解決分散式鎖失效的問題。對於RedLock實現原理是: 超過半數Redis節點加鎖成功之後才能算成功,否則返回false,和Zookeeper的"ZAB"原理很類似,而且與Redis Cluster叢集中解決腦裂問題的方案類似,但是RedLock方案有很大的弊端,也就是會造成Redis可用性的延遲,眾所周知,Redis的AP(可用性+分割區容忍性)機制,假如把Redis變成CP(一致性+分割區容忍性),這樣肯定會犧牲一定的可用性,與Redis初衷不符合,也就是說還不如使用Zookeeper。
  • Zookeeper具備CP機制以及實現了ZAB,能夠確保某一個節點宕機,也能保證資料一致性,而且效率會比Redis高很多,更適合做分散式鎖

場景2: 大廠線上大規模商品快取資料冷熱分離實戰

問題: 在高並行場景下,一定要把所有的快取資料一直儲存在快取不讓其失效嗎?

雖然一直快取所有資料沒什麼大問題,但是考慮到如果資料太多,就會一直佔用快取空間(記憶體資源非常寶貴),並且資料的維護性也是需要耗時的.

解決方案

  • 對快取資料做冷熱分離在查詢資料時,我們只需要在查詢程式碼中再次更新過期時間,這樣就能保證熱點資料一直在快取中,而不經常存取的資料過期了就自動從快取中刪除。

流程分析

  • 假如一個熱點資料每天存取特別高,不停的查詢該資料,每次查詢時再次更新過期時間,那麼在這個過期時間之內只要有人存取就會一直存在快取中,這樣就保證熱點商品資料不會因為過期時間而從快取中移除;
  • 而對於不經常存取的冷門資料到了過期時間就可以自動釋放了,同時也釋放除了一部分快取空間,而且當再次存取冷門資料的時候,從資料庫拿到的永遠是最新的資料,也減少了維護成本。

場景3: 基於DCL機制解決熱點快取並行重建問題實戰

DCL(雙重檢測鎖)

問題: 冷門資料突然變成了熱門資料,大量的請求突發性的對熱點資料進行快取重建導致系統壓力暴增

解決方案

  • 最容易想到的就是加鎖
  • DCL機制。先查一次,快取有資料就直接返回,沒有資料,就加鎖,在鎖的程式碼塊中再次先查詢快取。這樣鎖的目的就是為了當第一次快取從資料庫查詢更新到快取中,程式碼塊執行完,其他執行緒再次進來,此時快取中就已經存在資料了,這樣就減少了查詢資料庫的次數
public Product get(Long productId) {
    Product product = null;
    String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;
    //DCL機制:第一次先從快取裡查資料
    product = getProductFromCache(productCacheKey);
    if (product != null) {
        return product;
    }
  
    //加分散式鎖解決熱點快取並行重建問題
    RLock hotCreateCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX + productId);
    hotCreateCacheLock.lock();
    // 這個優化謹慎使用,防止超時導致的大規模並行重建問題
    // hotCreateCacheLock.tryLock(1, TimeUnit.SECONDS);
    try {
        //DCL機制:在分散式鎖裡面第二次查詢
        product = getProductFromCache(productCacheKey);
        if (product != null) {
            return product;
        }

        //RLock productUpdateLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
        RReadWriteLock productUpdateLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
        RLock rLock = productUpdateLock.readLock();
        //加分散式讀鎖解決快取雙寫不一致問題
        rLock.lock();
        try {
            product = productDao.get(productId);
            if (product != null) {
                redisUtil.set(productCacheKey, JSON.toJSONString(product),
                        genProductCacheTimeout(), TimeUnit.SECONDS);
            } else {
                //設定空快取解決快取穿透問題
                redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
            }
        } finally {
            rLock.unlock();
        }
    } finally {
        hotCreateCacheLock.unlock();
    }

    return product;
}

場景4: 突發性熱點快取重建導致系統壓力暴增

問題: 假如當前有10w個執行緒沒有拿到鎖正在排隊,這種情況只能等到獲取鎖的執行緒執行完程式碼釋放鎖後,那排隊的10w個執行緒才能再次競爭鎖。這裡需要關注的問題點就是又要再次競爭鎖,意味著執行緒競爭鎖的次數可能最少>1,頻繁的競爭鎖對Redis效能也是有消耗的,有沒有更好的辦法讓每個執行緒競爭鎖的次數儘可能減少呢?

解決方案

  • 可以通過tryLock(time,TimeUnit)先讓所有執行緒嘗試獲取鎖

  • 假如獲取鎖的執行緒執行資料庫查詢然後將資料更新到快取所需要的時間為1s,那麼當其他執行緒獲取鎖時間結束後,會解除阻塞狀態直接往下執行,然後再次查詢快取的時候發現快取有資料了就直接返回。

  • 這樣設計的好處就是把分散式鎖在某些特定的場景使其"序列變並行",不過這個優化需要謹慎使用,防止超時導致的大規模並行重建問題。畢竟沒有任何方案是完全解決問題的,主要是根據公司業務而定.

場景5: 解決大規模快取擊穿導致線上資料庫壓力暴增

快取擊穿/快取失效: 可能同一時間熱點資料全部過期而造成快取查不到資料,請求就會從資料庫查詢,高並行情況下會導致資料庫壓力

解決方案

  • 對於這個場景,可以給資料設定過期時間時,不要將所有快取資料的過期時間設定為相同的過期時間,最好可以給每個資料的過期時間設定一個亂數,保證資料在不同的時間段過期。

程式碼案例

private Integer genProductCacheTimeout() {
  //加隨機超時機制解決快取批次失效(擊穿)問題
  return PRODUCT_CACHE_TIMEOUT + new Random().nextInt(5) * 60 * 60;
}

場景6: 駭客工資導致快取穿透線上資料庫宕機

快取穿透: 如果駭客通過指令碼檔案不停的傳一些不存在的引數刷網站的介面,而這種垃圾引數在快取和資料庫又不存在,這樣就會一直地查資料庫,最終可能導致資料庫並行量過大而卡死宕機。

解決方案

  • 閘道器限流。Nginx、Sentinel、Hystrix都可以實現
  • 程式碼層面。可以使用多級快取,比如一級快取採用布隆過濾器,二級快取可以使用guava中的Cache,三級快取使用Redis,為什麼一級快取使用布隆過濾器呢,其結構和bitmap類似,用於儲存資料狀態,能存大量的key

布隆過濾器

  • 布隆過濾器就是一個大型的位陣列和幾個不一樣的無偏Hash函數.當布隆過濾器說某個值存在時,這個值可能不存在,當說不存在時,那就肯定不存在。

場景7: 大V直播帶貨導致線上商品系統崩潰原因分析

問題: 這種場景可能是在某個時刻把冷門商品一下子變成了熱門商品。因為冷門的資料可能在快取時間過期就刪除,而此時剛好有大量請求,比如直播期間推播一個商品連線,假如同時有幾十萬人搶購,而快取沒有的話,意味著所有的請求全部達到了資料庫中查詢,而對於資料庫單節點支撐並行量也就不到1w,此時這麼大的請求量,肯定會把資料庫整宕機(這種場景比較少,但是小概率還是會有)

解決方案

  • 可以通過tryLock(time,TimeUnit)先讓所有執行緒嘗試獲取鎖

  • 假如獲取鎖的執行緒執行資料庫查詢然後將資料更新到快取所需要的時間為1s,那麼當其他執行緒獲取鎖時間結束後,會解除阻塞狀態直接往下執行,然後再次查詢快取的時候發現快取有資料了就直接返回。

  • 這樣設計的好處就是把分散式鎖在某些特定的場景使其"序列變並行",不過這個優化需要謹慎使用,防止超時導致的大規模並行重建問題。畢竟沒有任何方案是完全解決問題的,主要是根據公司業務而定.

場景8: Redis分散式鎖解決快取與資料庫雙寫不一致問題實戰

解決方案

  • 重入鎖保證並行安全。通常說在分散式鎖中再加一把鎖,鎖太重,效能不是很好,還有優化空間
  • 分散式讀寫鎖(ReadWriteLock),實現機制和ReentranReadWriteLock一直,適合讀多寫少的場景,注意讀寫鎖的key得一致
  • 使用canal通過監聽binlog紀錄檔及時去修改快取,但是引入中介軟體,增加系統的維護度

Lua指令碼設定讀寫鎖

local mode = redis.call('hget', KEYS[1], 'mode');
if (mode == false) 
then redis.call('hset', KEYS[1], 'mode', 'read'); 
redis.call('hset', KEYS[1], ARGV[2], 1); 
redis.call('set', KEYS[2] .. ':1', 1); 
redis.call('pexpire', KEYS[2] .. ':1', ARGV[1]);
redis.call('pexpire', KEYS[1], ARGV[1]); 
return nil; 
end; 
if (mode == 'read') or (mode == 'write' and redis.call('hexists', KEYS[1], ARGV[3]) == 1) 
then local ind = redis.call('hincrby', KEYS[1], ARGV[2], 1); 
local key = KEYS[2] .. ':' .. ind;
redis.call('set', key, 1); 
redis.call('pexpire', key, ARGV[1]); redis.call('pexpire', KEYS[1], ARGV[1]); 
return nil; 
end;
return redis.call('pttl', KEYS[1]);

ReadWriteLock程式碼案例

@Transactional
public Product update(Product product) {
  Product productResult = null;
  //RLock productUpdateLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
  RReadWriteLock productUpdateLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + product.getId());
  // 新增寫鎖
  RLock writeLock = productUpdateLock.writeLock();
  //加分散式寫鎖解決快取雙寫不一致問題
  writeLock.lock();
  try {
      productResult = productDao.update(product);
      redisUtil.set(RedisKeyPrefixConst.PRODUCT_CACHE + productResult.getId(), JSON.toJSONString(productResult),
      genProductCacheTimeout(), TimeUnit.SECONDS);
   } finally {
          writeLock.unlock();
   }
  return productResult;
}

public Product get(Long productId) {
    Product product = null;
    String productCacheKey = RedisKeyPrefixConst.PRODUCT_CACHE + productId;

    //從快取裡查資料
    product = getProductFromCache(productCacheKey);
    if (product != null) {
        return product;
    }

    //加分散式鎖解決熱點快取並行重建問題
    RLock hotCreateCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX + productId);
    hotCreateCacheLock.lock();
    // 這個優化謹慎使用,防止超時導致的大規模並行重建問題
    // hotCreateCacheLock.tryLock(1, TimeUnit.SECONDS);
    try {
        product = getProductFromCache(productCacheKey);
        if (product != null) {
            return product;
        }

        //RLock productUpdateLock = redisson.getLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
        RReadWriteLock productUpdateLock = redisson.getReadWriteLock(LOCK_PRODUCT_UPDATE_PREFIX + productId);
        // 新增讀鎖
        RLock rLock = productUpdateLock.readLock();
        //加分散式讀鎖解決快取雙寫不一致問題
        rLock.lock();
        try {
            product = productDao.get(productId);
            if (product != null) {
                redisUtil.set(productCacheKey, JSON.toJSONString(product),
                        genProductCacheTimeout(), TimeUnit.SECONDS);
            } else {
                //設定空快取解決快取穿透問題
                redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
            }
        } finally {
            rLock.unlock();
        }
    } finally {
        hotCreateCacheLock.unlock();
    }

    return product;
}

場景9: 大促壓力暴增導致分散式鎖序列爭用問題優化

解決方案

  • 可以採用分段鎖,和JDK7的ConcurrentHashMap的實現原理很類似,將一個鎖,分成多個鎖,比如lock,分成lock_1、lock_2...
  • 然後將庫存平均分攤到每把鎖,這樣做的目的是分攤分散式鎖的壓力,本來只有一個鎖,意味著所有的執行緒進來只能一個執行緒獲取到鎖,如果分攤為10把鎖,那麼同一時間可以有10個執行緒同時獲取到鎖對同一個商品進行操作,也就意味著在同等環境下,分段鎖的效率比只用一個鎖要高得多

場景10: 利用多級快取解決Redis線上叢集快取雪崩問題

快取雪崩: 快取支撐不住或者宕機,然後大量請求湧入資料庫。

解決方案

  • 閘道器限流。Nginx、Sentinel、Hystrix都可以實現
  • 程式碼層面。可以使用多級快取,比如一級快取採用布隆過濾器,二級快取可以使用guava中的Cache,三級快取使用Redis,為什麼一級快取使用布隆過濾器呢,其結構和bitmap類似,用於儲存資料狀態,能存大量的key

場景11: 一次微博明顯熱點事件導致系統崩潰原因分析

問題: 比如微博上某一天某個明星事件成為了熱點新聞,此時很多吃瓜群眾全部湧入這個熱點,如果並行每秒達到幾十萬甚至上百萬的並行量,但是Redis伺服器單節點只能支撐並行10w而已,那麼可能因為這麼高的並行量導致很多請求卡死在那,要知道我們其他業務服務也會用到Redis,一旦Redis卡死,就會影響到其他業務,導致整個業務癱瘓,這就是典型的快取雪崩問題

解決方案: 參考場景10

場景12: 大廠對熱點資料處理方案

解決方案

  • 如果按照場景10的方案去實現,需要考慮資料一致性問題,這樣就不得不每次對資料進行增加、刪除、更新都要立馬通知其他節點更新資料,能做到及時更新資料的方案可能就是:Redis釋出/訂閱、MQ等
  • 雖然說這些方案實現也可以,但是不可避免的我們需要再維護相關的中介軟體,提高了維護成本
  • 目前大廠對於熱點資料專門會有一個類似於熱點快取系統來維護,所有的web應用只需要監聽這個系統,只要有熱點時,直接更新快取,這樣既能減少程式碼耦合,還能更好的維護熱點資料。
  • 那麼熱點資料來源怎麼獲取呢?可以在設計查詢的介面使用類似於Spring AOP的方式,每次查詢就把資料傳送到熱點資料,一般大廠都會有資料分析崗位,根據熱點規則將資料分類

到此這篇關於淺談Redis高並行快取架構效能優化實戰的文章就介紹到這了,更多相關Redis高並行快取內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com