Example implementation of interface cache for. NetCore

  • 2021-11-24 01:14:55
  • OfStack

1. Problem: We definitely use the cache function when we do development at ordinary times. The general writing method is to read the cache in the required business code and judge whether it exists. If it does not exist, read the database and then set the cache. However, if we have many places where caching is used in business, we need to write code about caching everywhere, which will cause a lot of duplicate code, and at the same time, it is not conducive to the subsequent development and maintenance of business intrusion.

2, 1-like solution is to extract the cache function, and then call the cache where it needs to be used. This does reduce a lot of duplicate code, but there will still be caching function common to the whole project to invade business code. What can we do to completely extract caching function and achieve zero intrusion of business code?

3. Since we cache the business data of the interface, why can't we cache the whole interface directly, that is, cache the data returned by the whole interface? At the same time, to achieve zero business intrusion, do we think of reflection and characteristics? Yes, we use ActionFilterAttribute, and ActionFilterAttribute is nothing more than OnActionExecuting (triggered before executing the action method), OnActionExecuted (triggered after executing the action method), OnResultExecuting (triggered before executing the operation result) and OnResultExecuted (triggered after executing the operation result). I believe many small partners have used it, so we will not elaborate here. Then our current solution is to judge whether there is cache in OnActionExecuting (trigger before executing action method), and if there is cache, we will not execute interface business and return data directly. There is also a problem, 1 interface will have input parameters, and the output data of different input parameters is different (for example, I have a paging interface, and the page parameters are different, and the results are different). How to solve this? We only need to piece together all the parameters of the interface, and then MD5 encrypts it into a string and uses it as a cached key, so even if the parameters are different from one interface, different key will be obtained.

4. Don't talk too much nonsense, just go to the code.


public class ApiCache : ActionFilterAttribute
 {
  /// <summary>
  /// Header Participate in cache validation 
  /// </summary>
  public bool SignHeader = false;
  /// <summary>
  ///  Cache valid time (minutes) 
  /// </summary>
  public int CacheMinutes = 5;/// <summary>
  /// 
  /// </summary>
  /// <param name="SignHeader">Header Whether to participate in the request body signature </param>
  /// <param name="CacheMinutes"> Cache valid time (minutes) </param>
  public ApiCache(bool SignHeader = false, int CacheMinutes = 5)
  {
   this.SignHeader = SignHeader;
   this.CacheMinutes = CacheMinutes;
  }


  public override void OnActionExecuting(ActionExecutingContext filterContext)
  {
   // Request body signature 
   string cacheKey = getKey(filterContext.HttpContext.Request);
   // Query cache according to signature 
   string data = CsRedisHepler.Get(cacheKey);
   if (!string.IsNullOrWhiteSpace(data))
   {
    // Set the return information if there is cache 
    var content = new Microsoft.AspNetCore.Mvc.ContentResult();
    content.Content = data;
    content.ContentType = "application/json; charset=utf-8";
    content.StatusCode = 200;
    filterContext.HttpContext.Response.Headers.Add("ContentType", "application/json; charset=utf-8");
    filterContext.HttpContext.Response.Headers.Add("CacheData", "Redis");
    filterContext.Result = content;
   }
  }

  public override void OnActionExecuted(ActionExecutedContext filterContext)
  {
   base.OnActionExecuted(filterContext);
  }

  public override void OnResultExecuting(ResultExecutingContext filterContext)
  {
   base.OnResultExecuting(filterContext);
  }

  public override void OnResultExecuted(ResultExecutedContext filterContext)
  {
   if (filterContext.HttpContext.Response.Headers.ContainsKey("CacheData")) return;
   // Get the cache key
   string cacheKey = getKey(filterContext.HttpContext.Request);
   var data = JsonSerializer.Serialize((filterContext.Result as Microsoft.AspNetCore.Mvc.ObjectResult).Value);
   // If the cache null Set a shorter expiration time (in this case, prevent cache penetration) 
   var disData = JsonSerializer.Deserialize<Dictionary<string, object>>(data);
   if(disData.ContainsKey("data") && disData["data"]==null)
   {
    CacheMinutes = 1;
   }
   CsRedisHepler.Set(cacheKey, data, TimeSpan.FromMinutes(CacheMinutes));
  }
  /// <summary>
  ///  Request body MDH Signature 
  /// </summary>
  /// <param name="request"></param>
  /// <returns></returns>
  private string getKey(HttpRequest request)
  {
   var keyContent = request.Host.Value + request.Path.Value + request.QueryString.Value + request.Method + request.ContentType + request.ContentLength;
   try
   {
    if (request.Method.ToUpper() != "DELETE" && request.Method.ToUpper() != "GET" && request.Form.Count > 0)
    {
     foreach (var item in request.Form)
     {
      keyContent += $"{item.Key}={item.Value.ToString()}";
     }
    }
   }
   catch (Exception e)
   {

   }
   if (SignHeader)
   {
    var hs = request.Headers.Where(a => !(new string[] { "Postman-Token", "User-Agent" }).Contains(a.Key)).ToDictionary(a => a);
    foreach (var item in hs)
    {
     keyContent += $"{item.Key}={item.Value.ToString()}";
    }
   }
         //md5 Encryption 
   return CryptographyHelper.MD5Hash(keyContent);
  }   

redis is used here, and others can be selected. The code is simple and there is no adaptation, so we only need to add [ApiCache (CacheMinutes = 1)] characteristics to the interface that uses cache, and the parameters can also be customized according to our own business needs.

5. About the three mountains of cache: cache penetration, cache breakdown and cache avalanche, there are a lot of information to see on this network, so here is only a simple introduction and solution.

Cache Penetration: When accessing a non-existent key, the request directly requests the database through the cache. For example, there is an interface that is paged now, and then when the client requests the interface, it gives the pageindex parameter so large that it is impossible for the interface to have so many pages of data, and every request will go through the cache to check the database. If someone intentionally attacks the interface, it will put great pressure on the database and even hang up. Of course, here we must also do 1 business parameter verification, such as the number of pages can not exceed the number of such, in short, can not trust the client over the parameters.

Solution: The simplest and most effective solution is to set 1 value as the cached value of null (the expiration time of this value should be as short as possible) when the data cannot be found in the database, so as to avoid malicious attacks. The other is to use Bloom filter.

The solution we use here is the first to set the null value, which is annotated in the above code. However, it is best to have a return specification for interfaces here. For example, each interface returns fixed values: message, code and data, so we only need to judge whether data is empty to set the expiration time.

Cache Breakdown: An key with a high number of visits expires, causing all requests to hit the database.

Solution: Set the access volume of Gaode key to never expire and use mutex. We use here to set key will never expire, the specific implementation is to add a field of whether to expire from the outside, and then judge whether to set the expiration time according to this field. At the same time, you can write a timed task to update the key value set to never expire.

Cache avalanche: Multiple high-traffic key expire at the same time at a certain time.

Solution: When setting the expiration time, distribute the expiration time setting of each key. In the above code, change the CacheMinutes field to the expiration time range from. . . To. . . And then the expiration time of key takes a random value from the range.

Of course, the solutions mentioned here are only commonly used by individuals, and other solutions can also be used.


Related articles: