Asp.Net Core中利用過濾器控制Nginx的快取時間

2023-02-10 21:01:40

前言

Web專案中很多網頁資源比如html、js、css通常會做伺服器端的快取,加快網頁的載入速度

一些週期性變化的API資料也可以做快取,例如廣告資源位資料,選單資料,商品類目資料,商品詳情資料,商品列表資料,公共設定資料等,這樣就可以省去很多在伺服器端手動實現快取的操作

最早資源快取大部分都用Expires、Cache-Control或Etag實現的,我們可以在WebServer中統一設定響應頭,或者指定規則單獨設定

以上都是基於Http協定的快取,如今很多WebServer,例如Nginx和阿里二次開發的Tengine,都是自己的一套快取實現,通過獨有的響應頭引數(X-Accel-Expires)來識別控制快取,優先順序是大於Http協定那些的

通常Nginx都是作為代理伺服器,反向代理多臺源伺服器,如果開啟了快取,二次請求到了Nginx就會直接響應給使用者端了,能減輕源伺服器的壓力

本文主要是基於 X-Accel-Expires 來實現快取的,前提是在Nginx中已經設定了Proxy Cache規則

 

Nginx的快取原理

1. 這是資源存取路徑,通過Nginx反向代理多個源伺服器,Nginx中設定了快取,第二次存取到了Nginx就直接返回了,不會再到後面的源伺服器

2. 常見的Http快取響應頭設定有以下幾種,其中Etag和Last-Modified是組合使用的,X-Accel-Expires是Nginx獨有的引數,優先順序高於其他幾個設定,值的單位是秒,0為不生效

Nginx快取識別優先順序如下

3. Nginx實現快取的原理是把Url和相關引數,通過自定義組合作為Key,並使用MD5演演算法對Key進行雜湊,把響應結果存到硬碟上的對應目錄,支援通過命令清除快取

具體可以參考以下文章,非常詳細:

https://www.nginx.com/blog/nginx-high-performance-caching/

https://czerasz.com/2015/03/30/nginx-caching-tutorial/

 

程式碼實現

以下是通過過濾器實現控制該引數,支援在Controller或Action上傳入滑動時間,或者固定時間,靈活控制快取時間

    /// <summary>
    /// 配合nginx快取
    /// </summary>
    [AttributeUsageAttribute(AttributeTargets.Class | AttributeTargets.Method,AllowMultiple = false)]
    public class NginxCacheFilterAttribute : Attribute, IAsyncActionFilter
    {
        /// <summary>
        /// 建構函式
        /// </summary>
        public NginxCacheFilterAttribute() { }
        
        /// <summary>
        /// 固定時間格式正則,例如:00:00 、10:30
        /// <summary>
        static Regex reg = new Regex(@"^(\d{1,2}):(\d{1,2})$",RegexOptions.IgnoreCase);

        /// <summary>
        /// 快取清除固定時間,new string[] { "00:00", "10:00", "14:00", "15:00" }
        /// </summary>
        public string[] MustCleanTimes { get; set; }

        /// <summary>
        /// 快取清除滑動時間,預設 300 (5分鐘)
        /// </summary>
        public int Period { get; set; } = 300;

        /// <summary>
        /// 請求頭變數
        /// </summary>
        const string X_Accel_Expires = "X-Accel-Expires";
        const string ETag = "ETag";
        const string Cache_Control = "Cache-Control";

        /// <summary>
        /// 過濾器執行
        /// </summary>
        /// <param name="context"></param>
        /// <param name="next"></param>
        /// <returns></returns>
        public Task OnActionExecutionAsync(ActionExecutingContext context,ActionExecutionDelegate next)
        {
            //非GET請求,不設定nginx快取頭
            if (context.HttpContext.Request.Method.ToUpper() != "GET") {
                return next.Invoke();
            }
            var response = context.HttpContext.Response;
            //判斷固定時間
            if (MustCleanTimes != null && MustCleanTimes.Length > 0) {
                var nowTime = DateTime.Now;                  //當前時間
                var nowYmd = nowTime.ToString("yyyy-MM-dd"); //當前日期
                List<DateTime> cleanTimes = new List<DateTime>();
                foreach (var time in MustCleanTimes) {
                    if (reg.IsMatch(time) && DateTime.TryParse($"{nowYmd} {time}",out DateTime _date)) {
                        //已超時的推到第二天,例如設定的是00:00,重新整理時間就應該是第二天的00:00
                        if (_date < nowTime)
                            cleanTimes.Add(_date.AddDays(1));
                        else
                            cleanTimes.Add(_date);
                    }
                }
                if (cleanTimes.Count > 0) {
                    var nextTime = cleanTimes.OrderBy(o => o).FirstOrDefault(); //下次重新整理時間
                    var leftSeconds = nextTime.Subtract(nowTime).TotalSeconds;  //下次重新整理剩餘秒數
                    if (leftSeconds >= 0 && leftSeconds < Period)
                        Period = (int)leftSeconds;
                }
            }

            //新增X_Accel_Expires
            if (response.Headers.ContainsKey(X_Accel_Expires)) {
                response.Headers.Remove(X_Accel_Expires);
            }
            response.Headers.Add(X_Accel_Expires,Period.ToString());

            //新增ETag
            if (response.Headers.ContainsKey(ETag)) {
                response.Headers.Remove(ETag);
            }
            response.Headers.Add(ETag,new System.Net.Http.Headers.EntityTagHeaderValue($"\"{DateTime.Now.Ticks.ToString()}\"",true).ToString());

            //移除Cache-Control
            response.Headers.Remove(Cache_Control);

            return next.Invoke();
        }
    }

 具體的使用方式如下:

