我们在项目中使用Redis时通常是写一个单例模式的RedisHelper
静态类,暴露一些常用的Get
、Set
等操作,在需要使用地方直接RedisHelper.StringGet(xx,xx)
就可以了,这样虽然简单粗暴地满足我们对Redis的所有操作需要,但是这在Asp.Net Core的项目显得不是那么优雅了。首先你的RedisHelper
静态类无法使用Asp.Net Core
容器,又如何优雅的通过依赖注入获取IConfiguration
中的配置项呢?既然我们使用Asp.Net Core
这么优秀的框架,最佳实践当然就是遵循官方建议的开发规范优雅的编写代码。
若要使用 SQL Server 分布式缓存,请添加对 Microsoft.Extensions.Caching.SqlServer
包的包引用。
若要使用 Redis 分布式缓存,请添加对 Microsoft.Extensions.Caching.StackExchangeRedis
包的包引用。
若要使用 NCache 分布式缓存,请添加对 NCache.Microsoft.Extensions.Caching.OpenSource
包的包引用。
无论选择哪种实现,应用都将使用
IDistributedCache
接口与缓存进行交互。
来看下IDistributedCache
这个接口的定义
namespace Microsoft.Extensions.Caching.Distributed;
/// <summary>
/// Represents a distributed cache of serialized values.
/// </summary>
public interface IDistributedCache
{
/// <summary>
/// Gets a value with the given key.
/// </summary>
byte[]? Get(string key);
/// <summary>
/// Gets a value with the given key.
/// </summary>
Task<byte[]?> GetAsync(string key, CancellationToken token = default(CancellationToken));
void Set(string key, byte[] value, DistributedCacheEntryOptions options);
/// <summary>
/// Sets the value with the given key.
/// </summary>
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>
void Refresh(string key);
/// <summary>
/// Refreshes a value in the cache based on its key, resetting its sliding expiration timeout (if any).
/// </summary>
Task RefreshAsync(string key, CancellationToken token = default(CancellationToken));
/// <summary>
/// Removes the value with the given key.
/// </summary>
void Remove(string key);
/// <summary>
/// Removes the value with the given key.
/// </summary>
Task RemoveAsync(string key, CancellationToken token = default(CancellationToken));
}
IDistributedCache
接口提供以下方法来处理分布式缓存实现中的项:
Get
、GetAsync
:如果在缓存中找到,则接受字符串键并以 byte[] 数组的形式检索缓存项。Set
、SetAsync
:使用字符串键将项(作为 byte[] 数组)添加到缓存。Refresh
、RefreshAsync
:根据键刷新缓存中的项,重置其可调到期超时(如果有)。Remove
、RemoveAsync
:根据字符串键删除缓存项。
RedisHelper
官方不仅提出了如何最佳实践分布式缓存的使用,还提供了基本的实现库给我们直接用,比如我们在项目中用Redis为我们提供缓存服务:
- 添加引用
Microsoft.Extensions.Caching.StackExchangeRedis
- 注册容器
AddStackExchangeRedisCache
,并配置参数
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("MyRedisConStr");
options.InstanceName = "SampleInstance";
});
- 在需要使用
Redis
的地方通过构造函数注入IDistributedCache
实例调用即可
这样就可以优雅的使用Redis
了,更加符合Asp.Net Core
的设计风格,养成通过容器注入的方式来调用我们的各种服务,而不是全局使用RedisHelper
静态类,通过IOC
的方式,结合面向接口
开发,能方便的替换我们的实现类,统一由容器提供对象的创建,这种控制反转带来的好处只可意会不可言传,这里就不赘述了。
AddStackExchangeRedisCache
到底干了什么
上面已经知道如何优雅的使用我们的Redis了,但是不看下源码就不知道底层实现,总是心里不踏实的。
源码比较好理解的,因为这个Nuget包的源码也就四个类,而上面注册容器的逻辑也比较简单
AddStackExchangeRedisCache
主要干的活
// 1.启用Options以使用IOptions
services.AddOptions();
// 2.注入配置自定义配置,可以通过IOptions<T>注入到需要使用该配置的地方
services.Configure(setupAction);
// 3.注入一个单例IDistributedCache的实现类RedisCache
services.Add(ServiceDescriptor.Singleton<IDistributedCache, RedisCache>());
所以我们在需要用Redis的地方通过构造函数注入IDistributedCache
,而它对应的实现就是RedisCache
,那看下它的源码。
这里就不细看所有的实现了,重点只需要知道它继承了IDistributedCache
就行了,通过AddStackExchangeRedisCache
传入的ConnectionString
,实现IDistributedCache
的Get
、Set
、Refresh
、Remove
四个核心的方法,我相信这难不倒你,而它也就是干了这么多事情,只不过它的实现有点巧妙。
通过LUA
脚本和HSET
数据结构实现,HashKey是我们传入的InstanceName
+key,做了一层包装。
源码中还有需要注意的就是,我们要保证Redis
连接对象IConnectionMultiplexer
的单例,不能重复创建多个实例,这个想必在RedisHelper
中也是要保证的,而且是通过lock
来实现的。
然而微软不是那么用的,玩了个花样,注意下面的_connectionLock.Wait();
:
private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(initialCount: 1, maxCount: 1);
[MemberNotNull(nameof(_cache), nameof(_connection))]
private void Connect()
{
CheckDisposed();
if (_cache != null)
{
Debug.Assert(_connection != null);
return;
}
_connectionLock.Wait();
try
{
if (_cache == null)
{
if (_options.ConnectionMultiplexerFactory == null)
{
if (_options.ConfigurationOptions is not null)
{
_connection = ConnectionMultiplexer.Connect(_options.ConfigurationOptions);
}
else
{
_connection = ConnectionMultiplexer.Connect(_options.Configuration);
}
}
else
{
_connection = _options.ConnectionMultiplexerFactory().GetAwaiter().GetResult();
}
PrepareConnection();
_cache = _connection.GetDatabase();
}
}
finally
{
_connectionLock.Release();
}
Debug.Assert(_connection != null);
}
通过SemaphoreSlim
限制同一时间只能有一个线程能访问_connectionLock.Wait();
后面的代码。
学到装逼技巧+1
思考IDistributedCache
只又四中操作:Get
、Set
、Refresh
、Remove
,我们表示很希望跟着官方走,但这个接口过于简单,不能满足我的其他需求咋办?
比如我们需要调用 StackExchange.Redis
封装的LockTake
,LockRelease
来实现分布式锁的功能,那该怎么通过注入IDistributedCache
调用?
我们可以理解官方上面是给我们做了示范,我们完全可以自己定义一个接口,比如:
public interface IDistributedCachePlus : IDistributedCache
{
bool LockRelease(string key, byte[] value);
bool LockTake(string key, byte[] value, TimeSpan expiry);
}
继承IDistributedCache
,对其接口进行增强,然后自己实现实现AddStackExchangeRedisCache
的逻辑,我们不用官方给的实现,但是我们山寨官方的思路,实现任意标准的接口,满足我们业务。
services.Add(ServiceDescriptor.Singleton<IDistributedCachePlus, RedisCachePlus>());
在需要使用缓存的地方通过构造函数注入IDistributedCachePlus
。
官方提供的IDistributedCache
标准及其实现类库,能方便的实现我们对缓存的简单的需求,通过遵循官方的建议,我们干掉了RedisHelper
,优雅的实现了分布式Redis缓存的使用,你觉得这样做是不是很优雅呢?