前言
我們在項目中使用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緩存的使用,你覺得這樣做是不是很優雅呢?