乘風破浪,遇見最佳跨平臺跨終端框架.Net Core/.Net生態 - 淺析ASP.NET Core性能設計,使用內存、分佈式緩存(Redis)敏捷響應

ASP.NET Core性能優化

避免阻塞調用

ASP.NET Core應用應設計爲可同時處理許多請求。異步API允許較小線程池處理數千個併發請求,無需等待阻塞調用。線程可以處理另一個請求,而不是等待長時間運行的同步任務完成。

ASP.NET Core應用中的一個常見性能問題是阻塞可以異步進行的調用。許多同步阻塞調用都會導致線程池飢餓和響應時間降低。

禁止行爲

  • 通過調用Task.WaitTask<TResult>.Result.
  • 獲取常見代碼路徑中的鎖。當構建爲並行運行代碼時,ASP.NET Core應用的性能最高。
  • 調用Task.Run並立即等待它。ASP.NET Core已經在普通線程池線程上運行應用代碼,因此調用Task.Run只會導致不必要的額外線程池計劃。即使計劃的代碼會阻止某個線程,Task.Run也不會阻止該線程。

建議做法

  • 使熱代碼路徑成爲異步。
  • 如果有異步API可用,則異步調用數據訪問、I/O和長時間運行的操作API。請勿用於Task.Run使同步API異步。
  • 使控制器/RazorPage操作成爲異步。爲了獲益於async/await模式,整個調用堆棧都是異步的。

最大程度減少大型對象分配

https://github.com/Microsoft/perfview

.NET Core垃圾回收器在ASP.NET Core應用中自動管理內存分配和釋放。自動垃圾回收通常意味着開發人員無需擔心如何或何時釋放內存。但是,清理未引用的對象會佔用CPU時間,因此開發人員應最大限度減少熱代碼路徑中的對象分配。垃圾回收在大型對象(>85K字節)上成本特別高昂。大型對象存儲在大型對象堆上,需要完整(第2代)垃圾回收才能清理。與第0代和第1代回收不同,第2代回收需要臨時暫停應用執行。頻繁分配和取消分配大型對象可能會導致性能不一致。

建議

  • 請考慮緩存經常使用的大型對象。緩存大型對象會阻止進行成本高昂的分配
  • 使用ArrayPool<T>池緩衝區來存儲大型數組。
  • 請勿在熱代碼路徑上分配許多生存期較短的大型對象。

可以通過在PerfView中查看垃圾回收(GC)統計信息並檢查以下內容來診斷內存問題:

  • 垃圾回收暫停時間。
  • 花費在垃圾回收上的處理器時間百分比。
  • 第0代、第1代和第2代的垃圾回收量。

優化數據訪問和I/O

與數據存儲和其他遠程服務的交互通常是ASP.NET Core應用的最慢部分。高效讀取和寫入數據對於良好的性能至關重要。

建議

  • 請異步調用所有數據訪問API
  • 請勿檢索不需要的數據。編寫查詢以便僅返回當前HTTP請求所需的數據。
  • 如果可接受稍微過時的數據,請考慮緩存從數據庫或遠程服務檢索的經常訪問的數據。根據方案使用MemoryCacheDistributedCache
  • 請儘量縮短網絡往返。目標是在單個調用而不是多個調用中檢索所需數據。
  • 當出於只讀目的訪問數據時,請在Entity Framework Core中使用無跟蹤查詢。EFCore可以更有效地返回無跟蹤查詢的結果。
  • 請篩選和聚合LINQ查詢(例如使用.Where.Select.Sum語句),以便數據庫執行篩選。
  • 請考慮在EFCore客戶端上解析某些查詢運算符,這可能會導致查詢執行效率低下。
  • 請勿對集合使用投影查詢,這可能會導致執行“N+1”個SQL查詢。

以瞭解可提高大規模應用性能的方法:

  • DbContext池
  • 顯式編譯的查詢

建議在提交基本代碼之前衡量前面高性能方法的影響。已編譯查詢的額外複雜性可能無法證明性能改進的合理性。

通過使用Application Insights或分析工具查看訪問數據所用的時間,可以檢測到查詢問題。大多數數據庫還提供有關頻繁執行的查詢的統計信息。

什麼是緩存

image

  • 緩存是計算結果的"臨時"存儲和重複使用
  • 緩存本質是用"空間"換取"時間"

現實生活中的緩存機制

  • 車站的等候大廳
  • 物流倉庫
  • 家裏的冰箱

這些都是現實生活中類似於緩存的場景。

緩存的場景

  • 計算結果,如:反射對象緩存
  • 請求結果,如:DNS緩存
  • 臨時共享數據,如:會話存儲
  • 熱點訪問內容頁:如:商品詳情
  • 熱點變更邏輯數據,如:秒殺的庫存數

緩存的策略

  • 越接近最終的輸出結果(靠前),效果越好
  • 緩存命中率越高越好,命中率低就意味着"空間"的浪費,因爲緩存實際上是佔用空間的,比如內存空間

緩存的位置

基於B/S結構的系統,緩存的位置包括如下

  • 瀏覽器中
  • 反向代理服務器中(負載均衡),比如CDN的負載均衡器,它會負責一些緩存的策略,將後端的響應數據緩存在代理服務器中,直接響應給客戶端
  • 應用進程內存中
  • 分佈式存儲系統中,比如Redis內
  • 數據庫系統中,在數據庫查詢的緩存

緩存實現的要點

  • 緩存Key生成策略,表示緩存數據的範圍、業務含義
  • 緩存失敗策略,如過期時間機制(比如說固定時間週期緩存30秒)、主動刷新機制(緩存永不失效,但是會有監聽緩存背後的數據如果發生變化的時候,主動刷新緩存)
  • 緩存更新策略,表示更新緩存數據的時機

緩存使用的問題

設計緩存系統的時候,經常會遇到如下問題

  • 緩存失效,導致數據不一致,這個不一致可能會導致一些業務問題,根據不同業務特性,需要去重點關注
  • 緩存穿透,查詢無數據時,導致緩存不生效,查詢都落在數據庫,一般建議說,當數據爲Null時,在緩存裏面強制返回一個默認值,避免緩存穿透的情況
  • 緩存擊穿,緩存失效瞬間,大量請求訪問到數據庫,比如說某一個頁面的併發請求量比較大,這個時候在緩存失效的瞬間,有請求需要去數據庫訪問並且獲取到新的緩存數據,這時候併發量大意味着大量的併發會訪問到數據庫,一般建議說,使用二級緩存的策略,當一級緩存失效時,允許一個請求落到數據庫上面,幫我們更新緩存數據,重置緩存有效時間,其他的請求仍然通過緩存去響應。
  • 緩存雪崩,大量緩存同一時間失效,導致數據庫壓力,它會週期性的隨着緩存失效壓力大起來,一般建議說,緩存的失效時間策略應該定義得相對來講比較均勻的,讓我們系統數據庫接收到請求相對均勻,不會出現說緩存Key會在同一時間有大量失效的情況,讓它們錯開,比如說有一部分緩存設計成五分鐘、七分鐘

緩存涉及的組件

  • ResponseCache,中間件
  • Microsoft.Extensions.Caching.Memory.IMemoryCache,內置內存緩存
  • Microsoft.Extensions.Caching.Distributed.IDistributedCache,內置分佈式緩存
  • EasyCaching,開源社區中國作品

