前言
在 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 SET
Value = 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 对值进行序列化,支持更复杂的数据类型。
- 提供缓存命中率统计等高级功能。
- 增加分布式缓存支持。