前言
在 WebAPI 开发中,缓存是一种常用的优化手段。Redis 是广泛使用的缓存解决方案,但在某些场景下,我们可能不希望引入第三方依赖,而是希望使用轻量级的方式实现一个支持持久化的缓存组件,满足以下需求:
- 缓存持久化:重启后缓存可以恢复。
- 过期删除:支持设置缓存过期时间。
- 基本操作:支持常见的增、删、查操作。
本文将指导如何设计和实现一个符合上述需求的缓存组件。
需求分析与设计思路
要实现这样的缓存组件,我们需要解决以下几个关键问题:
-
数据存储
选择适合持久化的数据存储方式。SQLite 是一个很好的选择,因为它内置于 .NET,性能良好,且无需额外安装服务。 -
过期管理
需要定期清理过期缓存,可以通过后台定时任务扫描和清理。 -
高效的操作接口
提供易用的Set
、Get
、Remove
等接口,并封装为服务供 API 使用。
实现步骤
1. 定义缓存模型
定义缓存的基本结构,包含键、值、过期时间等信息。
public class CacheItem
{public string Key { get; set; } = null!;public string Value { get; set; } = null!;public DateTime Expiration { get; set; }
}
2. 创建 SQLite 数据存储
在项目中配置 SQLite 数据库,用于存储缓存数据。
配置 SQLite 数据库
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net8.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup><ItemGroup><PackageReference Include="Microsoft.Data.Sqlite.Core" Version="8.0.11" /><PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" /><PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" /><PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" /><PackageReference Include="SQLitePCLRaw.core" Version="2.1.10" /><PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="2.1.10" /><PackageReference Include="SQLitePCLRaw.provider.e_sqlite3" Version="2.1.10" /></ItemGroup></Project>
初始化数据库
创建一个帮助类用于初始化和操作 SQLite 数据库。
using Microsoft.Data.Sqlite;
using SQLitePCL;namespace SqliteCache
{public class CacheDbContext{private readonly string _connectionString = "Data Source=cache.db";public CacheDbContext(){Batteries.Init();using var connection = new SqliteConnection(_connectionString);connection.Open();var command = connection.CreateCommand();command.CommandText = @"CREATE TABLE IF NOT EXISTS Cache (Key TEXT PRIMARY KEY,Value TEXT NOT NULL,Expiration TEXT NOT NULL);";command.ExecuteNonQuery();}public void AddOrUpdate(CacheItem item){using var connection = new SqliteConnection(_connectionString);connection.Open();var command = connection.CreateCommand();command.CommandText = @"INSERT INTO Cache (Key, Value, Expiration)VALUES (@Key, @Value, @Expiration)ON CONFLICT(Key) DO UPDATE SETValue = excluded.Value,Expiration = excluded.Expiration;";command.Parameters.AddWithValue("@Key", item.Key);command.Parameters.AddWithValue("@Value", item.Value);command.Parameters.AddWithValue("@Expiration", item.Expiration.ToString("o"));command.ExecuteNonQuery();}public CacheItem? Get(string key){using var connection = new SqliteConnection(_connectionString);connection.Open();var command = connection.CreateCommand();command.CommandText = "SELECT Key, Value, Expiration FROM Cache WHERE Key = @Key";command.Parameters.AddWithValue("@Key", key);using var reader = command.ExecuteReader();if (reader.Read()){return new CacheItem{Key = reader.GetString(0),Value = reader.GetString(1),Expiration = DateTime.Parse(reader.GetString(2))};}return null;}public void Remove(string key){using var connection = new SqliteConnection(_connectionString);connection.Open();var command = connection.CreateCommand();command.CommandText = "DELETE FROM Cache WHERE Key = @Key";command.Parameters.AddWithValue("@Key", key);command.ExecuteNonQuery();}public void ClearExpired(){using var connection = new SqliteConnection(_connectionString);connection.Open();var command = connection.CreateCommand();command.CommandText = "DELETE FROM Cache WHERE Expiration < @Now";command.Parameters.AddWithValue("@Now", DateTime.UtcNow.ToString("o"));command.ExecuteNonQuery();}}
}
3. 创建缓存服务
封装数据库操作,提供易用的缓存接口。
namespace SqliteCache
{public class PersistentCacheService{private readonly CacheDbContext _dbContext;public PersistentCacheService(){_dbContext = new CacheDbContext();}public void Set(string key, string value, TimeSpan expiration){var cacheItem = new CacheItem{Key = key,Value = value,Expiration = DateTime.UtcNow.Add(expiration)};_dbContext.AddOrUpdate(cacheItem);}public string? Get(string key){var item = _dbContext.Get(key);if (item == null || item.Expiration <= DateTime.UtcNow){_dbContext.Remove(key); // 自动删除过期项return null;}return item.Value;}public void Remove(string key){_dbContext.Remove(key);}}
}
4. 定期清理过期缓存
利用 ASP.NET Core 的后台任务机制清理过期缓存。
配置后台服务
using Microsoft.Extensions.Hosting;namespace SqliteCache
{public class CacheCleanupService : BackgroundService{private readonly CacheDbContext _dbContext = new();protected override async Task ExecuteAsync(CancellationToken stoppingToken){while (!stoppingToken.IsCancellationRequested){_dbContext.ClearExpired();await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);}}}
}
注册服务
在 SqliteCacheServiceCollectionExtensions.cs
中注册缓存服务和后台任务。
using SqliteCache;
namespace Microsoft.Extensions.DependencyInjection
{public static class SqliteCacheServiceCollectionExtensions{public static IServiceCollection AddSqliteCache(this IServiceCollection services){services.AddSingleton<PersistentCacheService>();services.AddHostedService<CacheCleanupService>();return services;}}
}
5. 使用缓存服务
新增一个asp.net core webapi项目 CacheProject,添加项目引用SqliteCache。在Main函数中添加服务引用。
namespace CacheProject
{public class Program{public static void Main(string[] args){var builder = WebApplication.CreateBuilder(args);builder.Services.AddControllers();builder.Services.AddEndpointsApiExplorer();builder.Services.AddSwaggerGen();//引入Sqlitecache缓存组件builder.Services.AddSqliteCache();var app = builder.Build();// Configure the HTTP request pipeline.if (app.Environment.IsDevelopment()){app.UseSwagger();app.UseSwaggerUI();}app.UseHttpsRedirection();app.UseAuthorization();app.MapControllers();app.Run();}}
}
在控制器中注入并使用缓存服务。
[ApiController]
[Route("api/[controller]")]
public class CacheController : ControllerBase
{private readonly PersistentCacheService _cacheService;public CacheController(PersistentCacheService cacheService){_cacheService = cacheService;}[HttpPost("set")]public IActionResult Set(string key, string value, int expirationMinutes){_cacheService.Set(key, value, TimeSpan.FromMinutes(expirationMinutes));return Ok("Cached successfully.");}[HttpGet("get")]public IActionResult Get(string key){var value = _cacheService.Get(key);if (value == null)return NotFound("Key not found or expired.");return Ok(value);}[HttpDelete("remove")]public IActionResult Remove(string key){_cacheService.Remove(key);return Ok("Removed successfully.");}
}
6.运行
启动WebApi项目
info: Microsoft.Hosting.Lifetime[14]Now listening on: https://localhost:7193
info: Microsoft.Hosting.Lifetime[14]Now listening on: http://localhost:5217
info: Microsoft.Hosting.Lifetime[0]Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]Content root path: E:\F\Projects\CSharp\CacheProject\CacheProject
- 浏览器查看
- 写入缓存
- 读缓存和重启程序
- 缓存已过期
总结
本文展示了如何在 ASP.NET Core WebAPI 中,使用 SQLite 构建一个支持持久化和过期管理的缓存组件。通过以上步骤,我们实现了一个轻量级、易扩展的缓存系统,无需引入 Redis 等第三方工具,同时满足了性能和持久化的需求。
扩展思路:
- 使用 JSON 或 Protobuf 对值进行序列化,支持更复杂的数据类型。
- 提供缓存命中率统计等高级功能。
- 增加分布式缓存支持。