緩存按位置分類

  • 內存緩存,內存中緩存使用服務器內存來存儲緩存的數據
  • 分佈式緩存,當應用託管在雲或服務器場中時,使用分佈式緩存將數據存儲在內存中

內存緩存和分佈式緩存的區別

  • 內存緩存可以存儲任意對象
  • 分佈式緩存對象需要支持序列化
  • 分佈式緩存遠程請求可能失敗,內存緩存不會

當我們使用分佈式緩存的時候,可能會用到MemcacheRedis

內存緩存就是在當前進程內的使用內存來存儲的緩存。

內存緩存可以存儲任意對象,這個對象不需要關心是否需要序列化,只要在內存中可以引用它即可。

分佈式緩存則要求對象支持序列化,需要將對象序列化並且通過網絡傳輸存儲到分佈式緩存系統裏面去,比如說二進制的序列化方式或者Json的序列化方式

目前基於Redis的緩存,一般都是使用Json序列化的方式,這就意味着分佈式緩存它的應用場景是受到序列化支持的限制的

分佈式緩存的另外一個問題是,它遠程請求可能失敗,內存緩存則不會有這個問題

響應緩存中間件

什麼是響應緩存

響應緩存可減少客戶端或代理對Web服務器發出的請求數。響應緩存還減少了Web服務器爲生成響應而執行的工作量。響應緩存在標頭中設置。

ResponseCache屬性可設置響應緩存標頭。客戶端和中間代理應遵循HTTP 1.1緩存規範下緩存響應的標頭。

對於遵循HTTP 1.1緩存規範的服務器端緩存,請使用響應緩存中間件。

中間件可以使用ResponseCacheAttribute屬性來影響服務器端緩存行爲。

響應緩存中間件:

  • 啓用基於HTTP緩存頭的緩存服務器響應。實現標準HTTP緩存語義。像代理一樣基於HTTP緩存標頭進行緩存。
  • 通過對RazorPages等UI應用沒有好處,因爲瀏覽器通常會設置阻止緩存的請求頭。正在考慮將輸出緩存用於下一版本的ASP.NETCore,這將使UI應用受益。使用輸出緩存,配置可決定了應獨立於HTTP標頭緩存的內容。有關詳細信息,請參閱此GitHub問題。
  • 對於來自滿足緩存條件的客戶端的公共GET或HEAD API請求可能有用

基於HTTP的響應緩存

HTTP 1.1緩存規範介紹了Internet緩存的行爲方式。用於緩存的主HTTP標頭是Cache-Control,它用於指定緩存指令。

當請求從客戶端到達服務器以及響應從服務器返回客戶端時,這些指令控制緩存行爲。

請求和響應在代理服務器之間移動,並且代理服務器還必須符合HTTP 1.1緩存規範。

Cache-Control標頭的HTTP 1.1緩存規範要求使用緩存來遵循客戶端發送的有效Cache-Control標頭。客戶端可以使用no-cache標頭值發出請求,並強制服務器針對每個請求生成新的響應。

如果考慮HTTP緩存的目標,則始終遵循客戶端Cache-Control請求標頭是有意義的。根據官方規範,緩存旨在減少在客戶端、代理和服務器網絡中滿足請求的延遲和網絡開銷。它不一定是控制源服務器上的負載的一種方法。

使用響應緩存中間件時,開發人員無法控制此緩存行爲,因爲該中間件遵循官方緩存規範。支持使用輸出緩存以更好地控制服務器負載是ASP.NET Core未來版本的設計方案。

常用Cache-Control指令

指令 操作
public 緩存可以存儲響應。
private 響應不得由共享緩存存儲。專用緩存可以存儲和重用響應。
max-age 客戶端不接受期限大於指定秒數的響應。示例:max-age=60(60秒),max-age=2592000(1個月)
no-cache 請求時:緩存不能使用存儲的響應來滿足請求。源服務器重新生成客戶端的響應,中間件更新其緩存中存儲的響應。
響應時:響應不得用於未經源服務器驗證的後續請求。
no-store 請求時:緩存不得存儲請求。
響應時:緩存不得存儲任何部分的響應。

在緩存中發揮了作用的其他緩存標頭

標頭 函數
Age 在源服務器上生成或成功驗證響應以來的估計時間量(以秒爲單位)。
Expires 響應被視爲過時後的時間。
Pragma 爲與用於設置no-cache行爲的HTTP/1.0緩存向後兼容而存在。如果Cache-Control標頭存在,則將忽略Pragma標頭。
Vary 指定除非所有Vary標頭字段在緩存響應的原始請求和新請求中都匹配,否則不得發送緩存響應。

啓用響應緩存中間件

需要在Startup.csConfigureServices的方法中註冊響應緩存中間件AddResponseCaching

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    // 添加響應緩存中間件
    services.AddResponseCaching();
}

它還可以配置一些參數,進行更高級的設置

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    // 添加響應緩存中間件
    services.AddResponseCaching(responseCachingOptions =>
    {
        // 響應正文的最大可緩存大小,默認值64 * 1024 * 1024(64MB)
        responseCachingOptions.MaximumBodySize = 64 * 1024 * 1024;
        // 響應緩存中間件的大小限制,默認值100 * 1024 * 1024(100MB)
        responseCachingOptions.SizeLimit = 100 * 1024 * 1024;
        // 確定是否將響應緩存在區分大小寫的路徑上,默認值爲false
        responseCachingOptions.UseCaseSensitivePaths = true;
    });
}

同時還需要在Configure方法中使用響應緩存中間件UseResponseCaching

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseResponseCaching();

    app.UseCors();

    app.UseAuthorization();

當啓用Cors中間件的時候,ResponseCaching的啓用必須放在它前面。

響應緩存中間件使用的HTTP頭

標頭 詳細信息
Authorization 如果標頭存在,則不會緩存響應。
Cache-Control 中間件僅考慮緩存通過public緩存指令標記的響應。使用以下參數的控制緩存:
max-age
max-stale
min-fresh
must-revalidate
no-cache
no-store
only-if-cached
private
public
s-maxage
proxy-revalidate
Pragma 請求中的Pragma:no-cache標頭具有與Cache-Control:no-cache相同的作用。此標頭由Cache-Control標頭中的相關指令(若存在)覆蓋。考慮提供與HTTP/1.0的向後兼容性。
Set-Cookie 如果標頭存在,則不會緩存響應。請求處理管道中的任何中間件都阻止Cookie響應緩存中間件,例如Cookie基於TempData提供程序緩存響應。
Vary Vary標頭用於根據另一個標頭更改緩存的響應。例如,通過包含Vary:Accept-Encoding標頭基於編碼緩存響應,該標頭分別緩存針對帶有Accept-Encoding:gzipAccept-Encoding:text/plain標頭的請求的響應。永遠不會存儲標頭值爲*的響應。
Expires 不會存儲或檢索此標頭認爲過時的響應,除非被其他Cache-Control標頭覆蓋。
If-None-Match 如果值不是*並且響應的ETag與提供的任何值都不匹配,則從緩存中提供完整響應。否則,會提供304(Not Modified)響應。
If-Modified-Since 如果If-None-Match標頭不存在,則當緩存的響應日期晚於提供的值時,將從緩存中提供完整響應。否則,會提供304-Not Modified響應。
Date 從緩存提供服務時,如果原始響應中未提供Date標頭,則中間件會設置該標頭。
Content-Length 從緩存提供服務時,如果原始響應中未提供Content-Length標頭,則中間件會設置該標頭。
Age 會忽略原始響應中發送的Age標頭。中間件在提供緩存的響應時會計算一個新值。

