it编程 > 数据库 > MsSqlserver

使用SQLite实现CacheHelper的示例代码

12人参与 2025-10-23 MsSqlserver

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.sqlitemessagepack

封装好的方法如下:

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<...>(...)

3.2 直接缓存管理

这些方法允许您更直接地控制缓存的添加和更新。

addcache(string key, object value)

addcachewithabsolute(string key, object value, int minute)

addcachewithrelative(string key, object value, int minute)

3.3 缓存移除

cleancache(string key)

4. 核心特性

5. 核心概念深入解析

5.1 数据库结构 (cache.db)

cachehelper 会自动创建名为 cachestore 的表,其结构如下:

字段名类型描述
keytext主键。缓存项的唯一标识符。
valueblob存储经 messagepack 序列化后的二进制数据。
insertiontimeutcinteger缓存项的创建时间 (utc ticks)。
expirationtimeutcinteger缓存项的过期时间点 (utc ticks)。这是判断缓存是否有效的核心字段。
slidingexpirationminutesinteger滑动过期策略的关键。0 表示绝对过期;>0 的值表示这是一个滑动过期的项,其值为滑动的分钟数。

5.2 滑动过期 (slidingexpirationminutes) 的工作原理

slidingexpirationminutes 字段的设计非常巧妙,它同时扮演了**“标记”“时长”**两个角色。

  1. 设置缓存时:

    • 调用 addcachewithabsolute 时,slidingexpirationminutes 被设为 0
    • 调用 addcachewithrelative(key, value, 30) 时,slidingexpirationminutes 被设为 30
  2. 获取缓存时 (trygetcache 内部逻辑):

    • 系统首先检查 expirationtimeutc 是否已过期。
    • 如果未过期且成功读取数据,系统会检查 slidingexpirationminutes 字段的值
    • 如果值为 0,则不执行任何额外操作。
    • 如果值大于 0(例如 30),系统识别出这是一个滑动缓存项,会立即执行一个 update 命令,将该项的 expirationtimeutc 更新为 当前时间 + 30分钟

这个“读取并续期”的原子操作,完美地实现了滑动过期的逻辑:只要你在它过期前访问它,它的生命就在不断延续。

6. 并发安全机制

在高并发环境下,多个线程可能同时请求同一个不存在的缓存项。如果没有锁定机制,这些线程会全部穿透缓存去执行昂贵的数据查询,这就是“缓存击穿”。

cachehelper 通过 getorset 方法中的 双重检查锁定模式 解决了这个问题:

  1. 第一次检查 (无锁): 在进入 lock 之前快速检查缓存是否存在。对于绝大多数缓存命中的情况,可以无锁返回,性能极高。
  2. 获取key专用锁: 如果第一次检查未命中,系统会从一个 concurrentdictionary 中为当前 key 获取一个专用的锁对象。这确保了对不同 key 的请求不会互相阻塞。
  3. 第二次检查 (有锁): 在获得锁之后,再次检查缓存。这是为了防止在等待锁的过程中,已有其他线程完成了数据查询和缓存设置。
  4. 执行查询与设置: 只有当第二次检查仍然未命中时,当前线程才会去执行数据查询,并将结果写入缓存。

这个机制确保了对于任意一个key,在同一时刻最多只有一个线程在执行数据源的查询操作。

7. 注意事项与最佳实践

到此这篇关于使用sqlite实现cachehelper的示例代码的文章就介绍到这了,更多相关sqlite实现cachehelper内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!

(0)

您想发表意见!!点此发布评论

推荐阅读

PostgreSQL实现跨数据库授权查询的详细步骤

10-23

SQL CTE (Common Table Expression) 高级用法与最佳实践

10-24

SQL中的键与约束

10-24

SQL Server INSERT操作实战与脚本生成方法

10-24

SQL中的CASE WHEN用法常用场景分析

10-21

在Django中如何执行原生SQL查询详解

10-21

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论