缓存在 App 中的应用
缓存(cache)工作原理
说到缓存,我们来看下百度百科的介绍:
缓存(cache),原始意义是指访问速度比一般 随机存取存储器(RAM) 快的一种 高速存储器,通常它不像系统主存那样使用
DRAM
技术,而使用昂贵但较快速的SRAM
技术。缓存的设置是所有现代计算机系统发挥高性能的重要因素之一。- 动态随机存取存储器(
Dynamic Random Access Memory,DRAM
) - 静态随机存取存储器(
Static Random-Access Memory,SRAM
)
- 动态随机存取存储器(
- 目的:提高数据存取速度(缓存性能优化
万金油
)。
在应用系统中通常会使用到缓存(cache
)技术,其中应用系统多级缓存的工作原理大概介绍如下:
说明:在 App 系统中存在多级缓存时,按顺序读取,从一级缓存(Level1 Cache,简称 L1 Cache)逐级往下获取。
项目环境准备
上一篇文章 我们介绍了 .net6
平台的 asp.net core webapi
框架中如何使用 ABP vNext
框架,本篇文章我们继续使用上次创建的 Demo.Abp.WebApplication1
项目,新增如下 NuGet
包文件:
- Volo.Abp.Caching,本地缓存;
- Volo.Abp.Caching.StackExchangeRedis,分布式缓存(Redis);
说明:Volo.Abp.Caching.StackExchangeRedis 包已经包含 Volo.Abp.Caching 包。
一、添加 NuGet 包文件
Demo.Abp.WebApplication1
项目中所有的 nuget packages
文件:
说明:在 上篇文章 中,ABP vNext
的这些nuget packages
文件还未发布v6.0.0
正式版,生产环境中推荐使用正式稳定版。关于asp.net core webapi
项目如何遵循Module
(模块化)改造的具体细节,此处不再详细介绍。
添加完成上图的 nuget packages
后,查看 Demo.Abp.WebApplication1.csproj
完整的工程项目文件,如下所示:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.AspNetCore" Version="6.0.0" />
<PackageReference Include="Volo.Abp.Caching" Version="6.0.0" />
<PackageReference Include="Volo.Abp.Caching.StackExchangeRedis" Version="6.0.0" />
<PackageReference Include="Volo.Abp.Swashbuckle" Version="6.0.0" />
</ItemGroup>
</Project>
项目结构如下:
二、改造 ApiController 类及相关文件
此处为了模拟业务操作从数据库获取数据,在 WeatherForecastController
控制器中添加一个获取数据的方法:
// 模拟数据库获取数据
private async Task<WeatherForecast> GetWeatherForecastAsync(Guid guid)
{
_logger.LogDebug($"{DateTime.Now:G},查询数据库数据...");
var index = Random.Shared.Next(-2, 3);
var data = new WeatherForecast
{
Id = Guid.NewGuid(), // WeatherForecast 模型新增 Id 字段
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
};
return await Task.FromResult(data);
}
WeatherForecast
模型新增 Id
字段。
namespace Demo.Abp.WebApplication1;
public class WeatherForecast
{
public Guid Id { get; set; }
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
说明:此处仅说明
WeatherForecastController
控制器新增的部分方法,完整的改造代码后面会展示,尽量展示思路步骤。
ABP vNext 缓存使用
接下来我们分别演示在 asp.net core webapi
框架中如何使用 ABP vNext
提供的缓存包文件:
- Volo.Abp.Caching,本地缓存;
- Volo.Abp.Caching.StackExchangeRed,分布式缓存(Redis);
一、本地缓存使用
由于Demo.Abp.WebApplication1
项目是使用默认创建的模式(MiniAPI
) ,在 Program.cs
中的 DemoWebApiModule
类声明式添加 Module
化的 NuGet
包依赖,如下所示:
- 导入命名空间(
Namespace
);
using Microsoft.OpenApi.Models;
using Volo.Abp;
using Volo.Abp.AspNetCore;
using Volo.Abp.Caching;
using Volo.Abp.Modularity;
using Volo.Abp.Swashbuckle;
DemoWebApiModule
类声明式添加Module
化的NuGet
包依赖;
[DependsOn(
typeof(AbpAspNetCoreModule),
typeof(AbpCachingModule),
typeof(AbpSwashbuckleModule)
)]
public class DemoWebApiModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var services = context.Services;
services.AddControllers();
services.AddEndpointsApiExplorer();
services.AddAbpSwaggerGen(options => {
options.SwaggerDoc("v1", new OpenApiInfo { Title = "Test DemoWebApiModule API", Version = "v1", Description = "Module 模块化 DemoWebApiModule 自定义类。" });
options.DocInclusionPredicate((docName, description) => true);
options.CustomSchemaIds(type => type.FullName);
options.HideAbpEndpoints(); // 隐藏 ABP vNext 的默认端点
});
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
var env = context.GetEnvironment();
if (env.IsDevelopment())
{
//app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseAbpSwaggerUI(options => {
options.SwaggerEndpoint("/swagger/v1/swagger.json", "TestAPI");
});
}
else
{
app.UseExceptionHandler("/Error");
}
//app.UseHttpsRedirection();
app.UseStaticFiles(); // 不使用静态文件中间件,SwaggerUI将无法渲染UI页面
app.UseRouting();
app.UseAuthorization();
app.UseConfiguredEndpoints(); //代替原来的 app.MapControllers();
}
}
WeatherForecastController
控制器中完整代码如下:
using Microsoft.AspNetCore.Mvc;
using Volo.Abp.Caching;
using Microsoft.Extensions.Caching.Distributed;
namespace Demo.Abp.WebApplication1.Controllers;
/// <summary>
/// WeatherForecast
/// </summary>
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
private readonly IDistributedCache<WeatherForecast> _cache; // 使用 abp vnext 本地缓存
// 构造函数 DI 注入本地缓存
public WeatherForecastController(ILogger<WeatherForecastController> logger, IDistributedCache<WeatherForecast> cache)
{
_logger = logger;
_cache = cache;
}
// 常规查询数据
[HttpGet("/GetList")]
public IEnumerable<WeatherForecast> GetDataFromWeatherForecastAsync()
{
_logger.LogDebug($"{DateTime.Now:G},查询数据库数据...");
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Id = Guid.NewGuid(),
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToList();
}
// 使用 ABP vNext 本地缓存查询数据
[HttpGet("/Get")]
public async Task<WeatherForecast> GetDataFromWeatherForecastAsync(Guid guid)
{
_logger.LogDebug($"{DateTime.Now:G},查询数据使用本地缓存...");
var data = await _cache.GetOrAddAsync(key: guid.ToString(),
factory: async () => await GetWeatherForecastAsync(guid),
optionsFactory: () => new DistributedCacheEntryOptions
{
// 添加绝对过期时间,设置 10 秒
AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(10)
});
return data;
}
// 模拟数据库获取数据
private async Task<WeatherForecast> GetWeatherForecastAsync(Guid guid)
{
_logger.LogDebug($"{DateTime.Now:G},查询数据库数据...");
var index = Random.Shared.Next(-2, 3);
var data = new WeatherForecast
{
Id = guid,
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
};
return await Task.FromResult(data); // 此处仅为了例演示异步方法使用,该方法本身没啥实际意义
}
}
1.1 启动项目运行
改造好后,我们习惯性的启动项目运行,验证下是否能成功运行,如下所示:
WeatherForecast
控制器中有两个对外公开的方法,分别是:
/GetList
,常规查询(未使用缓存)多条数据。/Get
,使用缓存查询单条数据。
1.2 测试访问接口【/GetList】
1.2.1、curl
curl -X 'GET' \
'http://localhost:5220/GetList' \
-H 'accept: text/plain' \
-H 'RequestVerificationToken: CfDJ8ODjDXixt1tDuxO1cuT_L2xih256N5ivD-oaqHU-qlOmT5DYo778fWPnN-FN9VwU4isJn1azpl9RMoXcaT_GuqD8ICyhdBtSTzizz-W56shntXTF7Uqp2iLUbKwbpURg_imHSk0sTJbF4uM_DGugscE' \
-H 'X-Requested-With: XMLHttpRequest'
1.2.2、Request URL,浏览器输入
http://localhost:5220/GetList
1.2.3、响应数据
- Response body
[
{
"id": "8b72f7bb-4ff2-449b-bc07-bbe175c0ba65",
"date": "2022-10-09T19:15:11.1299624+08:00",
"temperatureC": -15,
"temperatureF": 6,
"summary": "Chilly"
},
{
"id": "a1606cc5-63ed-4a88-ac83-0d1299b452aa",
"date": "2022-10-10T19:15:11.130221+08:00",
"temperatureC": 19,
"temperatureF": 66,
"summary": "Freezing"
},
{
"id": "3fe5cb1e-5148-49d2-a540-145a0f174abf",
"date": "2022-10-11T19:15:11.1302229+08:00",
"temperatureC": 50,
"temperatureF": 121,
"summary": "Mild"
},
{
"id": "0c7d9b88-7925-4439-a037-81cd1fe08173",
"date": "2022-10-12T19:15:11.1302232+08:00",
"temperatureC": 32,
"temperatureF": 89,
"summary": "Mild"
},
{
"id": "a76cc34b-af1a-421c-9719-742e07029f3a",
"date": "2022-10-13T19:15:11.1302234+08:00",
"temperatureC": 53,
"temperatureF": 127,
"summary": "Hot"
}
]
- Response headers
content-type: application/json; charset=utf-8
date: Sat,08 Oct 2022 11:15:11 GMT
server: Kestrel
transfer-encoding: chunked
1.3 测试访问接口【/Get】
1.3.1、curl
curl -X 'GET' \
'http://localhost:5220/Get?guid=a76cc34b-af1a-421c-9719-742e07029f3a' \
-H 'accept: text/plain' \
-H 'RequestVerificationToken: CfDJ8ODjDXixt1tDuxO1cuT_L2xih256N5ivD-oaqHU-qlOmT5DYo778fWPnN-FN9VwU4isJn1azpl9RMoXcaT_GuqD8ICyhdBtSTzizz-W56shntXTF7Uqp2iLUbKwbpURg_imHSk0sTJbF4uM_DGugscE' \
-H 'X-Requested-With: XMLHttpRequest'
1.3.2、Request URL,浏览器输入
http://localhost:5220/GetList
1.3.3、响应数据
缓存默认设置的 10s
失效时间,在该时间段内多次请求访问,都不会直接操作数据库获取数据,而是从本地进程缓存中获取数据,此时说明我们使用到了 ABP vNetx
提供的 Volo.Abp.Caching
包的功能。
二、分布式缓存使用
2.1 ABP vNext 使用 Redis 默认依赖的 NuGet 包
这里我们使用 Redis
作为分布式缓存服务,ABP vNext
默认使用了 StackExchange.Redis
包,连接字符串同 StackExchange.Redis
的写法,如下所示:
127.0.0.1:6379,defaultDatabase=1,password=Abc...123
# 或者
localhost:6379,defaultDatabase=1,password=Abc...123
# 或者
2.2 准备 Redis-Server 服务
说明:此处使用的 redis 是 docker 环境的容器服务,开放映射端口 6379,因此可以使用【127.0.0.1:6379】或者【localhost:6379】访问。
2.3 测试 Redis-Server 可用性
准备好 Redis-Server
后,使用 Redis 客户端 GUI(这里使用 AnotherRedis desktop manager
)工具访问连接,测试下该服务是否正常可用,工具连接如下图所示(说明 redis-server 正常可用):
2.4 修改 appsettings.json 配置文件
接下来在 appsettings.json
文件中添加如下配置:
"Redis": {
"IsEnabled": true,
"InstanceName": "StackExchangeRedis", // 可省略
"Configuration": "localhost:6379,defaultDatabase=1,password=Abc...123" // 未设置密码 password 可以不写
}
2.5 自定义模块类添加 Module 化依赖
最后修改自定义模块 DemoWebApiModule
类,修改代码如下:
using Volo.Abp.Caching.StackExchangeRedis; // 导入 abp vnext 提供的 redis 命名空间
...
[DependsOn(
typeof(AbpAspNetCoreModule),
typeof(AbpCachingStackExchangeRedisModule), // 声明 Volo.Abp.Caching.StackExchangeRedis 模块依赖
typeof(AbpSwashbuckleModule)
)]
public class DemoWebApiModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
...
/* 如果报异常,添加此处配置
Configure<RedisCacheOptions>(options => {
options.InstanceName = "StackExchangeRedis"; // 该实例名称可自定义
});*/
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
...
}
}
说明:"..." 省略的代码配置不变,和使用本地缓存一样即可。
2.6 启动项目测试接口
此处的测试环节和使用本地化缓存的测试一样,不再描述,成功访问接口后,我们使用 redis
可视化管理工具 ——AnotherRedis desktop manager
查看【DB1
】里面是否存在对应的 key
和 values
,如下图所示:
2.7 使用自定义 RedisModule 模块类
除了使用默认的 AbpCachingStackExchangeRedisModule
模块依赖,还可以自定义MyCachingStackExchangeRedisModule
模块类,该自定义模块类基本 copy 了默认的模块类,只是添加了 options.InstanceName
部分代码,修改如下:
using Microsoft.Extensions.Caching.StackExchangeRedis;
using Volo.Abp.Caching.StackExchangeRedis;
using Volo.Abp.Caching;
using Volo.Abp.Modularity;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Demo.Abp.WebApplication1.Moduls;
[DependsOn(typeof(AbpCachingModule))]
public class MyCachingStackExchangeRedisModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
IConfiguration configuration = context.Services.GetConfiguration();
string redisEnable = configuration["Redis:IsEnabled"];
if (!redisEnable.IsNullOrEmpty() && !bool.Parse(redisEnable))
{
return;
}
context.Services.AddStackExchangeRedisCache(delegate (RedisCacheOptions options)
{
// 新增部分
string redisInstanceName = configuration["Redis:InstanceName"];
if (!redisInstanceName.IsNullOrEmpty())
{
options.InstanceName = redisInstanceName;
}
string redisConfiguration = configuration["Redis:Configuration"];
if (!redisConfiguration.IsNullOrEmpty())
{
options.Configuration = redisConfiguration;
}
});
context.Services.Replace(ServiceDescriptor.Singleton<IDistributedCache, AbpRedisCache>());
}
}
自定义 Module
模块类创建好后,同样地使用 DependsOn
特性显示声明依赖,代码如下所示:
[DependsOn(
typeof(AbpAspNetCoreModule),
typeof(MyCachingStackExchangeRedisModule), // 声明自定义模块依赖
typeof(AbpSwashbuckleModule)
)]
总结
通过学习 ABP vNext
模块化的缓存(cache
)技术,分别提供了 本地化 和 分布式 两种模式的缓存,快速方便的为应用系统接入缓存,局部有效的提升了系统访问性能,当默认提供的分布式缓存服务不满足需求时,还可以自定义扩展分布式缓存模块,对 Module
模块化思想有更近一步的认识。