使用內置內存緩存

https://github.com/TaylorShi/HelloCaching

核心對象

  • IMemoryCache

啓用內存緩存

依賴包

https://www.nuget.org/packages/Microsoft.Extensions.Caching.Memory

dotnet add package Microsoft.Extensions.Caching.Memory

不過這個包實際上在Microsoft.AspNetCore.App中已經包含了

image

Startup.csConfigureServices方法中添加MemoryCache服務:AddMemoryCache

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    // 開啓內存緩存
    services.AddMemoryCache();
    // 添加響應緩存中間件
    services.AddResponseCaching();
}

我們看下AddMemoryCache定義

public static class MemoryCacheServiceCollectionExtensions
{
    public static IServiceCollection AddMemoryCache(this IServiceCollection services);
    public static IServiceCollection AddMemoryCache(this IServiceCollection services, Action<MemoryCacheOptions> setupAction);
}

其中MemoryCacheOptions定義

public class MemoryCacheOptions : IOptions<MemoryCacheOptions>
{
    public MemoryCacheOptions();

    public ISystemClock Clock { get; set; }
    //
    // 摘要:
    //     Gets or sets the amount to compact the cache by when the maximum size is exceeded.
    public double CompactionPercentage { get; set; }
    //
    // 摘要:
    //     Gets or sets the minimum length of time between successive scans for expired
    //     items.
    public TimeSpan ExpirationScanFrequency { get; set; }
    //
    // 摘要:
    //     Gets or sets the maximum size of the cache.
    public long? SizeLimit { get; set; }
}

使用內存緩存

我們來看下IMemoryCache的定義

public interface IMemoryCache : IDisposable
{
    ICacheEntry CreateEntry(object key);

    void Remove(object key);

    bool TryGetValue(object key, out object value);
}

ICacheEntry定義是

public interface ICacheEntry : IDisposable
{
    DateTimeOffset? AbsoluteExpiration { get; set; }

    TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }

    IList<IChangeToken> ExpirationTokens { get; }

    object Key { get; }

    IList<PostEvictionCallbackRegistration> PostEvictionCallbacks { get; }

    CacheItemPriority Priority { get; set; }

    long? Size { get; set; }

    TimeSpan? SlidingExpiration { get; set; }

    object Value { get; set; }
}

它定義了緩存的Key、緩存的值、過期時間、滑動過期時間(多長時間不訪問就失效),它還支持設計數據變更的更新、數據變更的監聽、過期的監聽。

可以通過IMemoryCacheTryGetValue來獲取對應的Key值,然後通過Set來設置。

/// <summary>
/// 獲取訂單
/// </summary>
/// <param name="memoryCache"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetOrder([FromServices]IMemoryCache memoryCache, [FromServices] ILogger<OrderController> logger, [FromQuery] string id)
{
    OrderModel order;
    var key = $"GetOrder-{id ?? ""}";
    if(!memoryCache.TryGetValue(key, out order))
    {
        order = new OrderModel
        {
            Id = id,
            Date = DateTime.Now
        };
        var cacheEntryOptions = new MemoryCacheEntryOptions
        {
            // 滑動過期,多長時間不訪問才失效,這裏30秒,30秒不訪問就失效
            SlidingExpiration = TimeSpan.FromSeconds(30),
            // 絕對到期,在滑動過期間隔內未請求該項時,到了60秒仍然會自動過期
            AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(60),
        };
        memoryCache.Set(key, order, cacheEntryOptions);
    }
    return await Task.FromResult(Ok(order));
}

還可以通過GetOrCreateAsync來獲取並創建,這裏我們可以在cacheEntry邏輯中從數據庫去拉取數據。


/// <summary>
/// 獲取訂單
/// </summary>
/// <param name="memoryCache"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetOrderV2([FromServices] IMemoryCache memoryCache, [FromServices] ILogger<OrderController> logger, [FromQuery] string id)
{
    var key = $"GetOrder-{id ?? ""}";
    var order = await memoryCache.GetOrCreateAsync(key, cacheEntry => {

        cacheEntry.SlidingExpiration = TimeSpan.FromSeconds(30);
        return Task.FromResult(new OrderModel
        {
            Id = id,
            Date = DateTime.Now
        });
    });
    return await Task.FromResult(Ok(order));
}

我們來看下MemoryCacheEntryOptions的定義

public class MemoryCacheEntryOptions
{
    public MemoryCacheEntryOptions();

    public DateTimeOffset? AbsoluteExpiration { get; set; }

    public TimeSpan? AbsoluteExpirationRelativeToNow { get; set; }

    public IList<IChangeToken> ExpirationTokens { get; }

    public IList<PostEvictionCallbackRegistration> PostEvictionCallbacks { get; }

    public CacheItemPriority Priority { get; set; }

    public long? Size { get; set; }

    public TimeSpan? SlidingExpiration { get; set; }
}

其中SlidingExpiration代表滑動過期,多長時間不訪問才失效,這裏30秒,30秒不訪問就失效,但是有可能用戶一直來訪問,那怎麼辦,可以結合AbsoluteExpirationRelativeToNow絕對到期時間來設置,這個緩存在滑動過期間隔內未請求該項時,允許設置提前過期的時間。

需要注意是,一般滑動過期時間應小於絕對到期時間。

運行下看看效果

首次訪問,沒有緩存,進入創建邏輯

image

第二次訪問,命中緩存,直接返回

image

通過響應緩存中間件使用

同時我們在Configure方法中啓用ResponseCache中間件: UseResponseCaching

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseResponseCaching();

    app.UseCors();

    app.UseAuthentication();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

ResponseCache中間件機制和身份認證是衝突的,包含了身份認證頭的請求,實際上是不支持ResponseCache的。

在Controller的層面,在Action上使用ResponseCache就行。

