Implementation example of interface cache in. NETCORE

Time:2021-1-14

1. Question:We usually use the cache function when we do development. The general writing method is to read the cache in the required business code, judge whether it exists or not, read the database and then set the cache. However, if we have many places where the business is useful for caching, we need to write code about caching in every place, which will cause a lot of duplicate code, and at the same time, it will invade the business, which is not conducive to the subsequent development and maintenance.

2. General solutionsIt is to extract the cache function, and then call it where the cache is needed. This does reduce a lot of duplicate code, but there will still be the common cache function of the whole project invading the business code. So how can we extract the cache function completely to achieve zero invasion of the business code?

3. Since we cache the business data of the interface, why can’t we cache the entire interface directly, that is, the data returned by the entire interface? At the same time, to achieve zero invasion of business, do we think of reflection and features? Yes, we use actionfilterattribute. About actionfilterattribute, there are only four methods: onactionexecuting, onactionexecuted, onresultexecuting and onresultexecuted I’ve used them all, so I won’t elaborate here. Our solution now is to determine whether there is a cache in onactionexecuting (trigger before executing action method). If there is, we will not execute interface business and return data directly. There is another problem. Generally, interfaces have input parameters, and different input parameters have different output data (for example, if I have a paging interface, the page parameters are different, and the results are different). How can I solve this problem? We just need to piece together all the parameters of the interface, and then MD5 encrypts them into a string, which is used as the cache key. Even if the same interface and parameters are different, we will get different keys.

4. No more nonsense, just code.

public class ApiCache : ActionFilterAttribute
 {
  /// <summary>
  ///Does the header participate in cache validation
  /// </summary>
  public bool SignHeader = false;
  /// <summary>
  ///Cache effective time (minutes)
  /// </summary>
  public int CacheMinutes = 5;/// <summary>
  /// 
  /// </summary>
  ///< param name = "signheader" > whether the header participates in the signature of the request body < / param >
  ///< param name = "cacheminutes" > cache effective 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 based on signature
   string data = CsRedisHepler.Get(cacheKey);
   if (!string.IsNullOrWhiteSpace(data))
   {
    //If there is a cache, set the return information
    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 cache key
   string cacheKey = getKey(filterContext.HttpContext.Request);
   var data = JsonSerializer.Serialize((filterContext.Result as Microsoft.AspNetCore.Mvc.ObjectResult).Value);
   //If the cache is null, set a shorter expiration time (here is to 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. You can also choose others. The code is simple, and no adaptation is made. In this way, we only need to add the [apicache (cacheminutes = 1)] feature to the interface using cache. The parameters can also be customized according to our own business requirements.

5. About the three mountains of cache: cache penetration, cache breakdown and cache avalanche, there are a lot of information on the Internet. Here is a simple introduction and solution.

Cache penetration: when accessing a nonexistent key, the request will directly request the database through the cache. For example, an interface is now paged. When the client requests the interface, the PageIndex parameter is given too large. When the interface is too large to have so many pages of data, every request will go through the cache to look up the database. If someone intentionally attacks the interface, it will cause great pressure on the database and even hang up. Of course, we must also check some business parameters, such as the number of items per page. In short, we can’t trust the parameters passed by the client.

Solution: the simplest and most effective solution is to set a cache value with null value (the expiration time of the value should be as short as possible) when no data can be found in the database, so as to avoid malicious attacks. Another is to use a bloom filter.

The solution we’re using here is the first to set the null value, which is annotated in the code above. However, the best interface here has a return specification. For example, each interface returns a fixed value: message, code and data. Then we only need to determine whether the data is empty to set the expiration time.

Cache breakdown: the expiration of a key with high access leads to all requests hitting the database.

Solution: set high access key to never expire and use mutex. Let’s just set the key never to expire. The specific implementation is to add a field to pass in from the outside, and then judge whether to set the expiration time according to the field. At the same time, you can write a scheduled task to update the key value set to never expire.

Cache avalanche: multiple high access keys expire at the same time.

Solution: when setting the expiration time, distribute the expiration time of each key. In the above code, change the cacheminutes field to the expiration time range from… To… Then the expiration time of the 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.

This article about. NETCORE’s interface cache is introduced here. For more related. NETCORE’s interface cache content, please search previous articles of developer or continue to browse the following related articles. I hope you can support developer more in the future!