前言
簡單整理一下快取。
正文
快取是什麼?
-
快取是計算結果的"臨時"儲存和重複使用
-
快取本質是用空間換取時間
快取的場景:
-
計算結果,如:反射物件快取
-
請求結果,如:DNS 快取
-
臨時共享資料,如:會話儲存
-
熱點訪問內容頁,如:商品詳情
-
熱點變更邏輯資料,如:秒殺的庫存數
快取的策略:
-
越接近最終的資料結構,效果比較好
-
快取命中率越高越好,命中率低意味著空間的浪費。
快取的位置:
-
瀏覽器中
-
反向代理伺服器中(nginx)
-
應用程式記憶體中
-
分散式儲存系統中(redis)
快取實現的要點:
-
儲存key生成策略,表示快取資料的範圍、業務含義
-
快取失效的策略,如:過期時間機制、主動重新整理機制
-
快取的更新策略,表示更新快取資料的時機
快取的幾個問題:
-
快取失效,導致資料不一致。是指快取的資料與我們資料庫裡面的資料不一致的情況。
-
快取穿透,查詢無資料時,導致快取不生效,查詢都落到了資料庫上
-
快取擊穿,快取失效瞬間,大量請求訪問到資料庫
-
快取雪崩,大量快取在同一時間失效,導致資料庫壓力大
上面這些哪裡看的最多呢?redis的面經的,我現在都沒有想明白這些和reids有什麼關係,這些本來就是快取問題,只不過redis當快取的時候,自然就遇到了快取的問題了。
下面來簡單介紹一下這幾個問題。
第一點,快取失效,就是和我們資料庫裡面的資料不一致,這個就是程式碼業務問題了,業務沒有做好。
第二個,快取穿透,因為一些資料不存在,然後快取中自然是沒有的,然後就會一直訪問資料庫,然後資料庫壓力就大。
這個很有可能是別人的攻擊。那麼防護措施可以這麼幹,當資料庫裡面沒有的時候,可以在快取中設定key:null,依然加入快取中取,這樣訪問的就是快取了。
第三點,快取擊穿,指的是大量使用者訪問同一個快取,當快取失效的時候,每個請求都會去訪問資料庫。
那麼這個時候,比較簡單的方式就是加鎖。
// xx查詢為空
if(xx==null)
{
lock(obj)
{
// 再查一次
....
//如果沒有去資料庫裡面取資料,加入快取中
if(xx=null)
{
// 進行資料庫查詢,加入快取,給xx賦值
....
}
}
}
這種是大量使用者訪問同一個快取的情況,當然也可以設定快取不過期,但是不能保證快取不被清理吧。就是說快取不過期是在理想情況,但是怎麼沒的,就屬於突發情況了。
第四點,快取雪崩。就是比如說有1w個使用者現在來請求了,然後艱難的給他們都加上了快取,然後就把他們的快取時間設定為半個小時,然後半個小時後,這一萬個請求又來了,但是快取沒了,這時候又要艱難的從資料庫裡面讀取。
那麼這種情況怎麼解決呢? 最簡單的就是不要設定設定固定的快取失效數字,可以隨機一個數字。但是如果使用者體過大,同樣面臨著某一個時間點大量使用者失效的情況。那麼同樣可以,當拿到使用者快取的時候,如果時間快到期了,然後給他續時間。
那麼就來舉例子。
需要用到的元件如下:
-
responseCache 中介軟體
-
Miscrosoft.Extensions.Caching.Memory.IMemoryCache MemoryCache
-
Miscrosoft.Extensions.Caching.Distributed.IDistributedCache 分散式cache
-
EasyCaching 開源chache元件
記憶體快取和分散式快取的區別
-
記憶體快取可以儲存任意的物件
-
分散式快取的物件需要支援序列化
-
分散式快取遠端請求可能失敗(網路問題,或者遠端服務快取程式崩潰等),記憶體快取不會(記憶體快取沒有網路問題)
下面是例子:
需要安裝的包:
-
EasyCaching.Redis
-
microsoft.extensions.Caching.StackExchangeRedis
先來介紹一下記憶體快取,記憶體快取是我們框架自帶的。
services.AddMemoryCache();
這樣就是就開啟了我們的記憶體快取。
簡答看下AddMemoryCache。
public static IServiceCollection AddMemoryCache(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.AddOptions();
services.TryAdd(ServiceDescriptor.Singleton<IMemoryCache, MemoryCache>());
return services;
}
實際上註冊了IMemoryCache,為MemoryCache。這個MemoryCache就不看了,就是一些key value 之類快取的方法。
那麼來看一下services.AddResponseCaching();,啟動Response cache 服務。
/// <summary>
/// Add response caching services.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
/// <returns></returns>
public static IServiceCollection AddResponseCaching(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.TryAddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
return services;
}
這個是我們請求結果的快取,那麼還得寫入中介軟體。
services.AddResponseCaching();
那麼簡單看一下AddResponseCaching這個中介軟體。
public static IApplicationBuilder UseResponseCaching(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
return app.UseMiddleware<ResponseCachingMiddleware>();
}
然後就是看下ResponseCachingMiddleware。
public ResponseCachingMiddleware(
RequestDelegate next,
IOptions<ResponseCachingOptions> options,
ILoggerFactory loggerFactory,
ObjectPoolProvider poolProvider)
: this(
next,
options,
loggerFactory,
new ResponseCachingPolicyProvider(),
new MemoryResponseCache(new MemoryCache(new MemoryCacheOptions
{
SizeLimit = options.Value.SizeLimit
})),
new ResponseCachingKeyProvider(poolProvider, options))
{ }
可以看到其使用的cache,是MemoryCache。好的就點到為止吧,後續的可能會寫在細節篇中,可能也不會出現在細節篇中,未在計劃內。
好吧,然後測試程式碼:
public class OrderController : Controller
{
[ResponseCache(Duration = 6000)]
public IActionResult Pay()
{
return Content("買買買:"+DateTime.Now);
}
}
看下效果:
第一次請求的時候:
給了這個引數,告訴瀏覽器,在該段時間內就不要來訪問後臺了,用快取就好。
第二次訪問:
黃色部分的意思該請求沒有發出去,用的是快取。
感覺這樣挺好的,那麼這個時候就有坑來了。
[ResponseCache(Duration = 6000)]
public IActionResult Pay(string name)
{
return Content("買買買:"+DateTime.Now+name);
}
訪問第一次:
訪問第二次:
顯然第二次是有問題的。
因為name 引數變化了,但是結果相同。
這顯然是有問題的,這是客戶端快取嗎?不是,瀏覽器只要是訪問連結發生任何變化的時候就會不使用。
可以看到上面實際上去訪問了我們的後臺的。
那麼應該這樣寫,表示當name 引數發生變化的時候就不會命中後臺的快取:
public class OrderController : Controller
{
[ResponseCache(Duration = 6000,VaryByQueryKeys =new String[]{ "name"})]
public IActionResult Pay(string name)
{
return Content("買買買:"+DateTime.Now+name);
}
}
為什麼這麼寫呢?這個就要從後臺快取的key開始說起。
看下:ResponseCache 裡面的,也就是這個屬性類。
public CacheProfile GetCacheProfile(MvcOptions options)
{
CacheProfile selectedProfile = null;
if (CacheProfileName != null)
{
options.CacheProfiles.TryGetValue(CacheProfileName, out selectedProfile);
if (selectedProfile == null)
{
throw new InvalidOperationException(Resources.FormatCacheProfileNotFound(CacheProfileName));
}
}
// If the ResponseCacheAttribute parameters are set,
// then it must override the values from the Cache Profile.
// The below expression first checks if the duration is set by the attribute's parameter.
// If absent, it checks the selected cache profile (Note: There can be no cache profile as well)
// The same is the case for other properties.
_duration = _duration ?? selectedProfile?.Duration;
_noStore = _noStore ?? selectedProfile?.NoStore;
_location = _location ?? selectedProfile?.Location;
VaryByHeader = VaryByHeader ?? selectedProfile?.VaryByHeader;
VaryByQueryKeys = VaryByQueryKeys ?? selectedProfile?.VaryByQueryKeys;
return new CacheProfile
{
Duration = _duration,
Location = _location,
NoStore = _noStore,
VaryByHeader = VaryByHeader,
VaryByQueryKeys = VaryByQueryKeys,
};
}
/// <inheritdoc />
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
{
if (serviceProvider == null)
{
throw new ArgumentNullException(nameof(serviceProvider));
}
var loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>();
var optionsAccessor = serviceProvider.GetRequiredService<IOptions<MvcOptions>>();
var cacheProfile = GetCacheProfile(optionsAccessor.Value);
// ResponseCacheFilter cannot take any null values. Hence, if there are any null values,
// the properties convert them to their defaults and are passed on.
return new ResponseCacheFilter(cacheProfile, loggerFactory);
}
可以看到CreateInstance 生成了一個ResponseCacheFilter。
那麼來看下這個ResponseCacheFilter:
/// <summary>
/// Creates a new instance of <see cref="ResponseCacheFilter"/>
/// </summary>
/// <param name="cacheProfile">The profile which contains the settings for
/// <see cref="ResponseCacheFilter"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public ResponseCacheFilter(CacheProfile cacheProfile, ILoggerFactory loggerFactory)
{
_executor = new ResponseCacheFilterExecutor(cacheProfile);
_logger = loggerFactory.CreateLogger(GetType());
}
/// <inheritdoc />
public void OnActionExecuting(ActionExecutingContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// If there are more filters which can override the values written by this filter,
// then skip execution of this filter.
var effectivePolicy = context.FindEffectivePolicy<IResponseCacheFilter>();
if (effectivePolicy != null && effectivePolicy != this)
{
_logger.NotMostEffectiveFilter(GetType(), effectivePolicy.GetType(), typeof(IResponseCacheFilter));
return;
}
_executor.Execute(context);
}
那麼來看一下ResponseCacheFilterExecutor:
internal class ResponseCacheFilterExecutor
{
private readonly CacheProfile _cacheProfile;
private int? _cacheDuration;
private ResponseCacheLocation? _cacheLocation;
private bool? _cacheNoStore;
private string _cacheVaryByHeader;
private string[] _cacheVaryByQueryKeys;
public ResponseCacheFilterExecutor(CacheProfile cacheProfile)
{
_cacheProfile = cacheProfile ?? throw new ArgumentNullException(nameof(cacheProfile));
}
public int Duration
{
get => _cacheDuration ?? _cacheProfile.Duration ?? 0;
set => _cacheDuration = value;
}
public ResponseCacheLocation Location
{
get => _cacheLocation ?? _cacheProfile.Location ?? ResponseCacheLocation.Any;
set => _cacheLocation = value;
}
public bool NoStore
{
get => _cacheNoStore ?? _cacheProfile.NoStore ?? false;
set => _cacheNoStore = value;
}
public string VaryByHeader
{
get => _cacheVaryByHeader ?? _cacheProfile.VaryByHeader;
set => _cacheVaryByHeader = value;
}
public string[] VaryByQueryKeys
{
get => _cacheVaryByQueryKeys ?? _cacheProfile.VaryByQueryKeys;
set => _cacheVaryByQueryKeys = value;
}
public void Execute(FilterContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (!NoStore)
{
// Duration MUST be set (either in the cache profile or in this filter) unless NoStore is true.
if (_cacheProfile.Duration == null && _cacheDuration == null)
{
throw new InvalidOperationException(
Resources.FormatResponseCache_SpecifyDuration(nameof(NoStore), nameof(Duration)));
}
}
var headers = context.HttpContext.Response.Headers;
// Clear all headers
headers.Remove(HeaderNames.Vary);
headers.Remove(HeaderNames.CacheControl);
headers.Remove(HeaderNames.Pragma);
if (!string.IsNullOrEmpty(VaryByHeader))
{
headers[HeaderNames.Vary] = VaryByHeader;
}
if (VaryByQueryKeys != null)
{
var responseCachingFeature = context.HttpContext.Features.Get<IResponseCachingFeature>();
if (responseCachingFeature == null)
{
throw new InvalidOperationException(
Resources.FormatVaryByQueryKeys_Requires_ResponseCachingMiddleware(nameof(VaryByQueryKeys)));
}
responseCachingFeature.VaryByQueryKeys = VaryByQueryKeys;
}
if (NoStore)
{
headers[HeaderNames.CacheControl] = "no-store";
// Cache-control: no-store, no-cache is valid.
if (Location == ResponseCacheLocation.None)
{
headers.AppendCommaSeparatedValues(HeaderNames.CacheControl, "no-cache");
headers[HeaderNames.Pragma] = "no-cache";
}
}
else
{
string cacheControlValue;
switch (Location)
{
case ResponseCacheLocation.Any:
cacheControlValue = "public,";
break;
case ResponseCacheLocation.Client:
cacheControlValue = "private,";
break;
case ResponseCacheLocation.None:
cacheControlValue = "no-cache,";
headers[HeaderNames.Pragma] = "no-cache";
break;
default:
cacheControlValue = null;
break;
}
cacheControlValue = $"{cacheControlValue}max-age={Duration}";
headers[HeaderNames.CacheControl] = cacheControlValue;
}
}
看裡面的Execute,這個。
可以看到對於我們的VaryByQueryKeys,其傳遞給了一個叫做IResponseCachingFeature的子類。
那麼什麼時候用到了呢?
就在我們中介軟體的ResponseCachingMiddleware的OnFinalizeCacheHeaders方法中。
/// <summary>
/// Finalize cache headers.
/// </summary>
/// <param name="context"></param>
/// <returns><c>true</c> if a vary by entry needs to be stored in the cache; otherwise <c>false</c>.</returns>
private bool OnFinalizeCacheHeaders(ResponseCachingContext context)
{
if (_policyProvider.IsResponseCacheable(context))
{
var storeVaryByEntry = false;
context.ShouldCacheResponse = true;
// Create the cache entry now
var response = context.HttpContext.Response;
var varyHeaders = new StringValues(response.Headers.GetCommaSeparatedValues(HeaderNames.Vary));
var varyQueryKeys = new StringValues(context.HttpContext.Features.Get<IResponseCachingFeature>()?.VaryByQueryKeys);
context.CachedResponseValidFor = context.ResponseSharedMaxAge ??
context.ResponseMaxAge ??
(context.ResponseExpires - context.ResponseTime.Value) ??
DefaultExpirationTimeSpan;
// Generate a base key if none exist
if (string.IsNullOrEmpty(context.BaseKey))
{
context.BaseKey = _keyProvider.CreateBaseKey(context);
}
// Check if any vary rules exist
if (!StringValues.IsNullOrEmpty(varyHeaders) || !StringValues.IsNullOrEmpty(varyQueryKeys))
{
// Normalize order and casing of vary by rules
var normalizedVaryHeaders = GetOrderCasingNormalizedStringValues(varyHeaders);
var normalizedVaryQueryKeys = GetOrderCasingNormalizedStringValues(varyQueryKeys);
// Update vary rules if they are different
if (context.CachedVaryByRules == null ||
!StringValues.Equals(context.CachedVaryByRules.QueryKeys, normalizedVaryQueryKeys) ||
!StringValues.Equals(context.CachedVaryByRules.Headers, normalizedVaryHeaders))
{
context.CachedVaryByRules = new CachedVaryByRules
{
VaryByKeyPrefix = FastGuid.NewGuid().IdString,
Headers = normalizedVaryHeaders,
QueryKeys = normalizedVaryQueryKeys
};
}
// Always overwrite the CachedVaryByRules to update the expiry information
_logger.VaryByRulesUpdated(normalizedVaryHeaders, normalizedVaryQueryKeys);
storeVaryByEntry = true;
context.StorageVaryKey = _keyProvider.CreateStorageVaryByKey(context);
}
// Ensure date header is set
if (!context.ResponseDate.HasValue)
{
context.ResponseDate = context.ResponseTime.Value;
// Setting the date on the raw response headers.
context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(context.ResponseDate.Value);
}
// Store the response on the state
context.CachedResponse = new CachedResponse
{
Created = context.ResponseDate.Value,
StatusCode = context.HttpContext.Response.StatusCode,
Headers = new HeaderDictionary()
};
foreach (var header in context.HttpContext.Response.Headers)
{
if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase))
{
context.CachedResponse.Headers[header.Key] = header.Value;
}
}
return storeVaryByEntry;
}
context.ResponseCachingStream.DisableBuffering();
return false;
}
重點關注一下context.StorageVaryKey 是如何生成的,StorageVaryKey就是快取的key。
if (context.CachedVaryByRules == null ||
!StringValues.Equals(context.CachedVaryByRules.QueryKeys, normalizedVaryQueryKeys) ||
!StringValues.Equals(context.CachedVaryByRules.Headers, normalizedVaryHeaders))
{
context.CachedVaryByRules = new CachedVaryByRules
{
VaryByKeyPrefix = FastGuid.NewGuid().IdString,
Headers = normalizedVaryHeaders,
QueryKeys = normalizedVaryQueryKeys
};
}
context.StorageVaryKey = _keyProvider.CreateStorageVaryByKey(context);
那麼可以看下CreateStorageVaryByKey:
// BaseKey<delimiter>H<delimiter>HeaderName=HeaderValue<delimiter>Q<delimiter>QueryName=QueryValue1<subdelimiter>QueryValue2
public string CreateStorageVaryByKey(ResponseCachingContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var varyByRules = context.CachedVaryByRules;
if (varyByRules == null)
{
throw new InvalidOperationException($"{nameof(CachedVaryByRules)} must not be null on the {nameof(ResponseCachingContext)}");
}
if (StringValues.IsNullOrEmpty(varyByRules.Headers) && StringValues.IsNullOrEmpty(varyByRules.QueryKeys))
{
return varyByRules.VaryByKeyPrefix;
}
var request = context.HttpContext.Request;
var builder = _builderPool.Get();
try
{
// Prepend with the Guid of the CachedVaryByRules
builder.Append(varyByRules.VaryByKeyPrefix);
// Vary by headers
var headersCount = varyByRules?.Headers.Count ?? 0;
if (headersCount > 0)
{
// Append a group separator for the header segment of the cache key
builder.Append(KeyDelimiter)
.Append('H');
var requestHeaders = context.HttpContext.Request.Headers;
for (var i = 0; i < headersCount; i++)
{
var header = varyByRules.Headers[i];
var headerValues = requestHeaders[header];
builder.Append(KeyDelimiter)
.Append(header)
.Append('=');
var headerValuesArray = headerValues.ToArray();
Array.Sort(headerValuesArray, StringComparer.Ordinal);
for (var j = 0; j < headerValuesArray.Length; j++)
{
builder.Append(headerValuesArray[j]);
}
}
}
// Vary by query keys
if (varyByRules?.QueryKeys.Count > 0)
{
// Append a group separator for the query key segment of the cache key
builder.Append(KeyDelimiter)
.Append('Q');
if (varyByRules.QueryKeys.Count == 1 && string.Equals(varyByRules.QueryKeys[0], "*", StringComparison.Ordinal))
{
// Vary by all available query keys
var queryArray = context.HttpContext.Request.Query.ToArray();
// Query keys are aggregated case-insensitively whereas the query values are compared ordinally.
Array.Sort(queryArray, QueryKeyComparer.OrdinalIgnoreCase);
for (var i = 0; i < queryArray.Length; i++)
{
builder.Append(KeyDelimiter)
.AppendUpperInvariant(queryArray[i].Key)
.Append('=');
var queryValueArray = queryArray[i].Value.ToArray();
Array.Sort(queryValueArray, StringComparer.Ordinal);
for (var j = 0; j < queryValueArray.Length; j++)
{
if (j > 0)
{
builder.Append(KeySubDelimiter);
}
builder.Append(queryValueArray[j]);
}
}
}
else
{
for (var i = 0; i < varyByRules.QueryKeys.Count; i++)
{
var queryKey = varyByRules.QueryKeys[i];
var queryKeyValues = context.HttpContext.Request.Query[queryKey];
builder.Append(KeyDelimiter)
.Append(queryKey)
.Append('=');
var queryValueArray = queryKeyValues.ToArray();
Array.Sort(queryValueArray, StringComparer.Ordinal);
for (var j = 0; j < queryValueArray.Length; j++)
{
if (j > 0)
{
builder.Append(KeySubDelimiter);
}
builder.Append(queryValueArray[j]);
}
}
}
}
return builder.ToString();
}
finally
{
_builderPool.Return(builder);
}
}
可以看到如果快取的key值和我們的VaryByQueryKeys的設定息息相關,只要我們的VaryByQueryKeys設定的key的value發生任何變化,也就是我們的引數的值發生變化,那麼生成的快取key絕對不同,那麼就不會命中。
下面就簡單介紹一下redis的快取。
services.AddStackExchangeRedisCache(options =>
{
Configuration.GetSection("redisCache").Bind(options);
});
這樣就是redis的快取。
然後第三方就是easycache 就是:
services.AddEasyCaching(options =>
{
options.UseRedis(Configuration,name:"easycaching");
});
這些都可以直接去看文件,這裡覺得沒什麼要整理的。
結
下一節 apollo 配置中心。