­

幹掉RedisHelper,請這樣用分散式快取

前言

我們在項目中使用Redis時通常是寫一個單例模式的RedisHelper靜態類,暴露一些常用的GetSet等操作,在需要使用地方直接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 介面提供以下方法來處理分散式快取實現中的項:

  • GetGetAsync:如果在快取中找到,則接受字元串鍵並以 byte[] 數組的形式檢索快取項。
  • SetSetAsync:使用字元串鍵將項(作為 byte[] 數組)添加到快取。
  • RefreshRefreshAsync:根據鍵刷新快取中的項,重置其可調到期超時(如果有)。
  • RemoveRemoveAsync:根據字元串鍵刪除快取項。

幹掉RedisHelper

官方不僅提出了如何最佳實踐分散式快取的使用,還提供了基本的實現庫給我們直接用,比如我們在項目中用Redis為我們提供快取服務:

  1. 添加引用Microsoft.Extensions.Caching.StackExchangeRedis
  2. 註冊容器AddStackExchangeRedisCache,並配置參數
 builder.Services.AddStackExchangeRedisCache(options =>
     {
         options.Configuration = builder.Configuration.GetConnectionString("MyRedisConStr");
         options.InstanceName = "SampleInstance";
     });
  1. 在需要使用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,實現IDistributedCacheGetSetRefreshRemove四個核心的方法,我相信這難不倒你,而它也就是幹了這麼多事情,只不過它的實現有點巧妙。

通過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只又四中操作:GetSetRefreshRemove,我們表示很希望跟著官方走,但這個介面過於簡單,不能滿足我的其他需求咋辦?
比如我們需要調用 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快取的使用,你覺得這樣做是不是很優雅呢?