Redis緩存與數據庫 數據一致性保障

為什么要保證數據一致性

只要使用redis做緩存,就必然存在緩存和DB數據一致性問題。若數據不一致,則業務應用從緩存讀取的數據就不是最新數據,可能導致嚴重錯誤。比如將商品的庫存緩存在Redis,若庫存數量不對,則下單時就可能出錯,這是不能接受的。

旁路緩存

在使用 Redis 作為緩存時,最常見的緩存模式是 Cache-Aside(旁路緩存)模式因其靈活性、容錯性以及與 Redis 的高效配合,適用于大多數業務場景。
Cache-Aside(旁路緩存) 模式,又叫 Lazy-Loading(懶加載)模式,在這種模式下,緩存的讀取和寫入由應用程序直接管理。

  • 讀策略:先查詢緩存,若緩存未命中,則從數據庫中加載數據并將其存儲在緩存中;
  • 寫策略:應用程序直接更新數據庫,并更新或刪除緩存中的相關數據。

旁路緩存因其按需觸發、避免冗余操作特點所以被稱為懶加載。
旁路緩存模式,存在緩存一致性問題,需要小心處理緩存的清除和更新。

更新數據庫并更新緩存

那么如何保證redis緩存和數據庫的數據一致性呢?
直觀的思維想到的是 更新數據庫并更新緩存。

這種直觀的做法會有連續兩次更新,數據不一致問題。并且這種情況經常出現,所以一般不會采用。
在這里插入圖片描述

對于緩存命中率要求很高的場景,我們可以使用更新數據庫+更新緩存的方案,因為只要緩存不過期,都是可以命中的。

解決方案:

  • 加分布式鎖,丟失一部分性能。
  • 加一個比較短的過期時間,哪怕緩存數據不一致也很快過期。

先刪除緩存再更新數據庫

請求A想要將數據庫的數據修改為21,先去redis中刪除了緩存,這個時候請求B來訪問查詢該數據,發現redis中沒有該數據,就去數據庫中查詢了舊數據并寫回到了緩存。這個時候請求A才更新數據庫。
此時緩存中的數據為20,而數據庫中存儲的為21產生了數據不一致。

在這里插入圖片描述

可以看到先刪除緩存,再更新數據庫會在【讀+寫】并發的時候,會出現數據不一致的問題。

延時雙刪

那么針對于先刪除緩存再更新數據庫的方案我們有沒有解決方案呢?
就是先刪除緩存再更新數據庫,然后睡一段時間再刪除一次緩存。

延遲一段時間刪除的目的是什么?

是讓線程B有足夠的時間去將(舊值)寫回到緩存。確保延遲一段時間后能夠將線程B的臟數據刪除。也是保證了最終一致性。

但是具體延遲多久,很難評估出來,所以這種方案也只能盡可能保證數據一致。但是在極端情況下,還是會有可能出現數據不一致。

先更新數據庫再刪除緩存

還是在【讀+寫】并發的場景下分析,請求A查詢數據,發現緩存沒有命中,查詢數據庫為20。此時請求B更新數據庫并刪除了緩存。最后請求A寫回了緩存。請求A寫回的是舊數據也就是20。
此時數據庫中為21(新值),緩存中是20(舊值)。出現了數據不一致。
在這里插入圖片描述

可以看到先更新數據庫再刪除緩存是有可能發生數據庫的,但是這個在實際當中發生的概率很小。

因為緩存的寫入通常要遠遠快于數據庫的寫入,所以實際當中,很少會出現請求B將數據庫更新并且刪除緩存后,請求A才寫回緩存的情況。

所以先更新數據庫再刪除緩存的方式是可以實現數據一致性的。但是為了保險起見,給緩存數據加上過期時間。也就是即使出現了以上這種小概率時間,還是可以通過過期時間,來達到一個最終一致性的目的。

刪除操作失敗

