二刷 黑馬點評 商戶查詢緩存

緩存

數據交換的緩沖區,俗稱的緩存是緩沖區內的數據,一般從數據庫中獲取,

1:Static final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<>(); 本地用于高并發例2:static final Cache<K,V> USER_CACHE = CacheBuilder.newBuilder().build(); 用于redis等緩存例3:Static final Map<K,V> map =  new HashMap(); 本地緩存
  1. ConcurrentHashMap
    線程安全的哈希表,支持高并發讀寫。適用于本地內存緩存,無需序列化,直接操作 Java 對象。但無法持久化或分布式共享。

  2. CacheBuilder(Guava Cache):
    功能更豐富的本地緩存,支持過期策略、最大容量、弱引用等。通常用于本地二級緩存,配合 Redis 等遠程緩存使用,減少遠程訪問壓力。

  3. HashMap
    非線程安全的哈希表,直接用于緩存會有并發問題(如數據不一致、死循環)。不推薦在高并發場景使用,除非通過外部同步(如Collections.synchronizedMap)。

使用緩存的目的

速度快,提高讀寫效率,降低響應時間
緩存數據存儲在內存中,而內存讀寫性能遠高于磁盤,緩存可以大大降低用戶訪問并發量帶來的服務器讀寫壓力(降低后端負載)
![[Pasted image 20250711171217.png]]

  1. 數據一致性成本
    若后端數據更新(如商品價格修改),緩存未及時同步,會出現 “緩存與源數據不一致” 的問題。需設計 緩存失效策略(如超時、主動更新),但這會增加代碼復雜度和異常處理成本。
  2. 代碼維護成本
    引入緩存后,代碼需新增 “緩存讀寫、失效、回源(緩存未命中時查后端)” 等邏輯,還需處理緩存穿透、擊穿、雪崩等異常場景,導致代碼更復雜,維護難度提升。
  3. 運維成本
    緩存系統(如 Redis、Memcached)需獨立部署、監控(內存、命中率、連接數)、擴容(集群化)、故障恢復,增加運維人力和資源投入。

如何使用緩存:
構建多級緩存,例如本地緩存和redis緩存并發使用

瀏覽器緩存:保存在瀏覽器端的緩存
應用層緩存:分為tomcat本地緩存,如使用map或redis
數據庫緩存:數據庫中有一個緩存池,增改查數據都會先加載到mysql緩存中
CPU緩存:CPU的L1、L2、L3級緩存
![[Pasted image 20250711171515.png]]

添加商戶緩存

在查詢商戶信息時,先到緩存中查詢
這里添加redis緩存
查詢時先訪問Redis,若沒有命中再訪問數據庫,同時寫緩存到Redis
![[Pasted image 20250711171743.png]]

String key = "cache:shop:" + id;  
String shopJson = stringRedisTemplate.opsForValue().get(key);  
if (StrUtil.isNotBlank(shopJson)) {  Shop shop = JSONUtil.toBean(shopJson, Shop.class);  return Result.ok(shop);  
}  
Shop shop = getById(id);  
if (shop == null) {  return Result.fail("店鋪不存在哦");  
}  
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop));  
return Result.ok(shop);

先從redis查詢店鋪數據
如果存在,將JSOn格式的字符串通過JSONUtil反序列化成Shop類對象的實例
如果不存在,去數據庫中查找,將返回值寫入redis,這里同樣將Shop類對象的實例轉化成String類型

緩存更新策略

若緩存中數據過多,redis會對部分數據進行更新或淘汰

內存淘汰

當內存達到設定的最大值時,自動淘汰一些不重要的數據

超時剔除

設置過期時間,redis會將超時的數據進行刪除

主動更新

手動調用方法刪除緩存,通常用于解決緩存和數據庫不一致的問題

![[Pasted image 20250711172822.png]]

數據庫緩存不一致解決方案

