系列文章目录
链接: 【ASP.NET Core】REST与RESTful详解,从理论到实现
链接: 【ASP.NET Core】深入理解Controller的工作机制
链接: 【ASP.NET Core】内存缓存(MemoryCache)原理、应用及常见问题解析
前言
分布式缓存是将缓存数据存储后供多个外部应用服务器中的服务共享使用。比起内存缓存仅支持本服务使用,分布式缓存扩展多服务器,多应用。故因此得名分布式缓存。本文将介绍ASP.NET Core中如何应用Redis作为分布式缓存服务。
一、Redis
在分享ASP.NET Core中应用Redis作为分布式缓存服务前,先简单介绍一下Redis。
1.1 Redis简介
Redis(Remote Dictionary Server)直译过来就是远程字典服务。作为一款开源的高性能内存数据结构存储系统,支持字符串、哈希表、列表等多种数据结构,支持持久化保存功能。并且由于数据存储在内存中,Redis的读写速度远超传统关系型数据库。
1.2 常用数据结构
- 字符串(String)
- 二进制字符串,存储文本、JSON、数字或二进制图片数据等
- 哈希(Hash)
- 键值对集合,存储对象结构的数据。
- 列表(List)
- 有序的字符串列表,一般用作存储消息队列,或者记录时间线。
- 集合(Set)
- 无序且唯一的字符串集合,支持交并差操作。因为集合的特性,方便去重的场景使用。
- 有序集合(Sorted Set)
- 类似于普通的集合,但每个成员都关联了一个分数(score),一般用作排行榜
1.3 Redis的持久化
Redis的持久化分为两种。一种是RDB,通过将内存中的数据快照写入磁盘达到数据的保存;另外一种是AOF,Redis 将每个写操作追加到 AOF 文件的末尾,通过日志的方式记录操作。
1.3.1 RDB
RDB,既Redis Database,是一种快照持久化机制。是Redis在某一个规则下将某一时刻的内存数据以二进制形式写入磁盘,生成RDB文件。
RDB的配置项内容在在Redis根目录下的名为redis.windows-service.conf
的文件里。找到如下的结构
save 900 1 # 900秒内至少1个key被修改
save 300 10 # 300秒内至少10个key被修改
save 60 10000 # 60秒内至少10000个key被修改
stop-writes-on-bgsave-error yes #当RDB 快照生成过程中发生错误时(如磁盘已满、权限不足)停止接受新的写操作,防止数据不一致(默认值:yes)
rdbcompression yes #对RDB文件中的字符串对象启用 LZF 压缩算法
rdbchecksum yes #在RDB文件末尾添加 CRC64 校验和,用于加载时验证文件完整性
dbfilename dump.rdb
RDB是默认开启的,执行快照存储的时候会在根目录下新建dump.rdb
文件记录快照。
1.3.2 AOF
AOF,既Append Only File。Redis通过将每个写操作(如 SET、INCR)追加到 AOF 文件的末尾,实现日志的记录。显然这种方式的数据安全性最高。
AOF的配置项内容在在Redis根目录下的名为redis.windows-service.conf
的文件里。找到如下的结构
appendonly yes
appendfilename "appendonly.aof"
AOF并不是默认开启的。考虑到每一步操作写操作都会记录日志。该生成的日志文件会随着服务的运行变得十分巨大。
1.4 常用应用场景
1.4.1 缓存
将数据库热点数据缓存到 Redis,减少数据库访问压力。Redis存储空值得时候会记录NULL,自动解决缓存穿透得问题。
1.4.2 计数器
Redis中INCR是用于将存储在指定键中的数值递增 1 的命令。如果键不存在,Redis会先将其初始化为 0,然后再执行 INCR 操作。由于指令是原子性的,这就为我们实现一个计数器提供很好的先决条件。以及接口限流等这种需要使用到计算的功能。
1.4.2 订阅发布
当出现新的报警通知之类的,发布消息通知所有订阅的客户端。
二、ASP.NET Core中应用Redis【使用IDistributedCache接口】
在ASP.NET Core中应用Redis还是比较简单的,本文应用StackExchangeRedis这个库来对Redis进行操作。
2.1 安装依赖
通过这个指令按照StackExchangeRedis包
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
2.2 Program.cs 注册
在Program.cs中,我们通过AddStackExchangeRedisCache这个静态扩展方法注册Redis服务。通过观察AddStackExchangeRedisCache源码,我们发现实际上这个扩展方法往DI容器里注册的是IDistributedCache接口以及实现类RedisCacheImpl。生命周期是singleton。
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration["Redis:ConnectionStrings"];
options.InstanceName = builder.Configuration["Redis:InstanceName"];
});
/// <summary>
/// Adds Redis distributed caching services to the specified <see cref="IServiceCollection" />.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
/// <param name="setupAction">An <see cref="Action{RedisCacheOptions}"/> to configure the provided
/// <see cref="RedisCacheOptions"/>.</param>
/// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
public static IServiceCollection AddStackExchangeRedisCache(this IServiceCollection services, Action<RedisCacheOptions> setupAction)
{
ArgumentNullThrowHelper.ThrowIfNull(services);
ArgumentNullThrowHelper.ThrowIfNull(setupAction);
services.AddOptions();
services.Configure(setupAction);
services.Add(ServiceDescriptor.Singleton<IDistributedCache, RedisCacheImpl>());
return services;
}
2.3 IDistributedCache
IDistributedCache是ASP.NET Core框架提供的一个接口,用于实现分布式缓存,支持多种缓存提供者。也就是说不仅仅是Redis能够通过这个接口被操作。
这个接口定义很简单,总的来说就四种方法。Get,Set,Refresh,Remove。以及对应的四个异步方法。
/// <summary>
/// Represents a distributed cache of serialized values.
/// </summary>
public interface IDistributedCache
{
/// <summary>
/// Gets a value with the given key.
/// </summary>
/// <param name="key">A string identifying the requested value.</param>
/// <returns>The located value or null.</returns>
byte[]? Get(string key);
/// <summary>
/// Gets a value with the given key.
/// </summary>
/// <param name="key">A string identifying the requested value.</param>
/// <param name="token">Optional. The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation, containing the located value or null.</returns>
Task<byte[]?> GetAsync(string key, CancellationToken token = default(CancellationToken));
/// <summary>
/// Sets a value with the given key.
/// </summary>
/// <param name="key">A string identifying the requested value.</param>
/// <param name="value">The value to set in the cache.</param>
/// <param name="options">The cache options for the value.</param>
void Set(string key, byte[] value, DistributedCacheEntryOptions options);
/// <summary>
/// Sets the value with the given key.
/// </summary>
/// <param name="key">A string identifying the requested value.</param>
/// <param name="value">The value to set in the cache.</param>
/// <param name="options">The cache options for the value.</param>
/// <param name="token">Optional. The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken));
/// <summary>
/// Refreshes a value in the cache based on its key, resetting its sliding expiration timeout (if any).
/// </summary>
/// <param name="key">A string identifying the requested value.</param>
void Refresh(string key);
/// <summary>
/// Refreshes a value in the cache based on its key, resetting its sliding expiration timeout (if any).
/// </summary>
/// <param name="key">A string identifying the requested value.</param>
/// <param name="token">Optional. The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
Task RefreshAsync(string key, CancellationToken token = default(CancellationToken));
/// <summary>
/// Removes the value with the given key.
/// </summary>
/// <param name="key">A string identifying the requested value.</param>
void Remove(string key);
/// <summary>
/// Removes the value with the given key.
/// </summary>
/// <param name="key">A string identifying the requested value.</param>
/// <param name="token">Optional. The <see cref="CancellationToken"/> used to propagate notifications that the operation should be canceled.</param>
/// <returns>The <see cref="Task"/> that represents the asynchronous operation.</returns>
Task RemoveAsync(string key, CancellationToken token = default(CancellationToken));
}
观察以上代码,我们发现返回的RedisValue类型都是byte[]字节数组。这是因为分布式缓存通常运行在独立的服务,与应用服务器可能使用不同的技术栈。为确保数据能被不同语言或框架正确解析,需要一种通用的数据表示形式。这也是IDistributedCache支持多种缓存提供者的原因。
也就是说实际上从分布式缓存里取到的结果从字节数组需要解析成指定格式的数据,存储的时候也需要序列化成字节数组。这样操作尤其麻烦,好在微软提供了一个名为DistributedCacheExtensions的静态扩展,内部帮我们通过 Encoding.UTF8.GetBytes(value)和Encoding.UTF8.GetString(data, 0, data.Length)的形式将结果集和字符串形成转换,相当于少转了一步。
DistributedCacheExtensions源码片段【namespace Microsoft.Extensions.Caching.Distributed】
public static Task SetStringAsync(this IDistributedCache cache, string key, string value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken))
{
ThrowHelper.ThrowIfNull(key);
ThrowHelper.ThrowIfNull(value);
return cache.SetAsync(key, Encoding.UTF8.GetBytes(value), options, token);
}
public static async Task<string?> GetStringAsync(this IDistributedCache cache, string key, CancellationToken token = default(CancellationToken))
{
byte[]? data = await cache.GetAsync(key, token).ConfigureAwait(false);
if (data == null)
{
return null;
}
return Encoding.UTF8.GetString(data, 0, data.Length);
}
2.4 ASP.NET Core Controller中操作Redis
2.4.1 获取缓存
根据key获取值,并且转型
// 尝试从分布式缓存获取数据
var cachedData = await _distributedCache.GetStringAsync(cacheKey);
Movie? movie = null;
if (!string.IsNullOrEmpty(cachedData))
{
// 反序列化缓存数据
movie = JsonSerializer.Deserialize<Movie>(cachedData);
_logger.LogInformation("从缓存中获取了电影数据");
}
2.4.2 设置缓存
// 缓存未命中,从数据源获取
movie = await _movieAssert.GetMovieAsync(id);
if (movie != null)
{
// 设置缓存选项
var cacheOptions = new DistributedCacheEntryOptions
{
// 同时设置绝对过期和滑动过期
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
SlidingExpiration = TimeSpan.FromMinutes(5)
};
// 序列化并存储到分布式缓存
var serializedData = JsonSerializer.Serialize(movie);
await _distributedCache.SetStringAsync(cacheKey, serializedData, cacheOptions);
_logger.LogInformation("已将电影数据存入缓存");
}
2.4.3 删除缓存
根据key删除缓存
await _distributedCache.RemoveAsync(cacheKey);
2.4.4 刷新缓存
一般是碰到需要手动续滑动过期时间的场景才会使用。Redis中如果请求了一个被设置了滑动过期时间的缓存,会自动刷新滑动过期时间的。
await _distributedCache.RefreshAsync(cacheKey);
2.4.5 完整代码
[Route("api/[controller]")]
[ApiController]
public class MovieController : ControllerBase
{
private readonly ILogger<MovieController> _logger;
private readonly IMovieAssert _movieAssert;
private readonly IDistributedCache _distributedCache;
public MovieController(ILogger<MovieController> logger, IMovieAssert movieAssert, IDistributedCache distributedCache = null)
{
_logger = logger;
_movieAssert = movieAssert;
_distributedCache = distributedCache;
}
[HttpGet("{id}")]
public async Task<ActionResult<Movie?>> Movies(int id)
{
_logger.LogDebug("开始获取数据");
var cacheKey = $"Movie:{id}";
// 尝试从分布式缓存获取数据
var cachedData = await _distributedCache.GetStringAsync(cacheKey);
Movie? movie = null;
if (!string.IsNullOrEmpty(cachedData))
{
// 反序列化缓存数据
movie = JsonSerializer.Deserialize<Movie>(cachedData);
_logger.LogInformation("从缓存中获取了电影数据");
}
else
{
// 缓存未命中,从数据源获取
movie = await _movieAssert.GetMovieAsync(id);
if (movie != null)
{
// 设置缓存选项
var cacheOptions = new DistributedCacheEntryOptions
{
// 同时设置绝对过期和滑动过期
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10),
SlidingExpiration = TimeSpan.FromMinutes(5)
};
// 序列化并存储到分布式缓存
var serializedData = JsonSerializer.Serialize(movie);
await _distributedCache.SetStringAsync(cacheKey, serializedData, cacheOptions);
_logger.LogInformation("已将电影数据存入缓存");
}
}
if (movie is null)
{
return NotFound("没有数据");
}
return movie;
}
}
总结
本文介绍了 Redis 的基本情况及在ASP.NET Core 中借助IDistributedCache接口使用Redis作为分布式缓存的具体操作。
IDistributedCache作为微软封装的一个通用的分布式缓存接口,只能说应用了Redis的一些基础服务。之后我们会讨论如何通过直接注册ConnectionMultiplexer这种方式获取Redis连接对象,使用Redis的那些高级用法。