day03-商家查詢緩存02

来源:https://www.cnblogs.com/liyuelian/archive/2023/04/20/17338427.html
-Advertisement-
Play Games

功能02-商鋪查詢緩存02 知識補充 (1)緩存穿透 https://blog.csdn.net/qq_45637260/article/details/125866738 緩存穿透(cache penetration)是指用戶訪問的數據既不在緩存當中,也不在資料庫中。出於容錯的考慮,如果從底層數據 ...


功能02-商鋪查詢緩存02

知識補充

(1)緩存穿透

https://blog.csdn.net/qq_45637260/article/details/125866738

緩存穿透(cache penetration)是指用戶訪問的數據既不在緩存當中,也不在資料庫中。出於容錯的考慮,如果從底層資料庫查詢不到數據,則不寫入緩存。這就導致每次請求都會到底層資料庫進行查詢,緩存也失去了意義。當高併發或有人利用不存在的Key頻繁攻擊時,資料庫的壓力驟增,甚至崩潰,這就是緩存穿透問題。

簡單地說,緩存穿透是指用戶請求的數據在緩存和資料庫中都不存在,則每次請求都會打到資料庫中,給資料庫帶來巨大壓力。

img

常見的兩種解決方案

(1)緩存空對象:是指在持久層沒有命中的情況下,對key進行set (key,null)。

緩存空對象會有兩個問題:

  1. value為null 不代表不占用記憶體空間,空值做了緩存,意味著緩存層中存了更多的鍵,需要更多的記憶體空間,比較有效的方法是針對這類數據設置一個較短的過期時間,讓其自動剔除。

  2. 緩存層和存儲層的數據會有一段時間視窗的不一致,可能會對業務有一定影響。例如過期時間設置為5分鐘,如果此時存儲層添加了這個數據,那此段時間就會出現緩存層和存儲層數據的不一致,此時可以利用消息系統或者其他方式清除掉緩存層中的空對象。

image-20230420164625433

(2)布隆過濾器:

在訪問緩存層和存儲層之前,將存在的key用布隆過濾器提前保存起來,做第一層攔截,當收到一個對key請求時,先用布隆過濾器驗證是key否存在,如果存在再進入緩存層、存儲層。

可以使用bitmap做布隆過濾器。這種方法適用於數據命中不高、數據相對固定、實時性低的應用場景,代碼維護較為複雜,但是緩存空間占用少。

image-20230420164759501

布隆過濾器實際上是一個很長的二進位向量和一系列隨機映射函數。布隆過濾器可以用於檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的演算法,缺點是有一定的誤識別率和刪除困難。

布隆過濾器攔截的演算法描述:

初始狀態時,BloomFilter是一個長度為m的位數組,每一位都置為0。添加元素x時,x使用k個hash函數得到k個hash值,對m取餘,對應的bit位設置為1。

判斷y是否屬於這個集合,對y使用k個哈希函數得到k個哈希值,對m取餘,所有對應的位置都是1,則認為y屬於該集合(哈希衝突,可能存在誤判),否則就認為y不屬於該集合。可以通過增加哈希函數和增加二進位位數組的長度來降低錯報率

兩種方案的比較:

緩存穿透的方案 使用場景 維護成本
緩存空對象 1.數據命中率不高 2.數據頻繁變化實時性高 1.代碼維護簡單 2.需要過多的緩存空間 3.數據不一致
布隆過濾器 1.數據命中不高 2.數據相對固定實時性低 1.代碼維護複雜 2.緩存空間占用少

緩存穿透的解決方案還有:

image-20230420174933275

(2)緩存雪崩

緩存雪崩

在使用緩存時,通常會對緩存設置過期時間,一方面目的是保持緩存與資料庫數據的一致性,另一方面是減少冷緩存占用過多的記憶體空間。但當緩存中大量熱點緩存採用了相同的實效時間,就會導致緩存在某一個時刻同時實效,請求全部轉發到資料庫,從而導致資料庫壓力驟增,甚至宕機。從而形成一系列的連鎖反應,造成系統崩潰等情況,這就是緩存雪崩(Cache Avalanche)。

