緩存設計模式

緩存設計模式(Cache Design Pattern)是一種用于存儲和管理頻繁訪問數據的技術,旨在提高系統性能、降低數據庫或后端服務的負載,并減少數據訪問延遲。以下是幾種常見的緩存設計模式,并用 Python + Redis 進行示例代碼實現:


📌 1. Cache Aside(旁路緩存)

🌟 適用場景:

  • 適用于讀多寫少的場景,如商品詳情、用戶資料等。
  • 應用先從緩存中讀取數據,緩存未命中時再查詢數據庫,并將數據寫入緩存。

📝 邏輯流程:

  1. 先查詢緩存,如果命中,則直接返回數據。
  2. 如果緩存未命中,則查詢數據庫,并將查詢結果寫入緩存。
  3. 返回數據庫查詢的數據。

🚀 代碼示例

import redis
import timeredis_client = redis.StrictRedis(host="localhost", port=6379, decode_responses=True)def get_data_cache_aside(key):cache_key = f"cache:{key}"# Step 1: 先查詢緩存data = redis_client.get(cache_key)if data:return data  # 直接返回緩存數據# Step 2: 緩存未命中,查詢數據庫data = query_database(key)# Step 3: 回填緩存if data:redis_client.setex(cache_key, 60, data)  # 緩存 60sreturn datadef query_database(key):"""模擬數據庫查詢"""time.sleep(1)  # 模擬查詢時間return f"data_for_{key}"# 測試
print(get_data_cache_aside("hot_item"))

存在的問題:旁路緩存(Cache Aside)如果數據庫中的數據為空(例如,數據確實不存在或者被刪除了),那么每次查詢都會緩存未命中,導致系統一直查數據庫,這就是緩存穿透(Cache Penetration)問題。

🎯 解決方案

? 方案 1:緩存空值

  • 思路:如果數據庫查詢結果為空,則緩存一個“空值”(如 null 或特殊占位符),并設置較短的 TTL(如 5~10 秒)。
  • 好處:防止短時間內同一個 Key 反復查詢數據庫。
🔹 改進的旁路緩存代碼
import redis
import timeredis_client = redis.StrictRedis(host="localhost", port=6379, decode_responses=True)def get_data_cache_aside(key):cache_key = f"cache:{key}"# Step 1: 查詢緩存data = redis_client.get(cache_key)if data is not None:  # 即使數據為空(緩存的空值),也不會去數據庫查詢return None if data == "NULL" else data# Step 2: 查詢數據庫data = query_database(key)# Step 3: 數據為空,緩存“空值”并設置較短過期時間if data is None:redis_client.setex(cache_key, 10, "NULL")  # 10秒緩存空值,避免短時間重復查庫return None# Step 4: 數據有效,回填緩存redis_client.setex(cache_key, 60, data)  # 60秒緩存數據return datadef query_database(key):"""模擬數據庫查詢"""time.sleep(1)  # 模擬數據庫查詢時間return None  # 假設數據不存在# 測試
print(get_data_cache_aside("hot_item"))  # 第一次查數據庫
print(get_data_cache_aside("hot_item"))  # 直接命中緩存(返回 None,不查數據庫)

? 效果

  • 第一次查詢,數據庫返回 None,緩存 "NULL" 并設置 10 秒過期。
  • 10 秒內相同的 Key 再次查詢時,直接返回 None不會重復訪問數據庫
  • 10 秒后,緩存過期,才會重新查詢數據庫。

? 方案 2:布隆過濾器(Bloom Filter)

  • 思路:使用布隆過濾器(Bloom Filter)提前判斷數據是否存在,不存在的 Key 直接返回 None,不查詢數據庫。
  • 適用場景:大規模 Key 查詢,如用戶 ID、商品 ID