[Route("api/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
    /// <summary>
    /// 獲取訂單
    /// 緩存過期時間6000秒,緩存Key的生成策略是基於id的值,不同id的值會緩存爲不同的Cache
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    [ResponseCache(Duration = 6000, VaryByQueryKeys = new string[] { "id" })]
    [HttpGet]
    public OrderModel GetOrder([FromQuery] string id)
    {
        return new OrderModel { Id = id, Date = DateTime.Now };
    }

    /// <summary>
    /// 獲取地址
    /// 緩存過期時間6000秒,緩存Key的生成策略是名爲rpc的Header,這個Header不同的值就會緩存不同的Cache
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    [ResponseCache(Duration = 6000, VaryByHeader = "rpc")]
    [HttpGet]
    public OrderModel GetAddress([FromQuery] string id)
    {
        return new OrderModel { Id = id, Date = DateTime.Now };
    }
}

基於查詢參數來生成緩存

有了前面的各種緩存組件加持之後,我們就可以通過響應緩存中間件來首先我們的接口緩存功能了。

[Route("api/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
    /// <summary>
    /// 獲取訂單
    /// 緩存過期時間6000秒,緩存Key的生成策略是基於id的值,不同id的值會緩存爲不同的Cache
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    [ResponseCache(Duration = 6000, VaryByQueryKeys = new string[] { "id" })]
    [HttpGet]
    public OrderModel GetOrder([FromQuery] string id)
    {
        return new OrderModel { Id = id, Date = DateTime.Now };
    }
}

這裏我們通過Attribute的方式可以直接在Action上面使用ResponseCache,它可以設置一系列參數,比如緩存過期時間Duration、緩存Key生成策略(基於查詢參數,VaryByQueryKeys)

我們在Postman裏面測試下

image

第一次會進入響應

image

這時候它會返回一些頭信息

image

其中關鍵的是

Cache-Control=public,max-age=6000

這會告訴客戶端,這個結果是可以緩存的,並且超時時間是6000秒。

當我們同樣的參數第二次請求是,發現它就不進去了,直接返回了同樣的內容

image

基於請求頭來生成緩存

我們來看下ResponseCacheAttribute定義

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ResponseCacheAttribute : Attribute, IFilterFactory, IFilterMetadata, IOrderedFilter
{
    public ResponseCacheAttribute();

    public string CacheProfileName { get; set; }

    public int Duration { get; set; }
    public bool IsReusable { get; }

    public ResponseCacheLocation Location { get; set; }

    public bool NoStore { get; set; }
    public int Order { get; set; }

    public string VaryByHeader { get; set; }

    public string[] VaryByQueryKeys { get; set; }

    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider);

    public CacheProfile GetCacheProfile(MvcOptions options);
}

這裏面CacheProfileName意味着我們可以指定緩存配置名稱,方便先定義好,再根據配置選擇。

其中緩存位置Location的類型是ResponseCacheLocation,默認是Any,代表緩存可以緩存在瀏覽器、允許緩存在反向代理中,也允許緩存在服務器內存中,如果Client代表只允許緩存存儲在我們瀏覽器中,如果None就表示說沒有緩存。

public enum ResponseCacheLocation
{
    //
    // 摘要:
    //     Cached in both proxies and client. Sets "Cache-control" header to "public".
    Any = 0,
    //
    // 摘要:
    //     Cached only in the client. Sets "Cache-control" header to "private".
    Client = 1,
    //
    // 摘要:
    //     "Cache-control" and "Pragma" headers are set to "no-cache".
    None = 2
}

參數中還有VaryByHeaderVaryByQueryKeys實際上是一樣的,它指基於不同的Header的值。

[Route("api/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
    /// <summary>
    /// 獲取地址
    /// 緩存過期時間6000秒,緩存Key的生成策略是名爲rpc的Header,這個Header不同的值就會緩存不同的Cache
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    [ResponseCache(Duration = 6000, VaryByHeader = "rpc")]
    [HttpGet]
    public OrderModel GetAddress([FromQuery] string id)
    {
        return new OrderModel { Id = id, Date = DateTime.Now };
    }
}

image

使用Redis分佈式緩存

什麼是Redis

https://redis.io

https://github.com/redis/redis

image

Redis是一種開放源代碼內存中數據存儲,通常用作分佈式緩存。

Redis(Remote Dictionary Server, 遠程鍵值服務)是一個內存數據結構存儲,用作分佈式內存鍵值數據庫、緩存和消息代理,具有可選的耐久性

Redis支持不同種類的抽象數據結構,如字符串、列表、地圖、集、排序集、HyperLogLogs、位圖、流和空間索引。該項目由Salvatore Sanfilippo開發和維護,始於2009年。從2015年到2020年,他帶領一個由Redis實驗室贊助的項目核心團隊。Salvatore Sanfilippo在2020年離開了Redis的維護者。它是在BSD3-clause許可證下發布的開源軟件。2021年,在原作者和主要維護者離開後不久,Redis實驗室從其名稱中刪除了實驗室,現在只被稱爲"Redis"。

Redis經常被稱爲數據結構服務器(data structures server)。這意味着Redis通過一組命令提供對可變數據結構的訪問,這些命令使用TCP套接字和一個簡單協議的服務器-客戶模型發送。因此,不同的進程可以以一種共享的方式查詢和修改相同的數據結構

在Redis中實現的數據結構有一些特殊的屬性。

  • Redis關心的是將它們存儲在磁盤上,即使它們總是被送達和修改到服務器內存中。這意味着Redis是快速的,但它也是非易失性的
  • 數據結構的實現強調內存效率,因此與使用高級編程語言建模的相同數據結構相比,Redis內的數據結構可能會使用更少的內存
  • Redis提供了許多在數據庫中自然找到的功能,如複製、可調整的耐久性水平、集羣和高可用性

另一個很好的例子是把Redis看作是一個更復雜的memcached版本,其中的操作不僅僅是SET和GET,而是操作複雜的數據類型,如Lists、Sets、有序數據結構等等。

Redis是用ANSI C編寫的,可以在大多數POSIX系統上工作,如Linux*BSDMac OS X,沒有外部依賴性。Linux和OS X是Redis開發和測試最多的兩個操作系統,我們推薦使用Linux進行部署。Redis可能在Solaris衍生的系統(如SmartOS)中工作,但支持是盡力而爲。對於Windows的構建,沒有官方支持

歷史

Redis這個名字的意思是遠程字典服務器。Redis項目開始於Salvatore Sanfilippo,綽號antirez,Redis的最初開發者,試圖提高他的意大利創業公司的可擴展性,開發一個實時網絡日誌分析器。在使用傳統數據庫系統擴展某些類型的工作負載時遇到重大問題後,Sanfilippo於2009年開始在Tcl中製作Redis的第一個概念驗證版本的原型。在內部成功使用該項目幾周後,Sanfilippo決定將其開源,並在HackerNews上公佈了該項目。該項目開始受到關注,尤其是在Ruby社區,GitHub和Instagram是第一批採用該項目的公司。

2010年3月,Sanfilippo被VMware公司聘用。

2013年5月,Redis被Pivotal軟件公司(VMware的子公司)贊助。

2015年6月,開發工作由Redis實驗室贊助。

2018年10月,Redis5.0發佈,引入了RedisStream--一種新的數據結構,允許在一個鍵上以自動的、基於時間的順序存儲多個字段和字符串值。

2020年6月,Salvatore Sanfilippo卸任Redis維護者。

與其他數據庫系統的區別

Redis普及了一個可以同時被認爲是存儲和緩存的系統的想法。它的設計使數據總是被修改並從計算機主內存中讀取,但也以不適合隨機數據訪問的格式存儲在磁盤上。格式化的數據只有在系統重新啓動後纔會被重建到內存中

Redis還提供了一個與關係型數據庫管理系統(RDBMS)相比非常不尋常的數據模型。用戶命令並不描述數據庫引擎要執行的查詢,而是描述對給定的抽象數據類型進行的具體操作。因此,數據必須以一種適合以後快速檢索的方式來存儲。檢索是在沒有數據庫系統的幫助下,以二級索引、聚合或其他傳統RDBMS的常見功能的形式進行的。Redis的實現大量使用fork系統調用,以複製持有數據的進程,這樣父進程繼續爲客戶服務,而子進程在磁盤上創建數據的內存副本。

數據類型

Redis將鍵映射爲值的類型。Redis與其他結構化存儲系統的一個重要區別是,Redis不僅支持字符串,而且還支持抽象數據類型。

  • 字符串的列表(Lists of strings)
  • 字符串集(Sets of strings),不重複的未排序元素的集合
  • 經過排序的字符串集(Sorted sets of strings),由非重複元素組成的集合,通過一個名爲score的浮點數字排序
  • 哈希表(Hash tables),鍵和值都是字符串
  • HyperLogLogs用於近似估計集合的cardinality大小,從2014年4月的Redis2.8.9開始提供。
  • 帶有消費者組的條目流(Stream of entries with consumer groups),允許你以自動的、基於時間的序列在單個鍵上存儲多個字段和字符串值,自2018年10月的Redis5.0起可用。
  • 通過實現geohash技術的地理空間數據(Geospatial data),自Redis3.2起可用。

一個值的類型決定了該值可以進行哪些操作(稱爲命令)。Redis支持高級的、原子的、服務器端的操作,如集合之間的交叉、聯合和差異,以及列表、集合和排序集合的排序。

基於Redis模塊的API,支持更多的數據類型:

  • JSON - RedisJSON實現ECMA-404(JavaScript對象符號數據交換標準)作爲本地數據類型。
  • Graph - RedisGraph實現了一個可查詢的屬性圖形
  • Time series - RedisTimeSeries實現了一個時間序列數據結構
  • Bloom filter,Cuckoo filter,Count-min sketch和Top-K-RedisBloom爲Redis實現了一套概率數據結構。

使用者

由於數據庫設計的性質,典型的用例是會話緩存全頁面緩存消息隊列應用排行榜計數等等。發佈-訂閱的消息傳遞模式允許服務器之間的實時通信。

大型公司如Twitter正在使用Redis,亞馬遜網絡服務提供了一個名爲ElastiCache的Redis管理服務,微軟在Azure中爲Redis提供AzureCache,阿里巴巴在阿里雲中爲Redis提供ApsaraDB。

使用Docker新建Redis實例

https://hub.docker.com/_/redis

image

其中一些標籤中可能有類似bullseye的名稱。這些是Debian版本的套件代碼名,並指示映像基於哪個版本

docker run -d --name redis --restart unless-stopped -p 6379:6379 redis:6.2.7 redis-server --requirepass xxxxxxxxx

image

docker exec -it redis /bin/bash

運行redis-cli,可以執行常用動作

image

通過Another Redis DeskTop Manager連接Redis實例

https://github.com/qishibo/AnotherRedisDesktopManager

https://github.com/qishibo/AnotherRedisDesktopManager/releases

image

更快、更好、更穩定的Redis桌面(GUI)管理客戶端,兼容Windows、Mac、Linux,性能出衆,輕鬆加載海量鍵值

Redis DeskTop Manager自從進入了0.9.9版本就開始付費使用或者貢獻代碼獲得免費使用期限,可以轉用Another Redis DeskTop Manager

獲取Another Redis DeskTop Manager

image

image

image

image

補一個Redis DeskTop Manager的截圖,獲取老版本: Redis DeskTop Manager v0.8.8

image

啓用Redis分佈式緩存

依賴包

https://www.nuget.org/packages/Microsoft.Extensions.Caching.StackExchangeRedis

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

image

Startup.csConfigureServices方法中添加RedisCache服務:AddStackExchangeRedisCache

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    // 添加RedisCache服務
    services.AddStackExchangeRedisCache(redisCacheOptions =>
    {
        Configuration.GetSection("Redis").Bind(redisCacheOptions);
    });
    // 添加響應緩存中間件
    services.AddResponseCaching();
}

這裏注意到,還添加了響應Cache服務AddResponseCaching,它對應傳統MVC應用中OutputCache

這裏我們查看下RedisCacheOptions的定義,

public class RedisCacheOptions : IOptions<RedisCacheOptions>
{
    public string Configuration { get; set; }

    public ConfigurationOptions ConfigurationOptions { get; set; }

    /// <summary>
    /// Gets or sets a delegate to create the ConnectionMultiplexer instance.
    /// </summary>
    public Func<Task<IConnectionMultiplexer>> ConnectionMultiplexerFactory { get; set; }

    public string InstanceName { get; set; }

    public Func<ProfilingSession> ProfilingSession { get; set; }

    RedisCacheOptions IOptions<RedisCacheOptions>.Value
    {
        get { return this; }
    }
}

它有兩個字段,配置地址Configuration和實例名稱InstanceName

所以,我們還需要在配置文件appsettings.json中配置Redis的節點內容

{
    "Redis": {
        "Configuration": "localhost:6379,password=xxxxxxxxxxxxx",
        "InstanceName": "CachingRedis31"
    }
}

如果Redis設置了密碼,記得在Configuration中追加,password=xxxxxxxxxxxxx

使用Redis分佈式緩存

我們看下AddStackExchangeRedisCache的定義

public static class StackExchangeRedisCacheServiceCollectionExtensions
{
    /// <summary>
    /// Adds Redis distributed caching services to the specified <see cref="IServiceCollection" />.
    /// </summary>
    /// <param name="services">The <see cref="IServiceCollection" /> to add services to.</param>
    /// <param name="setupAction">An <see cref="Action{RedisCacheOptions}"/> to configure the provided
    /// <see cref="RedisCacheOptions"/>.</param>
    /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns>
    public static IServiceCollection AddStackExchangeRedisCache(this IServiceCollection services, Action<RedisCacheOptions> 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, RedisCache>());

        return services;
    }
}

