Asp.Net Core 輕鬆學-正確使用分佈式緩存

前言

    原本昨天應該更新的,可是因爲各類緣由,抱歉,讓追這個系列的朋友久等了。上一篇文章 在.Net Core 使用緩存和配置依賴策略 講的是如何使用本地緩存,那麼本篇文章就來了解一下如何使用分佈式緩存,經過本章,你將瞭解到如何使用分佈式緩存,以及最重要的是,如何選擇適合本身的分佈式緩存;本章主要包含兩個部分:html

內容提要git

  1. 使用 SqlServer 分佈式緩存
  2. 使用 Redis 分佈式緩存
  3. 實現自定義的分佈式緩存客戶端註冊擴展
  4. 關於本示例的使用說明

1. 使用 SqlServer 分佈式緩存

1.1 準備工做,請依照如下步驟實施
  • 1 建立一個 Asp.Net Core MVC 測試項目:Ron.DistributedCacheDemo
  • 2 爲了使用 SqlServer 做爲分佈式緩存的數據庫,須要在項目中引用 Microsoft.EntityFrameworkCore 相關組件
  • 3 在 SqlServer 數據庫引擎中建立一個數據庫,命名爲:TestDb
  • 4 打開 Ron.DistributedCacheDemo 項目根目錄,執行建立緩存數據表的操做,執行命令後若是輸出信息:Table and index were created successfully. 表示緩存表建立成功
dotnet sql-cache create "Server=.\SQLEXPRESS;User=sa;Password=123456;Database=TestDb" dbo AspNetCoreCache

1.2 開始使用 SqlServer 分佈式緩存

.Net Core 中的分佈式緩存統一接口是 IDistributedCache 該接口定義了一些對緩存經常使用的操做,好比咱們常見的 Set/Get 方法,而 SqlServer 分佈式緩存由 SqlServerCache 類實現,該類位於命名空間 Microsoft.Extensions.Caching.SqlServer 中github

  • 在 Startup.cs 中註冊分佈式緩存
public void ConfigureServices(IServiceCollection services)
        {
            services.AddDistributedSqlServerCache(options =>
            {
                options.SystemClock = new BLL.LocalSystemClock();
                options.ConnectionString = this.Configuration["ConnectionString"];
                options.SchemaName = "dbo";
                options.TableName = "AspNetCoreCache";
                options.DefaultSlidingExpiration = TimeSpan.FromMinutes(1);
                options.ExpiredItemsDeletionInterval = TimeSpan.FromMinutes(5);
            });
            ...
        }

上面的方法 ConfigureServices(IServiceCollection services) 中使用 services.AddDistributedSqlServerCache() 這個擴展方法引入了 SqlServer 分佈式緩存,並做了一些簡單的配置,該配置是由 SqlServerCacheOptions 決定的,SqlServerCacheOptions 的配置很是重要,這裏強烈建議你們手動配置redis

1.3 瞭解 SqlServerCacheOptions,先來看一下SqlServerCacheOptions 的結構
namespace Microsoft.Extensions.Caching.SqlServer
{
    public class SqlServerCacheOptions : IOptions<SqlServerCacheOptions>
    {
        public SqlServerCacheOptions();
        // 緩存過時掃描時鐘
        public ISystemClock SystemClock { get; set; }
        // 緩存過時逐出時間,默認爲 30 分鐘
        public TimeSpan? ExpiredItemsDeletionInterval { get; set; }
        // 緩存數據庫鏈接字符串
        public string ConnectionString { get; set; }
        // 緩存表所屬架構
        public string SchemaName { get; set; }
        // 緩存表名稱
        public string TableName { get; set; }
        // 緩存默認過時時間,默認爲 20 分鐘
        public TimeSpan DefaultSlidingExpiration { get; set; }
    }
}

該配置很是簡單,僅是對緩存使用的基本配置
首先,使用 options.SystemClock 配置了一個本地時鐘,接着設置緩存過時時間爲 1 分鐘,緩存過時後逐出時間爲 5 分鐘,其它則是鏈接數據庫的各項配置
在緩存過時掃描的時候,使用的時間正是 options.SystemClock 該時鐘的時間,默認狀況下,該時鐘使用 UTC 時間,在個人電腦上,UTC 時間是獲得的是美國時間,因此這裏實現了一個本地時鐘,代碼很是簡單,只是獲取一個本地時間sql

public class LocalSystemClock : Microsoft.Extensions.Internal.ISystemClock
    {
        public DateTimeOffset UtcNow => DateTime.Now;
    }
