原本昨天应该更新的,可是因为各类缘由,抱歉,让追这个系列的朋友久等了。上一篇文章 在.Net Core 使用缓存和配置依赖策略 讲的是如何使用本地缓存,那么本篇文章就来了解一下如何使用分布式缓存,经过本章,你将了解到如何使用分布式缓存,以及最重要的是,如何选择适合本身的分布式缓存;本章主要包含两个部分:html
内容提要git
- 使用 SqlServer 分布式缓存
- 使用 Redis 分布式缓存
- 实现自定义的分布式缓存客户端注册扩展
- 关于本示例的使用说明
dotnet sql-cache create "Server=.\SQLEXPRESS;User=sa;Password=123456;Database=TestDb" dbo AspNetCoreCache
.Net Core 中的分布式缓存统一接口是 IDistributedCache 该接口定义了一些对缓存经常使用的操做,好比咱们常见的 Set/Get 方法,而 SqlServer 分布式缓存由 SqlServerCache 类实现,该类位于命名空间 Microsoft.Extensions.Caching.SqlServer 中github
public void ConfigureServices(IServiceCollection services) { services.AddDistributedSqlServerCache(options => { options.SystemClock = new BLL.LocalSystemClock(); options.ConnectionString = this.Configuration["ConnectionString"]; options.SchemaName = "dbo"; options.TableName = "AspNetCoreCache"; options.DefaultSlidingExpiration = TimeSpan.FromMinutes(1); options.ExpiredItemsDeletionInterval = TimeSpan.FromMinutes(5); }); ... }
上面的方法 ConfigureServices(IServiceCollection services) 中使用 services.AddDistributedSqlServerCache() 这个扩展方法引入了 SqlServer 分布式缓存,并做了一些简单的配置,该配置是由 SqlServerCacheOptions 决定的,SqlServerCacheOptions 的配置很是重要,这里强烈建议你们手动配置redis
namespace Microsoft.Extensions.Caching.SqlServer { public class SqlServerCacheOptions : IOptions<SqlServerCacheOptions> { public SqlServerCacheOptions(); // 缓存过时扫描时钟 public ISystemClock SystemClock { get; set; } // 缓存过时逐出时间,默认为 30 分钟 public TimeSpan? ExpiredItemsDeletionInterval { get; set; } // 缓存数据库链接字符串 public string ConnectionString { get; set; } // 缓存表所属架构 public string SchemaName { get; set; } // 缓存表名称 public string TableName { get; set; } // 缓存默认过时时间,默认为 20 分钟 public TimeSpan DefaultSlidingExpiration { get; set; } } }
该配置很是简单,仅是对缓存使用的基本配置
首先,使用 options.SystemClock 配置了一个本地时钟,接着设置缓存过时时间为 1 分钟,缓存过时后逐出时间为 5 分钟,其它则是链接数据库的各项配置
在缓存过时扫描的时候,使用的时间正是 options.SystemClock 该时钟的时间,默认状况下,该时钟使用 UTC 时间,在个人电脑上,UTC 时间是获得的是美国时间,因此这里实现了一个本地时钟,代码很是简单,只是获取一个本地时间sql
public class LocalSystemClock : Microsoft.Extensions.Internal.ISystemClock { public DateTimeOffset UtcNow => DateTime.Now; }
[Route("api/Home")] [ApiController] public class HomeController : Controller { private IDistributedCache cache; public HomeController(IDistributedCache cache) { this.cache = cache; } [HttpGet("Index")] public async Task<ActionResult<string>> SetTime() { var CurrentTime = DateTime.Now.ToString(); await this.cache.SetStringAsync("CurrentTime", CurrentTime); return CurrentTime; } [HttpGet("GetTime")] public async Task<ActionResult<string>> GetTime() { var CurrentTime = await this.cache.GetStringAsync("CurrentTime"); return CurrentTime; } }
在缓存过时后,每次调用 Get/GetAsync 方法都会 调用 SqlServerCache 的 私有方法 ScanForExpiredItemsIfRequired() 进行一次扫描,而后清除全部过时的缓存条目,扫描方法执行过程也很简单,就是直接执行数据库查询语句数据库
DELETE FROM {0} WHERE @UtcNow > ExpiresAtTime
值得注意的是,在异步方法中使用同步调用不会触发缓存逐出,由于其线程退出致使 Task.Run 未能运行,好比下面的代码api
[HttpGet("GetTime")] public async Task<ActionResult<string>> GetTime() { var CurrentTime = this.cache.GetString("CurrentTime"); return CurrentTime; }
将致使 SqlServerCache 没法完整执行方法 ScanForExpiredItemsIfRequired(),由于其内部使用了 Task 进行异步处理,正确的作法是使用 await this.cache.GetStringAsync("CurrentTime");缓存
private void ScanForExpiredItemsIfRequired() { var utcNow = _systemClock.UtcNow; if ((utcNow - _lastExpirationScan) > _expiredItemsDeletionInterval) { _lastExpirationScan = utcNow; Task.Run(_deleteExpiredCachedItemsDelegate); } }
在多线程环境下,该方法可能除非屡次重复扫描,便可能会屡次执行 SQL 语句 DELETE FROM {0} WHERE @UtcNow > ExpiresAtTime ,可是,这也仅仅是警告而已,并无任何可改变其行为的控制途径多线程
.Net Core 中还对 IDistributedCache 进行了扩展,甚至容许经过 Set 方法传入一个 DistributedCacheEntryOptions 以覆盖全局设置,这些扩展方法的使用都比较简单,直接传入相应的值便可,在此再也不一一介绍
但愿深刻研究的同窗,能够手动逐一测试架构
AddDistributedSqlServerCache 方法内部其实是进行了一系列的注册操做,其中最重要的是注册了 SqlServerCache 到 IDistributedCache 接口,该操做使得咱们能够在控制器中采用依赖注入的方式使用 IDistributedCache 的实例
查看 AddDistributedSqlServerCache 方法的代码片断
public static IServiceCollection AddDistributedSqlServerCache(this IServiceCollection services, Action<SqlServerCacheOptions> setupAction) { if (services == null) { throw new ArgumentNullException(nameof(services)); } if (setupAction == null) { throw new ArgumentNullException(nameof(setupAction)); } services.AddOptions(); AddSqlServerCacheServices(services); services.Configure(setupAction); return services; } internal static void AddSqlServerCacheServices(IServiceCollection services) { services.Add(ServiceDescriptor.Singleton<IDistributedCache, SqlServerCache>()); }
要在 Asp.Net Core 项目中使用 Redis 分布式缓存,须要引用包:Microsoft.Extensions.Caching.Redis,.Net Core 中的 Redis 分布式缓存客户端由 RedisCache 类提供实现 ,RedisCache 位于程序集 Microsoft.Extensions.Caching.StackExchangeRedis.dll 中,该程序集正是是依赖于大名鼎鼎的 Redis 客户端 StackExchange.Redis.dll,StackExchange.Redis 有许多的问题,其中最为严重的是超时问题,不过这不知本文的讨论范围,若是你但愿使用第三方 Redis 客户端替代 StackExchange.Redis 来使用分布式缓存,你须要本身实现 IDistributedCache 接口,好消息是,IDistributedCache 接口并不复杂,定义很是简单
public void ConfigureServices(IServiceCollection services) { services.AddDistributedRedisCache(options => { options.InstanceName = "TestDb"; options.Configuration = this.Configuration["RedisConnectionString"]; }); ... }
注册 Redis 分布式缓存配置和使用 StackExchange.Redis 的方式彻底相同,须要注意的是 RedisCacheOptions 包含 3 个属性,而 Configuration 和 ConfigurationOptions 的做用是相同的,一旦设置了 ConfigurationOptions ,就不该该再去设置属性 Configuration 的值,由于,在 AddDistributedRedisCache() 注册内部,会判断若是设置了 ConfigurationOptions 的值,则再也不使用 Configuration;可是,咱们建议仍是经过属性 Configuration 去初始化 Redis 客户端,由于,这是一个链接字符串,而各类配置均可以经过链接字符串进行设置,这和使用 StackExchange.Redis 的方式是彻底一致的
[Route("api/Home")] [ApiController] public class HomeController : Controller { private IDistributedCache cache; public HomeController(IDistributedCache cache) { this.cache = cache; } [HttpGet("Index")] public async Task<ActionResult<string>> SetTime() { var CurrentTime = DateTime.Now.ToString(); await this.cache.SetStringAsync("CurrentTime", CurrentTime); return CurrentTime; } [HttpGet("GetTime")] public async Task<ActionResult<string>> GetTime() { var CurrentTime = await this.cache.GetStringAsync("CurrentTime"); return CurrentTime; } }
细心的你可能已经发现了,上面的这段代码和以前演示的 SqlServerCache 彻底一致,是的,仅仅是修改一下注册的方法,咱们就能在项目中进行无缝的切换;可是,对于缓存有强依赖的业务,建议仍是须要作好缓存迁移,确保项目可以平滑过渡
惟一不一样的是,使用 Redis 分布式缓存容许你在异步方法中调用同步获取缓存的方法,这不会致使缓存清理的问题,由于缓存的管理已经彻底交给了 Redis 客户端 StackExchange.Redis 了
public class CSRedisCache : IDistributedCache, IDisposable { private CSRedis.CSRedisClient client; private CSRedisClientOptions _options; public CSRedisCache(IOptions<CSRedisClientOptions> optionsAccessor) { if (optionsAccessor == null) { throw new ArgumentNullException(nameof(optionsAccessor)); } _options = optionsAccessor.Value; if (_options.NodeRule != null && _options.ConnectionStrings != null) client = new CSRedis.CSRedisClient(_options.NodeRule, _options.ConnectionStrings); else if (_options.ConnectionString != null) client = new CSRedis.CSRedisClient(_options.ConnectionString); else throw new ArgumentNullException(nameof(_options.ConnectionString)); RedisHelper.Initialization(client); } public void Dispose() { if (client != null) client.Dispose(); } public byte[] Get(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } return RedisHelper.Get<byte[]>(key); } public async Task<byte[]> GetAsync(string key, CancellationToken token = default(CancellationToken)) { if (key == null) { throw new ArgumentNullException(nameof(key)); } token.ThrowIfCancellationRequested(); return await RedisHelper.GetAsync<byte[]>(key); } public void Refresh(string key) { throw new NotImplementedException(); } public Task RefreshAsync(string key, CancellationToken token = default(CancellationToken)) { throw new NotImplementedException(); } public void Remove(string key) { if (key == null) { throw new ArgumentNullException(nameof(key)); } RedisHelper.Del(key); } public async Task RemoveAsync(string key, CancellationToken token = default(CancellationToken)) { if (key == null) { throw new ArgumentNullException(nameof(key)); } await RedisHelper.DelAsync(key); } public void Set(string key, byte[] value, DistributedCacheEntryOptions options) { if (key == null) { throw new ArgumentNullException(nameof(key)); } RedisHelper.Set(key, value); } public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken)) { if (key == null) { throw new ArgumentNullException(nameof(key)); } await RedisHelper.SetAsync(key, value); } }
代码很少,都是实现 IDistributedCache 接口,而后在 IDisposable.Dispose 中释放资源
public class CSRedisClientOptions { public string ConnectionString { get; set; } public Func<string, string> NodeRule { get; set; } public string[] ConnectionStrings { get; set; } }
该配置类主要是为 CSRedis 客户端接收配置使用
public static class CSRedisCacheServiceCollectionExtensions { public static IServiceCollection AddCSRedisCache(this IServiceCollection services, Action<CSRedisClientOptions> setupAction) { if (services == null) { throw new ArgumentNullException(nameof(services)); } if (setupAction == null) { throw new ArgumentNullException(nameof(setupAction)); } services.AddOptions(); services.Configure(setupAction); services.Add(ServiceDescriptor.Singleton<IDistributedCache, CSRedisCache>()); return services; } }
自定义一个扩展方法,进行配置初始化工做,简化实际注册使用时的处理步骤
public void ConfigureServices(IServiceCollection services) { services.AddCSRedisCache(options => { options.ConnectionString = this.Configuration["RedisConnectionString"]; }); ... }
上面的代码就简单实现了一个第三方分布式缓存客户端的注册和使用
[Route("api/Customer")] [ApiController] public class CustomerController : Controller { private IDistributedCache cache; public CustomerController(IDistributedCache cache) { this.cache = cache; } [HttpGet("NewId")] public async Task<ActionResult<string>> NewId() { var id = Guid.NewGuid().ToString("N"); await this.cache.SetStringAsync("CustomerId", id); return id; } [HttpGet("GetId")] public async Task<ActionResult<string>> GetId() { var id = await this.cache.GetStringAsync("CustomerId"); return id; } }
该控制器简单实现两个接口,NewId/GetId,运行程序,输出结果正常
至此,咱们完整的实现了一个自定义分布式缓存客户端注册
该解决方案红框处定义了 3 个不一样的 Startup.cs 文件,分别是
- CSRedisStartup (自定义缓存测试启动文件)
- Sql_Startup (SqlServer 测试启动文件)
- StackChangeRedis_Startup(StackChange.Redis 测试启动文件)
public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Ron.DistributedCacheDemo.Startups.SqlServer.Startup>();
经过介绍,咱们了解到如何在 Asp.Net Core 中使用分布式缓存
了解了使用不一样的缓存类型,如 SqlServer 和 Redis
了解到了如何使用不一样的缓存类型客户端进行注册
了解到如何实现自定义缓存客户端
还知道了在调用 SqlServer 缓存的时候,异步方法中的同步调用会致使 SqlServerCache 没法进行过时扫描
CSRedisCore 此项目是由个人好朋友 nicye 维护,GitHub 仓库地址:访问CSRedisCore
https://github.com/lianggx/EasyAspNetCoreDemo/tree/master/Ron.DistributedCacheDemo