最後一句,我們看到,它將RedisCache註冊成了IDistributedCache接口的實現,也就意味着我們可以通過IDistributedCache來操作我們的Redis分佈式緩存。

我們來看下IDistributedCache的定義

public interface IDistributedCache
{
    byte[] Get(string key);

    Task<byte[]> GetAsync(string key, CancellationToken token = default(CancellationToken));

    void Set(string key, byte[] value, DistributedCacheEntryOptions options);

    Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken));

    void Refresh(string key);

    Task RefreshAsync(string key, CancellationToken token = default(CancellationToken));

    void Remove(string key);

    Task RemoveAsync(string key, CancellationToken token = default(CancellationToken));
}

ASP.NE TCore中內置了IDistributedCache的兩個實現。一個是基於SQLServer,另一個是基於Redis

接下來,我們回到OrderController來使用它,純粹爲了臨時演示,這裏的IDistributedCacheILogger<OrderController>都是通過FromServices的方式來從容器中獲取實例。

/// <summary>
/// 獲取訂單
/// </summary>
/// <param name="distributedCache"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetOrder([FromServices]IDistributedCache distributedCache, [FromServices]ILogger<OrderController> logger, [FromQuery] string id)
{
    OrderModel order = null;
    var key = $"GetOrder-{id ?? ""}";
    var value = await distributedCache.GetStringAsync(key);
    if (string.IsNullOrEmpty(value))
    {
        var option = new DistributedCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(60)
        };
        order = new OrderModel
        {
            Id = id,
            Date = DateTime.Now
        };
        await distributedCache.SetStringAsync(key, JsonConvert.SerializeObject(order), option);
    }
    if (!string.IsNullOrEmpty(value))
    {
        try
        {
            order = JsonConvert.DeserializeObject<OrderModel>(value);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "反序列化失敗");
        }
    }
    return Ok(order);
}

