使用SQLite實現(xiàn)CacheHelper的示例代碼
1. 概述
CacheHelper 是一個基于 SQLite 的靜態(tài)緩存工具類,旨在為 .NET 應用程序提供一個簡單、高效、持久化且線程安全的緩存解決方案。它將緩存數(shù)據(jù)存儲在應用程序根目錄下的 cache.db 文件中,這意味著即使應用程序重啟,緩存數(shù)據(jù)依然存在(只要未過期)。
該助手類封裝了常見的緩存操作,并內(nèi)置了絕對過期和滑動過期兩種策略,通過“Get-Or-Set”模式極大地簡化了從數(shù)據(jù)源(如數(shù)據(jù)庫、API)獲取并緩存數(shù)據(jù)的業(yè)務邏輯。
2. 安裝與環(huán)境準備 (Prerequisites)
要使 CacheHelper 類正常工作,您必須在您的項目中安裝以下兩個核心的 NuGet 包。
您可以使用 .NET CLI 命令來安裝它們:
# 1. 用于操作 SQLite 數(shù)據(jù)庫 dotnet add package Microsoft.Data.Sqlite # 2. 用于高效的對象序列化/反序列化 dotnet add package MessagePack
或者通過 Visual Studio 的 NuGet 包管理器搜索并安裝 Microsoft.Data.Sqlite 和 MessagePack。
封裝好的方法如下:
using Microsoft.Data.Sqlite;
using System;
using System.IO;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using pei_repspark_admin_webapi.Entities.Constants;
using MessagePack.Resolvers;
using MessagePack; // 請確保您的常量命名空間正確
namespace YOU_PROJECT.Utils
{
public static class CacheHelper
{
private static readonly string _dbPath;
private static readonly string _connectionString;
private static readonly MessagePackSerializerOptions _serializerOptions = ContractlessStandardResolver.Options;
private static readonly int default_limit = CacheConstants.FILTER_LIMIT;
// 用于線程安全的鎖管理器,確保每個 key 都有一個獨立的鎖對象
private static readonly ConcurrentDictionary<string, object> _locks = new ConcurrentDictionary<string, object>();
/// <summary>
/// 靜態(tài)構(gòu)造函數(shù),在類第一次被訪問時自動運行一次,用于初始化數(shù)據(jù)庫。
/// </summary>
static CacheHelper()
{
// 將數(shù)據(jù)庫文件放在應用程序的根目錄下,確保路徑一致性
_dbPath = Path.Combine(AppContext.BaseDirectory, "cache.db");
_connectionString = $"Data Source={_dbPath}";
InitializeDatabase();
}
/// <summary>
/// 初始化數(shù)據(jù)庫:如果數(shù)據(jù)庫文件或表不存在,則創(chuàng)建它們。
/// </summary>
private static void InitializeDatabase()
{
try
{
using var connection = new SqliteConnection(_connectionString);
connection.Open();
// 創(chuàng)建一個可重用的 command 對象
var command = connection.CreateCommand();
// 1. 執(zhí)行 PRAGMA 指令以優(yōu)化并發(fā)性能
command.CommandText = "PRAGMA journal_mode=WAL;";
command.ExecuteNonQuery();
// 2. 重用同一個 command 對象,執(zhí)行 CREATE TABLE 指令
command.CommandText = @"
CREATE TABLE IF NOT EXISTS CacheStore (
Key TEXT NOT NULL PRIMARY KEY,
Value BLOB NOT NULL,
InsertionTimeUtc INTEGER NOT NULL,
ExpirationTimeUtc INTEGER NOT NULL,
SlidingExpirationMinutes INTEGER NOT NULL
);";
command.ExecuteNonQuery();
}
catch (Exception ex)
{
// 如果數(shù)據(jù)庫初始化失敗,這是個嚴重問題,需要記錄下來
// 在生產(chǎn)環(huán)境中,應使用專業(yè)的日志庫(如 Serilog, NLog)
($"[CACHE CRITICAL] Database initialization failed. Error: {ex.Message}").LogErr();
// 拋出異常,因為如果數(shù)據(jù)庫無法初始化,整個緩存服務都無法工作
throw;
}
}
#region Public Cache Modification Methods (Add, Clean)
public static void AddCacheWithAbsolute(string key, object value, int minute)
{
Set(key, value, TimeSpan.FromMinutes(minute), isSliding: false);
}
public static void AddCacheWithRelative(string key, object value, int minute)
{
Set(key, value, TimeSpan.FromMinutes(minute), isSliding: true);
}
public static void AddCache(string key, object value)
{
// 對于永久緩存,我們設(shè)置一個極大的過期時間
var longLivedTimeSpan = TimeSpan.FromDays(30); // 30 days
Set(key, value, longLivedTimeSpan, isSliding: false);
}
public static void CleanCache(string key)
{
try
{
using var connection = new SqliteConnection(_connectionString);
connection.Open();
var command = connection.CreateCommand();
command.CommandText = "DELETE FROM CacheStore WHERE Key = $key;";
command.Parameters.AddWithValue("$key", key);
command.ExecuteNonQuery();
}
catch (Exception ex)
{
($"[CACHE ERROR] Failed to clean cache for key '{key}'. Error: {ex.Message}").LogErr();
}
}
#endregion
#region Public Cache Retrieval Methods (Get, GetOrQuery)
public static object? GetCache(string key)
{
var (found, value) = TryGetCache<object>(key);
return found ? value : null;
}
public static T GetCacheOrQuery<T>(string key, Func<T> myFunc)
{
return GetCacheOrQuery<T>(key, default_limit, myFunc);
}
public static T GetCacheOrQuery<T>(string key, int minute, Func<T> myFunc)
{
return GetOrSet(key, minute, () => myFunc());
}
public static r GetCacheOrQuery<p, r>(string key, Func<p, r> myFunc, p param1)
{
return GetCacheOrQuery<p, r>(key, default_limit, myFunc, param1);
}
public static r GetCacheOrQuery<p, r>(string key, int minter, Func<p, r> myFunc, p param1)
{
return GetOrSet(key, minter, () => myFunc(param1));
}
// --- 為了簡潔,這里省略了剩余的 GetCacheOrQuery 重載 ---
// --- 您可以按照下面的 `GetOrSet` 模式輕松地實現(xiàn)它們 ---
// 示例:
public static r GetCacheOrQuery<p1, p2, r>(string key, int minter, Func<p1, p2, r> myFunc, p1 param1, p2 param2)
{
return GetOrSet(key, minter, () => myFunc(param1, param2));
}
public static r GetCacheOrQuery<p1, p2, r>(string key, Func<p1, p2, r> myFunc, p1 param1, p2 param2)
{
return GetCacheOrQuery(key, default_limit, myFunc, param1, param2);
}
// ... 請為其他所有重載方法應用相同的模式 ...
#endregion
#region Core Logic (Private Methods)
/// <summary>
/// 核心的 "Get-Or-Set" 方法,實現(xiàn)了雙重檢查鎖定以確保線程安全。
/// </summary>
private static T GetOrSet<T>(string key, int minute, Func<T> queryFunc)
{
// 第一次檢查(在鎖之外),這是為了在緩存命中的情況下獲得最高性能
var (found, value) = TryGetCache<T>(key);
if (found)
{
Console.WriteLine($"Get [{key}] cache, survival time is [{minute}] minutes");
return value!;
}
// 獲取或為當前 key 創(chuàng)建一個唯一的鎖對象
var lockObject = _locks.GetOrAdd(key, k => new object());
// 進入鎖代碼塊,確保同一時間只有一個線程能為這個 key 生成緩存
lock (lockObject)
{
// 第二次檢查(在鎖之內(nèi)),防止在等待鎖的過程中,其他線程已經(jīng)生成了緩存
(found, value) = TryGetCache<T>(key);
if (found)
{
return value!;
}
// 執(zhí)行昂貴的數(shù)據(jù)查詢操作
var result = queryFunc();
// 將查詢結(jié)果存入緩存
if (result != null)
{
Set(key, result, TimeSpan.FromMinutes(minute), isSliding: false);
}
Console.WriteLine($"Added [{key}] cache, survival time is [{minute}] minutes");
return result;
}
}
/// <summary>
/// 統(tǒng)一的緩存寫入方法。
/// </summary>
private static void Set(string key, object value, TimeSpan expiration, bool isSliding)
{
if (value == null) return;
try
{
var serializedValue = MessagePackSerializer.Serialize(value, _serializerOptions);
var now = DateTime.UtcNow;
using var connection = new SqliteConnection(_connectionString);
connection.Open();
var command = connection.CreateCommand();
command.CommandText = @"
INSERT OR REPLACE INTO CacheStore (Key, Value, InsertionTimeUtc, ExpirationTimeUtc, SlidingExpirationMinutes)
VALUES ($key, $value, $insertion, $expiration, $sliding);";
command.Parameters.AddWithValue("$key", key);
command.Parameters.AddWithValue("$value", serializedValue);
command.Parameters.AddWithValue("$insertion", now.Ticks);
command.Parameters.AddWithValue("$expiration", now.Add(expiration).Ticks);
command.Parameters.AddWithValue("$sliding", isSliding ? expiration.TotalMinutes : 0);
command.ExecuteNonQuery();
}
catch (Exception ex)
{
($"[CACHE ERROR] Failed to set cache for key '{key}'. Error: {ex.Message}").LogErr();
// 吞掉異常,保證主程序繼續(xù)運行
}
}
/// <summary>
/// 嘗試從緩存中獲取數(shù)據(jù),并處理滑動過期的更新邏輯。
/// </summary>
private static (bool found, T? value) TryGetCache<T>(string key)
{
try
{
using var connection = new SqliteConnection(_connectionString);
connection.Open();
var command = connection.CreateCommand();
command.CommandText = @"
SELECT Value, SlidingExpirationMinutes FROM CacheStore
WHERE Key = $key AND ExpirationTimeUtc > $now;";
command.Parameters.AddWithValue("$key", key);
command.Parameters.AddWithValue("$now", DateTime.UtcNow.Ticks);
using var reader = command.ExecuteReader();
if (reader.Read())
{
var blob = reader.GetFieldValue<byte[]>(0);
var slidingMinutes = reader.GetInt64(1);
// 如果是滑動過期項,則更新其過期時間
if (slidingMinutes > 0)
{
try
{
var updateCmd = connection.CreateCommand();
updateCmd.CommandText = "UPDATE CacheStore SET ExpirationTimeUtc = $newExpiration WHERE Key = $key;";
updateCmd.Parameters.AddWithValue("$key", key);
updateCmd.Parameters.AddWithValue("$newExpiration", DateTime.UtcNow.AddMinutes(slidingMinutes).Ticks);
updateCmd.ExecuteNonQuery();
}
catch (Exception updateEx)
{
// 滑動過期更新失敗不是致命錯誤,只記錄警告
($"[CACHE WARNING] Failed to update sliding expiration for key '{key}'. Error: {updateEx.Message}").LogErr();
}
}
var deserializedValue = MessagePackSerializer.Deserialize<T>(blob, _serializerOptions);
return (true, deserializedValue);
}
}
catch (Exception ex)
{
($"[CACHE ERROR] Failed to get cache for key '{key}'. Error: {ex.Message}").LogErr();
}
// 如果發(fā)生任何錯誤或未找到,都返回“未命中”
return (false, default);
}
#endregion
}
}
3. 公共 API 參考 (封裝方法說明)
這是與 CacheHelper 交互的公共方法列表。
3.1 核心模式:獲取或查詢 (Get-Or-Set)
這是最推薦的使用方式。它將“檢查緩存、執(zhí)行查詢、設(shè)置緩存”的邏輯封裝為一步,確保了代碼的簡潔和線程安全。
GetCacheOrQuery<...>(...)
描述: 嘗試根據(jù)
key從緩存中獲取數(shù)據(jù)。如果緩存存在且未過期,則直接返回緩存數(shù)據(jù);否則,執(zhí)行您提供的查詢方法 (myFunc) 來獲取最新數(shù)據(jù),然后將結(jié)果存入緩存,并最終返回該結(jié)果。重載 (Overloads): 該方法提供多個重載版本,以支持無參、單參數(shù)、雙參數(shù)等不同簽名的查詢方法。
參數(shù):
string key: 緩存的唯一標識符。int minute(可選): 緩存的有效期(分鐘)。如果未提供,將使用一個默認值(例如60分鐘)。Func<...> myFunc: 一個委托或 Lambda 表達式。當緩存未命中時,此函數(shù)將被調(diào)用以獲取數(shù)據(jù)。p1, p2, ...(可選): 傳遞給myFunc的參數(shù)。
示例:
// 示例1: 無參數(shù)的查詢 string allProductsKey = "products:all"; var products = CacheHelper.GetCacheOrQuery(allProductsKey, 30, () => { // 這段代碼只會在緩存未命中時執(zhí)行 Console.WriteLine("從數(shù)據(jù)庫獲取所有產(chǎn)品..."); return database.GetAllProducts(); }); // 示例2: 帶一個參數(shù)的查詢 int userId = 123; string userKey = $"user:{userId}"; var user = CacheHelper.GetCacheOrQuery(userKey, 60, (id) => { // 這段代碼只會在緩存未命中時執(zhí)行 Console.WriteLine($"從數(shù)據(jù)庫獲取ID為 {id} 的用戶..."); return database.GetUserById(id); }, userId);
3.2 直接緩存管理
這些方法允許您更直接地控制緩存的添加和更新。
AddCache(string key, object value)
- 描述: 添加一個“永久”緩存(內(nèi)部設(shè)置為10年有效期)。適用于極少變動的基礎(chǔ)數(shù)據(jù)。
- 示例:
CacheHelper.AddCache("global_settings", siteSettings);
AddCacheWithAbsolute(string key, object value, int minute)
- 描述: 添加一個具有絕對過期策略的緩存。緩存將在
minute分鐘后過期,無論期間是否被訪問。 - 示例:
CacheHelper.AddCacheWithAbsolute("daily_report", reportData, 1440); // 緩存24小時
AddCacheWithRelative(string key, object value, int minute)
- 描述: 添加一個具有滑動過期策略的緩存。如果在
minute分鐘內(nèi)沒有被訪問,緩存將過期。每次訪問都會重置其生命周期。常用于用戶會話等場景。 - 示例:
CacheHelper.AddCacheWithRelative("user_session:xyz", sessionData, 20); // 20分鐘不活動則過期
3.3 緩存移除
CleanCache(string key)
- 描述: 從緩存中手動移除一個指定的項。這在底層數(shù)據(jù)更新后,需要強制讓緩存失效時非常有用。
- 示例:
// 更新了用戶ID為123的個人信息 database.UpdateUser(updatedUser); // 立即清除舊的緩存,確保下次請求獲取的是最新數(shù)據(jù) CacheHelper.CleanCache("user:123");
4. 核心特性
- 持久化存儲: 使用 SQLite 文件數(shù)據(jù)庫,緩存內(nèi)容在應用程序重啟后依然保留。
- 線程安全: 采用雙重檢查鎖定(Double-Checked Locking)模式和基于Key的鎖,有效防止在高并發(fā)場景下的“緩存擊穿”問題。
- 高效序列化: 使用
MessagePack對緩存對象進行二進制序列化,相比 JSON 序列化,性能更高,占用空間更小。 - 兩種過期策略:
- 絕對過期 (Absolute Expiration): 緩存項在設(shè)定的固定時間點后失效。
- 滑動過期 (Sliding Expiration): 緩存項在一段時間內(nèi)未被訪問則失效;每次訪問都會重置其生命周期。
- 簡潔的 API: 提供了簡單易用的
GetCacheOrQuery方法,將“檢查緩存、獲取數(shù)據(jù)、存入緩存”的邏輯封裝為原子操作。
5. 核心概念深入解析
5.1 數(shù)據(jù)庫結(jié)構(gòu) (cache.db)
CacheHelper 會自動創(chuàng)建名為 CacheStore 的表,其結(jié)構(gòu)如下:
| 字段名 | 類型 | 描述 |
|---|---|---|
| Key | TEXT | 主鍵。緩存項的唯一標識符。 |
| Value | BLOB | 存儲經(jīng) MessagePack 序列化后的二進制數(shù)據(jù)。 |
| InsertionTimeUtc | INTEGER | 緩存項的創(chuàng)建時間 (UTC Ticks)。 |
| ExpirationTimeUtc | INTEGER | 緩存項的過期時間點 (UTC Ticks)。這是判斷緩存是否有效的核心字段。 |
| SlidingExpirationMinutes | INTEGER | 滑動過期策略的關(guān)鍵。0 表示絕對過期;>0 的值表示這是一個滑動過期的項,其值為滑動的分鐘數(shù)。 |
5.2 滑動過期 (SlidingExpirationMinutes) 的工作原理
SlidingExpirationMinutes 字段的設(shè)計非常巧妙,它同時扮演了**“標記”和“時長”**兩個角色。
設(shè)置緩存時:
- 調(diào)用
AddCacheWithAbsolute時,SlidingExpirationMinutes被設(shè)為0。 - 調(diào)用
AddCacheWithRelative(key, value, 30)時,SlidingExpirationMinutes被設(shè)為30。
- 調(diào)用
獲取緩存時 (
TryGetCache內(nèi)部邏輯):- 系統(tǒng)首先檢查
ExpirationTimeUtc是否已過期。 - 如果未過期且成功讀取數(shù)據(jù),系統(tǒng)會檢查
SlidingExpirationMinutes字段的值。 - 如果值為
0,則不執(zhí)行任何額外操作。 - 如果值大于 0(例如
30),系統(tǒng)識別出這是一個滑動緩存項,會立即執(zhí)行一個UPDATE命令,將該項的ExpirationTimeUtc更新為當前時間 + 30分鐘。
- 系統(tǒng)首先檢查
這個“讀取并續(xù)期”的原子操作,完美地實現(xiàn)了滑動過期的邏輯:只要你在它過期前訪問它,它的生命就在不斷延續(xù)。
6. 并發(fā)安全機制
在高并發(fā)環(huán)境下,多個線程可能同時請求同一個不存在的緩存項。如果沒有鎖定機制,這些線程會全部穿透緩存去執(zhí)行昂貴的數(shù)據(jù)查詢,這就是“緩存擊穿”。
CacheHelper 通過 GetOrSet 方法中的 雙重檢查鎖定模式 解決了這個問題:
- 第一次檢查 (無鎖): 在進入
lock之前快速檢查緩存是否存在。對于絕大多數(shù)緩存命中的情況,可以無鎖返回,性能極高。 - 獲取Key專用鎖: 如果第一次檢查未命中,系統(tǒng)會從一個
ConcurrentDictionary中為當前key獲取一個專用的鎖對象。這確保了對不同key的請求不會互相阻塞。 - 第二次檢查 (有鎖): 在獲得鎖之后,再次檢查緩存。這是為了防止在等待鎖的過程中,已有其他線程完成了數(shù)據(jù)查詢和緩存設(shè)置。
- 執(zhí)行查詢與設(shè)置: 只有當?shù)诙螜z查仍然未命中時,當前線程才會去執(zhí)行數(shù)據(jù)查詢,并將結(jié)果寫入緩存。
這個機制確保了對于任意一個key,在同一時刻最多只有一個線程在執(zhí)行數(shù)據(jù)源的查詢操作。
7. 注意事項與最佳實踐
- 緩存鍵 (Key) 的命名: 緩存鍵應具有唯一性和良好的描述性。推薦使用如
object_type:id的格式,例如user:123或products:all。 - 緩存失效: 當?shù)讓訑?shù)據(jù)發(fā)生變化時(例如,用戶信息被修改),應主動調(diào)用
CacheHelper.CleanCache("user:123")來清除舊緩存,以避免數(shù)據(jù)不一致。 - 可序列化對象: 存入緩存的對象必須能被
MessagePack序列化。絕大多數(shù) POCO (Plain Old C# Object) 對象都沒有問題。 - 異常處理:
CacheHelper內(nèi)部已對數(shù)據(jù)庫操作和序列化等步驟進行了try-catch封裝。緩存操作失敗時會向控制臺輸出錯誤日志,但不會拋出異常中斷主程序流程,保證了系統(tǒng)的穩(wěn)定性。
到此這篇關(guān)于使用SQLite實現(xiàn)CacheHelper的示例代碼的文章就介紹到這了,更多相關(guān)SQLite實現(xiàn)CacheHelper內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
一文讀懂數(shù)據(jù)庫管理工具 Navicat 和 DBeaver
這篇文章主要介紹了數(shù)據(jù)庫管理工具 Navicat 和 DBeaver的相關(guān)資料,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-03-03
clickhouse數(shù)據(jù)庫刪除數(shù)據(jù)的五種方式
clickhouse數(shù)據(jù)庫清理數(shù)據(jù)的方式很多,每種方式都各有自己的優(yōu)缺點,請根據(jù)實際需要采用適合自己的方式,本文將逐一給大家介紹,并通過代碼示例講解的非常詳細,需要的朋友可以參考下2024-03-03
SQLServer 2005 和Oracle 語法的一點差異小結(jié)
Microsoft SQL Server 和Oracle 語法的一點差異小結(jié),需要的朋友可以參考下。2011-04-04
在PostgreSQL的基礎(chǔ)上創(chuàng)建一個MongoDB的副本的教程
這篇文章主要介紹了在PostgreSQL的基礎(chǔ)上創(chuàng)建一個MongoDB的副本的教程,使在使用NoSQL的同時又能用到PostgreSQL中的東西,需要的朋友可以參考下2015-04-04
sql連接查詢語句中on、where篩選的區(qū)別總結(jié)
接觸Sql語句時間挺長時間了,聽他人說過sql語句的連接查詢,但一直沒有認真研究和使用過!下面這篇文章主要給大家介紹了關(guān)于sql連接查詢語句中on、where篩選兩者之間區(qū)別的相關(guān)資料,文中通過圖文介紹的非常詳細,需要的朋友們下面來一起看看吧。2017-07-07
圖文詳解如何用IDEA和Navicat導入導出數(shù)據(jù)庫
這篇文章主要介紹了如何用IDEA和Navicat導入導出數(shù)據(jù)庫的相關(guān)資料,包括使用數(shù)據(jù)庫工具界面或mysqldump命令,并提示MySQL版本兼容性問題及Navicat導出文件不帶注釋的原因,需要的朋友可以參考下2025-06-06
DBeaver導入csv到數(shù)據(jù)庫的簡單步驟記錄
這篇文章主要介紹了DBeaver導入csv到數(shù)據(jù)庫的簡單步驟,DBeaver是一款功能強大的數(shù)據(jù)庫管理工具,支持導入CSV文件到數(shù)據(jù)庫,文中給出了完整的步驟記錄,需要的朋友可以參考下2025-01-01