1. 全域性用法,全域性Api都是設定的預設快取時間,不需要快取的Api在Controller或Action上單獨設定Period=0即可

//在Stratup中全域性新增過濾器       
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(config => {
        config.Filters.Add<NginxCacheFilterAttribute>();
    });
}


/// <summary>
/// 設定滑動時間
/// Period=0為不生效
/// </summary>
/// <returns></returns>
[HttpGet]
[NginxCacheFilter(Period = 0)]
public HttpResponseMessage TestCache1()
{
    return new HttpResponseMessage() { StatusCode = System.Net.HttpStatusCode.OK };
}

 2. 區域性用法

/// <summary>
/// 設定滑動時間
/// 30秒後自動過期
/// </summary>
/// <returns></returns>
[HttpGet]
[NginxCacheFilter(Period = 30)]
public HttpResponseMessage TestCache1()
{
    return new HttpResponseMessage() { StatusCode = System.Net.HttpStatusCode.OK };
}

/// <summary>
/// 設定固定時間
/// 例如:9點第一次請求,一直快取到10點失效,12點第一次請求,一直快取到15點失效
/// </summary>
/// <returns></returns>
[HttpGet]
[NginxCacheFilter(MustCleanTimes = new[] { "10:00","15:00","22:00" })]
public HttpResponseMessage TestCache2()
{
    return new HttpResponseMessage() { StatusCode = System.Net.HttpStatusCode.OK };
}

 

具體效果

1. 我們第一次請求介面,返回200狀態碼,Nginx在響應頭上會返回X-Cache:MISS,代表快取未命中

2. 第二次請求,會返回304狀態碼,Nginx在響應頭上會返回 X-Cache:HIT,代表已經命中快取

3. 我們開啟Chrome偵錯中的Disable Cache,這樣所有請求的請求頭中都會設定 Cache-Control: no-cache,再重新整理下介面看下

發現介面返回200狀態碼,Nginx在響應頭上會返回X-Cache:EXPIRED,說明快取已過期,已從源伺服器返回了資料,也說明通過請求頭設定Cache-Control為no cache是可以跳過快取的

更多含義:

 

高效能用法:

proxy_cache_lock:快取鎖

proxy_cache_lock_timeout:快取鎖過期時間

如果給快取規則設定了proxy_cache_lock,那麼該規則下同時進來多個同一個Key的請求,只會有一個請求被轉發到後面的源伺服器,其餘請求會被等待,直到源伺服器的內容被成功快取

可以配合設定proxy_cache_lock_timeout,設定一個快取鎖的過期時間,這樣其餘請求如果等待超時了,也會被釋放請求到後面的源伺服器

通過這兩個引數的組合使用,可以有效避免同一個請求大量打入時,瞬間壓垮後面的源伺服器

 

原創作者:Harry

原文出處:https://www.cnblogs.com/simendancer/articles/17109964.html