🔹 代碼示例
from bloom_filter import BloomFilter  # 需要安裝 pip install bloom-filter2# 初始化布隆過濾器(假設有 10 萬個 Key,誤判率 0.01)
bloom = BloomFilter(max_elements=100000, error_rate=0.01)# 預先加入一些存在的 Key
bloom.add("valid_key_1")
bloom.add("valid_key_2")def get_data_bloom_filter(key):cache_key = f"cache:{key}"# Step 1: 先檢查布隆過濾器,數據可能不存在if key not in bloom:return None  # 直接返回,不查數據庫# Step 2: 查詢緩存data = redis_client.get(cache_key)if data is not None:return None if data == "NULL" else data# Step 3: 查詢數據庫data = query_database(key)# Step 4: 緩存數據或空值if data is None:redis_client.setex(cache_key, 10, "NULL")return Noneredis_client.setex(cache_key, 60, data)return data# 測試
print(get_data_bloom_filter("invalid_key"))  # 布隆過濾器攔截,直接返回 None
print(get_data_bloom_filter("valid_key_1"))  # 查詢緩存或數據庫

? 效果

  • 布隆過濾器攔截不存在的數據,避免數據庫查詢
  • 誤判率極低(0.01%),但不會產生誤漏。

? 方案 3:限流 & 黑名單

  • 思路:針對異常高頻查詢某個 Key 的請求,可以加入限流機制黑名單,防止惡意攻擊導致數據庫壓力過大。
🔹 代碼示例
from collections import defaultdictrequest_count = defaultdict(int)def get_data_with_rate_limit(key):cache_key = f"cache:{key}"# Step 1: 統計 Key 查詢次數request_count[key] += 1if request_count[key] > 100:  # 限制單個 Key 短時間內查詢次數return "Too Many Requests"  # 直接拒絕請求# Step 2: 查詢緩存data = redis_client.get(cache_key)if data is not None:return None if data == "NULL" else data# Step 3: 查詢數據庫data = query_database(key)# Step 4: 緩存數據或空值if data is None:redis_client.setex(cache_key, 10, "NULL")return Noneredis_client.setex(cache_key, 60, data)return data

? 效果

  • 限制某個 Key 頻繁查詢,避免惡意攻擊導致數據庫崩潰。
  • 結合 IP 級別限流,進一步防護。

📌 總結

方案適用場景優勢缺點
緩存空值任何緩存穿透情況簡單高效,防止短時間重復查詢額外占用 Redis 空間
布隆過濾器大量 Key 查詢,如用戶 ID、商品 ID高效過濾無效 Key,減少數據庫壓力需要額外存儲布隆過濾器
限流 & 黑名單惡意攻擊、異常高頻請求防止惡意攻擊,減少數據庫壓力需要額外維護限流規則

🚀 推薦方案組合

  • 一般情況:? 緩存空值
  • 海量 Key 級別:? 布隆過濾器 + 緩存空值
  • 防惡意攻擊:? 限流 & 黑名單

這樣可以有效防止緩存穿透,保護數據庫不被高并發打崩!🔥

📌 2. Read-Through(讀穿透)

🌟 適用場景:

  • 適用于需要自動填充緩存的情況,例如 CDN、NoSQL 代理等。
  • 讀請求永遠只訪問緩存,如果緩存未命中,由緩存層自動加載數據。

📝 邏輯流程:

  1. 讀取數據時,應用程序只訪問緩存。
  2. 如果緩存未命中,則緩存層自動從數據庫加載數據并緩存
  3. 數據返回給用戶。

🚀 代碼示例

class Cache:def __init__(self):self.redis_client = redis.StrictRedis(host="localhost", port=6379, decode_responses=True)def get(self, key):"""從緩存讀取數據"""data = self.redis_client.get(key)if not data:data = self.load_from_db(key)  # 由緩存層自動加載self.redis_client.setex(key, 60, data)  # 緩存 60sreturn datadef load_from_db(self, key):"""模擬數據庫查詢"""time.sleep(1)  # 模擬查詢時間return f"data_for_{key}"# 測試
cache = Cache()
print(cache.get("hot_item"))

存在的問題:讀穿透(Read-Through)同樣可能會遇到類似的緩存穿透問題。如果緩存未命中且查詢結果為空(例如,數據庫中沒有該數據),那么每次查詢都會去數據庫查詢,造成重復查詢數據庫的情況。

🎯 解決方案

與旁路緩存的解決方案一致


📌 3. Write-Through(寫穿透)

