Redis實戰常用二、緩存的使用

一、什么是緩存

  1. 在實際開發中,系統需要"避震器",防止過高的數據訪問猛沖系統,導致其操作線程無法及時處理信息而癱瘓.

    • 這在實際開發中對企業講,對產品口碑,用戶評價都是致命的。所以企業非常重視緩存技術;
  2. 緩存(Cache):就是數據交換的緩沖區,俗稱的緩存就是緩沖區內的數據,一般從數據庫中獲取,存儲于本地代碼中。

    • 緩沖區:是存儲數據的臨時地方,一般讀寫性能較高
  • 例子:
// 例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(); // 本地緩存

由于其被Static修飾,所以隨著類的加載而被加載到內存之中,作為本地緩存,由于其又被final修飾,所以其引用(例3:map)和對象(例3:new HashMap())之間的關系是固定的,不能改變,因此不用擔心賦值(=)導致緩存失效。

二、為什么使用緩存

  • 優點:訪問速度快,好用
  1. 緩存數據存儲于代碼中,而代碼運行在內存中,內存的讀寫性能遠高于磁盤,緩存可以大大降低用戶訪問并發量帶來的服務器讀寫壓力。
    • 實際開發過程中,企業的數據量,少則幾十萬,多則幾千萬,這么大數據量,如果沒有緩存來作為"避震器",系統是幾乎撐不住的,所以企業會大量運用到緩存技術。
  • 緩存也會增加代碼復雜度和運營的成本

三、使用緩存的缺點與優點

在這里插入圖片描述

四、如何使用緩存

  1. 實際開發中,會構建多級緩存來使系統運行速度進一步提升,例如:本地緩存與redis中的緩存并發使用等等。
    • 瀏覽器緩存:主要是存在于瀏覽器端的緩存
    • 應用層緩存:可以分為tomcat本地緩存,比如之前提到的map,或者是使用redis作為緩存
    • 數據庫緩存:在數據庫中有一片空間是 buffer pool,增改查數據都會先加載到mysql的緩存中
    • CPU緩存:當代計算機最大的問題是 cpu性能提升了,但內存讀寫速度沒有跟上,所以為了適應當下的情況,增加了cpu的L1,L2,L3級的緩存
      在這里插入圖片描述

四、1、添加緩存的思路

  • 標準的操作方式就是查詢數據庫之前先查詢緩存

    • 如果緩存數據存在,則直接從緩存中返回。
    • 如果緩存數據不存在,再查詢數據庫,然后將查詢到的數據存入redis,再把數據庫的數據返回。
  • 不使用緩存
    在這里插入圖片描述

  • 使用緩存
    在這里插入圖片描述

  • 舉例:根據id查詢商鋪的信息
    在這里插入圖片描述

四、2、緩存的更新策略

  1. 緩存更新是redis為了節約內存而設計出來的一個東西,主要是因為內存數據寶貴,當我們向redis插入太多數據,此時就可能會導致緩存中的數據過多,所以redis會對部分數據進行更新,或者把它叫為淘汰更合適。主要有一下三種策略
    • 內存淘汰:redis自動進行,當redis內存達到咱們設定的max-memery的時候,會自動觸發淘汰機制,淘汰掉一些不重要的數據(可以自己設置策略方式)
    • 超時剔除:當我們給redis設置了過期時間ttl之后,redis會將超時的數據進行刪除,方便后面繼續使用緩存
    • 主動更新:我們可以手動調用方法把緩存刪掉通常用于解決緩存和數據庫不一致問題

在這里插入圖片描述

四、2.1、數據庫和緩存不一致的問題解決:

  1. 由于我們的緩存的數據源來自于數據庫,而數據庫的數據是會發生變化的,因此,如果當數據庫中數據發生變化,而緩存卻沒有同步,此時就會有一致性問題存在,其后果是:
    • 用戶使用緩存中的過時數據,就會產生類似多線程數據安全問題,從而影響業務,產品口碑等。怎么解決呢?有如下幾種方案:
      • 1、Cache Aside Pattern 人工編碼方式:緩存調用者在更新完數據庫后再去更新緩存,也稱之為雙寫方案
      • 2、Read/Write Through Pattern : 由系統本身完成,數據庫與緩存的問題交由系統本身去處理
      • 3、Write Behind Caching Pattern :調用者只操作緩存,其他線程去異步處理數據庫,實現最終一致
        在這里插入圖片描述