1.4 在控制器中使用分佈式緩存
  • 首先使用依賴注入,在 HomeController 中得到 IDistributedCache 的實例對象,該實例對象的實現類型爲 SqlServerCache,而後經過 Index 方法增長一項緩存 CurrentTime 並設置其值爲當前時間,而後再另外一接口 GetValue 中取出該 CurrentTime 的值
[Route("api/Home")]
    [ApiController]
    public class HomeController : Controller
    {
        private IDistributedCache cache;
        public HomeController(IDistributedCache cache)
        {
            this.cache = cache;
        }

        [HttpGet("Index")]
        public async Task<ActionResult<string>> SetTime()
        {
            var CurrentTime = DateTime.Now.ToString();
            await this.cache.SetStringAsync("CurrentTime", CurrentTime);
            return CurrentTime;
        }

        [HttpGet("GetTime")]
        public async Task<ActionResult<string>> GetTime()
        {
            var CurrentTime = await this.cache.GetStringAsync("CurrentTime");
            return CurrentTime;
        }
    }
  • 運行程序,打開地址:http://localhost:5000/api/home/settime,而後查看緩存數據庫,緩存項 CurrentTime 已存入數據庫中

  • 訪問接口:http://localhost:5000/api/home/gettime 獲得緩存項 CurrentTime 的值

  • 等到超時時間過時後,再到數據庫查看,發現緩存項 CurrentTime 還在數據庫中,這是由於緩存清理機制形成的
1.5 緩存清理

在緩存過時後,每次調用 Get/GetAsync 方法都會 調用 SqlServerCache 的 私有方法 ScanForExpiredItemsIfRequired() 進行一次掃描,而後清除全部過時的緩存條目,掃描方法執行過程也很簡單,就是直接執行數據庫查詢語句數據庫

DELETE FROM {0} WHERE @UtcNow > ExpiresAtTime

值得注意的是,在異步方法中使用同步調用不會觸發緩存逐出,由於其線程退出致使 Task.Run 未能運行,好比下面的代碼api

[HttpGet("GetTime")]
        public async Task<ActionResult<string>> GetTime()
        {
            var CurrentTime = this.cache.GetString("CurrentTime");
            return CurrentTime;
        }

將致使 SqlServerCache 沒法完整執行方法 ScanForExpiredItemsIfRequired(),由於其內部使用了 Task 進行異步處理,正確的作法是使用 await this.cache.GetStringAsync("CurrentTime");緩存

1.6 關於緩存清理方法 ScanForExpiredItemsIfRequired
private void ScanForExpiredItemsIfRequired()
        {
            var utcNow = _systemClock.UtcNow;
            if ((utcNow - _lastExpirationScan) > _expiredItemsDeletionInterval)
            {
                _lastExpirationScan = utcNow;
                Task.Run(_deleteExpiredCachedItemsDelegate);
            }
        }

在多線程環境下,該方法可能除非屢次重複掃描,便可能會屢次執行 SQL 語句 DELETE FROM {0} WHERE @UtcNow > ExpiresAtTime ,可是,這也僅僅是警告而已,並無任何可改變其行爲的控制途徑多線程

1.7 IDistributedCache 的其它擴展方法

.Net Core 中還對 IDistributedCache 進行了擴展,甚至容許經過 Set 方法傳入一個 DistributedCacheEntryOptions 以覆蓋全局設置,這些擴展方法的使用都比較簡單,直接傳入相應的值便可,在此再也不一一介紹
但願深刻研究的同窗,能夠手動逐一測試架構

1.8 關於 AddDistributedSqlServerCache() 方法

AddDistributedSqlServerCache 方法內部其實是進行了一系列的註冊操做,其中最重要的是註冊了 SqlServerCache 到 IDistributedCache 接口,該操做使得咱們能夠在控制器中採用依賴注入的方式使用 IDistributedCache 的實例
查看 AddDistributedSqlServerCache 方法的代碼片斷

public static IServiceCollection AddDistributedSqlServerCache(this IServiceCollection services, Action<SqlServerCacheOptions> setupAction)
        {
            if (services == null)
            {
                throw new ArgumentNullException(nameof(services));
            }

            if (setupAction == null)
            {
                throw new ArgumentNullException(nameof(setupAction));
            }

            services.AddOptions();
            AddSqlServerCacheServices(services);
            services.Configure(setupAction);

            return services;
        }

        internal static void AddSqlServerCacheServices(IServiceCollection services)
        {
            services.Add(ServiceDescriptor.Singleton<IDistributedCache, SqlServerCache>());
        }

2. 使用 Redis 分佈式緩存

