1. 程式人生 > >webapi 自定義緩存實現

webapi 自定義緩存實現

task sys memcached cheat span tps turn 客戶端 穿透

定義一個Filter

  public class MyOutputCacheAttribute : ActionFilterAttribute
    {

        MemoryCacheDefault _cache = new MemoryCacheDefault();



        /// <summary>
        /// 客戶端緩存(用戶代理)緩存相對過期時間
        /// </summary>
        public int ClientCacheExpiration { set; get; }


        /// <summary>
        /// 服務端緩存過期時間
        /// </summary>
        public int ServerCacheExpiration { set; get; }




        /// <summary>
        /// 
        /// </summary>
        /// <param name="clientCacheExpiration">客戶端過期時間。單位為秒,默認為600秒(10分鐘)</param>
        /// <param name="cerverCacheExpiration">服務端過期時間。單位為秒,默認為7200秒(120分鐘)</param>
        public MyOutputCacheAttribute(int clientCacheExpiration = 600, int serverCacheExpiration = 7200)
        {
            this.ClientCacheExpiration = clientCacheExpiration;
            this.ServerCacheExpiration = serverCacheExpiration;
        }



        /// <summary>
        /// 判定是否用緩存中的內容,還是執行action是去取內容
        /// 註:一旦給actionContext.Response賦值了,則會使用這個值來輸出響應
        /// </summary>
        /// <param name="actionContext"></param>
        public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext)
        {

            // ****************************************   穿透緩存 *********************************************
            // 若無緩存,則直接返回
            string cacheKey = getCacheKey(actionContext);
            if (_cache.Contains(cacheKey) == false)
                return;
            if (_cache.Contains(cacheKey + ":etag") == false)
                return;

            // ****************************************   使用緩存  *********************************************
            // 獲取緩存中的etag
            var etag = _cache.Get<string>(cacheKey + ":etag");
            // 若etag沒有被改變,則返回304,
            if (actionContext.Request.Headers.IfNoneMatch.Any(x => x.Tag == etag))  //IfNoneMatch為空時,返回的是一個集合對象
            {
                actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.NotModified); // 響應對象還沒有生成,需要先生成 // 304 not modified
                actionContext.Response.Headers.CacheControl = GetDefaultCacheControl();
                actionContext.Response.Headers.ETag = new EntityTagHeaderValue(etag);
                return;
            }
            else //從緩存中返回響應
            {
                actionContext.Response = actionContext.Request.CreateResponse(); // 響應對象還沒有生成,需要先生成
                // 設置協商緩存:etag
                actionContext.Response.Headers.ETag = new EntityTagHeaderValue(etag); //用緩存中的值(為最新的)更新它
                // 設置user agent的本地緩存
                actionContext.Response.Headers.CacheControl = GetDefaultCacheControl();

                // 從緩存中取中響應內容
                var content = _cache.Get<byte[]>(cacheKey);
                actionContext.Response.Content = new ByteArrayContent(content);
                return;
            }
        }






        /// <summary>
        ///  將輸出保存在緩存中。
        /// </summary>
        /// <param name="actionExecutedContext"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public override async System.Threading.Tasks.Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, System.Threading.CancellationToken cancellationToken)
        {
            // 響應對象已經生成,可以直接調用   actionExecutedContext.Response

            // 設置協商緩存:etag
            EntityTagHeaderValue etag = new EntityTagHeaderValue("\"" + Guid.NewGuid().ToString() + "\"");  // 根據http協議, ETag的值必須用引號包含起來
            actionExecutedContext.Response.Headers.ETag = etag;
            // 設置user agent的本地緩存
            actionExecutedContext.Response.Headers.CacheControl = GetDefaultCacheControl();

            // actionExecutedContext.Response.Headers.Remove("Content-Length"); // 改變了值,它會發生變化。刪除它的話,後續的程序會自動地再計算

            // 保存到緩存
            string cacheKey = getCacheKey(actionExecutedContext.ActionContext);
            var contentBytes = await actionExecutedContext.Response.Content.ReadAsByteArrayAsync();

            _cache.Add(cacheKey + ":etag", etag.Tag, DateTimeOffset.Now.AddSeconds(this.ServerCacheExpiration));
            _cache.Add(cacheKey, contentBytes, DateTimeOffset.Now.AddSeconds(this.ServerCacheExpiration));

        }





        /// <summary>
        /// 默認的用戶代理本地緩存配置,10分鐘的相對過期時間
        /// 對應響應header中的 Cache-Control
        /// 這裏設置裏面的子項max-age。如:Cache-Control: max-age=3600
        /// </summary>
        /// <returns></returns>
        CacheControlHeaderValue GetDefaultCacheControl()
        {
            CacheControlHeaderValue control = new CacheControlHeaderValue();
            control.MaxAge = TimeSpan.FromSeconds(this.ClientCacheExpiration);  // 它對應響應頭中的 cacheControl :max-age=10項

            return control;
        }




        /// <summary>
        /// 
        /// </summary>
        /// <param name="actionContext"></param>
        /// <returns></returns>
        string getCacheKey(System.Web.Http.Controllers.HttpActionContext actionContext)
        {
            string cacheKey = null;

            cacheKey = actionContext.Request.RequestUri.PathAndQuery; // 路徑和查詢部分,如: /api/values/1?ee=33&oo=5

            // 對路徑中的參數進行重排,升序排列

            //string controllerName = actionContext.ControllerContext.ControllerDescriptor.ControllerName;
            //string actionName = actionContext.ActionDescriptor.ActionName;

            //if (actionContext.ActionArguments.ContainsKey("id") == false)
            //{
            //    cacheKey = controllerName + "/" + actionName;
            //}
            //else
            //{
            //    string id = actionContext.ActionArguments["id"].ToString();
            //    cacheKey = controllerName + "/" + actionName + "/" + id;
            //}

            return cacheKey;
        }


    }

上面的這段代碼嚴格遵循RFC2626中定義的緩存協議。

定義一個服務器端緩存實現

這裏采用MemoryCache,也可以采用memcached, redis之類的。

public class MemoryCacheDefault
{
    private static readonly MemoryCache _cache = MemoryCache.Default;

    public void RemoveStartsWith(string key)
    {
        lock (_cache)
        {
            _cache.Remove(key);
        }
    }

    public T Get<T>(string key) where T : class
    {
        var o = _cache.Get(key) as T;
        return o;
    }



    [Obsolete("Use Get<T> instead")]
    public object Get(string key)
    {
        return _cache.Get(key);
    }





    public void Remove(string key)
    {
        lock (_cache)
        {
            _cache.Remove(key);
        }
    }

    public bool Contains(string key)
    {
        return _cache.Contains(key);
    }

    public void Add(string key, object o, DateTimeOffset expiration, string dependsOnKey = null)
    {
        var cachePolicy = new CacheItemPolicy
        {
            AbsoluteExpiration = expiration
        };

        if (!string.IsNullOrWhiteSpace(dependsOnKey))
        {
            cachePolicy.ChangeMonitors.Add(
                _cache.CreateCacheEntryChangeMonitor(new[] { dependsOnKey })
            );
        }
        lock (_cache)
        {
            _cache.Add(key, o, cachePolicy);
        }
    }


    public IEnumerable<string> AllKeys
    {
        get
        {
            return _cache.Select(x => x.Key);
        }
    }
}

將filter應用到action中

    public class ValuesController : ApiController
    {

        [MyOutputCache(10)]
        public string Get(int id)
        {
            return "value" + id.ToString();
        }
    }

webapi 自定義緩存實現