但是在實際開發當中還是會出現問題,如果刪除緩存操作失敗,那么緩存中就會一直存放臟數據,直到key過期。
在這里插入圖片描述

也就是我們要保證更新數據庫之后緩存能被成功刪除?

對應的就有兩種解決方案:

  • 消息隊列重試機制
  • 訂閱 MySQL binlog日志,更新緩存。

消息隊列重試機制

引入消息隊列,將第二步操作(刪除緩存) 要操作的數據放到消息隊列中,由消費者來刪除緩存。

  • 如果應用刪除緩存失敗,可以通過消費者重試機制。設置一個最大的重試次數。如果超過最大重試次數還是失敗,那就要向服務層發送報錯信息。
  • 如果刪除緩存成功后返回ack。避免重復消費。

這種方案的缺點就是對代碼侵入性比較強,需要修改原本的業務代碼。

消費者:交換機和隊列都持久化防止消息丟失。

@RabbitListener(bindings = @QueueBinding(value = @Queue(name = MqConstant.REDIS_MQ_QUEUE, durable = "true", arguments = @Argument(name = "x-queue-mode", value = "lazy")),exchange = @Exchange(name = MqConstant.REDIS_DELETE_EXCHANGE_NAME, type = ExchangeTypes.DIRECT),key = MqConstant.REDIS_MQ_QUEUE_ROUTING_KEY
))
public void listenRedisDeleteMqMessage( String msg) {log.info("RedisDeleteMqListener:redis刪除數據:" + msg);Set keys = redisTemplate.keys(msg);redisTemplate.delete(keys);
}

配置好相關的生產者消費者確認機制,以及消費者失敗重試機制,確保消息正確路由和重試。

spring:      rabbitmq:host: 192.168.215.140 # 你的虛擬機IPport: 5672 # 端口virtual-host: /bigevents # 虛擬主機username: big-events # 用戶名password: 123321 # 密碼publisher-returns: true # 開啟publisher return機制listener:simple:retry:enabled: true # 開啟消費者失敗重試initial-interval: 1000ms # 初始的失敗等待時長為1秒multiplier: 1 # 失敗的等待時長倍數,下次等待時長 = multiplier * last-intervalmax-attempts: 3 # 最大重試次數stateless: true # true無狀態;false有狀態。如果業務中包含事務,這里改為falseacknowledge-mode: auto  #消費者自動ack 異常nack

Canal+MQ

先更新數據庫,對數據庫的寫操作都會被記錄在binlog日志中。所以我們更新數據庫,也就會產生一條對應的變更日志在binlog中。

于是我們可以通過訂閱binlog日志,拿到具體要操作的數據,然后再執行刪除緩存,阿里巴巴開源的Canal中間件就是基于這個實現的。

Canal模擬MySQL主從復制的交互協議,把自己偽裝成一個MySQL的從節點,向MySQL的主節點發送dump命令請求,MySQL收到請求后,就會推送Binlog給Canal,Canal解析binlog字節流后,轉換為便于讀取的結構化數據。供下游服務訂閱使用。
在這里插入圖片描述
前面我們講了MQ的解決方案對原有的業務代碼有很強的侵入性。但是這種方案就不會,直接監聽binlog,因此我們可以用binlog+MQ的解決方案。既保證了數據的一致性又不會對原有業務代碼有侵入。

具體實現

mysql添加用戶和權限

CREATE USER canal IDENTIFIED BY 'canal'; # 添加從節點權限
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';FLUSH PRIVILEGES;

修改canal相關配置:

vi canal-server/conf/example/instance.properties

在這里插入圖片描述

通過spring定時任務監聽canal消息,解析canal的消息,并投遞給mq

