Asp.Net Core 轻松学-正确使用分布式缓存
前言
本来昨天应该更新的,但是由于各种原因,抱歉,让追这个系列的朋友久等了。上一篇文章 在.Net Core 使用缓存和配置依赖策略 讲的是如何使用本地缓存,那么本篇文章就来了解一下如何使用分布式缓存,通过本章,你将了解到如何使用分布式缓存,以及最重要的是,如何选择适合自己的分布式缓存;本章主要包含两个部分:
内容提要
- 使用 SqlServer 分布式缓存
- 使用 Redis 分布式缓存
- 实现自定义的分布式缓存客户端注册扩展
- 关于本示例的使用说明
1. 使用 SqlServer 分布式缓存
1.1 准备工作,请依照以下步骤实施
- 1 创建一个 Asp.Net Core MVC 测试项目:Ron.DistributedCacheDemo
- 2 为了使用 SqlServer 作为分布式缓存的数据库,需要在项目中引用 Microsoft.EntityFrameworkCore 相关组件
- 3 在 SqlServer 数据库引擎中创建一个数据库,命名为:TestDb
- 4 打开 Ron.DistributedCacheDemo 项目根目录,执行创建缓存数据表的操作,执行命令后如果输出信息:Table and index were created successfully. 表示缓存表创建成功
dotnet sql-cache create "Server=.\SQLEXPRESS;User=sa;Password=123456;Database=TestDb" dbo AspNetCoreCache
1.2 开始使用 SqlServer 分布式缓存
.Net Core 中的分布式缓存统一接口是 IDistributedCache 该接口定义了一些对缓存常用的操作,比如我们常见的 Set/Get 方法,而 SqlServer 分布式缓存由 SqlServerCache 类实现,该类位于命名空间 Microsoft.Extensions.Caching.SqlServer 中
- 在 Startup.cs 中注册分布式缓存
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 的配置非常重要,这里强烈建议大家手动配置
1.3 了解 SqlServerCacheOptions,先来看一下SqlServerCacheOptions 的结构
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 时间是得到的是美国时间,所以这里实现了一个本地时钟,代码非常简单,只是获取一个本地时间
public class LocalSystemClock : Microsoft.Extensions.Internal.ISystemClock
{
public DateTimeOffset UtcNow => DateTime.Now;
}
1.4 在控制器中使用分布式缓存
- 首先使用依赖注入,在 HomeController 中获得 IDistributedCache 的实例对象,该实例对象的实现类型为 SqlServerCache,然后通过 Index 方法增加一项缓存 CurrentTime 并设置其值为当前时间,然后再另一接口 GetValue 中取出该 CurrentTime 的值
[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;
}
}
- 运行程序,打开地址:http://localhost:5000/api/home/settime,然后查看缓存数据库,缓存项 CurrentTime 已存入数据库中
- 访问接口:http://localhost:5000/api/home/gettime 得到缓存项 CurrentTime 的值
- 等到超时时间过期后,再到数据库查看,发现缓存项 CurrentTime 还在数据库中,这是因为缓存清理机制造成的
1.5 缓存清理
在缓存过期后,每次调用 Get/GetAsync 方法都会 调用 SqlServerCache 的 私有方法 ScanForExpiredItemsIfRequired() 进行一次扫描,然后清除所有过期的缓存条目,扫描方法执行过程也很简单,就是直接执行数据库查询语句
DELETE FROM {0} WHERE @UtcNow > ExpiresAtTime
值得注意的是,在异步方法中使用同步调用不会触发缓存逐出,因为其线程退出导致 Task.Run 未能运行,比如下面的代码
[HttpGet("GetTime")]
public async Task<ActionResult<string>> GetTime()
{
var CurrentTime = this.cache.GetString("CurrentTime");
return CurrentTime;
}
将导致 SqlServerCache 无法完整执行方法 ScanForExpiredItemsIfRequired(),因为其内部使用了 Task 进行异步处理,正确的做法是使用 await this.cache.GetStringAsync("CurrentTime");
1.6 关于缓存清理方法 ScanForExpiredItemsIfRequired
private void ScanForExpiredItemsIfRequired()
{
var utcNow = _systemClock.UtcNow;
if ((utcNow - _lastExpirationScan) > _expiredItemsDeletionInterval)
{
_lastExpirationScan = utcNow;
Task.Run(_deleteExpiredCachedItemsDelegate);
}
}
在多线程环境下,该方法可能除非多次重复扫描,即可能会多次执行 SQL 语句 DELETE FROM {0} WHERE @UtcNow > ExpiresAtTime ,但是,这也仅仅是警告而已,并没有任何可改变其行为的控制途径
1.7 IDistributedCache 的其它扩展方法
.Net Core 中还对 IDistributedCache 进行了扩展,甚至允许通过 Set 方法传入一个 DistributedCacheEntryOptions 以覆盖全局设置,这些扩展方法的使用都比较简单,直接传入相应的值即可,在此不再一一介绍
希望深入研究的同学,可以手动逐一测试
1.8 关于 AddDistributedSqlServerCache() 方法
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>());
}
2. 使用 Redis 分布式缓存
要在 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 接口并不复杂,定义非常简单
2.1 在 Startup.cs 中注册 Redis 分布式缓存配置
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 的方式是完全一致的
2.2 使用缓存
[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 了
3. 实现自定义的分布式缓存客户端,下面的代码表示实现一个 CSRedis 客户端的分布式缓存注册扩展
3.1 定义 CSRedisCache 实现 IDistributedCache 接口
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 中释放资源
3.2 自定义一个配置类 CSRedisClientOptions
public class CSRedisClientOptions
{
public string ConnectionString { get; set; }
public Func<string, string> NodeRule { get; set; }
public string[] ConnectionStrings { get; set; }
}
该配置类主要是为 CSRedis 客户端接收配置使用
3.3 注册扩展方法 CSRedisCacheServiceCollectionExtensions
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;
}
}
自定义一个扩展方法,进行配置初始化工作,简化实际注册使用时的处理步骤
3.4 在 Startup.cs 中使用扩展
public void ConfigureServices(IServiceCollection services)
{
services.AddCSRedisCache(options =>
{
options.ConnectionString = this.Configuration["RedisConnectionString"];
});
...
}
上面的代码就简单实现了一个第三方分布式缓存客户端的注册和使用
3.5 测试自定义分布式缓存客户端,创建一个测试控制器 CustomerController
[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,运行程序,输出结果正常
- 调用 NewId 接口创建一条缓存记录
- 调用 GetId 接口获取缓存记录
至此,我们完整的实现了一个自定义分布式缓存客户端注册
4. 关于本示例的使用说明
4.1 首先看一下解决方案结构
该解决方案红框处定义了 3 个不同的 Startup.cs 文件,分别是
- CSRedisStartup (自定义缓存测试启动文件)
- Sql_Startup (SqlServer 测试启动文件)
- StackChangeRedis_Startup(StackChange.Redis 测试启动文件)
- 在使用本示例的时候,通过在 Program.cs 中切换不同的启动文件进行测试
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
Asp.Net Core 轻松学-正确使用分布式缓存的更多相关文章
- Asp.Net Core 轻松学系列-1阅读指引目录
https://www.cnblogs.com/viter/p/10474091.html 目录 前言 1. 从安装到配置 2. 业务实现 3. 日志 4. 测试 5. 缓存使用 6.网络和通讯 7. ...
- Asp.Net Core 轻松学-多线程之Task(补充)
前言 在上一章 Asp.Net Core 轻松学-多线程之Task快速上手 文章中,介绍了使用Task的各种常用场景,但是感觉有部分内容还没有完善,在这里补充一下. 1. 任务的等待 在使用 ...
- WebAPI调用笔记 ASP.NET CORE 学习之自定义异常处理 MySQL数据库查询优化建议 .NET操作XML文件之泛型集合的序列化与反序列化 Asp.Net Core 轻松学-多线程之Task快速上手 Asp.Net Core 轻松学-多线程之Task(补充)
WebAPI调用笔记 前言 即时通信项目中初次调用OA接口遇到了一些问题,因为本人从业后几乎一直做CS端项目,一个简单的WebAPI调用居然浪费了不少时间,特此记录. 接口描述 首先说明一下,基于 ...
- C# 中一些类关系的判定方法 C#中关于增强类功能的几种方式 Asp.Net Core 轻松学-多线程之取消令牌
1. IsAssignableFrom实例方法 判断一个类或者接口是否继承自另一个指定的类或者接口. public interface IAnimal { } public interface ID ...
- Asp.Net Core 轻松学-一行代码搞定文件上传 JSONHelper
Asp.Net Core 轻松学-一行代码搞定文件上传 前言 在 Web 应用程序开发过程中,总是无法避免涉及到文件上传,这次我们来聊一聊怎么去实现一个简单方便可复用文件上传功能:通过创建 ...
- Asp.Net Core 轻松学-使用MariaDB/MySql/PostgreSQL和支持多个上下文对象
前言 在上一篇文章中(Asp.Net Core 轻松学-10分钟使用EFCore连接MSSQL数据库)[https://www.cnblogs.com/viter/p/10243577.html],介 ...
- Asp.Net Core 轻松学-利用文件监视进行快速测试开发
前言 在进行 Asp.Net Core 应用程序开发过程中,通常的做法是先把业务代码开发完成,然后建立单元测试,最后进入本地系统集成测试:在这个过程中,程序员的大部分时间几乎都花费在开发.运行 ...
- 如何从40亿整数中找到不存在的一个 webservice Asp.Net Core 轻松学-10分钟使用EFCore连接MSSQL数据库 WPF实战案例-打印 RabbitMQ与.net core(五) topic类型 与 headers类型 的Exchange
如何从40亿整数中找到不存在的一个 前言 给定一个最多包含40亿个随机排列的32位的顺序整数的顺序文件,找出一个不在文件中的32位整数.(在文件中至少确实一个这样的数-为什么?).在具有足够内存的情况 ...
- Asp.Net Core 轻松学-在.Net Core 使用缓存和配置依赖策略
前言 几乎在所有的应用程序中,缓存都是一个永恒的话题,恰当的使用缓存可以有效提高应用程序的性能:在某些业务场景下,使用缓存依赖会有很好的体验:在 Asp.Net Core 中,支持了多种缓存组 ...
随机推荐
- 分布式消息队列XXL-MQ
<分布式消息队列XXL-MQ> 一.简介 1.1 概述 XXL-MQ是一款轻量级分布式消息队列,支持串行.并行和广播等多种消息模型.现已开放源代码,开箱即用. 支持三种消息模式: ...
- 如何利用Python网络爬虫爬取微信朋友圈动态--附代码(下)
前天给大家分享了如何利用Python网络爬虫爬取微信朋友圈数据的上篇(理论篇),今天给大家分享一下代码实现(实战篇),接着上篇往下继续深入. 一.代码实现 1.修改Scrapy项目中的items.py ...
- 3、js无缝滚动轮播
另一个无缝滚动轮播,带暂停,由于js是异步,用C面向过程的思想开始会很怪异很怪异,因为当你定时器里面需要执行的函数时间比较长或是有一段延时时,异步的代码会完全不同,但习惯就好了. 这个代码有几个问题, ...
- i++ 和 ++i;&& 和 &
一.算数运算符(自增运算符i++.自减运算符i++) ※ i++是先赋值(计算)再加1 :++i是先加1再赋值(计算) : int m = 5; boolean bool = ++m > 5 ...
- xcode6.1 设置中文输入
XCode6.1中设置中文输入方法:Product->scheme->Edit Scheme->Options->Application Region->中国 ios 模 ...
- jquery遍历table为每一个单元格取值及赋值
表格代码 <tr> <td> <input type="text" style="border: none; text-align: cen ...
- 第二天 Java语言基础
一.如何定义Java中的类 Java代码都定义在类中,类由class来定义,区分public class和class: 二.main方法的作用 main方法是程序的入口:保证程序的独立运行:被JVM调 ...
- channel.go
) c.RLock() client, ok := c.clients[msg.clientID] c.RUnlock() if ok ...
- 【bzoj1758】[Wc2010]重建计划
Description Input 第一行包含一个正整数N,表示X国的城市个数. 第二行包含两个正整数L和U,表示政策要求的第一期重建方案中修建道路数的上下限 接下来的N-1行描述重建小组的原有方案, ...
- BZOJ_2622_[2012国家集训队测试]深入虎穴_最短路
BZOJ_2622_[2012国家集训队测试]深入虎穴_最短路 Description 虎是中国传统文化中一个独特的意象.我们既会把老虎的形象用到喜庆的节日装饰画上,也可能把它视作一种邪恶的可怕的动物 ...