using System.Net;
using Microsoft.Extensions.Caching.Distributed;
using Core.Caching;
using Core.Configuration;
using StackExchange.Redis;
namespace Services.Caching
{
/// <summary>
/// 【Redis分布式缓存数据库软件管理器--类】
/// <remarks>
/// 摘要:
/// 通过该类中的方法成员实现了通过“StackExchange.Redis”中间件实例对Redis软件缓存数据更加底层的操作及其的“DistributedCacheManager”抽象类的实例化。
/// </remarks>
/// </summary>
public class RedisCacheManager : DistributedCacheManager
{
#region 变量--私有/保护
/// <summary>
/// 【Redis连接封装器】
/// <remarks>
/// 摘要:
/// Redis连接封装器的1个指定实例。
/// </remarks>
/// </summary>
private static RedisConnectionWrapper _connectionWrapper;
/// <summary>
/// 【数据库】
/// <remarks>
/// 摘要:
/// 用于建立当前程序与Redis分布式缓存数据库软件中分布式缓存数据库的连接。
/// </remarks>
/// </summary>
private readonly IDatabase _db;
#endregion
#region 拷贝构造方法
/// <param name="appSettings">应用配置类的1个指定实例。</param>
/// <param name="distributedCache">分布式缓存接口实例,该实例实际上是:“StackExchange.Redis”中间件的实例,通过该实例实现当前程序通过“StackExchange.Redis”中间件的实例与Redis分布式缓存数据库的交互操作。</param>
/// <summary>
/// 【拷贝构造方法】
/// <remarks>
/// 摘要:
/// 通过拷贝构造方法,对当前类中的同名变量成员进行实例化。
/// </remarks>
/// </summary>
public RedisCacheManager(AppSettings appSettings, IDistributedCache distributedCache) : base(appSettings,
distributedCache)
{
_connectionWrapper ??=
new RedisConnectionWrapper(appSettings.Get<DistributedCacheConfig>().ConnectionString);
_db = _connectionWrapper.GetDatabase();
}
#endregion
#region 方法--私有/保护
/// <param name="endPoint">Redis分布式缓存数据库软件中所有可用的终结点。</param>
/// <param name="prefix">1个指定的前缀字符串,默认值:null,即获取所有的Redis缓存键实例。</param>
/// <summary>
/// 【获取服务】
/// <remarks>
/// 摘要:
/// 从Redis分布式缓存数据库软件所有的分布式缓存数据库(0-15)中,获取与指定前缀字符串相匹配的所有的Redis缓存键实例。
/// </remarks>
/// <returns>
/// 返回:
/// 与指定前缀字符串相匹配的所有的Redis缓存键实例。
/// </returns>
/// </summary>
protected virtual IEnumerable<RedisKey> GetKeys(EndPoint endPoint, string prefix = null)
{
var server = _connectionWrapper.GetServer(endPoint);
var keys = server.Keys(_db.Database, string.IsNullOrEmpty(prefix) ? null : $"{prefix}*");
return keys;
}
#endregion
#region 方法
/// <param name="prefix">1个指定的前缀字符串。</param>
/// <param name="prefixParameters">数组实例,该实例中存储着n个泛型实例,这些实例为前缀字符串的拼接提供数据支撑。</param>
/// <summary>
/// 【异步通过前缀字符串移除实例】
/// <remarks>
/// 摘要:
/// 该方法的覆写方法用于根据1个新的前缀字符串,从缓存数据库中移除1/n个指定的缓存项(键/(“JSON”编码格式的)值对)。
/// </remarks>
/// </summary>
public override async Task RemoveByPrefixAsync(string prefix, params object[] prefixParameters)
{
prefix = PrepareKeyPrefix(prefix, prefixParameters);
foreach (var endPoint in _connectionWrapper.GetEndPoints())
{
var keys = GetKeys(endPoint, prefix);
_db.KeyDelete(keys.ToArray());
}
await RemoveByPrefixInstanceDataAsync(prefix);
}
/// <param name="prefix">1个指定的前缀字符串。</param>
/// <param name="prefixParameters">数组实例,该实例中存储着n个泛型实例,这些实例为前缀字符串的拼接提供数据支撑。</param>
/// <summary>
/// 【通过前缀字符串移除实例】
/// <remarks>
/// 摘要:
/// 该方法是抽象方法,该方法的覆写方法用于根据1个新的前缀字符串,从缓存数据库中移除1/n个指定的缓存项(键/(“JSON”编码格式的)值对)。
/// </remarks>
/// </summary>
public override void RemoveByPrefix(string prefix, params object[] prefixParameters)
{
prefix = PrepareKeyPrefix(prefix, prefixParameters);
foreach (var endPoint in _connectionWrapper.GetEndPoints())
{
var keys = GetKeys(endPoint, prefix);
_db.KeyDelete(keys.ToArray());
}
RemoveByPrefixInstanceData(prefix);
}
/// <summary>
/// 【异步清理终结点】
/// <remarks>
/// 摘要:
/// 清理释放Redis分布式缓存数据库软件分布式缓存数据库(0-15)中所有缓存项。
/// </remarks>
/// </summary>
public override async Task ClearAsync()
{
await _connectionWrapper.FlushDatabaseAsync();
ClearInstanceData();
}
#endregion
#region 嵌套类
/// <summary>
/// 【Redis连接封装器--类】
/// <remarks>
/// 摘要:
/// 通过该类中的方法成员实现了调用“StackExchange.Redis”中间件的实例,从而Redis软件对缓存数据的管理操作,这些管理操作包含:缓存数据加载,缓存数据移除和缓存数据清理等。。
/// </remarks>
/// </summary>
protected class RedisConnectionWrapper
{
#region 变量--私有/保护
/// <summary>
/// 【锁】
/// <remarks>
/// 摘要:
/// 锁实例。
/// </remarks>
/// </summary>
private readonly object _lock = new();
/// <summary>
/// 【分布式缓存】
/// <remarks>
/// 摘要:
/// Redis分布式缓存数据库软件连接器实例,该实例实际上是:“StackExchange.Redis”中间件的实例,通过该实例实现当前程序通过“StackExchange.Redis”中间件的实例与Redis分布式缓存数据库的交互操作。
/// </remarks>
/// </summary>
private volatile ConnectionMultiplexer _connection;
/// <summary>
/// 【连接字符串】
/// <remarks>
/// 摘要:
/// Redis分布式缓存数据库软件的连接字符串。
/// </remarks>
/// </summary>
private readonly Lazy<string> _connectionString;
#endregion
#region 构造方法
/// <param name="connectionString">应用配置类的1个指定实例。</param>
/// <summary>
/// 【默认构造方法】
/// <remarks>
/// 摘要:
/// 通过默认构造方法,实例化Redis分布式缓存数据库软件的连接字符串。
/// </remarks>
/// </summary>
public RedisConnectionWrapper(string connectionString)
{
_connectionString = new Lazy<string>(connectionString);
}
#endregion
#region 方法----私有/保护
/// <summary>
/// 【获取连接】
/// <remarks>
/// 摘要:
/// 建立当前程序与Redis分布式缓存数据库软件的连接。
/// </remarks>
/// <returns>
/// 返回:
/// Redis分布式缓存数据库软件的连接器实例。
/// </returns>
/// </summary>
protected ConnectionMultiplexer GetConnection()
{
if (_connection != null && _connection.IsConnected)
return _connection;
lock (_lock)
{
if (_connection != null && _connection.IsConnected)
return _connection;
//显式销毁释放内存中Redis分布式缓存数据库软件的连接器实例,该实例实际上是:显式销毁释放内存中的“StackExchange.Redis”中间件实例。
_connection?.Dispose();
//实例化Redis分布式缓存数据库软件的连接器实例,该实例实际上是:“StackExchange.Redis”中间件实例。
_connection = ConnectionMultiplexer.Connect(_connectionString.Value);
}
return _connection;
}
#endregion
#region 方法--销毁
/// <summary>
/// 【销毁】
/// <remarks>
/// 摘要:
/// 显式销毁释放内存中的连接器实例,该实例实际上是:显式销毁释放内存中的“StackExchange.Redis”中间件实例。
/// </remarks>
/// </summary>
public void Dispose()
{
//显式销毁释放内存中的连接器实例,该实例实际上是:显式销毁释放内存中的“StackExchange.Redis”中间件实例。
_connection?.Dispose();
}
#endregion
#region 方法
/// <summary>
/// 【获取数据库】
/// <remarks>
/// 摘要:
/// 建立当前程序与Redis分布式缓存数据库软件中分布式缓存数据库的连接。
/// </remarks>
/// <returns>
/// 返回:
/// 数据库连接接口实例,该实例用于建立当前程序与Redis分布式缓存数据库软件中分布式缓存数据库的连接。
/// </returns>
/// </summary>
public IDatabase GetDatabase()
{
return GetConnection().GetDatabase();
}
/// <param name="endPoint">Redis分布式缓存数据库软件中所有可用的终结点。</param>
/// <summary>
/// 【获取服务】
/// <remarks>
/// 摘要:
/// 获取Redis分布式缓存数据库软件中所有可用的终结点,即所有的分布式缓存数据库(0-15)。
/// </remarks>
/// <returns>
/// 返回:
/// 服务接口实例,该实例用于建立当前程序与Redis分布式缓存数据库软件中分布式缓存数据库的连接。
/// </returns>
/// </summary>
public IServer GetServer(EndPoint endPoint)
{
return GetConnection().GetServer(endPoint);
}
/// <summary>
/// 【获取终结点】
/// <remarks>
/// 摘要:
/// 获取Redis分布式缓存数据库软件中所有可用的终结点,即所有的分布式缓存数据库(0-15)。
/// </remarks>
/// <returns>
/// 返回:
/// 数组实例,该实例存储着Redis分布式缓存数据库软件中所有可用的终结点,即所有的分布式缓存数据库(0-15)。
/// </returns>
/// </summary>
public EndPoint[] GetEndPoints()
{
return GetConnection().GetEndPoints();
}
/// <summary>
/// 【异步清理终结点】
/// <remarks>
/// 摘要:
/// 清理释放Redis分布式缓存数据库软件分布式缓存数据库(0-15)中所有缓存项。
/// </remarks>
/// </summary>
public async Task FlushDatabaseAsync()
{
var endPoints = GetEndPoints();
foreach (var endPoint in endPoints)
await GetServer(endPoint).FlushDatabaseAsync();
}
#endregion
}
#endregion
}
}
2 Srvices.Caching.MemoryDistributedCacheManager
using Microsoft.Extensions.Caching.Distributed;
using Core.Caching;
using Core.Configuration;
namespace Nop.Services.Caching
{
/// <summary>
/// 【Redis分布式缓存委托事件内存管理器--类】
/// <remarks>
/// 摘要:
/// 通过该类中的方法成员实现了把对Redis分布式缓存数据库操作的委托事件存储到一个列表实例(内存)中,从而通过对该列表实例(内存)的操作实现对Redis分布式缓存数据库操作委托事件的集中管理。
/// </remarks>
/// </summary>
public class MemoryDistributedCacheManager : DistributedCacheManager
{
#region 变量--私有/保护
/// <summary>
/// 【键列表实例】
/// <remarks>
/// 摘要:
/// 1个指定的列表实例,该列表实例(内存)中存储着Redis分布式缓存数据库操作的所有委托事件。
/// </remarks>
/// </summary>
private static readonly List<string> _keysList = new();
#endregion
#region 拷贝构造方法
/// <param name="appSettings">应用配置类的1个指定实例。</param>
/// <param name="distributedCache">分布式缓存接口实例,该实例实际上是:“StackExchange.Redis”中间件的实例,通过该实例实现当前程序通过“StackExchange.Redis”中间件的实例与Redis分布式缓存数据库的交互操作。</param>
/// <summary>
/// 【拷贝构造方法】
/// <remarks>
/// 摘要:
/// 通过拷贝构造方法,把Redis分布式缓存数据库操作的所有委托事件存储到列表实例中或从列表实例中移除。
/// </remarks>
/// </summary>
public MemoryDistributedCacheManager(AppSettings appSettings, IDistributedCache distributedCache) : base(
appSettings, distributedCache)
{
_onKeyAdded += key =>
{
using var _ = _locker.Lock();
if (!_keysList.Contains(key.Key))
_keysList.Add(key.Key);
};
_onKeyRemoved += key =>
{
using var _ = _locker.Lock();
if (_keysList.Contains(key.Key))
_keysList.Remove(key.Key);
};
}
#endregion
#region 方法
/// <param name="prefix">1个指定的前缀字符串。</param>
/// <param name="prefixParameters">数组实例,该实例中存储着n个泛型实例,这些实例为前缀字符串的拼接提供数据支撑。</param>
/// <summary>
/// 【异步通过前缀字符串移除实例】
/// <remarks>
/// 摘要:
/// 该方法的覆写方法用于根据1个新的前缀字符串,从缓存数据库中移除1/n个指定的缓存项(键/(“JSON”编码格式的)值对)后,并从列表实例中移除相应的Redis分布式缓存数据库操作的委托事件。
/// </remarks>
/// </summary>
public override async Task RemoveByPrefixAsync(string prefix, params object[] prefixParameters)
{
using (var _ = _locker.Lock())
{
prefix = PrepareKeyPrefix(prefix, prefixParameters);
//从缓存数据库中移除1/n个指定的缓存项(键/(“JSON”编码格式的)值对)后,并从列表实例中移除相应的Redis分布式缓存数据库操作的委托事件。
foreach (var key in _keysList
.Where(key => key.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase))
.ToList())
{
await _distributedCache.RemoveAsync(key);
_keysList.Remove(key);
}
}
await RemoveByPrefixInstanceDataAsync(prefix);
}
/// <param name="prefix">1个指定的前缀字符串。</param>
/// <param name="prefixParameters">数组实例,该实例中存储着n个泛型实例,这些实例为前缀字符串的拼接提供数据支撑。</param>
/// <summary>
/// 【通过前缀字符串移除实例】
/// <remarks>
/// 摘要:
/// 该方法的覆写方法用于根据1个新的前缀字符串,从缓存数据库中移除1/n个指定的缓存项(键/(“JSON”编码格式的)值对)后,并从列表实例中移除相应的Redis分布式缓存数据库操作的委托事件。
/// </remarks>
/// </summary>
public override void RemoveByPrefix(string prefix, params object[] prefixParameters)
{
using (var _ = _locker.Lock())
{
prefix = PrepareKeyPrefix(prefix, prefixParameters);
//从缓存数据库中移除1/n个指定的缓存项(键/(“JSON”编码格式的)值对)后,并从列表实例中移除相应的Redis分布式缓存数据库操作的委托事件。
foreach (var key in _keysList
.Where(key => key.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase))
.ToList())
{
_distributedCache.Remove(key);
_keysList.Remove(key);
}
}
RemoveByPrefixInstanceData(prefix);
}
/// <summary>
/// 【异步清理】
/// <remarks>
/// 摘要:
/// 根据列表实例从缓存数据库中移除与其相关的所有缓存项后,清理该列表实例。
/// </remarks>
/// </summary>
public override async Task ClearAsync()
{
using (var _ = _locker.Lock())
{
//根据列表实例从缓存数据库中移除与其相关的所有缓存项。
foreach (var key in _keysList)
await _distributedCache.RemoveAsync(key);
//执行列表实例清理操作。
_keysList.Clear();
}
ClearInstanceData();
}
#endregion
}
}
3 重构appsettings.json
{
"ConnectionStrings": {
"ConnectionString": "Data Source=.;Initial Catalog=ShopRazor;Integrated Security=False;Persist Security Info=False;User ID=zz;Password=zz;MultipleActiveResultSets=true;Trust Server Certificate=True",
"DataProvider": "sqlserver",
"SQLCommandTimeout": null
},
"CacheConfig": {
"DefaultCacheTime": 60,
"ShortTermCacheTime": 3,
"BundledFilesCacheTime": 120
},
"DistributedCacheConfig": {
"DistributedCacheType": "redis",
"Enabled": true,
"ConnectionString": "127.0.0.1:6379,ssl=False",
"SchemaName": "dbo",
"TableName": "DistributedCache"
}
}
4 重构Program.cs文件
using Core.Configuration;
using Core.Infrastructure;
using Data;
using Data.Configuration;
using Microsoft.EntityFrameworkCore;
using Framework.Infrastructure.Extensions;
using Core.Caching;
using Core.Events;
using Services.Events;
using Nop.Services.Caching;
using Services.Caching;
var builder = WebApplication.CreateBuilder(args);
//如果启动项中不存在“appsettings.json”文件,则通过.Net(Core)的内置方法自动新建“appsettings.json”文件。
builder.Configuration.AddJsonFile("appsettings.json", true, true);
//把当前程序中所有继承了“IConfig”接口的具体实现类的实例,依赖注入到.Net(Core)内置依赖注入容器实例中,如果需要并把这些数据持久化存储到"appsettings.json"文件。
builder.Services.ConfigureApplicationSettings(builder);
builder.Services.AddScoped<INopFileProvider, NopFileProvider>();
var appSettings = Singleton<AppSettings>.Instance;
var distributedCacheConfig = appSettings.Get<DistributedCacheConfig>();
//必须先设定 "Enabled": true,且启动Redis分布式数据库,如果 "Enabled": false,则必须把下面2行给注释掉。
if (!distributedCacheConfig.Enabled)
return;
switch (distributedCacheConfig.DistributedCacheType)
{
case DistributedCacheType.Memory:
builder.Services.AddDistributedMemoryCache();
break;
case DistributedCacheType.Redis:
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = distributedCacheConfig.ConnectionString;
});
break;
}
if (distributedCacheConfig.Enabled)
{
switch (distributedCacheConfig.DistributedCacheType)
{
case DistributedCacheType.Memory:
builder.Services.AddScoped<ILocker, MemoryDistributedCacheManager>();
builder.Services.AddScoped<IStaticCacheManager, MemoryDistributedCacheManager>();
break;
case DistributedCacheType.Redis:
builder.Services.AddScoped<ILocker, RedisCacheManager>();
builder.Services.AddScoped<IStaticCacheManager, RedisCacheManager>();
break;
}
}
else
{
builder.Services.AddSingleton<ILocker, MemoryCacheManager>();
builder.Services.AddSingleton<IStaticCacheManager, MemoryCacheManager>();
}
builder.Services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
builder.Services.AddSingleton<IEventPublisher, EventPublisher>();
var typeFinder = Singleton<ITypeFinder>.Instance;
var consumers = typeFinder.FindClassesOfType(typeof(IConsumer<>)).ToList();
foreach (var consumer in consumers)
foreach (var findInterface in consumer.FindInterfaces((type, criteria) =>
{
var isMatch = type.IsGenericType && ((Type)criteria).IsAssignableFrom(type.GetGenericTypeDefinition());
return isMatch;
}, typeof(IConsumer<>)))
builder.Services.AddScoped(findInterface, consumer);
//注意:在动态对EntityFrameworkCore中间件进行实例化时,必须使用“AddDbContextPool” 内置方法替换“AddDbContext”内置方法,
//否则就会出现异常:“System.InvalidOperationException: The service collection cannot be modified because it is read-only”。
builder.Services.AddDbContextPool<EFCoreContext>(options => {
//从单例实例的字典成员实例中获取当前程序所有配置相关数据。
AppSettings _appSettings = Singleton<AppSettings>.Instance;
//从应用配置类实例中获取数据库连接相关数据。
DataConfig _dataConfig = _appSettings.Get<DataConfig>();
//说明:如果想要“EntityFrameworkCore”中间件支持多数据库软件,则把选择条件中的所有中间件都注入到依赖注入到.Net(Core)框架内置容器即可,
//选择条件来限定当前程序只支持所设定的1个数据库软件,当然“DataConfig”类与“appsettings.json”文件也必须为支持多数据库软件进行重构。
if (_dataConfig.DataProvider.ToString().Equals("sqlserver", StringComparison.InvariantCultureIgnoreCase))
{
//通过“DbContextOptionsBuilder”实例中的参数实例,为“Microsoft.EntityFrameworkCore.SqlServer”中间件的实例化提供参数实例,
//最终把“Microsoft.EntityFrameworkCore.SqlServer”中间件实例,依赖注入到.Net(Core)框架内置容器中。
//IIS发布部署连接字符串必须使用“SQL Server身份认证”数据库连接方式,才能实现发布部署程序与数据库的CURD的操作。
options.UseSqlServer(_dataConfig.ConnectionString);
}
});
//通过AddRazorRuntimeCompilation依赖注入中间件实现页面修改热加载(Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation)。
builder.Services
.AddControllersWithViews()
.AddRazorRuntimeCompilation();
//把具体现类和中间件注入到内置依赖注入容器后,并把.NetCore框架内置依赖注入容器接口实例所存储的当前程序中的具体现类和中间件的实例通过“Engine”(引擎)单例实例存储到单例类的字典属性成员实例中。
//注意:从依赖注入到.Net(Core)框架内置容器中,获取“IServiceProvider”接口实例,必须定义在最后,
//否则“GetServices”/“GetRequiredService”方法将有可能不能获取取1个指定类的实例,因为该类的实例还没有依赖注入到.Net(Core)框架内置容器中。
builder.Services.ConfigureApplicationServices(builder);
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
/*注意:
在.net7最好不要直接使用app.UseEndpoints或app.UseMvc来集成路由匹配模式,否则会出现:“ASP0014”警告信息,
为了避免该警告信息直接使用最小API:app.MapControllerRoute来集成路由匹配模式。
*/
app.MapControllerRoute(
name: "areaRoute",
pattern: $"{{area:exists}}/{{controller=Install}}/{{action=Index}}/{{id?}}");
//动态实例化EntityFrameworkCore中间件,必须把InstallRefactoring/Index设定为默认启动页面,
//否则依然会因为当前类的拷贝构造方法实例化EntityFrameworkCore中间件是在程序启动前就被实例化的,如果无数据库如果数据库连接字符串,
//那么就会因EntityFrameworkCore中间件实例中不包含数据库如果数据库连接字符串,从而造成上述异常。
app.MapControllerRoute(
name: "default",
pattern: "{controller=InstallRefactoring}/{action=Index}/{id?}");
// 通过.NetCore框架的内置管道接口实例,实例化“IServiceProvider”接口实例,同时把继承于“IStartup”所有具体现类中的中间件实例集成到.NetCore框架内置管道中。
app.ConfigureRequestPipeline();
app.Run();
5 运行过程
对以上功能更为具体实现和注释见230510_010ShopRazor(CURD操作与RedisCache缓存的强制清理的实现)。