數據庫和緩存不一致采用Cache Aside Pattern 人工編碼方式

  • 數據庫和緩存不一致采用方法為:Cache Aside Pattern 人工編碼方式:緩存調用者在更新完數據庫后再去更新緩存,也稱之為雙寫方案

操作緩存和數據庫時有三個問題需要考慮:

  • 假設我們每次操作數據庫后,都操作緩存,但是中間如果沒有人查詢,那么這個更新動作實際上只有最后一次生效,中間的更新動作意義并不大,我們可以把緩存刪除,等待再次查詢時,將緩存中的數據加載出來.
  • 問題一:選擇刪除緩存還是更新緩存?刪除緩存
    • 更新緩存:每次更新數據庫都更新緩存,無效寫操作較多
    • 刪除緩存:更新數據庫時讓緩存失效,查詢時再更新緩存
  • 為什么不更新緩存兒選擇直接刪除緩存呢?
    • 因為緩存的更新成本比刪除成本更高。(因為你寫入數據庫的值在很多情況下并不是直接寫入緩存的,而是要經過一系列復雜的計算再寫入緩存。那么每次寫入數據庫后都再次計算寫入緩存的值,無疑是浪費性能的,所以刪除緩存更為適合。
  • 問題二:如何保證緩存與數據庫的操作的同時成功或失敗?
    • 單體系統,將緩存與數據庫操作放在一個事務
    • 分布式系統,利用TCC等分布式事務方案
  • 問題三:選擇先操作緩存還是先操作數據庫?先操作數據庫,再刪除緩存
    • 先刪除緩存,再操作數據庫
    • 先操作數據庫,再刪除緩存
  • 具體操作緩存還是操作數據庫,我們應當是先操作數據庫,再刪除緩存
    • 原因:如果你選擇第一種方案(Cache Aside Pattern 人工編碼方式),在兩個線程并發來訪問時,假設線程1先來,他先把緩存刪了,此時線程2過來查詢緩存數據并不存在,此時線程2查詢數據庫并把查詢出來的數據寫入緩存,當線程2把數據寫入緩存后,線程1再執行更新動作時,實際上寫入的就是舊的數據,新的數據被舊數據覆蓋了。

先刪除緩存還是先執行數據庫操作的選擇分析

  • 寫緩存的時間是很快的(幾毫秒),而操作數據庫的時間是很長的。(操作緩存和操作數據庫效率差別大
1、先刪除緩存,再操作數據
  • 先刪除緩存,再操作數據的正常情況
    在這里插入圖片描述
  • 先刪除緩存,再操作數據的異常情況(出現數據庫和緩存數據不一致的問題)
    • 解決辦法一延遲雙刪:最終一致性的方法,即使用雙刪的辦法。(就是每次修改完數據之后再把緩存刪除了(使用延遲刪除,避免刪除緩存操作在舊數據的寫入緩存操作之前)),這樣就保證了數據的一致性了,但還是會出現一次數據不一致的問題,如果想避免一次數據一致性都不出現就得使用強一致性的方法了)(推薦使用這個方法
      • 這方法感覺就相當于先操作數據庫再刪除緩存一樣了,由此看來還是選擇先操作再刪除緩存
    • 解決辦法二:使用強一致性的辦法(就是保證redis的操作和數據庫的操作的原子性),可以使用加鎖的方法(使用這種方法會影響性能,我們使用redis的意義就是為了提高性能,所以加鎖就得不償失了,所以不太推薦了)
      在這里插入圖片描述
2、 先操作數據,再刪除緩存
  • 先操作數據,再刪除緩存的正常情況
    在這里插入圖片描述
  • 先操作數據,再刪除緩存的異常情況1(出現數據庫和緩存數據不一致的問題)因為緩存操作是很快的,所以這種異常情況幾乎不可能發生
    在這里插入圖片描述
  • 先操作數據,再刪除緩存的異常情況2:刪除緩存失敗的情況,就是線程2執行刪除緩存操作時出現刪除失敗的問題,導致最終的緩存數據還是舊數據。
    • 解決方案一:使用異步刪除重試的辦法(結合mq)
    • 解決方案二:cannal解耦(使用cannal提供的java客戶端)

最終選擇:先操作數據庫再刪除緩存

在這里插入圖片描述

緩存穿透

緩存雪崩

緩存擊穿(熱點key)

最后可以把緩存擊穿和緩存穿透的解決方法封裝成一個工具類

基于StringRedisTemplate封裝一個緩存工具類,滿足下列需求:

  • 方法1:將任意Java對象序列化為json并存儲在string類型的key中,并且可以設置TTL過期時間
  • 方法2:將任意Java對象序列化為json并存儲在string類型的key中,并且可以設置邏輯過期時間,用于處理緩

存擊穿問題

  • 方法3:根據指定的key查詢緩存,并反序列化為指定類型,利用緩存空值的方式解決緩存穿透問題
  • 方法4:根據指定的key查詢緩存,并反序列化為指定類型,需要利用邏輯過期解決緩存擊穿問題
@Component
public class CacheClient {private final StringRedisTemplate stringRedisTemplate;private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);//使用構造器注入第三方beanStringRedisTemplatepublic CacheClient(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}// 添加緩存,設置key的過期時間public void set(String key, Object value, Long time, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}// 添加緩存,設置邏輯過期時間public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {// 設置邏輯過期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));// 寫入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}// 獲取緩存,解決緩存穿透public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){String key = keyPrefix + id;// 1.從redis查詢商鋪緩存String json = stringRedisTemplate.opsForValue().get(key);// 2.判斷是否存在if (StrUtil.isNotBlank(json)) {// 3.存在,直接返回return JSONUtil.toBean(json, type);}// 判斷命中的是否是空值if (json != null) {// 返回一個錯誤信息return null;}// 4.不存在,根據id查詢數據庫R r = dbFallback.apply(id);// 5.不存在,返回錯誤if (r == null) {// 將空值寫入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回錯誤信息return null;}// 6.存在,寫入redisthis.set(key, r, time, unit);return r;}// 使用邏輯過期時間,解決緩存擊穿public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + 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);R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();// 5.判斷是否過期if(expireTime.isAfter(LocalDateTime.now())) {// 5.1.未過期,直接返回店鋪信息return r;}// 5.2.已過期,需要緩存重建// 6.緩存重建// 6.1.獲取互斥鎖String lockKey = LOCK_SHOP_KEY + id;boolean isLock = tryLock(lockKey);// 6.2.判斷是否獲取鎖成功if (isLock){// 6.3.成功,開啟獨立線程,實現緩存重建CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 查詢數據庫R newR = dbFallback.apply(id);// 重建緩存this.setWithLogicalExpire(key, newR, time, unit);} catch (Exception e) {throw new RuntimeException(e);}finally {// 釋放鎖unlock(lockKey);}});}// 6.4.返回過期的商鋪信息return r;}// 使用互斥鎖解決緩存穿透public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.從redis查詢商鋪緩存String shopJson = stringRedisTemplate.opsForValue().get(key);// 2.判斷是否存在if (StrUtil.isNotBlank(shopJson)) {// 3.存在,直接返回return JSONUtil.toBean(shopJson, type);}// 判斷命中的是否是空值if (shopJson != null) {// 返回一個錯誤信息return null;}// 4.實現緩存重建// 4.1.獲取互斥鎖String lockKey = LOCK_SHOP_KEY + id;R r = null;try {boolean isLock = tryLock(lockKey);// 4.2.判斷是否獲取成功if (!isLock) {// 4.3.獲取鎖失敗,休眠并重試Thread.sleep(50);return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);}// 4.4.獲取鎖成功,根據id查詢數據庫r = dbFallback.apply(id);// 5.不存在,返回錯誤if (r == null) {// 將空值寫入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回錯誤信息return null;}// 6.存在,寫入redisthis.set(key, r, time, unit);} catch (InterruptedException e) {throw new RuntimeException(e);}finally {// 7.釋放鎖unlock(lockKey);}// 8.返回return r;}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);}
}

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

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

相關文章

STM32八股【2】-----ARM架構

1、架構包含哪幾部分內容 寄存器處理模式流水線MMU指令集中斷FPU總線架構 2、以STM32為例進行介紹 2.1 寄存器 寄存器名稱作用R0-R3通用寄存器用于數據傳遞、計算及函數參數傳遞&#xff1b;R0 也用于存儲函數返回值。R4-R12通用寄存器用于存儲局部變量&#xff0c;減少頻繁…

effective Java 學習筆記(第二彈)

effective Java 學習筆記&#xff08;第一彈&#xff09; 整理自《effective Java 中文第3版》 本篇筆記整理第3&#xff0c;4章的內容。 重寫equals方法需要注意的地方 自反性&#xff1a;對于任何非空引用 x&#xff0c;x.equals(x) 必須返回 true。對稱性&#xff1a;對于…

mac命令行快捷鍵

光標移動 Ctrl A: 將光標移動到行首。Ctrl E: 將光標移動到行尾。Option 左箭頭: 向左移動一個單詞。Option 右箭頭: 向右移動一個單詞。 刪除和修改 Ctrl K: 刪除從光標到行尾的所有內容。Ctrl U: 刪除從光標到行首的所有內容。Ctrl W: 刪除光標前的一個單詞。Ctrl …

CentOS 7部署主域名服務器 DNS

1. 安裝 BIND 服務和工具 yum install -y bind bind-utils 2. 配置 BIND 服務 vim /etc/named.conf 修改以下配置項: listen-on port 53 { any; }; # 監聽所有接口allow-query { any; }; # 允許所有設備查詢 3 . 添加你的域名區域配置 …

優化 SQL 語句方向和提升性能技巧

優化 SQL 語句是提升 MySQL 性能的關鍵步驟之一。通過優化 SQL 語句,可以減少查詢時間、降低服務器負載、提高系統吞吐量。以下是優化 SQL 語句的方法、策略和技巧: 一、優化 SQL 語句的方法 1. 使用 EXPLAIN 分析查詢 作用:查看 SQL 語句的執行計劃,了解查詢是如何執行的…

C++ 多線程簡要講解

std::thread是 C11 標準庫中用于多線程編程的核心類&#xff0c;提供線程的創建、管理和同步功能。下面我們一一講解。 一.構造函數 官網的構造函數如下&#xff1a; 1.默認構造函數和線程創建 thread() noexcept; 作用&#xff1a;創建一個 std::thread 對象&#xff0c;但…

Vscode HTML5新增元素及屬性

一、?HTML5 語義化標簽 HTML5 語義化標簽&#xff08;Semantic Elements&#xff09;是一組 ?具有明確含義的 HTML 元素?&#xff0c;通過標簽名稱直接描述其內容或結構的功能&#xff0c;而非僅作為樣式容器&#xff08;如 <div> 或 <span>&#xff09;。它們旨…

【PostgreSQL教程】PostgreSQL 特別篇之 語言接口Python

博主介紹:?全網粉絲22W+,CSDN博客專家、Java領域優質創作者,掘金/華為云/阿里云/InfoQ等平臺優質作者、專注于Java技術領域? 技術范圍:SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大數據、物聯網、機器學習等設計與開發。 感興趣的可…

Three學習入門(四)

9-Three.js 貼圖與材質學習指南 環境準備 <!DOCTYPE html> <html> <head><title>Three.js Texture Demo</title><style> body { margin: 0; } </style> </head> <body><script src"https://cdnjs.cloudflare.…

前端NVM安裝

https://v0.dev/chat/settings 本地啟動環境 1安裝 nvm 2安裝node nvm install v18.19.0 nvm install v20.9.0 nvm use 18 node -v 3安裝 pnpm npm install -g pnpm 或者 npm i -g pnpm 4啟動 代碼 目錄下 執行 pnpm i pnpm run dev 4.1到代碼目錄下 4.2直接cmd…

藍橋杯算法精講:二分查找實戰與變種解析

適合人群&#xff1a;藍橋杯備考生 | 算法競賽入門者 | 二分查找進階學習者 目錄 一、二分查找核心要點 1. 算法思想 2. 適用條件 3. 算法模板 二、藍橋杯真題實戰 例題&#xff1a;分巧克力&#xff08;藍橋杯2017省賽&#xff09; 三、二分查找變種與技巧 1. 查找左邊…

cmd命令查看電腦的CPU、內存、存儲量

目錄 獲取計算機硬件的相關信息的命令分別的功能結果展示結果說明獲取計算機硬件的相關信息的命令 wmic cpu get name wmic memorychip get capacity wmic diskdrive get model,size,mediaType分別的功能 獲取計算機中央處理器(CPU)的名稱 獲取計算機內存(RAM)芯片的容量…

SCI論文閱讀指令(特征工程)

下面是一個SCI論文閱讀特征工程V3.0&#xff0c;把指令輸入大模型中&#xff0c;并上傳PDF論文&#xff0c;就可以幫你快速閱讀論文。 優先推薦kimi&#xff0c;當然DeepSeek、QwQ-32B等大語言模型也可以。測試了一下總結的還不錯&#xff0c;很詳細。 請仔細并深入地閱讀所提…

如何監控 SQL Server

監控 SQL Server 對于維護數據庫性能、確保數據可用性和最大限度地減少停機時間至關重要。隨著企業越來越依賴數據驅動的決策&#xff0c;高效的SQL Server監控策略能顯著提升組織生產力和用戶滿意度。 為什么要監控 SQL Server SQL Server 是許多關鍵應用程序的支柱&#xf…

python腳本處理excel文件

1.對比perl和python 分別嘗試用perl和python處理excel文件&#xff0c;發現perl的比較復雜&#xff0c;比如說read excel就有很多方式 Spreadsheet::Read use Spreadsheet::ParseExcel 不同的method&#xff0c;對應的取sheet的cell方式也不一樣。更復雜的是處理含有中文內…

3、pytest實現參數化

在 pytest 中&#xff0c;參數化&#xff08;parametrization&#xff09;是一種強大的功能&#xff0c;可以讓你用不同的輸入數據重復執行同一個測試函數。這種功能非常有用&#xff0c;可以幫助你顯著減少重復代碼并提高測試覆蓋率。 參數化的主要作用是&#xff1a; 測試多…

Cursor:超強AI變成神器

是一個強大的 AI 編程助手&#xff0c;可以幫助開發者快速地編寫、編輯和討論代碼&#xff0c;支持 Python、Java、C# 等多種編程語言&#xff0c;并且可以與 GitHub、Slack 等平臺集成。 Cursor 是什么&#xff1f; 想象一下&#xff0c;你有一個能把你的創意變成現實的造夢 …

畫秒殺系統流程圖

秒殺系統流程圖 秒殺系統關鍵點 高并發處理: 使用網關&#xff08;如 Nginx&#xff09;進行流量限流&#xff0c;避免過載。分布式鎖或 Redis 原子操作控制并發。 活動狀態檢查: Redis 存儲活動狀態&#xff08;如 seckill:activity:1:status&#xff09;&#xff0c;快速…

【js逆向入門】圖靈爬蟲練習平臺 第九題

地址&#xff1a;aHR0cHM6Ly9zdHUudHVsaW5ncHl0b24uY24vcHJvYmxlbS1kZXRhaWwvOS8 f12進入了debugger&#xff0c;右擊選擇一律不在此處暫停&#xff0c; 點擊繼續執行 查看請求信息 查看載荷&#xff0c;2個加密參數&#xff0c;m和tt 查看啟動器&#xff0c;打上斷點 進來 往…

Vue中的狀態管理器Vuex被Pinia所替代-上手使用指南

Pinia.js 是新一代的狀態管理器&#xff0c;由 Vue.js團隊中成員所開發的&#xff0c;因此也被認為是下一代的 Vuex&#xff0c;即 Vuex5.x&#xff0c;在 Vue3.0 的項目中使用也是備受推崇 Pinia.js 有如下特點&#xff1a; 完整的 typescript 的支持&#xff1b;足夠輕量&…