🌟 適用場景:

  • 適用于寫多讀少的情況,如用戶狀態、計數器等。
  • 所有寫操作都會先更新緩存,然后再更新數據庫,確保緩存和數據庫的一致性。

📝 邏輯流程:

  1. 當數據寫入時,先更新緩存,再更新數據庫。
  2. 讀請求仍然直接從緩存讀取數據。

🚀 代碼示例

class Cache:def __init__(self):self.redis_client = redis.StrictRedis(host="localhost", port=6379, decode_responses=True)def set(self, key, value):"""寫入緩存 + 數據庫"""self.redis_client.setex(key, 60, value)  # 先寫入緩存self.write_to_db(key, value)  # 再寫入數據庫def get(self, key):"""讀取緩存"""return self.redis_client.get(key)def write_to_db(self, key, value):"""模擬數據庫寫入"""print(f"數據 {key} 已寫入數據庫: {value}")# 測試
cache = Cache()
cache.set("user:123", "UserData")
print(cache.get("user:123"))

存在問題
一致性問題:緩存和數據庫可能在短時間內處于不同步的狀態。例如,如果緩存成功寫入,但數據庫寫入失敗,數據就不一致了。為了避免這種情況,你可能需要引入一些機制來確保最終一致性,如 異步寫入 或 消息隊列 來處理數據庫更新

錯誤處理和重試:如果數據庫寫入失敗,如何處理這個問題是一個關鍵點。可以考慮將數據庫寫入操作異步化,或者通過定期的任務檢查數據一致性并做修復。


📌 4. Write-Behind(異步寫回)

🌟 適用場景:

  • 適用于高并發寫入,如日志存儲、計數器等。
  • 先寫入緩存,異步批量更新數據庫,提高寫入性能。

📝 邏輯流程:

  1. 寫操作只寫入緩存,不直接更新數據庫。
  2. 后臺異步線程定期批量同步緩存數據到數據庫

🚀 代碼示例

import threadingclass Cache:def __init__(self):self.redis_client = redis.StrictRedis(host="localhost", port=6379, decode_responses=True)self.batch_data = {}  # 臨時存儲待寫入的數據def set(self, key, value):"""寫入緩存"""self.redis_client.setex(key, 60, value)  # 先寫入緩存self.batch_data[key] = value  # 添加到待寫入數據庫的批量任務def get(self, key):"""讀取緩存"""return self.redis_client.get(key)def batch_write_to_db(self):"""批量寫入數據庫(定期執行)"""while True:if self.batch_data:for key, value in self.batch_data.items():print(f"批量寫入數據庫: {key} -> {value}")self.batch_data.clear()time.sleep(5)  # 每 5 秒寫入一次# 啟動異步寫線程
cache = Cache()
threading.Thread(target=cache.batch_write_to_db, daemon=True).start()# 測試
cache.set("user:123", "UserData")
cache.set("user:124", "UserData2")
print(cache.get("user:123"))
time.sleep(6)  # 等待異步寫入數據庫

📌 5. 分布式鎖(Mutex + Double Check)

🌟 適用場景:

  • 解決緩存擊穿問題,防止多個線程同時查詢數據庫。
  • 適用于高并發的熱點數據,如商品詳情、排行榜等。

📝 邏輯流程:

  1. 先查詢緩存,如果命中,則直接返回數據。
  2. 如果緩存未命中,嘗試獲取 Redis 互斥鎖
    • 獲取鎖成功:查詢數據庫,并回填緩存,最后釋放鎖。
    • 獲取鎖失敗:等待一段時間后重試,避免并發查詢數據庫。

🚀 代碼示例

import uuiddef get_data_with_mutex(key):cache_key = f"cache:{key}"lock_key = f"lock:{key}"lock_value = str(uuid.uuid4())# 先查詢緩存data = redis_client.get(cache_key)if data:return data# 嘗試獲取鎖if redis_client.set(lock_key, lock_value, nx=True, ex=5):  try:# 再次檢查緩存data = redis_client.get(cache_key)if data:return data# 查詢數據庫data = query_database(key)# 回填緩存redis_client.setex(cache_key, 60, data)return datafinally:if redis_client.get(lock_key) == lock_value:redis_client.delete(lock_key)else:# 其他線程等待后重試time.sleep(0.2)return get_data_with_mutex(key)# 測試
print(get_data_with_mutex("hot_item"))

