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

来源:https://www.cnblogs.com/viter/archive/2018/12/24/10161581.html
-Advertisement-
Play Games

本來昨天應該更新的,但是由於各種原因,抱歉,讓追這個系列的朋友久等了。上一篇文章 [在.Net Core 使用緩存和配置依賴策略](https://www.cnblogs.com/viter/p/10146312.html). 講的是如何使用本地緩存,那麼本篇文章就來瞭解一下如何使用分散式緩存,通過... ...


前言

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

內容提要

  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 中

  • 在 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 的配置非常重要,這裡強烈建議大家手動配置

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 時間是得到的是美國時間,所以這裡實現了一個本地時鐘,代碼非常簡單,只是獲取一個本地時間

    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 未能運行,比如下麵的代碼

        [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://files.cnblogs.com/files/viter/Ron.DistributedCacheDemo.zip


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Python類練習 定義一個類 列印Point: Point實例化為一個p1: 我們直接列印 結果為 __main__.Point` 給實例p1添加屬性 (3, 4) 原點(0, 0)與p1之間的距離:5.0 (3, 4) 定義矩形 實例化一個例子r1 (100, 200) 列印矩形的寬度width ...
  • 這篇文章一起來回顧複習下spring的事務操作.事務是spring的重點, 也是面試的必問知識點之一. 說來這次面試期間,也問到了我,由於平時用到的比較少,也沒有關註過這一塊的東西,所以回答的不是特別好,所以借這一篇文章來回顧總結一下,有需要的朋友,也可以點贊收藏一下,複習一下這方面的知識,為年後的 ...
  • 我們可以在application.properties中配置自定義的屬性值,為了獲取這些值,我們可以使用spring提供的@value註解,還可以使用springboot提供的@ConfigurationProperties註解非常方便的完成屬性值的註入。 ...
  • 剛開始接觸 python 的時候,對 python 中的 wargs (可變參數) 和 kwargs (關鍵字參數)的理解不是很透徹,看了一下 《Explore Python》一書,裡面對這一部分的描述相對淺顯易懂, 這裡依據個人理解進行相關總結。 可變參數( args) 對於可變參數可以聯想到 C ...
  • 一.函數調用 在python中內置了很多函數,我們可以直接調用 。想要調用函數首先要知道函數的名稱及包含的參數,還可以通過查看python官方的文檔:https://docs.python.org/3/library/functions.html 就拿abs()函數來舉例 用來返回數值的絕對值 >> ...
  • 上面的mixins、generics都是rest_framework里的模塊,我們可以繼承其中的某些類,達到代碼量減少的效果,這裡充分體現出了面向對象的繼承 一、mixins模塊 二、generics模塊 三、通過一個簡單的例子,順帶寫mixins,generics的用處 ...
  • 昨天面試碰到了面試官問spring的時候,問完ioc,然後下個問題我以為是aop,後來才聽清是動態代理,感覺回答的不是太好,也是好久不接觸 1.靜態代理 代理模式也就委托模式。 三個角色: 1. Subject抽象主題角色:介面 2. RealSubject具體主題角色:介面的實現類,業務邏輯的具體 ...
  • python爬蟲scrapy項目(二) 爬取目標:房天下全國租房信息網站(起始url:http://zu.fang.com/cities.aspx) 爬取內容:城市;名字;出租方式;價格;戶型;面積;地址;交通 反反爬措施:設置隨機user-agent、設置請求延時操作、 1、開始創建項目 2、進入 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...