幹掉RedisHelper,請這樣用分散式快取
- 2022 年 5 月 20 日
- 筆記
- aspnetcore, Redis, 分散式, 快取
前言
我們在項目中使用Redis時通常是寫一個單例模式的RedisHelper
靜態類,暴露一些常用的Get
、Set
等操作,在需要使用地方直接RedisHelper.StringGet(xx,xx)
就可以了,這樣雖然簡單粗暴地滿足我們對Redis的所有操作需要,但是這在Asp.Net Core的項目顯得不是那麼優雅了。首先你的RedisHelper
靜態類無法使用Asp.Net Core
容器,又如何優雅的通過依賴注入獲取IConfiguration
中的配置項呢?既然我們使用Asp.Net Core
這麼優秀的框架,最佳實踐當然就是遵循官方建議的開發規範優雅的編寫程式碼。
IDistributedCache
若要使用 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快取的使用,你覺得這樣做是不是很優雅呢?