目錄
緩存的概念
客戶端響應緩存
cache-control
服務器端響應緩存
內存緩存(In-memory cache)
用法
GetOrCreateAsync
緩存過期時間策略
緩存的過期時間
解決方法:
兩種過期時間策略:
絕對過期時間
滑動過期時間
兩種過期時間混用
總結
?封裝內存緩存操作的幫助類
使用
緩存的概念
緩存(Caching)是用來保存數據的區域,從緩存區域讀取數據的速度比從數據源讀取數據的速度快很多,從數據源獲取數據后,我們可以把數據保存到緩存中,下次再需要獲取同樣數據時,可以直接從緩存中獲取之前保存的數據,是系統優化中簡單又有效的工具,投入小收效大。數據庫中的索引等簡單有效的優化功能本質上都是緩存。
- 緩存命中:從緩存中獲取了要獲取的數據
- 緩存命中率:多次請求中,命中的請求占全部請求的百分比
- 緩存數據不一致:數據源中數據保存到緩存后,發生了變化
- 多級緩存:在Web開發中,存在多級緩存,瀏覽器存在“瀏覽器端緩存”,網關節電服務器存在“節點緩存”,,Web服務器上可能存在“服務器端緩存”,只要在任何一個節點上命中緩存,請求就會直接返回,而不會繼續向后傳遞。
?
?
客戶端響應緩存
cache-control
- RFC7324是HTTP協議中對緩存進行控制的規范,其中重要的是cache-control這個響應報文頭。服務器如果返回cache-control:max-age=60,則表示服務器指示瀏覽器端“可以緩存這個響應內容60秒”。
- 我們只要給需要進行緩存控制的控制器的操作方法添加ResponseCacheAttribute這個Attribute,ASP.NET Core會自動添加cache-control報文頭。
[Route("api/[controller]/[action]")]
[ApiController]
public class TestController : ControllerBase
{[HttpGet][ResponseCache(Duration = 20)]public DateTime Now(){return DateTime.Now;}
}
服務器端響應緩存
- 如果ASP.NET Core中安裝了“響應緩存中間件” ,那么ASP.NET Core不僅會繼續根據[ResponseCache]設置來生成cache-control響應報文頭來設置客戶端緩存,而且服務器端也會按照[ResponseCache]的設置來對響應進行服務器端緩存。和客戶端端緩存的區別?來自多個不同客戶端的相同請求。
- “響應緩存中間件”的好處:對于來自不同客戶端的相同請求或者不支持客戶端緩存的客戶端,能降低服務器端的壓力。
- 用法:app.MapControllers()之前加上app.UseResponseCaching()。請確保app.UseCors()寫到app.UseResponseCaching()之前。
缺點:
- 無法解決惡意請求給服務器帶來的壓力。
- 服務器端響應緩存還有很多限制,包括但不限于:響應狀態碼為200的GET或者HEAD響應才可能被緩存;報文頭中不能含有Authorization、Set-Cookie等。
- 可以采用內存緩存、分布式緩存等。
由于服務器端響應緩存開發調試的麻煩以及過于苛刻的限制,因此除非開發人員能夠靈活掌握并應用,否則不建議啟用“響應緩存中間件”。對于只需要進行客戶端響應緩存處理的操作方法,標注ResponseCache即可,如果還需要在服務器端進行緩存處理,建議采用ASP.NET Core提供的內存緩存、分布式緩存等機制來編寫程序。
內存緩存(In-memory cache)
把緩存數據放到應用程序的內存。內存緩存中保存的是一系列的鍵值對,就像Dictionary類型一樣。
內存緩存的數據保存在當前運行的網站程序的內存中,是和進程相關的。因為在Web服務器中,多個不同網站是運行在不同的進程中的,因此不同網站的內存緩存是不會互相干擾的,而且網站重啟后,內存緩存中的所有數據也就都被清空了。
用法
- 添加服務:builder.Services.AddMemoryCache()
- 注入IMemoryCache接口,查看接口的方法:TryGetValue、Remove、Set、GetOrCreate、GetOrCreateAsync
GetOrCreateAsync
GetOrCreateAsync<TItem>(object key, Func<ICacheEntry, Task<TItem>> factory)
獲取緩存鍵為key的緩存值,方法的返回值為獲取的緩存值,如果緩存中沒有緩存鍵為key的緩存值,則調用factory指向的回調從數據源獲取數據,把獲取的數據作為緩存值保存到緩存中,并且把獲取的數據作為方法的返回值。
Program.cs
//添加內存緩存服務
services.AddMemoryCache();
//添加DbContext
services.AddScoped<MyDbContext>();public record Book(long Id, string Name);public class BookConfig : IEntityTypeConfiguration<Book>
{public void Configure(EntityTypeBuilder<Book> builder){builder.ToTable("T_Books");}
}public class MyDbContext:DbContext
{DbSet<Book> books { get; set; }protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){base.OnConfiguring(optionsBuilder);optionsBuilder.UseSqlServer("Server=.;Database=demo;Trusted_Connection=true;MultipleActiveResultSets=true;TrustServerCertificate=true;");}protected override void OnModelCreating(ModelBuilder modelBuilder){base.OnModelCreating(modelBuilder);modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);}
}
[Route("api/[controller]/[action]")]
[ApiController]
public class TestController : ControllerBase
{private readonly IMemoryCache memoryCache;private readonly ILogger<TestController> logger;public TestController(IMemoryCache memoryCache, ILogger<TestController> logger){this.memoryCache = memoryCache;this.logger = logger;}[HttpGet]public async Task<ActionResult<Book?>> GetById(int id){using (MyDbContext ctx = new MyDbContext()){//沒加內存緩存//var result = await ctx.Set<Book>().SingleOrDefaultAsync(o => o.Id == id);//if (result == null)//{// return NotFound($"找不到id={id}的書");//}//else//{// return result;//}//內存緩存logger.LogInformation($"開始執行GetBookById,id={id}");Book? b = await memoryCache.GetOrCreateAsync("Book" + id, async (e) =>{logger.LogInformation($"緩存沒找到,到數據庫查詢,id={id}");return await ctx.Set<Book>().SingleOrDefaultAsync(o => o.Id == id);});logger.LogInformation($"GetBookById結果:{b}");if (b == null){return NotFound($"找不到id={id}的書");}else{return Ok(b);}}}
}
?查詢兩次結果
緩存過期時間策略
緩存的過期時間
上面的例子中的緩存不會過期,除非重啟服務器。
解決方法:
- 在數據改變的時候調用Remove或者Set來刪除或者修改緩存(優點:及時);
- 過期時間(只要過期時間比較短,緩存數據不一致的情況也不會持續很長時間。)
兩種過期時間策略:
GetOrCreateAsync()方法的回調方法中有一個ICacheEntry類型的參數,通過ICacheEntry對當前的緩存項做設置。
絕對過期時間
設置緩存完的指定時間后,緩存項被清除。
AbsoluteExpirationRelativeToNow:用來設定緩存項的絕對過期時間。
Book? b = await memoryCache.GetOrCreateAsync("Book" + id, async (e) =>
{logger.LogInformation($"緩存沒找到,到數據庫查詢,id={id}");//緩存有效期10se.AbsoluteExpirationRelativeToNow=TimeSpan.FromSeconds(10);return await ctx.Set<Book>().SingleOrDefaultAsync(o => o.Id == id);
});
滑動過期時間
設置緩存完的指定時間后,如果對應的緩存數據沒有被訪問,緩存項被清除,如果在指定時間內,被訪問一次,則緩存項的過期時間會自動續期。
Book? b = await memoryCache.GetOrCreateAsync("Book" + id, async (e) =>
{logger.LogInformation($"緩存沒找到,到數據庫查詢,id={id}");e.SlidingExpiration=TimeSpan.FromSeconds(10);return await ctx.Set<Book>().SingleOrDefaultAsync(o => o.Id == id);
});
兩種過期時間混用
使用滑動過期時間策略,如果一個緩存項一直被頻繁訪問,那么這個緩存項就會一直被續期而不過期。可以對一個緩存項同時設定滑動過期時間和絕對過期時間,并且把絕對過期時間設定的比滑動過期時間長,這樣緩存項的內容會在絕對過期時間內隨著訪問被滑動續期,但是一旦超過了絕對過期時間,緩存項就會被刪除。
總結
- 無論用那種過期時間策略,程序中都會存在緩存數據不一致的情況。部分系統(博客等)無所謂,部分系統不能忍受(比如金融)。
- 可以通過其他機制獲取數據源改變的消息,再通過代碼調用IMemoryCache的Set方法更新緩存。
?封裝內存緩存操作的幫助類
- IQueryable、IEnumerable等類型可能存在著延遲加載的問題,如果把這兩種類型的變量指向的對象保存到緩存中,在我們把它們取出來再去執行的時候,如果它們延遲加載時候需要的對象已經被釋放的話,就會執行失敗。因此緩存禁止這兩種類型。
- 實現隨機緩存過期時間
使用
Install-Package Zack.ASPNETCore
NETBookMaterials/最后大項目代碼/YouZack-VNext/Zack.ASPNETCore/MemoryCacheHelper.cs at main · yangzhongke/NETBookMaterialshttps://github.com/yangzhongke/NETBookMaterials/blob/main/%E6%9C%80%E5%90%8E%E5%A4%A7%E9%A1%B9%E7%9B%AE%E4%BB%A3%E7%A0%81/YouZack-VNext/Zack.ASPNETCore/MemoryCacheHelper.cshttps://github.com/yangzhongke/NETBookMaterials/blob/main/%E6%9C%80%E5%90%8E%E5%A4%A7%E9%A1%B9%E7%9B%AE%E4%BB%A3%E7%A0%81/YouZack-VNext/Zack.ASPNETCore/MemoryCacheHelper.cshttps://github.com/yangzhongke/NETBookMaterials/blob/main/%E6%9C%80%E5%90%8E%E5%A4%A7%E9%A1%B9%E7%9B%AE%E4%BB%A3%E7%A0%81/YouZack-VNext/Zack.ASPNETCore/MemoryCacheHelper.cshttps://github.com/yangzhongke/NETBookMaterials/blob/main/%E6%9C%80%E5%90%8E%E5%A4%A7%E9%A1%B9%E7%9B%AE%E4%BB%A3%E7%A0%81/YouZack-VNext/Zack.ASPNETCore/MemoryCacheHelper.cs
services.AddMemoryCache();
services.AddScoped<IMemoryCacheHelper,MemoryCacheHelper>();public TestController(IMemoryCache memoryCache, MyDbContext ctx, MemoryCacheHelper memoryCacheHelper)
{this.memoryCache = memoryCache;this.ctx = ctx;this.memoryCacheHelper = memoryCacheHelper;
}[HttpGet]
public async Task<ActionResult<Book?>> Test(long id)
{var b = memoryCacheHelper.GetOrCreateAsync("Book" + id, async (e) =>{return await ctx.Set<Book>().SingleOrDefaultAsync(o => o.Id == id);}, 10);if (b.Result == null){return NotFound("不存在");}else{return Ok(b.Result);}
}