要在 Asp.Net Core 項目中使用 Redis 分佈式緩存,須要引用包:Microsoft.Extensions.Caching.Redis,.Net Core 中的 Redis 分佈式緩存客戶端由 RedisCache 類提供實現 ,RedisCache 位於程序集 Microsoft.Extensions.Caching.StackExchangeRedis.dll 中,該程序集正是是依賴於大名鼎鼎的 Redis 客戶端 StackExchange.Redis.dll,StackExchange.Redis 有許多的問題,其中最爲嚴重的是超時問題,不過這不知本文的討論範圍,若是你但願使用第三方 Redis 客戶端替代 StackExchange.Redis 來使用分佈式緩存,你須要本身實現 IDistributedCache 接口,好消息是,IDistributedCache 接口並不複雜,定義很是簡單

2.1 在 Startup.cs 中註冊 Redis 分佈式緩存配置
public void ConfigureServices(IServiceCollection services)
        {
            services.AddDistributedRedisCache(options =>
            {
                options.InstanceName = "TestDb";
                options.Configuration = this.Configuration["RedisConnectionString"];
            });

            ...
        }

註冊 Redis 分佈式緩存配置和使用 StackExchange.Redis 的方式徹底相同,須要注意的是 RedisCacheOptions 包含 3 個屬性,而 Configuration 和 ConfigurationOptions 的做用是相同的,一旦設置了 ConfigurationOptions ,就不該該再去設置屬性 Configuration 的值,由於,在 AddDistributedRedisCache() 註冊內部,會判斷若是設置了 ConfigurationOptions 的值,則再也不使用 Configuration;可是,咱們建議仍是經過屬性 Configuration 去初始化 Redis 客戶端,由於,這是一個鏈接字符串,而各類配置均可以經過鏈接字符串進行設置,這和使用 StackExchange.Redis 的方式是徹底一致的

2.2 使用緩存
[Route("api/Home")]
    [ApiController]
    public class HomeController : Controller
    {
        private IDistributedCache cache;
        public HomeController(IDistributedCache cache)
        {
            this.cache = cache;
        }

        [HttpGet("Index")]
        public async Task<ActionResult<string>> SetTime()
        {
            var CurrentTime = DateTime.Now.ToString();
            await this.cache.SetStringAsync("CurrentTime", CurrentTime);
            return CurrentTime;
        }

        [HttpGet("GetTime")]
        public async Task<ActionResult<string>> GetTime()
        {
            var CurrentTime = await this.cache.GetStringAsync("CurrentTime");
            return CurrentTime;
        }
    }

細心的你可能已經發現了,上面的這段代碼和以前演示的 SqlServerCache 徹底一致,是的,僅僅是修改一下注冊的方法,咱們就能在項目中進行無縫的切換;可是,對於緩存有強依賴的業務,建議仍是須要作好緩存遷移,確保項目可以平滑過渡
惟一不一樣的是,使用 Redis 分佈式緩存容許你在異步方法中調用同步獲取緩存的方法,這不會致使緩存清理的問題,由於緩存的管理已經徹底交給了 Redis 客戶端 StackExchange.Redis 了

3. 實現自定義的分佈式緩存客戶端,下面的代碼表示實現一個 CSRedis 客戶端的分佈式緩存註冊擴展

3.1 定義 CSRedisCache 實現 IDistributedCache 接口
public class CSRedisCache : IDistributedCache, IDisposable
    {
        private CSRedis.CSRedisClient client;
        private CSRedisClientOptions _options;
        public CSRedisCache(IOptions<CSRedisClientOptions> optionsAccessor)
        {
            if (optionsAccessor == null)
            {
                throw new ArgumentNullException(nameof(optionsAccessor));
            }

            _options = optionsAccessor.Value;

            if (_options.NodeRule != null && _options.ConnectionStrings != null)
                client = new CSRedis.CSRedisClient(_options.NodeRule, _options.ConnectionStrings);
            else if (_options.ConnectionString != null)
                client = new CSRedis.CSRedisClient(_options.ConnectionString);
            else
                throw new ArgumentNullException(nameof(_options.ConnectionString));

            RedisHelper.Initialization(client);
        }
        public void Dispose()
        {
            if (client != null)
                client.Dispose();
        }

        public byte[] Get(string key)
        {
            if (key == null)
            {
                throw new ArgumentNullException(nameof(key));
            }

            return RedisHelper.Get<byte[]>(key);
        }

        public async Task<byte[]> GetAsync(string key, CancellationToken token = default(CancellationToken))
        {
            if (key == null)
            {
                throw new ArgumentNullException(nameof(key));
            }
            token.ThrowIfCancellationRequested();

            return await RedisHelper.GetAsync<byte[]>(key);
        }

        public void Refresh(string key)
        {
            throw new NotImplementedException();
        }

        public Task RefreshAsync(string key, CancellationToken token = default(CancellationToken))
        {
            throw new NotImplementedException();
        }

        public void Remove(string key)
        {
            if (key == null)
            {
                throw new ArgumentNullException(nameof(key));
            }

            RedisHelper.Del(key);
        }

        public async Task RemoveAsync(string key, CancellationToken token = default(CancellationToken))
        {
            if (key == null)
            {
                throw new ArgumentNullException(nameof(key));
            }

            await RedisHelper.DelAsync(key);
        }

        public void Set(string key, byte[] value, DistributedCacheEntryOptions options)
        {
            if (key == null)
            {
                throw new ArgumentNullException(nameof(key));
            }

            RedisHelper.Set(key, value);
        }

        public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken))
        {
            if (key == null)
            {
                throw new ArgumentNullException(nameof(key));
            }

            await RedisHelper.SetAsync(key, value);
        }
    }

代碼很少,都是實現 IDistributedCache 接口,而後在 IDisposable.Dispose 中釋放資源

3.2 自定義一個配置類 CSRedisClientOptions
public class CSRedisClientOptions
    {
        public string ConnectionString { get; set; }
        public Func<string, string> NodeRule { get; set; }
        public string[] ConnectionStrings { get; set; }
    }

該配置類主要是爲 CSRedis 客戶端接收配置使用

3.3 註冊擴展方法 CSRedisCacheServiceCollectionExtensions
public static class CSRedisCacheServiceCollectionExtensions
    {
        public static IServiceCollection AddCSRedisCache(this IServiceCollection services, Action<CSRedisClientOptions> setupAction)
        {
            if (services == null)
            {
                throw new ArgumentNullException(nameof(services));
            }

            if (setupAction == null)
            {
                throw new ArgumentNullException(nameof(setupAction));
            }

            services.AddOptions();
            services.Configure(setupAction);
            services.Add(ServiceDescriptor.Singleton<IDistributedCache, CSRedisCache>());

            return services;
        }
    }

自定義一個擴展方法,進行配置初始化工做,簡化實際註冊使用時的處理步驟

3.4 在 Startup.cs 中使用擴展
public void ConfigureServices(IServiceCollection services)
        {
            services.AddCSRedisCache(options =>
            {
                options.ConnectionString = this.Configuration["RedisConnectionString"];
            });

            ...
        }

上面的代碼就簡單實現了一個第三方分佈式緩存客戶端的註冊和使用

3.5 測試自定義分佈式緩存客戶端,建立一個測試控制器 CustomerController
[Route("api/Customer")]
    [ApiController]
    public class CustomerController : Controller
    {
        private IDistributedCache cache;
        public CustomerController(IDistributedCache cache)
        {
            this.cache = cache;
        }

        [HttpGet("NewId")]
        public async Task<ActionResult<string>> NewId()
        {
            var id = Guid.NewGuid().ToString("N");
            await this.cache.SetStringAsync("CustomerId", id);
            return id;
        }

        [HttpGet("GetId")]
        public async Task<ActionResult<string>> GetId()
        {
            var id = await this.cache.GetStringAsync("CustomerId");
            return id;
        }
    }

該控制器簡單實現兩個接口,NewId/GetId,運行程序,輸出結果正常

  • 調用 NewId 接口建立一條緩存記錄

  • 調用 GetId 接口獲取緩存記錄

至此,咱們完整的實現了一個自定義分佈式緩存客戶端註冊

4. 關於本示例的使用說明

4.1 首先看一下解決方案結構

該解決方案紅框處定義了 3 個不一樣的 Startup.cs 文件,分別是

  1. CSRedisStartup (自定義緩存測試啓動文件)
  2. Sql_Startup (SqlServer 測試啓動文件)
  3. StackChangeRedis_Startup(StackChange.Redis 測試啓動文件)
  • 在使用本示例的時候,經過在 Program.cs 中切換不一樣的啓動文件進行測試
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup<Ron.DistributedCacheDemo.Startups.SqlServer.Startup>();

結束語

經過介紹,咱們瞭解到如何在 Asp.Net Core 中使用分佈式緩存
瞭解了使用不一樣的緩存類型,如 SqlServer 和 Redis
瞭解到瞭如何使用不一樣的緩存類型客戶端進行註冊
瞭解到如何實現自定義緩存客戶端
還知道了在調用 SqlServer 緩存的時候,異步方法中的同步調用會致使 SqlServerCache 沒法進行過時掃描
CSRedisCore 此項目是由個人好朋友 nicye 維護,GitHub 倉庫地址:訪問CSRedisCore

示例代碼下載

https://github.com/lianggx/EasyAspNetCoreDemo/tree/master/Ron.DistributedCacheDemo

相關文章
相關標籤/搜索