由于緩存數據來源于數據庫,而數據庫中的數據是會發生變化的,若數據庫數據發生變化,而緩存未同步,就會出現一致性問題
后果是用戶可能使用緩存中過時數據,從而產生類似多線程數據安全問題
有如下解決方案:
人工編碼:內存調用者在更新完數據庫后更新緩存
讀寫穿透模式:系統作為中間層,同時管理緩存與數據庫的讀寫操作
寫回緩存模式:應用層僅操作緩存,數據庫更新由異步線程批量處理,調用者寫入緩存后直接返回,由異步線程定期將緩存數據批量寫入數據庫

綜合推薦使用方案一,
在操作數據庫時,我們可以將緩存刪除,待查詢時再從緩存中加載數據
為保證數據庫操作同時成功或失敗:
采用單體系統的情況,則將緩存與數據庫放在一個事務中
采用分布式系統,則利用TCC等分布式事務方案

具體操作緩存和數據庫時,應該采用先操作數據庫,后刪除緩存的操作
若先刪除緩存,再操作數據庫:
當有兩個線程并發查詢的時候,假設線程1先查詢,刪除緩存后此時線程2發現沒有緩存數據,從數據庫中讀取舊數據寫入到緩存中,此時線程1再進行更新數據庫的操作,那么緩存就是舊數據

![[Pasted image 20250711174001.png]]

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

具體到代碼在執行更新操作是,先更新數據庫,再刪除緩存

緩存穿透問題的解決思路

緩存穿透:客戶端請求的數據在緩存中和數據庫總都不存在,都有緩存永遠不會生效,這些請求都會打到數據庫。
解決方案:

將空對象緩存:哪怕數據在數據庫中不存在也存入redis中,這樣就不會訪問數據庫
實現簡單,但會造成額外的內存消耗

布隆過濾:通過一個龐大的二進制數據,走哈希的思想判斷這個數據是否存在,若存在才會放行
內存占用少,但實現復雜并有誤判可能
![[Pasted image 20250711174637.png]]

緩存雪崩及解決思路

緩存雪崩是指同一時間大量緩存key同時失效導致Redis服務宕機,導致大量請求到達數據庫,從而造成巨大的壓力
解決方案:
給不同Key的TTL添加隨機值
使用Redis集群
給緩存業務進行降級限流
給業務添加多級緩存
![[Pasted image 20250711174901.png]]

緩存擊穿及解決思路

也叫熱點key,就是一個高并發且緩存重建業務比較復雜(重建時間長)的key突然失效,無數請求的訪問會瞬間給數據庫帶來巨大的沖擊
解決方案:

互斥鎖

將并行查詢改為串行,一次只能一個線程訪問數據庫,使用tryLock和double check解決問題
![[Pasted image 20250711175126.png]]

邏輯過期

不設置過期時間,將過期時間設置在redis的value中,當線程1查詢緩存時,發現數據已經過期了,他會開啟一個新的線程去進行重構數據的邏輯,而線程1直接返回過期數據,假設線程3過來訪問,由于線程2持有鎖,線程3無法獲得鎖,它也直接返回過期數據
特點是在完成緩存重建之前,所有線程返回的都是臟數據
![[Pasted image 20250711175417.png]]

對比:

互斥鎖:簡單,保證數據一致,可能存在死鎖風險且性能低
邏輯過期:讀取不需要等待,性能好,在重構之前都是臟數據,實現復雜
![[Pasted image 20250711175513.png]]