/*** 每2秒執行一次** @throws Exception*/
@Scheduled(fixedRate = 2 * 1000)
public void run() throws Exception {//進行連接canalConnector.connect();//進行訂閱canalConnector.subscribe();int batchSize = 5 * 1024;//獲取Message對象Message message = canalConnector.getWithoutAck(batchSize);long id = message.getId();int size = message.getEntries().size();System.out.println("當前監控到的binLog消息數量是:" + size);//判斷是否有數據//如果有數據,進行數據解析List<CanalEntry.Entry> entries = message.getEntries();//遍歷獲取到的Entry集合for (CanalEntry.Entry entry : entries) {System.out.println("----------------------------------------");System.out.println("當前的二進制日志的條目(entry)類型是:" + entry.getEntryType());//如果屬于原始數據ROWDATA,進行打印內容if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {try {//獲取存儲的內容CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());//打印事件的類型,增刪改查哪種 eventTypeSystem.out.println("事件類型是:" + rowChange.getEventType());ArrayList<Long> idList = new ArrayList<>();//打印改變的內容(增量數據)for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {System.out.println("改變后的數據:" + rowData.getAfterColumnsList());List<Column> after;if (rowChange.getEventType().equals(CanalEntry.EventType.INSERT)) {after = rowData.getAfterColumnsList();} else if (rowChange.getEventType().equals(CanalEntry.EventType.DELETE)) {after = rowData.getBeforeColumnsList();} else {//updateafter = rowData.getAfterColumnsList();}for (Column column : after) {if (column.getName().equals("id")) {sendMessageToMq(column.getValue());}}}} catch (Exception e) {e.printStackTrace();}}}//消息確認已經處理了canalConnector.ack(id);
}

消費者監聽mq,最后刪除緩存。

@Slf4j
@Component
@AllArgsConstructor
public class CanalMqListener {private final RedisTemplate redisTemplate;@RabbitListener(bindings = @QueueBinding(value = @Queue(name = MqConstant.CANAL_MQ_QUEUE, durable = "true", arguments = @Argument(name = "x-queue-mode", value = "lazy")),exchange = @Exchange(name = MqConstant.CANAL_MQ_EXCHANGE_NAME, type = ExchangeTypes.DIRECT),key = MqConstant.CANAL_MQ_EXCHANGE_NAME))public void listenCanalMqMessage( String msg) {log.info("listenCanalMqMessage:redis變更數據:" + msg);try {Set keys = redisTemplate.keys(msg);redisTemplate.delete(keys);} catch (Exception e) {throw new RuntimeException(e);}}
}

總結

[圖片]

解決方案

延時雙刪

優點:

  • 無需引入中間件
  • 實現簡單,代碼改動小

缺點:

  • 延遲時間不好控制
  • 極端情況會出現數據不一致

消息隊列

優點:

  • 確保緩存被刪除

缺點:

  • 需要引入消息隊列,增加了系統復雜度。
  • 延遲稍高(對于能接受一定延遲的場景使用)
  • 對原有業務代碼有侵入

Canal+MQ

優點:

  • 確保緩存被刪除
  • 直接監聽binlog,對原有代碼無侵入

缺點:

  • 需要引入多個中間件對團隊運維能力有較高要求
  • 延遲稍高

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

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

相關文章

19.哈希表的實現

1.哈希的概念 哈希(hash)?稱散列&#xff0c;是?種組織數據的?式。從譯名來看&#xff0c;有散亂排列的意思。本質就是通過哈希函數把關鍵字Key跟存儲位置建??個映射關系&#xff0c;查找時通過這個哈希函數計算出Key存儲的位置&#xff0c;進?快速查找。 1.2.直接定址法…

IoTDB TTL不生效

問題 時序數據庫 IoTDB 1.3.0 版本數據庫的 TTL 設置為兩天&#xff0c;show databases details 看到設置也是正確的&#xff0c;怎么還是可以查到好幾天前的數據&#xff1f;因為有很多不活躍的測點&#xff0c;所以專門設置了兩天過期&#xff0c;有什么辦法可以自動清理呢&…

【C++基礎】Lambda 函數 基礎知識講解學習及難點解析