簡單地說,緩存雪崩是指在同一時間段大量的熱點key同時失效,或者Redis服務宕機,導致大量請求到達資料庫,給資料庫帶來巨大壓力。

img image-20230420180118368

解決方案

  • 給不同的key的TTL添加隨機值(比如隨機1-5分鐘),讓key均勻地失效
  • 利用redis集群提高服務的可用性(提高高可用性)
  • 給緩存業務添加熔斷、降級、限流策略
  • 給業務添加多級緩存

(3)緩存擊穿

緩存擊穿

如果有一個熱點key,在不停的扛著大併發,在這個key失效的瞬間,持續的大併發請求就會擊破緩存,直接請求到資料庫,好像蠻力擊穿一樣。這種情況就是緩存擊穿(Cache Breakdown)。

緩存擊穿問題也叫做熱點key問題,簡單來說,就是一個被高併發訪問並且緩存重建業務較複雜的key突然失效了,無數的請求訪問在瞬間給資料庫帶來巨大的衝擊。

img image-20230420182344124

從定義上可以看出,緩存擊穿和緩存雪崩很類似,只不過是緩存擊穿是一個熱點key失效,而緩存雪崩是大量熱點key失效。因此,可以將緩存擊穿看作是緩存雪崩的一個子集。

解決方案

方案一:使用互斥鎖(Mutex Key),只讓一個線程構建緩存,其他線程等待構建緩存執行完畢,重新從緩存中獲取數據。單機通過synchronized或lock來處理,分散式環境採用分散式鎖。

image-20230420182634671

方案二:邏輯過期。熱點數據不設置過期時間,只在value中設置邏輯上的過期時間。後臺非同步更新緩存,適用於不嚴格要求緩存一致性的場景。

image-20230420183158051

兩種方案的對比:

image-20230420183536240

3.功能02-商鋪查詢緩存

3.4查詢商鋪id的緩存穿透問題

3.4.3需求分析

解決查詢商鋪查詢可能存在的緩存穿透問題:當訪問不存在的店鋪時,請求會直接打到資料庫上,並且redis緩存永遠不會生效。

這裡使用緩存空對象的方式來解決。

image-20230420165956369

3.4.4代碼實現

(1)修改ShopServiceImpl.java的queryById方法

@Override
public Result queryById(Long id) {
    String key = CACHE_SHOP_KEY + id;

    //1.從redis中查詢商鋪緩存
    String shopJson = stringRedisTemplate.opsForValue().get(key);

    //2.判斷緩存是否命中
    if (StrUtil.isNotBlank(shopJson)) {
        //2.1若命中,直接返回商鋪信息
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return Result.ok(shop);
    }
    //判斷命中的是否是redis的空值
    if (shopJson != null) {
        return Result.fail("店鋪不存在!");
    }

    //2.2未命中,根據id查詢資料庫,判斷商鋪是否存在資料庫中
    Shop shop = getById(id);
    if (shop == null) {
        //2.2.1不存在,防止緩存穿透,將空值存入redis,TTL設置為2min
        stringRedisTemplate.opsForValue().set(key, "",
                CACHE_NULL_TTL, TimeUnit.MINUTES);
        //返回錯誤信息
        return Result.fail("店鋪不存在!");
    }

    //2.2.2存在,則將商鋪數據寫入redis中
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),
            CACHE_SHOP_TTL, TimeUnit.MINUTES);

    return Result.ok(shop);
}

(2)測試,訪問一個緩存和資料庫都不存在的數據:

image-20230420173908205

可以看到redis已經緩存了一個空值

image-20230420173901864

之後再訪問該數據,只要redis的空值對沒有過期,就不會訪問到資料庫,從而起到保護資料庫的作用。

3.5查詢商鋪id的緩存擊穿問題

當查詢店鋪id時,可能會出現該店鋪id對應的緩存失效,從而大量請求發送到資料庫的情況,這裡使用兩種方案分別解決該問題。

3.5.1基於互斥鎖方案解決

3.5.1.1需求分析

