1. 概述
cachehelper 是一个基于 sqlite 的静态缓存工具类,旨在为 .net 应用程序提供一个简单、高效、持久化且线程安全的缓存解决方案。它将缓存数据存储在应用程序根目录下的 cache.db 文件中,这意味着即使应用程序重启,缓存数据依然存在(只要未过期)。
该助手类封装了常见的缓存操作,并内置了绝对过期和滑动过期两种策略,通过“get-or-set”模式极大地简化了从数据源(如数据库、api)获取并缓存数据的业务逻辑。
2. 安装与环境准备 (prerequisites)
要使 cachehelper 类正常工作,您必须在您的项目中安装以下两个核心的 nuget 包。
您可以使用 .net cli 命令来安装它们:
# 1. 用于操作 sqlite 数据库 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>
/// 静态构造函数,在类第一次被访问时自动运行一次,用于初始化数据库。
/// </summary>
static cachehelper()
{
// 将数据库文件放在应用程序的根目录下,确保路径一致性
_dbpath = path.combine(appcontext.basedirectory, "cache.db");
_connectionstring = $"data source={_dbpath}";
initializedatabase();
}
/// <summary>
/// 初始化数据库:如果数据库文件或表不存在,则创建它们。
/// </summary>
private static void initializedatabase()
{
try
{
using var connection = new sqliteconnection(_connectionstring);
connection.open();
// 创建一个可重用的 command 对象
var command = connection.createcommand();
// 1. 执行 pragma 指令以优化并发性能
command.commandtext = "pragma journal_mode=wal;";
command.executenonquery();
// 2. 重用同一个 command 对象,执行 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)
{
// 如果数据库初始化失败,这是个严重问题,需要记录下来
// 在生产环境中,应使用专业的日志库(如 serilog, nlog)
($"[cache critical] database initialization failed. error: {ex.message}").logerr();
// 抛出异常,因为如果数据库无法初始化,整个缓存服务都无法工作
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)
{
// 对于永久缓存,我们设置一个极大的过期时间
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` 模式轻松地实现它们 ---
// 示例:
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" 方法,实现了双重检查锁定以确保线程安全。
/// </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 创建一个唯一的锁对象
var lockobject = _locks.getoradd(key, k => new object());
// 进入锁代码块,确保同一时间只有一个线程能为这个 key 生成缓存
lock (lockobject)
{
// 第二次检查(在锁之内),防止在等待锁的过程中,其他线程已经生成了缓存
(found, value) = trygetcache<t>(key);
if (found)
{
return value!;
}
// 执行昂贵的数据查询操作
var result = queryfunc();
// 将查询结果存入缓存
if (result != null)
{
set(key, result, timespan.fromminutes(minute), issliding: false);
}
console.writeline($"added [{key}] cache, survival time is [{minute}] minutes");
return result;
}
}
/// <summary>
/// 统一的缓存写入方法。
/// </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();
// 吞掉异常,保证主程序继续运行
}
}
/// <summary>
/// 尝试从缓存中获取数据,并处理滑动过期的更新逻辑。
/// </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();
}
// 如果发生任何错误或未找到,都返回“未命中”
return (false, default);
}
#endregion
}
}
3. 公共 api 参考 (封装方法说明)
这是与 cachehelper 交互的公共方法列表。
3.1 核心模式:获取或查询 (get-or-set)
这是最推荐的使用方式。它将“检查缓存、执行查询、设置缓存”的逻辑封装为一步,确保了代码的简洁和线程安全。
getcacheorquery<...>(...)
描述: 尝试根据
key从缓存中获取数据。如果缓存存在且未过期,则直接返回缓存数据;否则,执行您提供的查询方法 (myfunc) 来获取最新数据,然后将结果存入缓存,并最终返回该结果。重载 (overloads): 该方法提供多个重载版本,以支持无参、单参数、双参数等不同签名的查询方法。
参数:
string key: 缓存的唯一标识符。int minute(可选): 缓存的有效期(分钟)。如果未提供,将使用一个默认值(例如60分钟)。func<...> myfunc: 一个委托或 lambda 表达式。当缓存未命中时,此函数将被调用以获取数据。p1, p2, ...(可选): 传递给myfunc的参数。
示例:
// 示例1: 无参数的查询 string allproductskey = "products:all"; var products = cachehelper.getcacheorquery(allproductskey, 30, () => { // 这段代码只会在缓存未命中时执行 console.writeline("从数据库获取所有产品..."); return database.getallproducts(); }); // 示例2: 带一个参数的查询 int userid = 123; string userkey = $"user:{userid}"; var user = cachehelper.getcacheorquery(userkey, 60, (id) => { // 这段代码只会在缓存未命中时执行 console.writeline($"从数据库获取id为 {id} 的用户..."); return database.getuserbyid(id); }, userid);
3.2 直接缓存管理
这些方法允许您更直接地控制缓存的添加和更新。
addcache(string key, object value)
- 描述: 添加一个“永久”缓存(内部设置为10年有效期)。适用于极少变动的基础数据。
- 示例:
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分钟内没有被访问,缓存将过期。每次访问都会重置其生命周期。常用于用户会话等场景。 - 示例:
cachehelper.addcachewithrelative("user_session:xyz", sessiondata, 20); // 20分钟不活动则过期
3.3 缓存移除
cleancache(string key)
- 描述: 从缓存中手动移除一个指定的项。这在底层数据更新后,需要强制让缓存失效时非常有用。
- 示例:
// 更新了用户id为123的个人信息 database.updateuser(updateduser); // 立即清除旧的缓存,确保下次请求获取的是最新数据 cachehelper.cleancache("user:123");
4. 核心特性
- 持久化存储: 使用 sqlite 文件数据库,缓存内容在应用程序重启后依然保留。
- 线程安全: 采用双重检查锁定(double-checked locking)模式和基于key的锁,有效防止在高并发场景下的“缓存击穿”问题。
- 高效序列化: 使用
messagepack对缓存对象进行二进制序列化,相比 json 序列化,性能更高,占用空间更小。 - 两种过期策略:
- 绝对过期 (absolute expiration): 缓存项在设定的固定时间点后失效。
- 滑动过期 (sliding expiration): 缓存项在一段时间内未被访问则失效;每次访问都会重置其生命周期。
- 简洁的 api: 提供了简单易用的
getcacheorquery方法,将“检查缓存、获取数据、存入缓存”的逻辑封装为原子操作。
5. 核心概念深入解析
5.1 数据库结构 (cache.db)
cachehelper 会自动创建名为 cachestore 的表,其结构如下:
| 字段名 | 类型 | 描述 |
|---|---|---|
| key | text | 主键。缓存项的唯一标识符。 |
| value | blob | 存储经 messagepack 序列化后的二进制数据。 |
| insertiontimeutc | integer | 缓存项的创建时间 (utc ticks)。 |
| expirationtimeutc | integer | 缓存项的过期时间点 (utc ticks)。这是判断缓存是否有效的核心字段。 |
| slidingexpirationminutes | integer | 滑动过期策略的关键。0 表示绝对过期;>0 的值表示这是一个滑动过期的项,其值为滑动的分钟数。 |
5.2 滑动过期 (slidingexpirationminutes) 的工作原理
slidingexpirationminutes 字段的设计非常巧妙,它同时扮演了**“标记”和“时长”**两个角色。
设置缓存时:
- 调用
addcachewithabsolute时,slidingexpirationminutes被设为0。 - 调用
addcachewithrelative(key, value, 30)时,slidingexpirationminutes被设为30。
- 调用
获取缓存时 (
trygetcache内部逻辑):- 系统首先检查
expirationtimeutc是否已过期。 - 如果未过期且成功读取数据,系统会检查
slidingexpirationminutes字段的值。 - 如果值为
0,则不执行任何额外操作。 - 如果值大于 0(例如
30),系统识别出这是一个滑动缓存项,会立即执行一个update命令,将该项的expirationtimeutc更新为当前时间 + 30分钟。
- 系统首先检查
这个“读取并续期”的原子操作,完美地实现了滑动过期的逻辑:只要你在它过期前访问它,它的生命就在不断延续。
6. 并发安全机制
在高并发环境下,多个线程可能同时请求同一个不存在的缓存项。如果没有锁定机制,这些线程会全部穿透缓存去执行昂贵的数据查询,这就是“缓存击穿”。
cachehelper 通过 getorset 方法中的 双重检查锁定模式 解决了这个问题:
- 第一次检查 (无锁): 在进入
lock之前快速检查缓存是否存在。对于绝大多数缓存命中的情况,可以无锁返回,性能极高。 - 获取key专用锁: 如果第一次检查未命中,系统会从一个
concurrentdictionary中为当前key获取一个专用的锁对象。这确保了对不同key的请求不会互相阻塞。 - 第二次检查 (有锁): 在获得锁之后,再次检查缓存。这是为了防止在等待锁的过程中,已有其他线程完成了数据查询和缓存设置。
- 执行查询与设置: 只有当第二次检查仍然未命中时,当前线程才会去执行数据查询,并将结果写入缓存。
这个机制确保了对于任意一个key,在同一时刻最多只有一个线程在执行数据源的查询操作。
7. 注意事项与最佳实践
- 缓存键 (key) 的命名: 缓存键应具有唯一性和良好的描述性。推荐使用如
object_type:id的格式,例如user:123或products:all。 - 缓存失效: 当底层数据发生变化时(例如,用户信息被修改),应主动调用
cachehelper.cleancache("user:123")来清除旧缓存,以避免数据不一致。 - 可序列化对象: 存入缓存的对象必须能被
messagepack序列化。绝大多数 poco (plain old c# object) 对象都没有问题。 - 异常处理:
cachehelper内部已对数据库操作和序列化等步骤进行了try-catch封装。缓存操作失败时会向控制台输出错误日志,但不会抛出异常中断主程序流程,保证了系统的稳定性。
到此这篇关于使用sqlite实现cachehelper的示例代码的文章就介绍到这了,更多相关sqlite实现cachehelper内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论