一、引入 在 C 中&#xff0c;我們通常使用函數來完成特定的功能。但有時候&#xff0c;我們需要在一個函數內部定義一個小型的功能塊&#xff0c;這時如果單獨寫一個函數會顯得繁瑣。C11 引入了 Lambda 函數&#xff0c;它是一種匿名函數&#xff0c;可以在需要的地方直接定義…

OpenCV 基礎模塊 Python 版

OpenCV 基礎模塊權威指南&#xff08;Python 版&#xff09; 一、模塊全景圖 plaintext OpenCV 架構 (v4.x) ├─ 核心層 │ ├─ core&#xff1a;基礎數據結構與操作&#xff08;Mat/Scalar/Point&#xff09; │ └─ imgproc&#xff1a;圖像處理流水線&#xff08;濾…

iStoreOS軟路由對硬盤格式化分區(轉化ext4)

一、為什么要格式化分區&#xff1f; 格式化硬盤分區是軟路由安裝或配置過程中的重要步驟&#xff0c;主要用于清除舊數據、優化文件系統、確保系統穩定性和兼容性。 二、通過iStoreOS硬盤格式化步驟 使用場景&#xff1a;Docker遷移到外置移動硬盤為例&#xff0c;考慮兼容現…

打造用戶認證系統,構筑信息安全防線

在當今的數字化時代&#xff0c;信息安全和用戶隱私保護變得越來越重要。用戶身份認證是確保信息安全的第一道防線。通過驗證用戶身份&#xff0c;可以防止未經授權的訪問和數據泄露。它有助于保護用戶的個人信息、賬戶資金和其他敏感數據。此外&#xff0c;用戶身份認證還可以…

北京南文觀點:品牌如何搶占AI 認知的 “黃金節點“

在算法主導的信息洪流中&#xff0c;品牌正在經歷一場隱蔽的認知權爭奪戰&#xff0c;當用戶向ChatGPT咨詢"哪家新能源車企技術最可靠"時&#xff0c;AI調取的知識圖譜數據源將直接決定品牌認知排序。南文樂園科技文化&#xff08;北京&#xff09;有限公司&#xff…

音視頻系列——Websockets接口封裝為Http接口

模型服務示例&#xff1a;實時語音轉文本服務 本示例展示一個支持雙協議&#xff08;WebSocket流式接口HTTP同步接口&#xff09;的語音轉文本模型服務&#xff0c;并提供將WebSocket接口封裝為HTTP接口的代碼實現。 一、服務架構設計 #mermaid-svg-nw0dMZ4uKfS4vGZR {font-fa…

Axure項目實戰:智慧城市APP(一)(動態面板、拖動效果)

親愛的小伙伴&#xff0c;在您瀏覽之前&#xff0c;煩請關注一下&#xff0c;在此深表感謝&#xff01; 課程主題&#xff1a;智慧城市APP便民服務平臺 主要內容&#xff1a;完整智慧APP原型設計 應用場景&#xff1a;各類政務型、B端APP均可參考 案例展示&#xff1a;&…

MySQL 入門大全:數據類型

&#x1f9d1; 博主簡介&#xff1a;CSDN博客專家&#xff0c;歷代文學網&#xff08;PC端可以訪問&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移動端可微信小程序搜索“歷代文學”&#xff09;總架構師&#xff0c;15年工作經驗&#xff0c;…

Java 記憶鏈表,LinkedList 的升級版

文章目錄 記憶鏈表 MemoryLinkedList實戰源代碼 眾所周知&#xff0c;ArrayList 和 LinkedList 是 Java 集合中兩個基本的數據結構&#xff0c;對應數據結構理論中的數組和鏈表。但在這兩個數據結構&#xff0c;開發者們通常使用 ArrayList&#xff0c;而不使用 LinkedList。JD…

《白帽子講 Web 安全》之開發語言安全深度解讀