通過IDistributedCacheGetStringAsync方法,我們可以輕鬆的獲取到Redis存儲的數據值,同時SetStringAsync也可以很方便的寫值,同時通過DistributedCacheEntryOptions可以設置緩存值的策略。

public class DistributedCacheEntryOptions
{
    private DateTimeOffset? _absoluteExpiration;
    private TimeSpan? _absoluteExpirationRelativeToNow;
    private TimeSpan? _slidingExpiration;

    /// <summary>
    /// Gets or sets an absolute expiration date for the cache entry.
    /// </summary>
    public DateTimeOffset? AbsoluteExpiration
    {
        get
        {
            return _absoluteExpiration;
        }
        set
        {
            _absoluteExpiration = value;
        }
    }

    /// <summary>
    /// Gets or sets an absolute expiration time, relative to now.
    /// </summary>
    public TimeSpan? AbsoluteExpirationRelativeToNow
    {
        get
        {
            return _absoluteExpirationRelativeToNow;
        }
        set
        {
            if (value <= TimeSpan.Zero)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(AbsoluteExpirationRelativeToNow),
                    value,
                    "The relative expiration value must be positive.");
            }

            _absoluteExpirationRelativeToNow = value;
        }
    }

    /// <summary>
    /// Gets or sets how long a cache entry can be inactive (e.g. not accessed) before it will be removed.
    /// This will not extend the entry lifetime beyond the absolute expiration (if set).
    /// </summary>
    public TimeSpan? SlidingExpiration
    {
        get
        {
            return _slidingExpiration;
        }
        set
        {
            if (value <= TimeSpan.Zero)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(SlidingExpiration),
                    value,
                    "The sliding expiration value must be positive.");
            }
            _slidingExpiration = value;
        }
    }
}

實際上運行效果

image

image

緩存策略

實際上,我們還可以在這裏去設計一些二級緩存來去避免緩存雪崩的情況、緩存穿透的情況。

使用EasyCaching緩存

什麼是EasyCaching

https://github.com/dotnetcore/EasyCaching

EasyCaching是一個開源的緩存庫,它包含了緩存的基本用法和一些高級用法,可以幫助我們更容易地處理緩存問題。

啓用EasyCaching緩存

依賴包

https://www.nuget.org/packages/EasyCaching.Redis

dotnet add package EasyCaching.Redis
dornet add package EasyCaching.Serialization.Json
dotnet add package Microsoft.Bcl.AsyncInterfaces

image

Startup.csConfigureServices方法中添加EasyCaching服務: AddEasyCaching

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    // 添加EasyCaching服務
    services.AddEasyCaching(easyCachingOptions =>
    {
        easyCachingOptions.UseRedis(Configuration, name: "EasyCaching").WithJson("redis");
    });
}

這裏需要添加它對應的配置信息

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "EasyCaching": {
    "redis": {
      "MaxRdSecond": 120,
      "EnableLogging": false,
      "LockMs": 5000,
      "SleepMs": 300,
	  "SerializerName": "redis",
      "dbconfig": {
        "Password": "xxxxxxxxxxxx",
        "IsSsl": false,
        "SslHost": null,
        "ConnectionTimeout": 5000,
        "AllowAdmin": true,
        "Endpoints": [
          {
            "Host": "localhost",
            "Port": 6379
          }
        ],
        "Database": 0
      }
    }
  }
}

這裏特別注意的是,要設置好SerializerName,它的名字要和前面的WithJson方法指定的一樣,不然會引發錯誤:Can not find the matched serializer instance, serializer name is redis1 #370,報錯:Can not find the matched serializer instance

如果繼續使用的時候,報錯Could not load file or assembly 'Microsoft.Bcl.AsyncInterfaces,那麼需要補裝下這個包。

這裏的MaxRdSecond作用是預防在同一時刻出現大批量緩存同時失效,爲每個key原有的過期時間上面加了一個隨機的秒數,儘可能的分散它們的過期時間

接下來,我們可以去OrderController來添加使用的方法。