修改根據id查詢商鋪的業務,基於互斥鎖方式來解決緩存擊穿問題。

如下,當出現緩存擊穿問題,首先需要判斷當前的線程是否能夠獲取鎖:

  1. 若可以,則進行緩存重建(將資料庫數據重新寫入緩存中),然後釋放鎖。
  2. 如果不能,則線程等待一段時間,然後再判斷緩存是否能命中。
    • 如果未命中,則重覆獲取鎖的流程,直到緩存命中,或者獲得鎖,重建緩存。
image-20230420184120133

根據redis的setnx命令,當setnx設置某個key之後,如果該key存在,則其他線程無法設置該key。

我們可以根據這個特性,作為一個lock的邏輯標誌,當一個線程setnx某個key後,代表獲取了“鎖”。當刪除這個key時,代表釋放“鎖”,這樣其他線程就可以重新獲取“鎖”。此外,可以對該key設置一個有效期,防止刪除key失敗,產生“死鎖”。

3.5.1.2代碼實現

(1)修改 ShopServiceImpl.java

package com.hmdp.service.impl;

import ...

/**
 * 服務實現類
 *
 * @author 李
 * @version 1.0
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop>
        implements IShopService {
    @Resource
    StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryById(Long id) {
        Shop shop = queryWithMutex(id);
        if (shop == null) {
            return Result.fail("店鋪不存在!");
        }
        return Result.ok(shop);
    }

    //緩存穿透(存儲空對象)+緩存擊穿解決(互斥鎖解決)
    public Shop queryWithMutex(Long id) {
        String key = CACHE_SHOP_KEY + id;
        //從redis中查詢商鋪緩存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //判斷緩存是否命中
        if (StrUtil.isNotBlank(shopJson)) {
            //命中,直接返回商鋪信息
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        //判斷命中的是否是redis的空值(緩存擊穿解決)
        if (shopJson != null) {
            return null;
        }
        //未命中,嘗試獲取互斥鎖
        String lockKey = "lock:shop:" + id;
        boolean isLock = false;
        Shop shop = null;
        try {
            //獲取互斥鎖
            isLock = tryLock(lockKey);
            //判斷是否獲取成功
            if (!isLock) {//失敗
                //等待並重試
                Thread.sleep(50);
                //直到緩存命中,或者獲取到鎖
                return queryWithMutex(id);
            }
            //獲取鎖成功,開始重建緩存
            //根據id查詢資料庫,判斷商鋪是否存在資料庫中
            shop = getById(id);
            //模擬重建緩存的延遲-----------
            Thread.sleep(200);
            if (shop == null) {
                //不存在,防止緩存穿透,將空值存入redis,TTL設置為2min
                stringRedisTemplate.opsForValue().set(key, "",
                        CACHE_NULL_TTL, TimeUnit.MINUTES);
                //返回錯誤信息
                return null;
            }
            //存在,則將商鋪數據寫入redis中
            stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),
                    CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            //釋放互斥鎖
            unLock(lockKey);
        }
        //返回從緩存或資料庫中查到的數據
        return shop;
    }

    //緩存穿透方案
//    public Shop queryWithPassThrough(Long id) {
//        String key = CACHE_SHOP_KEY + id;
//        //1.從redis中查詢商鋪緩存
//        String shopJson = stringRedisTemplate.opsForValue().get(key);
//        //2.判斷緩存是否命中
//        if (StrUtil.isNotBlank(shopJson)) {
//            //2.1若命中,直接返回商鋪信息
//            return JSONUtil.toBean(shopJson, Shop.class);
//        }
//        //判斷命中的是否是redis的空值
//        if (shopJson != null) {
//            return null;
//        }
//        //2.2未命中,根據id查詢資料庫,判斷商鋪是否存在資料庫中
//        Shop shop = getById(id);
//        if (shop == null) {
//            //2.2.1不存在,防止緩存穿透,將空值存入redis,TTL設置為2min
//            stringRedisTemplate.opsForValue().set(key, "",
//                    CACHE_NULL_TTL, TimeUnit.MINUTES);
//            //返回錯誤信息
//            return null;
//        }
//        //2.2.2存在,則將商鋪數據寫入redis中
//        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop),
//                CACHE_SHOP_TTL, TimeUnit.MINUTES);
//        return shop;
//    }

    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue()
                .setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }

    @Override
    @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if (id == null) {
            return Result.fail("店鋪id不能為空");
        }
        //1.更新資料庫
        updateById(shop);
        //2.刪除redis緩存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + id);
        return Result.ok();
    }
}

(2)使用jemeter模擬高併發的情況:

5秒發起1000個請求線程:

image-20230420205354887

模擬http請求:

image-20230420205456072 image-20230420210317214

全部請求成功,獲取到數據:

image-20230420211650622

在伺服器的控制臺中可以看到:對於資料庫的請求只觸發了一次,證明在高併發的場景下,只有一個線程對資料庫發起請求,並對redis對應的緩存重新設置。

image-20230420210258377

3.5.2基於邏輯過期方案解決


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 由於當前社會人力成本越來越昂貴,機器取代人力是大勢所趨,自動化的發展也隨之越來越快 。當製造公司需 要一雙手和一對 眼睛的時候卻不得不雇佣一個人的苦惱日益加重,而傳統的機器設計和電氣自動化的發展,解決一雙手的問題已經漸漸得到了緩解,現在就到了需要解決一雙眼睛的時候,機器視覺的出現和廣泛應用也隨著到來 ...
  • 概述 C#是微軟開發的一種流行的編程語言,廣泛用於開發桌面,Web和移動應用程式。在每個新版本中,C# 都會帶來令人興奮的功能和改進,使其更強大、更具表現力和更高效。C# 的最新版本是2022年發佈的 C#11,它引入了一系列新功能,例如abstract 和 virtual 引入到靜態方法中、泛型 ...
  • 分散式緩存是由多個應用伺服器共用的緩存,通常作為訪問它的應用伺服器的外部服務進行維護。 分散式緩存可以提高 ASP.NET Core 應用的性能和可伸縮性,尤其是當應用由雲服務或伺服器場托管時。 與其他將緩存數據存儲在單個應用伺服器上的緩存方案相比,分散式緩存具有多個優勢。 當分發緩存數據時,數據: ...
  • 前言 創建一個簡單的字元設備驅動程式。 ​ 本文命令的運行基本上都需要root許可權,使用root賬號,或者在命令前面加上sudo。 ​ 如果你使用ssh遠程連接的伺服器進行代碼編寫。那麼不要在root用戶下創建文件或者文件夾。這會導致你ssh連接vscode編寫代碼的許可權問題。可以在普通用戶創建好所 ...
  • 如何編寫 Windows Server 的日誌篩選器,你需要先瞭解以下概念: 1、Windows Event Log:Windows Event Log 是 Windows Server 操作系統提供的一種記錄系統事件的機制,它可以記錄操作系統、應用程式、安全、系統和其他類型的事件。 2、Event ...
  • VMware17安裝Ubuntu22.04.2-Desktop詳細記錄 1. 前置準備 VMware軟體,這裡用的VMware17 Ubuntu系統鏡像文件(.iso文件) 官網下載:Ubuntu系統下載 | Ubuntu I Tell You舊版站點:MSDN, 我告訴你 - 做一個安靜的工具站 ...
  • Vim的6種基本模式 1. 普通模式在普通模式中,用的編輯器命令,比如移動游標,刪除文本等等。這也是Vim啟動後的預設模式。這正好和許多新用戶期待的操作方式相反(大多數編輯器預設模式為插入模式)。 2. 插入模式在這個模式中,大多數按鍵都會向文本緩衝中插入文本。大多數新用戶希望文本編輯器編輯過程中一 ...
  • 1. 數據分組 1.1. SQL的語句中具有分組功能的是GROUP BY和PARTITION BY 1.1.1. 兩者都有數學的理論基礎 1.1.2. 都可以根據指定的列為表分組 1.1.3. 區別僅僅在於,GROUP BY在分組之後會把每個分組聚合成一行數據 1.1.4. GROUP BY的作用是 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...