目錄 引言 1.PHP 安全 1.1變量覆蓋 1.2空字節問題 1.3弱類型 1.4反序列化 1.5安全配置 2Java 安全 2.1Security Manager 2.2反射 2.3反序列化 3Python 安全 3.1反序列化 3.2代碼保護 4.JavaScript 安全 4.1第三方 JavaScript 資源 4.2JavaScript 框架 5.Node.…

鴻蒙HarmonyOS NEXT應用崩潰分析及修復

鴻蒙HarmonyOS NEXT應用崩潰分析及修復 如何保證應用的健壯性&#xff0c;其中一個指標就是看崩潰率&#xff0c;如何降低崩潰率&#xff0c;就需要知道存在哪些崩潰&#xff0c;然后對癥下藥&#xff0c;解決崩潰。那么鴻蒙應用中存在哪些崩潰類型呢&#xff1f;又改如何解決…

分析K8S中Node狀態為`NotReady`問題

在Kubernetes&#xff08;k8s&#xff09;集群中&#xff0c;Node狀態為NotReady通常意味著節點上存在某些問題&#xff0c;下面為你分析正常情況下節點應運行的容器以及解決NotReady狀態的方法。 正常情況下Node節點應運行的容器 1. kubelet kubelet是節點上的核心組件&…

第六屆機電一體化技術與智能制造國際學術會議(ICMTIM 2025)

重要信息 4月11-13日 南京江北新區工業大學亞朵酒店 www.icmtim.org&#xff08;點擊了解參會投稿等&#xff09; 簡介 由南京工業大學主辦&#xff0c;南京工業大學電氣工程與控制科學學院、中國礦業大學、黑龍江大學、江蘇省自動化學會承辦的第六屆機電一體化技術…

INT202 Complexity of Algroithms 算法的復雜度 Pt.2 Search Algorithm 搜索算法

文章目錄 1.樹的數據結構1.1 有序數據(Ordered Data)1.1.1 有序字典&#xff08;Ordered Dictonary&#xff09;1.1.1.1 排序表&#xff08;Sorted Tables&#xff09; 1.2 二分查找&#xff08;Binary Search&#xff09;1.2.1 二分查找的時間復雜度 1.3 二叉搜索樹&#xff0…

【AVRCP】藍牙鏈路控制器(LC)與AVRCP互操作性要求深度解析

目錄 一 、Link Controller&#xff08;LC&#xff09;概述 1.1 LC的定義與功能 1.2 LC在藍牙技術中的重要性 二、Link Controller&#xff08;LC&#xff09;互操作性要求 2.1 互操作性要求概述 2.2 物理層互操作性要求 2.3 鏈路管理互操作性要求 2.4 其他互操作性要求…

高級背景摳圖工具(python)

這是一個專業的圖像背景處理工具,基于Python開發,主要功能包括:1. 智能背景去除 - 使用rembg庫的深度學習模型自動識別并移除圖片背景。 2. 背景自定義 - 支持純色背景替換,保留透明通道(Alpha通道)。3. 高級參數調節 - 提供5種專業級圖像處理參數。4. 實時預覽 - 雙窗口…

如何設計外貿郵件開發信主題

開發信是打開客戶大門的第一步&#xff0c;而郵件主題則是決定客戶是否打開郵件的關鍵。一個吸引人的主題不僅能提高打開率&#xff0c;還能為后續溝通打下良好基礎。 一、突出價值和利益 郵件主題要直接傳達收件人能從中獲得的價值和利益&#xff0c;引起他們的興趣和關注。…

wordpress表單插件CF7調用方式

Contact Form 7(CF7)是WordPress中非常流行的表單插件&#xff0c;以下是其常見的調用方式&#xff1a; 通過短代碼調用 在頁面或文章編輯器中添加&#xff1a;完成表單設置后&#xff0c;復制表單對應的短代碼&#xff0c;然后在需要顯示表單的頁面或文章的編輯器中直接粘貼…