天天看點

重新整理 .net core 實踐篇————緩存相關[四十二]

前言

簡單整理一下緩存。

正文

緩存是什麼?

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

緩存的場景:

  1. 計算結果,如:反射對象緩存
  2. 請求結果,如:DNS 緩存
  3. 臨時共享資料,如:會話存儲
  4. 熱點通路内容頁,如:商品詳情
  5. 熱點變更邏輯資料,如:秒殺的庫存數

緩存的政策:

  1. 越接近最終的資料結構,效果比較好
  2. 緩存命中率越高越好,命中率低意味着空間的浪費。

緩存的位置:

  1. 浏覽器中
  2. 反向代理伺服器中(nginx)
  3. 應用程序記憶體中
  4. 分布式存儲系統中(redis)

緩存實作的要點:

  1. 存儲key生成政策,表示緩存資料的範圍、業務含義
  2. 緩存失效的政策,如:過期時間機制、主動重新整理機制
  3. 緩存的更新政策,表示更新緩存資料的時機

緩存的幾個問題:

  1. 緩存失效,導緻資料不一緻。是指緩存的資料與我們資料庫裡面的資料不一緻的情況。
  2. 緩存穿透,查詢無資料時,導緻緩存不生效,查詢都落到了資料庫上
  3. 緩存擊穿,緩存失效瞬間,大量請求通路到資料庫
  4. 緩存雪崩,大量緩存在同一時間失效,導緻資料庫壓力大

上面這些哪裡看的最多呢?redis的面經的,我現在都沒有想明白這些和reids有什麼關系,這些本來就是緩存問題,隻不過redis當緩存的時候,自然就遇到了緩存的問題了。

下面來簡單介紹一下這幾個問題。

第一點,緩存失效,就是和我們資料庫裡面的資料不一緻,這個就是代碼業務問題了,業務沒有做好。

第二個,緩存穿透,因為一些資料不存在,然後緩存中自然是沒有的,然後就會一直通路資料庫,然後資料庫壓力就大。

這個很有可能是别人的攻擊。那麼防護措施可以這麼幹,當資料庫裡面沒有的時候,可以在緩存中設定key:null,依然加入緩存中取,這樣通路的就是緩存了。

第三點,緩存擊穿,指的是大量使用者通路同一個緩存,當緩存失效的時候,每個請求都會去通路資料庫。

那麼這個時候,比較簡單的方式就是加鎖。

// xx查詢為空
if(xx==null)
{
   lock(obj)
   {
      // 再查一次
       ....
      //如果沒有去資料庫裡面取資料,加入緩存中
      if(xx=null)
      {
       // 進行資料庫查詢,加入緩存,給xx指派
       ....
      }
   }
}
           

這種是大量使用者通路同一個緩存的情況,當然也可以設定緩存不過期,但是不能保證緩存不被清理吧。就是說緩存不過期是在理想情況,但是怎麼沒的,就屬于突發情況了。

第四點,緩存雪崩。就是比如說有1w個使用者現在來請求了,然後艱難的給他們都加上了緩存,然後就把他們的緩存時間設定為半個小時,然後半個小時後,這一萬個請求又來了,但是緩存沒了,這時候又要艱難的從資料庫裡面讀取。

那麼這種情況怎麼解決呢? 最簡單的就是不要設定設定固定的緩存失效數字,可以随機一個數字。但是如果使用者體過大,同樣面臨着某一個時間點大量使用者失效的情況。那麼同樣可以,當拿到使用者緩存的時候,如果時間快到期了,然後給他續時間。

那麼就來舉例子。

需要用到的元件如下:

  1. responseCache 中間件
  2. Miscrosoft.Extensions.Caching.Memory.IMemoryCache MemoryCache
  3. Miscrosoft.Extensions.Caching.Distributed.IDistributedCache 分布式cache
  4. EasyCaching 開源chache元件

記憶體緩存和分布式緩存的差別

  1. 記憶體緩存可以存儲任意的對象
  2. 分布式緩存的對象需要支援序列化
  3. 分布式緩存遠端請求可能失敗(網絡問題,或者遠端服務緩存程序崩潰等),記憶體緩存不會(記憶體緩存沒有網絡問題)

下面是例子:

需要安裝的包:

  1. EasyCaching.Redis
  2. 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);
	}
}
           

看下效果:

第一次請求的時候:

重新整理 .net core 實踐篇————緩存相關[四十二]

給了這個參數,告訴浏覽器,在該段時間内就不要來通路背景了,用緩存就好。

第二次通路:

重新整理 .net core 實踐篇————緩存相關[四十二]

黃色部分的意思該請求沒有發出去,用的是緩存。

感覺這樣挺好的,那麼這個時候就有坑來了。

[ResponseCache(Duration = 6000)]
public IActionResult Pay(string name)
{
	return Content("買買買:"+DateTime.Now+name);
}
           

通路第一次:

重新整理 .net core 實踐篇————緩存相關[四十二]

通路第二次:

重新整理 .net core 實踐篇————緩存相關[四十二]

顯然第二次是有問題的。

因為name 參數變化了,但是結果相同。

這顯然是有問題的,這是用戶端緩存嗎?不是,浏覽器隻要是通路連結發生任何變化的時候就會不使用。

重新整理 .net core 實踐篇————緩存相關[四十二]

可以看到上面實際上去通路了我們的背景的。

那麼應該這樣寫,表示當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 配置中心。