🎯 總結

設計模式適用場景主要特點
Cache Aside讀多寫少業務代碼控制緩存邏輯
Read-Through讀寫頻繁讀請求只訪問緩存,自動回填
Write-Through寫多讀少寫請求先更新緩存,再寫數據庫
Write-Behind高并發寫先寫緩存,異步批量寫數據庫
分布式鎖緩存擊穿互斥鎖防止多個線程并發查詢數據庫

不同的緩存模式可以根據實際的業務需求和系統架構進行組合使用。組合的依據主要取決于以下幾個因素:

1. 一致性要求

不同緩存模式的選擇和組合通常取決于對數據一致性的要求:

  • 強一致性:如果你需要確保緩存和數據庫的嚴格一致性,可以使用 寫緩存(Write-Through Cache),確保每次數據寫入時,緩存和數據庫同步更新。這可以保證數據庫和緩存中的數據始終一致。
  • 最終一致性:如果你可以接受數據暫時的不一致(例如稍微過期或延遲同步),可以考慮使用 寫回緩存(Write-Back Cache),將數據先寫入緩存,異步地將數據寫入數據庫,從而減少對數據庫的壓力。

2. 數據讀取頻率與緩存容量

  • 高讀取頻率、低寫入頻率:如果應用程序主要是讀取數據,且數據不經常變化,可以使用 讀緩存(Read-Through Cache)旁路緩存(Cache-Aside)。通過將熱點數據加載到緩存中,減少對數據庫的查詢壓力。
  • 高寫入頻率、低讀取頻率:如果寫操作頻繁,但讀取較少(如日志系統),可以考慮 寫回緩存(Write-Back Cache),這樣可以減少對數據庫的寫入頻率,提高性能。

3. 緩存失效策略

  • 過期時間(TTL):某些數據可能會在一定時間后過期,可以結合 過期緩存(TTL) 策略使用。例如,你可以在使用 旁路緩存(Cache-Aside) 時設置一個緩存過期時間,確保緩存中不存儲過時數據,同時保證不頻繁查詢數據庫。
  • 清除策略:結合 LRU(Least Recently Used) 等緩存淘汰策略,當緩存容量達到上限時,移除不常用的數據。

4. 緩存失效和更新策略

  • 緩存雪崩(Cache Avalanche):當大量緩存同時失效時,可能導致大量請求直接打到數據庫上,造成壓力。通過 設置不同的過期時間分布式緩存策略,可以避免緩存雪崩現象。組合使用 緩存分片(Sharded Cache)緩存預熱 等方式,有助于平衡緩存更新帶來的負擔。

  • 緩存穿透(Cache Penetration):如果某些數據根本不在緩存中(例如查詢不存在的用戶信息),則每次都會訪問數據庫。可以通過 布隆過濾器(Bloom Filter) 或其他方式來過濾這些請求,減少數據庫壓力。

5. 性能與可擴展性需求

  • 分布式緩存:如果系統需要高可擴展性,可以使用 分布式緩存(Sharded Cache),將數據分片存儲到多個緩存節點中,保證高并發時的緩存命中率,同時避免單個緩存節點的負載過高。
  • 異步緩存更新:可以使用消息隊列或后臺任務來異步更新緩存和數據庫,避免在請求處理過程中進行阻塞操作。

常見的緩存模式組合

組合 1:讀緩存(Read-Through Cache) + 過期緩存(TTL)
  • 適用場景:讀取頻繁、數據變化不大。
  • 組合原因:緩存數據可以在一段時間后過期,從而確保緩存不存儲過時的數據,同時不需要每次都查詢數據庫。
def read_through_with_ttl(key):value = cache.get(key)if value is None:value = db.get(key)cache.set(key, value, ex=3600)  # 1 小時后過期return value
組合 2:旁路緩存(Cache-Aside) + LRU 淘汰策略
  • 適用場景:數據訪問較不頻繁,但需要緩存熱點數據。
  • 組合原因:通過 LRU 策略確保緩存中存儲的始終是最近訪問的數據,而不需要每次都清理整個緩存。
