一. 緩存重點概念
1. 緩存命中
指可以直接通過緩存獲取到需要的數據.
2. 緩存命中率
從緩存中拿到數據的次數/查詢的總次數,緩存的命中率越高則表示使用緩存的收益越高,應用的性能越好(響應時間越短、吞吐量越高),抗併發的能力越強.
3. 緩存穿透
業務請求中數據緩存中沒有,DB中也沒有,導致類似請求直接跨過緩存,反覆在DB中查詢,與此同時緩存也不會得到更新。
4. 緩存雪崩
同一時刻,大量的緩存同時過期失效。
5. 緩存擊穿
特指某熱點Key扛着大量的併發請求,當key失效的一瞬間,大量的QPS打到DB上,導致系統癱瘓。
(更多概念和詳細解決方案詳見redis章節:https://www.cnblogs.com/yaopengfei/p/13878124.html)
二. 客戶端緩存
1. 說明
RFC7324是HTTP協議中對緩存進行控制的規範,其中重要的是cache-control這個響應報文頭。服務器如果返回cache-control:max-age=60,則表示服務器指示瀏覽器端“可以緩存這個響應內容60秒”。
我們只要給需要進行緩存控制的控制器的操作方法添加ResponseCacheAttribute這個Attribute,ASP.NET Core會自動添加cache-control報文頭。
2. 測試
訪問 http://localhost:5164/swagger/index.html, 多次調用DemoApi/GetNowTime, 發現第一次從緩存中獲取當前時間,然後後續15s內,觀察請求都是從disk cache,即本地緩存中獲取的,且內容值是一樣的. 詳見運行結果圖
PS: 另外還有服務端響應緩存,用法:app.MapControllers()之前加上app.UseResponseCaching()。請確保app.UseCors()寫到app.UseResponseCaching()之前 【很雞肋,很少使用】
代碼分享:
/// <summary>
/// 測試客戶端緩存
/// </summary>
/// <returns></returns>
[ResponseCache(Duration = 15)]
[HttpGet]
public DateTime GetNowTime()
{
return DateTime.Now;
}
運行結果測試:
三. 內存緩存
1.含義
(1)把緩存數據放到應用程序的內存。內存緩存中保存的是一系列的鍵值對,就像Dictionary類型一樣。
(2)內存緩存的數據保存在當前運行的網站程序的內存中,是和進程相關的。因爲在Web服務器中,多個不同網站是運行在不同的進程中的,因此不同網站的內存緩存是不會互相干擾的, 而且網站重啓後,內存緩存中的所有數據也就都被清空了。
2.用法
【推薦使用 Microsoft.Extensions.Caching.Memory/IMemoryCache 而非 System.Runtime.Caching/MemoryCache】
(1) 在program中添加如下代碼:builder.Services.AddMemoryCache()
(2). 控制器中注入IMemoryCache接口,常用用的接口方法有:TryGetValue、Remove、Set、Get、GetOrCreate、GetOrCreateAsync
這裏重點使用:GetOrCreateAsync 用法,表示:如果緩存中有則從緩存中讀取,如果沒有則存入緩存
測試:
寫法1:利用Get和Set方法實現
寫法2:利用TryGetValue和set方法實現, 其中TryGetValue返回true 或 false,true表示對應的key在緩存中存在(是以key爲依據判斷的)
寫法3:利用GetOrCreate方法實現,其中GetOrCreate有則從緩存中讀取,沒有則執行回調業務(從DB中查詢→寫入緩存並返回) 【簡潔,省略了if判空和set寫入,推薦】
代碼分享:
private readonly IMemoryCache _memoryCache;
public DemoApiController(IMemoryCache memoryCache)
{
this._memoryCache = memoryCache;
}
/// <summary>
/// 測試內存緩存
/// </summary>
/// <returns></returns>
[HttpPost]
public dynamic TestMemoryCache()
{
//寫法1:
//var goodsNum = _memoryCache.Get("goodsNum");
//if (goodsNum==null)
//{
// //從數據庫中查詢
// goodsNum = DbHelp.GetNum();
// //寫入緩存
// _memoryCache.Set("goodsNum", goodsNum);
//}
//Console.WriteLine("數量爲:" + goodsNum);
//return goodsNum;
//寫法2
//string myTime;
//if (!_memoryCache.TryGetValue("mySpecialTime",out myTime))
//{
// //從數據庫中查詢
// myTime = DbHelp.GetTime();
// //寫入緩存
// _memoryCache.Set("mySpecialTime", myTime);
//}
//Console.WriteLine("時間爲:" + myTime);
//return myTime;
//寫法3
string myTime = _memoryCache.GetOrCreate("mySpecialTime", (cacheEnty) =>
{
//將數據庫中查詢的結果寫入緩存
return DbHelp.GetTime();
});
Console.WriteLine("時間爲:" + myTime);
return myTime;
}
3. 緩存過期時間
(1) 默認寫法是永不過期的, 除非重啓項目
(2) 絕對過期時間: 兩種時間寫法
(3) 滑動過期時間
(4).混用:滑動和絕對同時存在,滑動和絕對都生效
場景1:
絕對時間要長於滑動
比如:絕對10分鐘,滑動1分鐘, 第一次訪問後,如果一直不訪問,則1分鐘緩存失效,這1分鐘期間可以不斷續命滑動,但是隻要過了10分鐘,肯定失效。
場景2:
絕對時間是時間點
比如:絕對時間2022-09-01:12:00:00,滑動爲1分鐘,第一次訪問後,如果一直不訪問,則1分鐘緩存失效,在這1分鐘期間,可以不斷續命滑動,但是無論如何續命,只要過了 2022-09-01:12:00:00,緩存一定失效
代碼分享:
/// <summary>
/// 03-測試內存緩存-過期時間
/// </summary>
/// <returns></returns>
[HttpPost]
public String TestMemoryCacheTimeOut()
{
//1.絕對過期時間
//string myTime = _memoryCache.GetOrCreate<string>("mySpecialTime", (cacheEnty) =>
// {
// //配置過期時間(寫入緩存後,15s過期)
// cacheEnty.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(15);
// //或者直接具體到某個時間點過期
// //cacheEnty.AbsoluteExpiration = new DateTimeOffset(DateTime.Parse("2022-07-16 16:33:10"));
// //將數據庫中查詢的結果寫入緩存
// return DbHelp.GetTime();
// });
//2. 滑動過期時間
//string myTime = _memoryCache.GetOrCreate<string>("mySpecialTime", (cacheEnty) =>
//{
// //配置過期時間(每次調用緩存續命10s)
// cacheEnty.SlidingExpiration = TimeSpan.FromSeconds(10);
// //將數據庫中查詢的結果寫入緩存
// return DbHelp.GetTime();
//});
//3. 絕對和滑動混合使用
string myTime = _memoryCache.GetOrCreate<string>("mySpecialTime", (cacheEnty) =>
{
//配置過期時間(每次調用緩存續命10s)
cacheEnty.SlidingExpiration = TimeSpan.FromSeconds(10);
//配置過期時間(寫入緩存後,20s過期)
//cacheEnty.AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(20);
//或者直接具體到某個時間點過期
cacheEnty.AbsoluteExpiration = new DateTimeOffset(DateTime.Parse("2022-05-19 7:02:00"));
//將數據庫中查詢的結果寫入緩存
return DbHelp.GetTime();
});
Console.WriteLine("時間爲:" + myTime);
return myTime;
}
4. 設置大小
如果使用 SetSize、Size 和 SizeLimit 限制緩存大小,建議設置單例類
詳見:https://docs.microsoft.com/zh-cn/aspnet/core/performance/caching/memory?view=aspnetcore-6.0
5. 緩存穿透的解決方案
cache null策略:DB查詢的結果即使爲null,也給緩存的value設置爲null,同時可以設置一個較短的過期時間,這樣就避免不存在的數據跨過緩存直接打到DB上。
分析我們前面 DemoApi中TestMemoryCache中,從數據庫中查詢的數據,並沒有判空直接存入到緩存中了,這就是cache null策略,實際場景結合業務添加一個過期時間, 或者DB中的數據更新了刪除緩存。
這裏還是推薦使用GetOrCreateAsync方法即可,因爲它會把null值也當成合法的緩存值
測試代碼省略
6. 緩存雪崩的解決方案
設置不同的緩存失效時間,比如可以在緩存基礎過期時間後面加個隨機數,這樣就避免同一時刻緩存大量過期失效
詳見後面的代碼封裝即可
四. 分佈式緩存
(這裏基於redis進行配置,實際上可以直接用redis相關程序集進行處理的)
1. 說明
分佈式緩存是由多個應用服務器共享的緩存,通常作爲訪問它的應用服務器的外部服務進行維護, Asp.NET Core中提供了統一的分佈式緩存服務器的操作接口IDistributedCache,用法和內存緩存類似。
分佈式緩存和內存緩存的區別:緩存值的類型爲byte[],需要我們進行類型轉換,也提供了一些按照string類型存取緩存值的擴展方法,比如:GetString、SetString
2. 用法
(1). 啓動Redis服務器
(2). 安裝程序集 【Microsoft.Extensions.Caching.StackExchangeRedis】
(3). 在Program中註冊Redis服務配置,其中Configuration是redis的鏈接字符串,InstanceName表示給所有存入redis的key加個前綴(可以不配置哦)
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost";
options.InstanceName = "ypf_";
});
(4). 在控制器中注入 IDistributedCache接口, 使用即可
代碼詳見:05-DistributeCacheDemo/Test1Controller/GetMyTime
public Test1Controller(IDistributedCache distributeCache)
{
this._distributeCache = distributeCache;
}
/// <summary>
/// 測試基於Redis的分佈式緩存
/// </summary>
[HttpPost]
public String GetMyTime()
{
string myTime = _distributeCache.GetString("mySpecialTime");
if (string.IsNullOrEmpty(myTime))
{
//從數據庫中查詢
myTime = DbHelp.GetTime();
//配置過期時間
var opt = new DistributedCacheEntryOptions
{
//絕對過期時間
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(15)
};
//存入redis緩存中
_distributeCache.SetString("mySpecialTime", myTime, opt);
}
Console.WriteLine("時間爲:" + myTime);
return myTime;
}
補充:(如何配置密碼呢? 參考:【119.45.143.22:6379,password=123456,defaultDatabase=0】)
五. 相關封裝剖析
1. 老楊的內存緩存框架
(1).需求:
A. IQueryable、IEnumerable等類型可能存在着延遲加載的問題,如果把這兩種類型的變量指向的對象保存到緩存中,在我們把它們取出來再去執行的時候,如果它們延遲加載時候需要的對象已經被釋放的話,就會執行失敗。因此緩存禁止這兩種類型。
B. 實現隨機緩存過期時間
源碼:https://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
(2). 源碼剖析
重點理解一下GetOrCreate方法中委託的使用, 詳見 Utils/MemoryCacheHelper類
接口代碼:
public interface IMemoryCacheHelper { /// <summary> /// 從緩存中獲取數據,如果緩存中沒有數據,則調用valueFactory獲取數據。 /// 可以用AOP+Attribute的方式來修飾到Service接口中實現緩存,更加優美,但是沒有這種方式更靈活。 /// 默認最長的緩存過期時間是expireSeconds秒,當然也可以在領域事件的Handler中調用Update更新緩存,或者調用Remove刪除緩存。 /// 因爲IMemoryCache會把null當成合法的值,因此不會有緩存穿透的問題,但是還是建議用我這裏封裝的ICacheHelper,原因如下: /// 1)可以切換別的實現類,比如可以保存到MemCached、Redis等地方。這樣可以隔離變化。 /// 2)IMemoryCache的valueFactory用起來麻煩,還要單獨聲明一個ICacheEntry參數,大部分時間用不到這個參數。 /// 3)這裏把expireSeconds加上了一個隨機偏差,這樣可以避免短時間內同樣的請求集中過期導致“緩存雪崩”的問題 /// 4)這裏加入了緩存數據的類型不能是IEnumerable、IQueryable等類型的限制 /// </summary> /// <typeparam name="TResult">緩存的值的類型</typeparam> /// <param name="cacheKey">緩存的key</param> /// <param name="valueFactory">提供數據的委託</param> /// <param name="expireSeconds">緩存過期秒數的最大值,實際緩存時間是在[expireSeconds,expireSeconds*2)之間,這樣可以一定程度上避免大批key集中過期導致的“緩存雪崩”的問題</param> /// <returns></returns> TResult? GetOrCreate<TResult>(string cacheKey, Func<ICacheEntry, TResult?> valueFactory, int expireSeconds = 60); Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<ICacheEntry, Task<TResult?>> valueFactory, int expireSeconds = 60); /// <summary> /// 刪除緩存的值 /// </summary> /// <param name="cacheKey"></param> void Remove(string cacheKey); }
類代碼:
/// <summary> /// 用ASP.NET的IMemoryCache實現的內存緩存 /// </summary> public class MemoryCacheHelper : IMemoryCacheHelper { private readonly IMemoryCache memoryCache; public MemoryCacheHelper(IMemoryCache memoryCache) { this.memoryCache = memoryCache; } private static void ValidateValueType<TResult>() { //因爲IEnumerable、IQueryable等有延遲執行的問題,造成麻煩,因此禁止用這些類型 Type typeResult = typeof(TResult); if (typeResult.IsGenericType)//如果是IEnumerable<String>這樣的泛型類型,則把String這樣的具體類型信息去掉,再比較 { typeResult = typeResult.GetGenericTypeDefinition(); } //注意用相等比較,不要用IsAssignableTo if (typeResult == typeof(IEnumerable<>) || typeResult == typeof(IEnumerable) || typeResult == typeof(IAsyncEnumerable<TResult>) || typeResult == typeof(IQueryable<TResult>) || typeResult == typeof(IQueryable)) { throw new InvalidOperationException($"TResult of {typeResult} is not allowed, please use List<T> or T[] instead."); } } private static void InitCacheEntry(ICacheEntry entry, int baseExpireSeconds) { //過期時間.Random.Shared 是.NET6新增的 double sec = Random.Shared.NextDouble(baseExpireSeconds, baseExpireSeconds * 2); TimeSpan expiration = TimeSpan.FromSeconds(sec); entry.AbsoluteExpirationRelativeToNow = expiration; } public TResult? GetOrCreate<TResult>(string cacheKey, Func<ICacheEntry, TResult?> valueFactory, int baseExpireSeconds = 60) { ValidateValueType<TResult>(); //因爲IMemoryCache保存的是一個CacheEntry,所以null值也認爲是合法的,因此返回null不會有“緩存穿透”的問題 //不調用系統內置的CacheExtensions.GetOrCreate,而是直接用GetOrCreate的代碼,這樣免得包裝一次委託 if (!memoryCache.TryGetValue(cacheKey, out TResult result)) { using ICacheEntry entry = memoryCache.CreateEntry(cacheKey); InitCacheEntry(entry, baseExpireSeconds); result = valueFactory(entry)!; entry.Value = result; } return result; } public async Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<ICacheEntry, Task<TResult?>> valueFactory, int baseExpireSeconds = 60) { ValidateValueType<TResult>(); if (!memoryCache.TryGetValue(cacheKey, out TResult result)) { using ICacheEntry entry = memoryCache.CreateEntry(cacheKey); InitCacheEntry(entry, baseExpireSeconds); result = (await valueFactory(entry))!; entry.Value = result; } return result; } public void Remove(string cacheKey) { memoryCache.Remove(cacheKey); } }
(3). 補充1個方法:
A..Net6中新增的生成隨機數的方法,解決了舊版本高併發下的問題 Random.Shared.Next(1,100);
B. 另外默認的NextDouble方法只能生成0-1取件,這裏自己擴展一個可以指定大小範圍的NextDouble方法
public static class RandomExtensions
{
/// <summary>
/// 擴展一個可以指定大小範圍生成隨機數的方法
/// </summary>
/// <param name="random"></param>
/// <param name="minValue">The inclusive lower bound of the random number returned.</param>
/// <param name="maxValue">The exclusive upper bound of the random number returned. maxValue must be greater than or equal to minValue.</param>
/// <returns></returns>
public static double NextDouble(this Random random, double minValue, double maxValue)
{
if (minValue >= maxValue)
{
throw new ArgumentOutOfRangeException(nameof(minValue), "minValue cannot be bigger than maxValue");
}
//https://stackoverflow.com/questions/65900931/c-sharp-random-number-between-double-minvalue-and-double-maxvalue
double x = random.NextDouble();
return x * maxValue + (1 - x) * minValue;
}
}
(4). 測試
A. 註冊成單例模式 builder.Services.AddScoped<IMemoryCacheHelper, MemoryCacheHelper>();
B. 通過[FromServices] IMemoryCacheHelper cacheHelp 注入使用即可
/// <summary>
/// 測試內存緩存封裝類
/// </summary>
/// <returns></returns>
[HttpPost]
public string? TestMemoryCacheHelper([FromServices] IMemoryCacheHelper cacheHelp)
{
string? myTime = cacheHelp.GetOrCreate<String>("mySpecialTime", (cacheEnty) =>
{
//將數據庫中查詢的結果寫入緩存
return DbHelp.GetTime();
}, 50);
Console.WriteLine("時間爲:" + myTime);
return myTime;
}
2. 老楊的分佈式緩存框架
(1).需求:
A. 解決緩存穿透、緩存雪崩等問題。
B. 自動地進行其他類型的轉換。
源碼:https://github.com/yangzhongke/NETBookMaterials/tree/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/DistributedCacheHelper.cs
(2). 源碼剖析
詳見 DistributedCacheHelper 方法,詳見 Utils/DistributedCacheHelper類
接口代碼:
public interface IDistributedCacheHelper { TResult? GetOrCreate<TResult>(string cacheKey, Func<DistributedCacheEntryOptions, TResult?> valueFactory, int expireSeconds = 60); Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<DistributedCacheEntryOptions, Task<TResult?>> valueFactory, int expireSeconds = 60); void Remove(string cacheKey); Task RemoveAsync(string cacheKey); }
類代碼:
public class DistributedCacheHelper : IDistributedCacheHelper { private readonly IDistributedCache distCache; public DistributedCacheHelper(IDistributedCache distCache) { this.distCache = distCache; } private static DistributedCacheEntryOptions CreateOptions(int baseExpireSeconds) { //過期時間.Random.Shared 是.NET6新增的 double sec = Random.Shared.NextDouble(baseExpireSeconds, baseExpireSeconds * 2); TimeSpan expiration = TimeSpan.FromSeconds(sec); DistributedCacheEntryOptions options = new() { AbsoluteExpirationRelativeToNow = expiration }; return options; } public TResult? GetOrCreate<TResult>(string cacheKey, Func<DistributedCacheEntryOptions, TResult?> valueFactory, int expireSeconds = 60) { string jsonStr = distCache.GetString(cacheKey); //緩存中不存在 if (string.IsNullOrEmpty(jsonStr)) { var options = CreateOptions(expireSeconds); TResult? result = valueFactory(options);//如果數據源中也沒有查到,可能會返回null //null會被json序列化爲字符串"null",所以可以防範“緩存穿透” string jsonOfResult = JsonSerializer.Serialize(result, typeof(TResult)); distCache.SetString(cacheKey, jsonOfResult, options); return result; } else { //"null"會被反序列化爲null //TResult如果是引用類型,就有爲null的可能性;如果TResult是值類型 //在寫入的時候肯定寫入的是0、1之類的值,反序列化出來不會是null //所以如果obj這裏爲null,那麼存進去的時候一定是引用類型 distCache.Refresh(cacheKey);//刷新,以便於滑動過期時間延期 return JsonSerializer.Deserialize<TResult>(jsonStr)!; } } public async Task<TResult?> GetOrCreateAsync<TResult>(string cacheKey, Func<DistributedCacheEntryOptions, Task<TResult?>> valueFactory, int expireSeconds = 60) { string jsonStr = await distCache.GetStringAsync(cacheKey); if (string.IsNullOrEmpty(jsonStr)) { var options = CreateOptions(expireSeconds); TResult? result = await valueFactory(options); string jsonOfResult = JsonSerializer.Serialize(result, typeof(TResult)); await distCache.SetStringAsync(cacheKey, jsonOfResult, options); return result; } else { await distCache.RefreshAsync(cacheKey); return JsonSerializer.Deserialize<TResult>(jsonStr)!; } } public void Remove(string cacheKey) { distCache.Remove(cacheKey); } public Task RemoveAsync(string cacheKey) { return distCache.RemoveAsync(cacheKey); } }
(3). 測試
A. 註冊成單例模式 builder.Services.AddScoped<IDistributedCacheHelper, DistributedCacheHelper>();
B. 通過[FromServices] IDistributedCacheHelper cacheHelp 注入使用即可
/// <summary>
/// 02-測試分佈式緩存封裝類
/// </summary>
/// <returns></returns>
[HttpPost]
public string? TestDistributedCacheHelper([FromServices] IDistributedCacheHelper cacheHelp)
{
string? myTime = cacheHelp.GetOrCreate<String>("mySpecialTime", (cacheEnty) =>
{
//將數據庫中查詢的結果寫入緩存
return DbHelp.GetTime();
}, 50);
Console.WriteLine("時間爲:" + myTime);
return myTime;
}
!
- 作 者 : Yaopengfei(姚鵬飛)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 聲 明1 : 如有錯誤,歡迎討論,請勿謾罵^_^。
- 聲 明2 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。