[Route("api/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
    private readonly IEasyCachingProvider _provider;
    public OrderController(IEasyCachingProvider provider)
    {
        this._provider = provider;
    }

    /// <summary>
    /// 獲取模型
    /// </summary>
    /// <param name="easyCachingProvider"></param>
    /// <param name="id"></param>
    /// <returns></returns>
    [HttpGet]
    public IActionResult GetModel([FromQuery] string id)
    {
        var key = $"GetModel-{id ?? ""}";
        // 獲取這個Key的值,默認值直接返回當前時間,過期時間爲60秒
        var value = _provider.Get(key, () => DateTime.Now.ToString(), TimeSpan.FromSeconds(60));
        return Content(value.Value);
    }
}

這裏我們設計了一個GetModel的接口,通過傳入的Id+自定義的前綴來生成Key值,通過IEasyCachingProvider來獲取Key值即可。

image

我們還可以看看其他方法,並且將這裏改成異步的

/// <summary>
/// 獲取模型
/// </summary>
/// <param name="easyCachingProvider"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetModelV2([FromQuery] string id)
{
    var key = $"GetModel-{id ?? ""}";
    // 獲取緩存key的值,沒有默認值
    var value = await _provider.GetAsync<string>(key);
    // 獲取緩存key的值,默認值返回當前時間,過期時間一分鐘
    var value2 = await _provider.GetAsync(key, async () => await Task.FromResult(DateTime.Now.ToString()), TimeSpan.FromMinutes(1));
    // 設置緩存Key=demo,value=123,過期時間爲一分鐘
    await _provider.SetAsync("demo", "123", TimeSpan.FromMinutes(1));
    // 刪除緩存key=demo
    await _provider.RemoveAsync("demo");
    return Content(value2.Value);
}

image

如果用的Get方法是帶有查詢的,它在沒有命中緩存的情況下去數據庫查詢前,會有一個加鎖操作,避免一個key在同一時刻去查了n次數據庫,這個鎖的生存時間和休眠時間是由配置中的LockMs和SleepMs決定的

多實例緩存

當存在多個緩存實例需要支持的時候,我們可以使用IEasyCachingProviderFactory工程模式來獲取IEasyCachingProvider

先從配置文件配置多個來區分,這裏配置了兩個redis節點:EasyCachingDefaultRedis

{
    "EasyCaching": {
        "redis": {
            "MaxRdSecond": 120,
            "EnableLogging": false,
            "LockMs": 5000,
            "SleepMs": 300,
            "SerializerName": "redis",
            "dbconfig": {
                "Password": "xxxx",
                "IsSsl": false,
                "SslHost": null,
                "ConnectionTimeout": 5000,
                "AllowAdmin": true,
                "Endpoints": [
                    {
                        "Host": "localhost",
                        "Port": 6379
                    }
                ],
                "Database": 0
            }
        }
    },
    "DefaultRedis": {
        "redis": {
            "MaxRdSecond": 120,
            "EnableLogging": false,
            "LockMs": 5000,
            "SleepMs": 300,
            "SerializerName": "redis",
            "dbconfig": {
                "Password": "xxxxxxxxxx",
                "IsSsl": false,
                "SslHost": null,
                "ConnectionTimeout": 5000,
                "AllowAdmin": true,
                "Endpoints": [
                    {
                        "Host": "localhost",
                        "Port": 6379
                    }
                ],
                "Database": 1
            }
        }
    }
}

然後在Controller中,我們通過IEasyCachingProviderFactory來根據配置名稱獲取實例GetCachingProvider

[Route("api/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
    private readonly IEasyCachingProvider _defaultProvider;
    private readonly IEasyCachingProvider _easyProvider;

    public OrderController(IEasyCachingProviderFactory factory)
    {
        this._defaultProvider = factory.GetCachingProvider("DefaultRedis");
        this._easyProvider = factory.GetCachingProvider("EasyCaching");
    }

    /// <summary>
    /// 獲取模型
    /// </summary>
    /// <param name="easyCachingProvider"></param>
    /// <param name="id"></param>
    /// <returns></returns>
    [HttpGet]
    public async Task<IActionResult> GetModelV3([FromQuery] string id)
    {
        var key = $"GetModel-{id ?? ""}";
        // 獲取緩存key的值,沒有默認值
        var value = await _defaultProvider.GetAsync<string>(key);
        // 獲取緩存key的值,默認值返回當前時間,過期時間一分鐘
        var value2 = await _defaultProvider.GetAsync(key, async () => await Task.FromResult(DateTime.Now.ToString()), TimeSpan.FromMinutes(1));
        // 設置緩存Key=demo,value=123,過期時間爲一分鐘
        await _easyProvider.SetAsync("demo", "123", TimeSpan.FromMinutes(1));
        // 刪除緩存key=demo
        //await _easyProvider.RemoveAsync("demo");
        return Content(value2.Value);
    }
}

AOP操作

依賴包

https://www.nuget.org/packages/EasyCaching.Interceptor.AspectCore

dotnet add package EasyCaching.Interceptor.AspectCore
public interface IOrderService
{
    //[EasyCachingAble(Expiration = 30, CacheKeyPrefix = "GetOrder-", CacheProviderName = "DefaultRedis")]
    [EasyCachingAble(Expiration = 30)]
    Task<OrderModel> GetOrderAsync(string id);
}

public class OrderService : IOrderService
{
    public Task<OrderModel> GetOrderAsync(string id)
    {
        return Task.FromResult(new OrderModel { Id = id, Date = DateTime.Now });
    }
}

需要在ConfigureService增加下中間件配置

services.ConfigureAspectCoreInterceptor(options =>
{
    // 可以在這裏指定你要用那個provider
    // 或者在Attribute上面指定
    options.CacheProviderName = "DefaultRedis";
});

Controller這裏添加調用的接口

/// <summary>
/// 獲取模型
/// </summary>
/// <param name="easyCachingProvider"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetOrderV2([FromServices]IOrderService orderService, [FromQuery] string id)
{
    var order = await orderService.GetOrderAsync(id);
    return Ok(order);
}

多級緩存

依賴包

https://www.nuget.org/packages/EasyCaching.InMemory

https://www.nuget.org/packages/EasyCaching.HybridCache

https://www.nuget.org/packages/EasyCaching.Bus.Redis

dotnet add package EasyCaching.InMemory
dotnet add package EasyCaching.HybridCache
dotnet add package EasyCaching.Bus.Redis
services.AddEasyCaching(easyCachingOptions =>
{
    easyCachingOptions.UseInMemory("m1");
    easyCachingOptions.UseRedis(Configuration, name: "EasyCaching").WithJson("redis");

    easyCachingOptions.UseHybrid(options =>
    {
        options.EnableLogging = true;
        // 緩存總線的訂閱主題
        options.TopicName = "test_topic";
        // 本地緩存的名字
        options.LocalCacheProviderName = "m1";
        // 分佈式緩存的名字
        options.DistributedCacheProviderName = "EasyCaching";
    });
    easyCachingOptions.WithRedisBus(Configuration, name: "DefaultRedis");
});

這裏使用redis作爲緩存總線。

接下來就是通過IHybridCachingProvider使用它

/// <summary>
/// 獲取模型
/// </summary>
/// <param name="easyCachingProvider"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetHybird([FromServices] IHybridCachingProvider hybridCachingProvider, [FromQuery] string id)
{
    var key = $"GetModel-{id ?? ""}";
    var value = await hybridCachingProvider.GetAsync<string>(key, async () => await Task.FromResult(DateTime.Now.ToString()), TimeSpan.FromMinutes(1));
    return Ok(value.Value);
}

專供Redis的模式

Redis支持多種數據結構,還有一些原子遞增遞減的操作等等。爲了支持這些操作,EasyCaching提供了一個獨立的接口,IRedisCachingProvider

/// <summary>
/// 獲取模型
/// </summary>
/// <param name="easyCachingProvider"></param>
/// <param name="id"></param>
/// <returns></returns>
[HttpGet]
public async Task<IActionResult> GetRedis([FromServices] IEasyCachingProviderFactory factory, [FromQuery] string id)
{
    var key = $"GetModel-{id ?? ""}";
    var redisProvider = factory.GetRedisProvider("EasyCaching");
    var value = await redisProvider.StringGetAsync(key);
    if(value == null)
    {
        await redisProvider.StringSetAsync(key, DateTime.Now.ToString(), TimeSpan.FromMinutes(1));
        value = await redisProvider.StringGetAsync(key);
    }
    return Ok(value);
}

使用SQL Server分佈式緩存

什麼是SQL Server分佈式緩存

所謂的針對SQL Server的分佈式緩存,實際上就是將標識緩存數據的字節數組存放在SQL Server數據庫中某個具有固定結構的數據表中

使用SQL Server分佈式緩存

依賴包

https://www.nuget.org/packages/Microsoft.Extensions.Caching.SqlServer

dotnet add package Microsoft.Extensions.Caching.SqlServer

image

使用NCahe分佈式緩存

什麼是NCache

https://www.alachisoft.com

https://github.com/Alachisoft/NCache

NCache是在.NET和.NET Core中以原生方式開發的開放源代碼內存中分佈式緩存。

NCache是一個速度極快、可擴展的開源分佈式緩存,適用於.NET應用程序。使用NCache進行數據庫緩存、ASP.NET會話狀態存儲、ASP.NET視圖狀態緩存,以及更多。

NCache被世界各地的數百家公司用於關鍵任務的應用。

功能

  • 發佈/訂閱(Pub/Sub)與主題
  • 緩存CRUD操作
  • 批量CRUD操作
  • 鎖定/解鎖緩存項目
  • 項目級別的事件通知
  • 驅逐
  • 絕對和滑動過期
  • ASP.NET會話狀態提供者
  • ASP.NET視圖狀態緩存
  • 鏡像緩存拓撲結構
  • NHibernate二級緩存提供者
  • NuGet包
  • 可在微軟Azure、亞馬遜和其他任何雲平臺上運行

對象池重用對象

什麼是對象池

Microsoft.Extensions.ObjectPool是ASP.NET Core基礎結構的一部分,它支持將一組對象保留在內存中以供重用,而不是允許對對象進行垃圾回收

如果要管理的對象具有以下特徵,你可能希望使用對象池:

  • 分配/初始化成本高昂。
  • 表示某些有限資源。
  • 可預見地頻繁使用。

例如,ASP.NET Core框架在某些地方使用對象池來重用StringBuilder實例。StringBuilder分配並管理自己的緩衝區來保存字符數據。ASP.NET Core經常使用StringBuilder來實現功能,重用這些對象會帶來性能優勢。

對象池並不總是能提高性能:

  • 除非對象的初始化成本很高,否則從池中獲取對象通常較慢。
  • 在池解除分配之前,池管理的對象無法解除分配。
  • 僅在使用應用或庫的真實場景收集性能數據後才使用對象池。

注意:ObjectPool不限制將分配的對象數量,但限制將保留的對象數量。

使用對象池

依賴包

https://www.nuget.org/packages/Microsoft.Extensions.ObjectPool

dotnet add package Microsoft.Extensions.ObjectPool

image

註冊對象池提供服務到容器,添加針對StringBuilder的對象池策略。

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    // 註冊對象池提供服務到容器
    services.TryAddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
    // 添加針對StringBuilder的對象池策略
    services.TryAddSingleton<ObjectPool<StringBuilder>>(serviceProvider =>
    {
        var provider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
        var policy = new StringBuilderPooledObjectPolicy();
        return provider.Create(policy);
    });
}

使用它

[Route("api/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
    /// <summary>
    /// 獲取訂單
    /// </summary>
    /// <param name="distributedCache"></param>
    /// <param name="id"></param>
    /// <returns></returns>
    [HttpGet]
    public async Task<string> GetOrder([FromServices] ObjectPool<StringBuilder> builderPool, [FromServices]ILogger<OrderController> logger, [FromQuery] string id)
    {
        // 從對象池中獲取StringBuilder對象
        var stringBuilder = builderPool.Get();
        for (int i = 0; i < 10000; i++)
        {
            // 從對象池中獲取StringBuilder對象
            var stringBuilderItem = builderPool.Get();
            stringBuilderItem.Append(id);
            stringBuilder.Append(stringBuilderItem.ToString());
        }
        return await Task.FromResult(stringBuilder.ToString());
    }
}

響應壓縮

爲什麼需要響應壓縮

網絡帶寬是一種有限資源。減小響應大小通常可顯著提高應用的響應速度。減小有效負載大小的一種方式是壓縮應用的響應。

通常,任何未本機壓縮的響應都可以從響應壓縮中獲益。本機壓縮的響應通常包括CSS、JavaScript、HTML、XML和JSON。不要壓縮本機壓縮的資產,例如PNG文件。嘗試進一步壓縮本機壓縮的響應時,任何較小的額外減少大小和傳輸時間都可能會被處理壓縮所需的時間所掩蓋。不要壓縮小於150-1000字節的文件,具體取決於文件的內容和壓縮效率。壓縮小文件的開銷可能會產生比未壓縮文件更大的壓縮文件

當客戶端可以處理壓縮內容時,客戶端必須通過隨請求發送Accept-Encoding標頭來通知服務器其功能。當服務器發送壓縮的內容時,它必須在Content-Encoding標頭中包含有關如何對壓縮響應進行編碼的信息。

下表顯示了響應壓縮中間件支持的內容編碼指定

Accept-Encoding標頭值 支持的中間件 說明
br 是(默認) Brotli壓縮數據格式
deflate DEFLATE壓縮數據格式
exi W3C高效XML交換
gzip Gzip文件格式
identity “無編碼”標識符:不得對響應進行編碼。
pack200-gzip Java存檔的網絡傳輸格式
* 任何未顯式請求的可用內容編碼

響應壓縮中間件允許爲自定義Accept-Encoding標頭值添加其他壓縮提供程序。

響應壓縮中間件能夠響應質量值(qvalue,q)客戶端發送的權重來確定壓縮方案的優先級。

壓縮算法需要在壓縮速度和壓縮效率之間進行權衡。在此上下文中,有效性是指壓縮後的輸出大小。最小大小是通過最佳壓縮實現的。

下表介紹了請求、發送、緩存和接收壓縮內容所涉及的標頭。

標頭 角色
Accept-Encoding 從客戶端發送到服務器以指示客戶端可接受的內容編碼方案。
Content-Encoding 從服務器發送到客戶端以指示有效負載中內容的編碼。
Content-Length 發生壓縮時,會刪除Content-Length標頭,因爲壓縮響應時正文內容會發生更改。
Content-MD5 發生壓縮時,會刪除Content-MD5標頭,因爲正文內容已更改,哈希不再有效。
Content-Type 指定內容的MIME類型。每個響應都應指定其Content-Type。響應壓縮中間件檢查此值以確定是否應壓縮響應。響應壓縮中間件指定一組可以編碼的默認MIME類型,並且可以替換或添加它們。
Vary 當服務器將值爲Accept-Encoding的Vary標頭髮送到客戶端和代理時,該標頭會指示客戶端或代理應該根據請求的Accept-Encoding標頭值緩存(改變)響應。帶有Vary:Accept-Encoding標頭的返回內容的結果是,壓縮的響應和未壓縮的響應都單獨進行緩存。

啓用響應壓縮中間件

注入響應壓縮中間件: AddResponseCompression

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    // 啓用響應壓縮中間件
    services.AddResponseCompression(options =>
    {
        // 爲Https也啓用
        options.EnableForHttps = true;
    });
}

啓用它: UseResponseCompression

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseResponseCompression();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

未開啓前

image

content-type: application/json; charset=utf-8
date: Fri, 28 Oct 2022 14:51:29 GMT
server: Kestrel

開啓後

image

content-encoding: br
content-type: application/json; charset=utf-8
date: Fri, 28 Oct 2022 14:52:26 GMT
server: Kestrel
vary: Accept-Encoding

壓力測試

Azure負載測試

負載測試和壓力測試對於確保web應用的性能和可縮放性非常重要。儘管它們的某些測試是相同的,但目標不同。

  • 負載測試:測試應用是否可以在特定情況下處理指定的用戶負載,同時仍滿足響應目標。應用在正常狀態下運行。
  • 壓力測試:在極端條件下(通常爲長時間)運行時測試應用的穩定性。測試會對應用施加高用戶負載(峯值或逐漸增加的負載)或限制應用的計算資源。

壓力測試可確定壓力下的應用是否能夠從故障中恢復,並正常返回到預期的行爲。在壓力下,應用不會在正常狀態下運行。

Azure負載測試(預覽版)是一項完全託管的負載測試服務,可用於生成大規模負載。該服務可以模擬應用的流量,且無需其託管位置。通過Azure負載測試(預覽版),可以使用現有的ApacheJMeter腳本生成大規模負載。

第三方工具

列表包含具有各種功能集的第三方web性能工具:

  • Apache JMeter
  • ApacheBench(ab)
  • Gatling
  • k6
  • Locust
  • West Wind WebSurge
  • Netling
  • Vegeta
  • NBomber

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章