def cache_aside_lru(key):value = cache.get(key)if value is None:value = db.get(key)cache.set(key, value)return value
組合 3:寫回緩存(Write-Back Cache) + 過期緩存(TTL)
  • 適用場景:寫入頻繁,且數據庫更新不需要立刻完成。
  • 組合原因:緩存首先寫入,而后臺異步更新數據庫,同時使用緩存過期時間,確保不存儲過時的數據。
def write_back_with_ttl(key, value):cache.set(key, value)  # 先寫緩存write_queue.append((key, value))  # 異步寫數據庫cache.expire(key, 3600)  # 設置過期時間
組合 4:分片緩存(Sharded Cache) + 寫回緩存(Write-Back Cache)
  • 適用場景:數據量巨大,且需要分布式緩存支持。
  • 組合原因:將緩存數據分布到不同的節點,并且使用寫回緩存減少數據庫訪問頻率,提高系統性能。

總結

緩存模式組合的依據主要取決于你的應用場景,特別是數據一致性要求、性能需求、讀取/寫入頻率、以及緩存過期策略等因素。在實際開發中,可以根據具體的需求靈活選擇或組合這些模式,以達到最佳的系統性能和數據一致性。

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

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

相關文章

Java算法隊列和棧經常用到的ArrayDeque

主要是記錄一下add,push,poll這三個常用api,因為這三個就是棧和隊列一念之差的關鍵 1.add(E e) 方法 ?作用:將元素添加到雙端隊列的尾部?(等價于 addLast(E e))。?行為: ?成功時&#xff1…

機器學習——一元線性回歸(算法實現與評估)

一元線性回歸是統計學中最基礎的回歸分析方法,用于建立兩個變量之間的線性關系模型。 1. 模型表達式 一元線性回歸的數學模型為: :因變量(預測值):自變量(輸入變量):回…

Ubuntu下用QEMU模擬運行OpenBMC

1、前言 在調試過程中,安裝了很多依賴庫,具體沒有記錄。關于kvm,也沒理清具體有什么作用。本文僅記錄,用QEMU成功的將OpenBMC跑起來的過程,做備忘,也供大家參考。 2、環境信息 VMware Workstation 15 Pro…

Gradle/Maven 本地倉庫默認路徑遷移 (減少系統磁盤占用)

Gradle 配置環境變量 GRADLE_USER_HOME,如D:/.gradle同時將 %userprofile%/.gradle 移動到配置路徑 Maven 修改settings.xml文件,localRepository同時將 %userprofile%/.m2/repository 移動到配置路徑 IDEA默認用的bundle maven, 路徑為安裝目錄下 p…

MinGW與使用VScode寫C語言適配

壓縮包 通過網盤分享的文件:MinGW.zip 鏈接: https://pan.baidu.com/s/1QB-Zkuk2lCIZuVSHc-5T6A 提取碼: 2c2q 需要下載的插件 1.翻譯 找到VScode頁面,從上數第4個,點擊擴展(以下通此) 搜索---Chinese--點擊---安裝--o…

【C++初階】從零開始模擬實現vector(含迭代器失效詳細講解)

目錄 1、基本結構 1.1成員變量 1.2無參構造函數 1.3有參構造函數 preserve()的實現 代碼部分: push_back()的實現 代碼部分: 代碼部分: 1.4拷貝構造函數 代碼部分: 1.5支持{}初始化的構造函數 代碼部分: …

Java實習生面試題(2025.3.23 be)

一、v-if與v-show的區別 v-show 和 v-if 都是 Vue 中的條件渲染指令,它們的主要區別在于渲染策略:v-if 會根據條件決定是否編譯元素,而 v-show 則始終編譯元素,只是通過改變 CSS 的 display 屬性來控制顯示與隱藏。 二、mybatis-…

stm32標準庫開發需要的基本文件結構

使用STM32標準庫(STM32 Standard Peripheral Library,SPL)開發時,項目中必須包含一些必要的文件,這些文件確保項目能夠正常運行并與MCU硬件交互。以下詳細說明: 一、標準庫核心文件夾說明 使用標準庫開發S…