使用互斥鎖解決緩存擊穿問題

 public Shop queryWithMutex(Long id)  {String key = CACHE_SHOP_KEY + id;// 1、從redis中查詢商鋪緩存String shopJson = stringRedisTemplate.opsForValue().get("key");// 2、判斷是否存在if (StrUtil.isNotBlank(shopJson)) {// 存在,直接返回return JSONUtil.toBean(shopJson, Shop.class);}//判斷命中的值是否是空值if (shopJson != null) {//返回一個錯誤信息return null;}// 4.實現緩存重構//4.1 獲取互斥鎖String lockKey = "lock:shop:" + id;Shop shop = null;try {boolean isLock = tryLock(lockKey);// 4.2 判斷否獲取成功if(!isLock){//4.3 失敗,則休眠重試Thread.sleep(50);return queryWithMutex(id);}//4.4 成功,根據id查詢數據庫shop = getById(id);// 5.不存在,返回錯誤if(shop == null){//將空值寫入redisstringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);//返回錯誤信息return null;}//6.寫入redisstringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_NULL_TTL,TimeUnit.MINUTES);}catch (Exception e){throw new RuntimeException(e);}finally {//7.釋放互斥鎖unlock(lockKey);}return shop;}

先進行獲取鎖,未獲取到迭代繼續獲取,知道拿到后查詢數據庫,如果數據庫沒有,將空對象寫入redis并返回null
如果有就寫入redis,然后釋放鎖,最后返回數據庫的結果

使用邏輯過期解決緩存擊穿問題

在查詢redis時,先判斷是否命中,如果沒有命中直接返回空數據,不查詢數據庫,一旦命中將value取出,判斷value的過期時間,如果沒過期直接返回數據,過期則開啟獨立線程后返回之前的數據,獨立線程單獨重構數據,重構完成后釋放互斥鎖
![[Pasted image 20250711204829.png]]

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
public Shop queryWithLogicalExpire( Long id ) {String key = CACHE_SHOP_KEY + id;// 1.從redis查詢商鋪緩存String json = stringRedisTemplate.opsForValue().get(key);// 2.判斷是否存在if (StrUtil.isBlank(json)) {// 3.存在,直接返回return null;}// 4.命中,需要先把json反序列化為對象RedisData redisData = JSONUtil.toBean(json, RedisData.class);Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);LocalDateTime expireTime = redisData.getExpireTime();// 5.判斷是否過期if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未過期,直接返回店鋪信息return shop;}// 5.2.已過期,需要緩存重建// 6.緩存重建// 6.1.獲取互斥鎖String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判斷是否獲取鎖成功if (isLock){CACHE_REBUILD_EXECUTOR.submit( ()->{try{//重建緩存this.saveShop2Redis(id,20L);}catch (Exception e){throw new RuntimeException(e);}finally {unlock(lockKey);}});}// 6.4.返回過期的商鋪信息return shop;
}
  1. 線程池的運用
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    
    這里創建了一個固定大小為 10 的線程池,目的是管控緩存重建任務。借助線程池,可以避免因大量創建線程而導致系統資源被過度占用。
  2. 異步任務的提交
    CACHE_REBUILD_EXECUTOR.submit( ()->{// 任務內容
    });
    
    當商鋪緩存過期后,會向線程池提交一個重建緩存的任務,這樣可以讓主線程繼續執行后續操作,不用等待緩存重建完成。
  3. 緩存重建的流程
    try{// 重建緩存this.saveShop2Redis(id, 20L);
    } catch (Exception e) {throw new RuntimeException(e);
    }
    
    • saveShop2Redis(id, 20L) 方法會從數據庫獲取最新的商鋪數據,然后把這些數據存入 Redis,同時設置 20 秒的邏輯過期時間。
    • 對可能出現的異常進行捕獲,將其封裝成運行時異常后重新拋出。
  4. 鎖的釋放操作
    finally {unlock(lockKey);
    }
    
    不管緩存重建成功與否,最終都會執行 unlock(lockKey) 方法來釋放鎖,防止出現死鎖的情況。

封裝redis工具類

基于StringRedisTemplate封裝一個緩存工具類
方法1:將任意Java對象序列化為Json并儲存在string類型的key中,可設置TTL過期時間
方法2:將任意Java對象序列化為Json并儲存在String類型的key中,可以設置邏輯過期時間,用于處理緩存擊穿問題
方法3:根據指定的key查詢緩存,并反序列化為指定類型,利用緩存空值的方式解決緩存穿透的問題
根據指定的key查詢緩存,并反序列化為指定類型,需要利用邏輯過期解決緩存擊穿問題

Shop shop = cacheClient.queryWithPassThrough( CACHE_SHOP_KEY, // 緩存鍵前綴 
id, // 商鋪ID 
Shop.class, // 返回類型 
this::getById, // 數據庫查詢回調
CACHE_SHOP_TTL, // 緩存時間 
TimeUnit.MINUTES // 時間單位 );
public <R, ID> R queryWithPassThrough( String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) { // ... R r = dbFallback.apply(id); // 調用傳入的函數 // ... }

這里的 Function<ID, R> 是一個函數式接口,表示接受一個 ID 類型的參數,返回一個 R 類型的結果。
等價于id->getById(id),表示傳入一個id參數,調用當前對象的getById方法處理它,并返回結果

總結

  1. 緩存的核心作用是什么?
    緩存通過將高頻訪問數據存儲在內存中,顯著提升讀寫效率、降低響應時間,同時減少后端數據庫的訪問壓力,緩解高并發場景下的服務器負載。

  2. 常見的本地緩存實現有哪些?核心區別是什么?
    常見實現包括ConcurrentHashMap(線程安全,適用于高并發本地緩存,無持久化)、Guava Cache(功能豐富,支持過期策略、容量控制等,適合本地二級緩存)、HashMap(非線程安全,高并發下易出問題,不推薦直接使用)。核心區別在于線程安全性、功能豐富度及適用場景。

  3. 如何解決緩存與數據庫的數據一致性問題?
    推薦 “先更新數據庫,后刪除緩存” 的策略:更新操作時,先保證數據庫數據正確,再刪除對應緩存,避免舊數據殘留。單體系統中可通過事務保證操作原子性,分布式系統需結合 TCC 等分布式事務方案。

  4. 什么是緩存穿透?如何解決?
    緩存穿透指請求數據在緩存和數據庫中均不存在,導致請求直接穿透緩存沖擊數據庫。解決方式包括:①緩存空對象(將不存在的數據以空值存入緩存,避免重復穿透);②布隆過濾(通過哈希判斷數據是否存在,提前攔截無效請求)。

  5. 緩存擊穿的解決方式有哪些?各有什么特點?
    緩存擊穿指高并發下熱點 Key 突然失效,大量請求瞬間沖擊數據庫。解決方式包括:①互斥鎖(串行化請求,保證緩存重建時僅一個線程訪問數據庫,數據一致但性能略低);②邏輯過期(不設置物理過期,通過 value 中的邏輯時間判斷,過期時異步重建緩存,性能高但可能返回臟數據)。

  6. 緩存雪崩的成因及預防措施是什么?
    緩存雪崩指大量緩存 Key 同時失效,導致 Redis 壓力驟降、請求集中沖擊數據庫。預防措施包括:①給 Key 的 TTL 添加隨機值,避免集中過期;②使用 Redis 集群提高可用性;③對緩存業務降級限流;④引入多級緩存減少單一層級依賴。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/90622.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/90622.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/90622.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【前端】【組件庫開發】【原理】【無框架開發】現代網頁彈窗開發指南:從基礎到優化

效果 現代網頁彈窗開發指南&#xff1a;從基礎到優化 彈窗&#xff08;Modal&#xff09;作為網頁交互的重要組件&#xff0c;在用戶通知、確認操作和表單輸入等場景中廣泛應用。本文將循序漸進地講解彈窗的技術實現與最佳實踐。 一、彈窗基礎概念 彈窗是一種覆蓋在主內容之…

【操作系統】線程

JavaEE—線程 一、進程與線程 1.包含管理 2.資源布局 2.1公共資源 2.2私有資源 二、并發編程 1.多線程優勢 1.1創建 1.1.1多線程 1.1.2多進程 1.2通信 1.2.1多線程 1.2.2多進程 1.3調度 1.3.1多線程 1.3.2多進程 1.4銷毀 1.4.1多線程 1.4.2多進程 2.多進程…

React 自定義Hook——頁面或元素滾動到底部監聽 Hook

功能簡介 useReachBottom 是一個 React 自定義 Hook&#xff0c;支持監聽頁面&#xff08;body&#xff09;或任意可滾動元素&#xff08;如 div&#xff09;是否滾動到底部。它能幫助你在用戶滑動到底部時觸發加載更多、顯示提示等操作&#xff0c;極大提升前端交互體驗。 亮…

當Powerbi遇到quickbi,性能優化方式對比

powerbi性能優化對于powerbi&#xff0c;性能優化可以從15個方面考慮&#xff1a; 1.過濾源數據【quickbi數據集過濾或sql過濾】2.刪除無關列 【quickbi不選字段或sql不查詢】3.聚合分析粒度 【quickbi使用sql聚合或計算字段聚合】4.整理字段 【quickbi使用sql聚合或計算字段聚…

ValueConverter轉換器WPF

屬性搭橋 比如BoolToVisibility 創建兩個屬性 Bool Visibility 這樣不好 混亂了viewmodels 降低了泛用性系統自帶的convertor <Window.Resources><BooleanToVisibilityConverter x:Key"booltovis"></BooleanToVisibilityConverter><…

Qt數據庫編程詳解:SQLite實戰指南

Qt數據庫編程詳解&#xff1a;SQLite實戰指南 目錄 SQLite數據庫簡介Qt數據庫核心類數據庫操作全流程CRUD操作實戰運行效果展示 1. SQLite數據庫簡介 SQLite是Qt內置的輕量級嵌入式數據庫&#xff1a; #mermaid-svg-OiZ2cgq9n1G69iH5 {font-family:"trebuchet ms",…

FastAPI 與 OpenIddict 的微服務鑒權整合方案

架構概述基于微服務的身份認證架構采用OAuth 2.0/OpenID Connect協議&#xff0c;OpenIddict作為認證服務器&#xff0c;FastAPI作為資源服務器。系統包含三個核心組件&#xff1a;認證服務、API網關和業務微服務。OpenIddict負責頒發令牌&#xff0c;FastAPI通過JWT驗證訪問權…

計算兩個點的歐式距離

目錄 一、概述 二、公式 1、二維空間 2、三維空間 3、n 維空間 三、python實現 一、概述 歐式距離&#xff08;Euclidean Distance&#xff09;是一種在歐幾里得空間中度量兩個點之間距離的常用方法&#xff0c;其公式根據空間維度的不同而不同 二、公式 1、二維空間 對于二…

八股訓練--RabbitMQ

一、經典問題 1.為什么要用MQ&#xff1f; MQ的作用主要是3個&#xff0c; 第一個是流量削峰&#xff1a;當某個活動舉行時&#xff0c;訪問量可能是平時的幾百倍&#xff0c;可能一下會把服務器弄崩潰&#xff0c;所以通過MQ的形式&#xff0c;引入中間者&#xff0c;客戶端…

Elasticsearch 文檔檢索系統

學習筆記&#xff1a;Elasticsearch 文檔檢索系統 1. 技術棧與核心組件 Node.js&#xff1a;后端運行環境&#xff0c;適合構建高性能 Web 服務。Express&#xff1a;Node.js 的 Web 框架&#xff0c;簡化 API 開發。Elasticsearch&#xff1a;分布式全文檢索引擎&#xff0c;支…

如何準確查看服務器網絡的利用率?

在服務器運維與性能調優過程中&#xff0c;網絡利用率是一個不容忽視的關鍵指標。它反映了服務器帶寬資源的實際使用情況&#xff0c;是判斷系統瓶頸、規劃資源擴展、排查連接問題的重要依據。很多人誤以為網絡是否正常只要“能上網”或“Ping得通”就可以了&#xff0c;實際上…

掌握Spring聲明式事務傳播機制:AOP與ThreadLocal的協同工作

聲明式事務的傳播機制是解決多個事務方法嵌套調用時&#xff0c;事務如何創建、復用、掛起或隔離的核心邏輯。它的實現依賴于事務管理器、事務狀態管理、線程上下文綁定等組件的協同&#xff0c;本質是通過一套 “規則判斷 狀態維護” 的邏輯&#xff0c;在方法調用時動態決定…

@Transactional事務注解的批量回滾機制

關鍵機制說明&#xff1a;1.??事務注解生效??&#xff1a;Transactional(rollbackFor Exception.class)Override Transactional(rollbackFor Exception.class) public Boolean saveUser(UserDTO userDto) {SysUser sysUser new SysUser();BeanUtils.copyProperties(user…

飛算 JavaAI 深度體驗:開啟 Java 開發智能化新紀元

個人主頁&#xff1a;?喜歡做夢 歡迎 &#x1f44d;點贊 ?關注 ??收藏 &#x1f4ac;評論 目錄 一、引言 二、飛算 JavaAI 初印象與功能概覽 &#xff08;一&#xff09;初識飛算 JavaAI &#xff08;二&#xff09;核心功能模塊概覽 三、智能代碼生成功能深度體…

pandas銷售數據分析

pandas銷售數據分析 數據保存在data目錄 消費者數據&#xff1a;customers.csv商品數據&#xff1a;products.csv交易數據&#xff1a;transactions.csv customers.csv數據結構&#xff1a;字段描述customer_id客戶IDgender性別age年齡region地區membership_date會員日期produc…

訪問Windows服務器備份SQL SERVER數據庫

以前沒有直接訪問過Windows服務器,今天剛一看到的是時候有點懵,竟然下意識的使用SecureCRT遠程工具去連了一下,然后領導說,看一下用戶名,突然意識到,跟我們平時遠程桌面是一樣的。 一、 win + R 打開命令窗口 二、 輸入 mstsc 三、 輸入遠程地址 四、點擊連接,如果有彈…

C++ 面向對象 - 對象定義方法匯總

C對象定義方法匯總 1. 棧上定義方式 1.1 調用無參構造函數的定義方式 無參構造函數有兩種&#xff1a; 默認無參構造函數Demo(){}默認值列表構造函數。Demo():a{1},b{2}{} // 使用初始化列表實現對象定義方式&#xff1a; Demo d; Demo d1{}; // 以下定義方式還調用了拷貝構造…

指尖上的魔法:優雅高效的Linux命令手冊

一、Linux基礎指令 1. ls ls&#xff1a;對于目錄&#xff0c;列出該目錄下的所有子目錄與文件&#xff0c;對于文件&#xff0c;將列出文件名以及其他信息。 -a&#xff1a;列出目錄下的所有文件&#xff0c;包含以.開頭的隱藏文件 -l:列出文件的詳細信息 -d&#xff1a;將目錄…

《磁力下載工具實測:資源搜索+高速下載一站式解決方案》

嘿&#xff0c;朋友們&#xff01;我是阿燦&#xff0c;今天給大家帶來一個超實用的看片神器&#xff0c;特別適合老司機們使用&#xff0c;保證讓你眼前一亮&#xff01;推薦一款比某雷更好用的下載工具&#xff0c;搭配資源搜索神器&#xff0c;輕松獲取資源不限速。超強磁力…

Go網絡編程基礎:網絡模型與協議棧概述 - 從理論到實踐的完整指南

1. 引言 在當今的互聯網時代&#xff0c;網絡編程已經成為后端開發的核心技能。Go語言以其出色的并發性能和簡潔的語法&#xff0c;在網絡編程領域展現出了強大的優勢。從Docker、Kubernetes到眾多微服務框架&#xff0c;Go已經成為構建高性能網絡應用的首選語言之一。 你是否…