學生管理系統(需求文檔)

需求: 采取控制臺的方式去書寫學生管理系統 分析: 初始菜單: “----------歡迎來到java學生管理系統----------” “1:添加學生” “2:刪除學生” “3:修改學生” “4:查詢學生” “5:…

Java算法OJ(13)雙指針

目錄 1.前言 2.正文 2.1快樂數 2.2盛最多水的容器 2.3有效的三角形的個數 2.4和為s的兩個數 2.5三數之和 2.6四數之和 3.小結 1.前言 哈嘍大家好吖,今天繼續加練算法題目,一共六道雙指針,希望能對大家有所幫助,廢話不多…

SpringBoot分布式定時任務實戰:告別重復執行的煩惱

場景再現:你剛部署完基于SpringBoot的集群服務,凌晨3點突然收到監控告警——優惠券發放量超出預算兩倍!檢查日志發現,兩個節點同時執行了定時任務。這種分布式環境下的定時任務難題,該如何徹底解決? 本文將…

MySQL 設置允許遠程連接完整指南:安全與效率并重

一、為什么需要遠程連接MySQL? 在分布式系統架構中,應用程序與數據庫往往部署在不同服務器。例如: Web服務器(如NginxPHP)需要連接獨立的MySQL數據庫數據分析師通過BI工具直連生產庫多服務器集群間的數據同步 但直接…

系統架構書單推薦(一)領域驅動設計與面向對象

本文主要是個人在學習過程中所涉獵的一些經典書籍,有些已經閱讀完,有些還在閱讀中。于我而言,希望追求軟件系統設計相關的原則、方法、思想、本質的東西,并希望通過不斷的學習、實踐和積累,提升自身的知識和認知。希望…

動態規劃-01背包

兜兜轉轉了半天,發現還是Carl寫的好。 看過動態規劃-基礎的讀者,大概都清楚。 動態規劃是將大問題,分解成子問題。并將子問題的解儲存下來,避免重復計算。 而背包問題,就是動態規劃延申出來的一個大類。 而01背包&…

使用VS2022編譯CEF

前提 選擇編譯的版本 CEF自動編譯,在這里可以看到最新的穩定版和Beta版。 從這里得出,最新的穩定版是134.0.6998.118,對應的cef branch是6998。通過這個信息可以在Build requirements查到相關的軟件配置信息。 這里主要看Windows下的編譯要…

C++20:玩轉 string 的 starts_with 和 ends_with

文章目錄 一、背景與動機二、string::starts_with 和 string::ends_with(一)語法與功能(二)使用示例1\. 判斷字符串開頭2\. 判斷字符串結尾 (三)優勢 三、string_view::starts_with 和 string_view::ends_w…

智能飛鳥監測 守護高壓線安全

飛鳥檢測新紀元:視覺分析技術的革新應用 在現代化社會中,飛鳥檢測成為了多個領域不可忽視的重要環節。無論是高壓線下的安全監測、工廠內的生產秩序維護,還是農業區的作物保護,飛鳥檢測都扮演著至關重要的角色。傳統的人工檢測方…

ADC噪聲全面分析 -04- 有效噪聲帶寬簡介

為什么要了解ENBW? 了解模數轉換器 (ADC) 噪聲可能具有挑戰性,即使對于最有經驗的模擬設計人員也是如此。 Delta-sigma ADC 具有量化和熱噪聲的組合,這取決于 ADC 的分辨率、參考電壓和輸出數據速率 (ODR)。 在系統級別,額外的信…

STM32單片機uCOS-Ⅲ系統10 內存管理

目錄 一、內存管理的基本概念 二、內存管理的運作機制 三、內存管理的應用場景 四、內存管理函數接口講解 1、內存池創建函數 OSMemCreate() 2、內存申請函數 OSMemGet() 3、內存釋放函數 OSMemPut() 五、實現 一、內存管理的基本概念 在計算系統中,變量、中…

考研課程安排(自用)

文章目錄 408數據結構(王道)計算機組成原理(王道)操作系統(王道)計算機網絡(湖科大版) 數學一高等數學(微積分)線性代數和概率論 408 數據結構(王…