.Net MVC4 被坑心得 (九) WebApi下的数据级缓存

    使用webapi做rest的服务接口时,有些读取数据表的操作,数据本身变化不频繁,但是访问量却不小,比如频道分类,地市选择信息等等等等。这时,必然想到使用缓存。

    在普通controller下,由于controller实现了一堆接口,其中包括了很多的filter,所以,可以轻松的实现缓存,如果只需要页面级别缓存,则大可以使用之前提到的OutputCacheAttribute,轻松搞定。

    但是,在webapi下,一切都简化了,apicontroller只实现了IHttpController, IDisposable两个接口,其他什么都没留下。所以,不要指望重写OnActionExecuting、OnActionExecuted之类的事件来向action(webapi下是不是还能叫做action不清楚)执行前后添加代码了。

    怎么办呢。考虑写个页面级别的缓存。由于没法默认在方法前后添加代码,我们将方法封装成匿名方法,用一个专门的调用类来调用,并作出调用前判断和调用后缓存等处理。看看效果:


   首先定义一个静态类,其中的静态属性用于存储缓存数据:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace XXX.Models
{
    public static class CacheContainer
    {
        private static Hashtable Data = new Hashtable();

        public static void SetData(string key, object value)
        {
            if (Data[key] != null)
            {
                if (value == null)
                {
                    Data.Remove(key);
                }
                else
                {
                    Data[key] = value;
                }
            }
            else
            {
                Data.Add(key, value);
            }
        }

        public static T GetData<T>(string key)
        {
            return (T)Data[key];
        }
    }

    public class DataCacheUnit
    {
        public object CacheData { get; set; }
        public DateTime LastCache { get; set; }
    }
}



就是一个简单的全局数据存取。用哈希表是便于通过唯一标示来快速找到缓存的数据。这里是使用method级别的标示符,并且不带参数。如果需要根据参数来缓存数据,请在计算唯一标示时加上参数。

另外定义了一种用于存储的数据类型,也就是哈希表中的值,包含上次缓存时间和缓存内容。


然后,我们定义一个用于method的attribute,用于标识需要缓存的action(姑且认为还是action吧)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http.Controllers;
using System.Web.Mvc;

namespace XXX.Filter
{
    [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
    public class DataCacheAttribute : Attribute
    {
        public int CacheSeconds { get; set; }

        public DataCacheAttribute()
        {
            CacheSeconds = 60;
        }

        public DataCacheAttribute(int CacheSeconds)
        {
            this.CacheSeconds = CacheSeconds;
        }
    }

}

这个attribute非常非常简单,只有一个属性,就是缓存时间。

接下来是重点了,定义一个调用类,来代理我们的action(method)调用实现,并在调用之前和之后做些事情:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using XXX.Models;

namespace XXX.Filter
{
        /// <summary>
        /// 调用操作的工具类
        /// </summary>
    public static class ActionInvoker
    {
        public static T Invoke<T>(object target,string actionName, Func<T> method)
        {
            var targetType = target.GetType();
            var targetInfo = targetType.GetMethod(actionName);

            if (targetInfo.IsDefined(typeof(DataCacheAttribute), true))
            {
                var attr = targetInfo
                    .GetCustomAttributes(typeof(DataCacheAttribute), true)
                    .OfType<DataCacheAttribute>()
                    .FirstOrDefault();

                var key = targetType.Name + "-" + actionName;

                DataCacheUnit data = CacheContainer.GetData<DataCacheUnit>(key);
                if (data != null && DateTime.Now < data.LastCache.AddSeconds(attr.CacheSeconds))
                {
                    return (T)data.CacheData;
                }
                else
                {
                    var rsl = method.Invoke();
                    CacheContainer.SetData(key, new DataCacheUnit
                    {
                        CacheData = rsl,
                        LastCache = DateTime.Now
                    });
                    return rsl;
                }
            }
            else
            {
                return method.Invoke();
            }

        }
    }
}

这次,没写注释,含义是,首先判断该method(通过target和acitonName传递,target为class的对象,actionName为这个class的method名称)是不是包含DataCache的特性,如果不包含,直接返回method.Invode()。否则,进行判断,如果存在缓存数据且没有过期,则返回缓存的数据,否则,执行 method.Invode(),并将结果缓存下来,然后返回。

其中,使用

 targetType.Name + "-" + actionName;

作为唯一标示,并不包含参数。


这个结构搭建好之后,使用起来不是很复杂:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Mvc;
using XXX.Models.Results;
using XXX.Models;
using XXX.Filter;

namespace XXX.Controllers
{
    
    public class UnitController : ApiController
    {
        [DataCache]
        public IEnumerable<UnitResultItem> Get()
        {
            return ActionInvoker.Invoke(this, "Get", () =>
            {
                List<UnitResultItem> rsl = new List<UnitResultItem>();

                using (ProtectRightsEntities db = new ProtectRightsEntities())
                {
                    var units = db.XXXunit;
                    foreach (var unit in units)
                    {
                        UnitResultItem item = new UnitResultItem
                        {
                            id = unit.id,
                            logo = unit.logo,
                            name = unit.name
                        };
                        rsl.Add(item);
                    }
                }

                return rsl;
            });
        }
    }
}

这里是一个普通的apicontroller的get方法,无参数(需要参数的话,自己修改代码。但是不建议使用带参数的缓存,这样参数必须限制在有限的几个内,否则换存量会很大)。 get方式返回一个UnitResultItem类型的实体列表。这里,将之前写的代码完全封装成一个匿名函数,(()=>{}部分),然后,将此匿名函数用ActionInvoker.Invoke代为调用,传参时,第一个传递this,第二个传递当前action名(method),第三个就是封装起来的匿名函数。在匿名函数中,一切原样返回,而整个api的action变成了一句return语句,return ActionInvoker.Invoke(this,"Get",()=>{ /*处理过程*/ return rsl;});


试用下,效果不错,默认是缓存1分钟,改变缓存时间,只需要在[DataCache]修饰的时候,加上参数[DataCache(120)]


昨天写到这里就完了。

今天继续,发现这货还是有很多问题的:


1是确实不支持参数,那么对于可以翻页的接口,明显不能使用了。

2是多线操作静态类,没有做任何互斥,可能产生数据访问冲突。

3是没有对和缓存数量的上限限制,有可能造成大量内存占用。

4是没有对长期不用的缓存数据进行及时清理,可能产生永久性的垃圾数据。


基于以上几点,做了一些修改,如下:



首先是用于存储的存储体:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Web;

namespace XXX.Models
{
    public static class CacheContainer
    {
        private static Hashtable Data = new Hashtable();
        private static Mutex DataLock = new Mutex();
        private const int MaxCacheCount = 3000;

        public static bool SetData(string key, object value, int seconds)
        {
            bool successLock = DataLock.WaitOne(30000);
            if (!successLock)
            {
                return false;
            }


            if (Data[key] != null)
            {
                if (value == null)
                {
                    Data.Remove(key);
                }
                else
                {
                    Data[key] = new DataCacheUnit
                    {
                        CacheData = value,
                        ExpTime = DateTime.Now.AddSeconds(seconds)
                    };
                }
            }
            else
            {
                if (Data.Count >= MaxCacheCount)
                {
                    DataLock.ReleaseMutex();
                    return false;
                }

                Data.Add(key, new DataCacheUnit
                {
                    CacheData = value,
                    ExpTime = DateTime.Now.AddSeconds(seconds)
                });
            }
            DataLock.ReleaseMutex();
            return true;
        }

        public static T GetData<T>(string key)
        {
            if (Data[key] == null)
            {
                return default(T);
            }

            if (DateTime.Now > ((DataCacheUnit)Data[key]).ExpTime)
            {
                return default(T);
            }
            else
            {
                return (T)((DataCacheUnit)Data[key]).CacheData;
            }
        }

        public static bool ClearUp()
        {
            bool successLock = DataLock.WaitOne(30000);
            if (!successLock)
            {
                return false;
            }

            DateTime now = DateTime.Now;
            List<string> delKeyList = new List<string>();
            foreach (string key in Data.Keys)
            {
                if (now > ((DataCacheUnit)Data[key]).ExpTime)
                {
                    delKeyList.Add(key);
                }
            }

            foreach (string key in delKeyList)
            {
                Data.Remove(key);
            }

            DataLock.ReleaseMutex();
            return true;
        }

        private class DataCacheUnit
        {
            public object CacheData { get; set; }
            public DateTime ExpTime { get; set; }
        }
    }

}


这里首先增加了一个互斥量,用于线程间互斥。由于是hash表,读取无需互斥,写操作需要互斥,这里在清理和设置缓存时加了锁,如果锁在超时时间内未获取,则直接返回,不进行缓存。其次增加的是缓存数量上限,在新增操作时如果已达上限,则放弃缓存,返回失败,当然之前要先释放信号量。最后是增加了定时调用的清理操作ClearUp() ,清理过期的数据缓存,这个需要又全局定时器定时执行,在Global.asax中实现(见下面代码)。数据结构也发生了一些改变,set和get方法均是直接对cachedata进行操作,exptime为过期时间,已经整合进这个数据结构中,判断也全部封装在这个类中了。 如果调用get,遇到了过期的数据,这里并不直接做删除处理,而是直接返回null,因为外部调用者很可能接下来会做缓存赋值set操作。


using System;
using System.Collections.Generic;
using System.Linq;
using System.Timers;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using XXX.Models;

namespace XXX
{
    // 注意: 有关启用 IIS6 或 IIS7 经典模式的说明,
    // 请访问 http://go.microsoft.com/?LinkId=9394801

    public class WebApiApplication : System.Web.HttpApplication
    {
        Timer tmClearCache = new Timer(600000);

        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            WebApiConfig.Register(GlobalConfiguration.Configuration);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            tmClearCache.Elapsed += tmClearCache_Elapsed;
            tmClearCache.Start();
            
        }

        void tmClearCache_Elapsed(object sender, ElapsedEventArgs e)
        {
            CacheContainer.ClearUp();
        }


    }
}


这里定时为10分钟执行一次清理。


接下来是Invoker,也做了一些修改,增加了一些可选参数:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using XXX.Models;
using System.Reflection;
using System.Text;

namespace XXX.Filter
{
        /// <summary>
        /// 调用操作的工具类
        /// </summary>
    public static class ActionInvoker
    {
        public static T Invoke<T>(object target,string actionName, Func<T> method, object @params = null, bool cancel = false)
        {
            if (cancel)
            {
                return method.Invoke();
            }

            var targetType = target.GetType();
            var targetInfo = targetType.GetMethod(actionName);

            if (targetInfo.IsDefined(typeof(DataCacheAttribute), true))
            {
                var attr = targetInfo
                    .GetCustomAttributes(typeof(DataCacheAttribute), true)
                    .OfType<DataCacheAttribute>()
                    .FirstOrDefault();

                StringBuilder sbKey = new StringBuilder();
                sbKey.Append(targetType.Name);
                sbKey.Append("-");
                sbKey.Append(actionName);

                if (@params != null)
                {
                    var pros = @params.GetType().GetProperties();
                    foreach (var pro in pros.OrderBy(item => item.Name))
                    {
                        var value = pro.GetValue(@params);
                        if (value != null)
                        {
                            sbKey.Append("-");
                            sbKey.Append(pro.Name);
                            sbKey.Append("-");
                            sbKey.Append(value.ToString());
                        }
                    }
                }

                string key = sbKey.ToString();

                T data = CacheContainer.GetData<T>(key);
                if (data != null)
                {
                    return (T)data;
                }
                else
                {
                    var rsl = method.Invoke();
                    
                    CacheContainer.SetData(key, rsl, attr.CacheSeconds);
                    return rsl;
                }
            }
            else
            {
                return method.Invoke();
            }

        }
    }
}


这里的@params为object类型的参数列表(也可以做成数组类型或者键值对类型等),是可选的,默认为null。 cancel为取消缓存标记,可以传递简单的逻辑表达式来选择是否跳过缓存,以适应同一个action下不同的缓存策略。 这里通过反射获取@params的全部属性名和值,然后拼成标识字串key,用于存取缓存数据。 这里,调用method.Invoke()时,也可以传递参数列表。只是由于我使用()=>{}形式的匿名函数,函数执行时上下文不用重新定义,所有参数均可从上下文获得,所以这里没有将参数传给method,参数列表在这里仅仅作为计算标识符key用。


下面我们看看调用,为了体现这货的用法,我选了个比较复杂的apicontroller的get方法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using XXX.Models.Results;
using XXX.Models;
using System.Web;
using System.Text.RegularExpressions;
using XXX.Common;
using XXX.Filter;

namespace XXX.Controllers
{
    public class ComplaintController : ApiController
    {
        XXXEntities prdb = new XXXEntities();

        [DataCache]
        public ComplaintListResult Get()
        {
            ComplaintListResult rsl = new ComplaintListResult();

            var request = HttpContext.Current.Request;

            int pageIndex = 1;
            if (request.QueryString["pindex"] != null)
            {
                try
                {
                    pageIndex = int.Parse(request.QueryString["pindex"]);
                }
                catch
                {
                    rsl.success = false;
                    rsl.error_msg = "参数不正确";
                    return rsl;
                }
            }

            int pageSize = 20;
            if (request.QueryString["psize"] != null)
            {
                try
                {
                    pageSize = int.Parse(request.QueryString["psize"]);
                }
                catch
                {
                    rsl.success = false;
                    rsl.error_msg = "参数不正确";
                    return rsl;
                }
            }

            bool selStatus = (request.QueryString["status"] != null);
            bool selUser = (request.QueryString["phone"] != null);
            if (selStatus && selUser)
            {
                rsl.success = false;
                rsl.error_msg = "参数过多";
                return rsl;
            }

            int status = 0;
            if (selStatus)
            {
                try
                {
                    status = Convert.ToInt32(request.QueryString["status"]);
                }
                catch
                {
                    rsl.success = false;
                    rsl.error_msg = "参数不正确";
                    return rsl;
                }
            }

            string phone = null;
            if (selUser)
            {
                phone = request.QueryString["phone"];
            }

            return ActionInvoker.Invoke<ComplaintListResult>(this, "Get", () =>
            {
                int allCount = 0;
                if (selStatus)
                {
                    allCount = prdb.XXX.Count(item => item.ispassed == 0 && item.status == status && (item.visible ?? false));
                }
                else if (selUser)
                {
                    allCount = prdb.XXX.Count(item => item.user_phone == phone);
                }
                else
                {
                    allCount = prdb.XXX.Count(item => item.ispassed == 0 && (item.visible ?? false));
                }
                rsl.all_count = allCount;

                //基于0based的pageindex来计算
                pageIndex--;

                if (pageIndex < 0)
                {
                    pageIndex = 0;
                }

                if (pageSize < 1)
                {
                    pageSize = 1;
                }

                if (pageSize * pageIndex >= allCount)
                {
                    pageIndex = (allCount - 1) / pageSize;
                }

                int startIndex = pageIndex * pageSize;
                int endIndex = startIndex + pageSize - 1;
                if (endIndex > allCount - 1)
                {
                    endIndex = allCount - 1;
                }

                rsl.all_list = new List<ComplaintListResult.ComplaintListItem>();
                IQueryable<XXX> prelist = null;
                if (selStatus)
                {
                    prelist = prdb.XXX.Where(item => item.ispassed == 0 && item.status == status && (item.visible ?? false));
                }
                else if (selUser)
                {
                    prelist = prdb.XXX.Where(item => item.user_phone == phone);
                }
                else
                {
                    prelist = prdb.XXX.Where(item => item.ispassed == 0 && (item.visible ?? false));
                }

                if (prelist.Any())
                {
                    var all_list = prelist.OrderByDescending(item => item.subtime).Skip(startIndex).Take(endIndex - startIndex + 1);
                    foreach (var i in all_list)
                    {
                        ComplaintListResult.ComplaintListItem i2 = new ComplaintListResult.ComplaintListItem
                        {
                            id = i.id,
                            summary = i.summary,
                            status = i.status,
                            subtime = (i.subtime == null) ? "" : ((DateTime)i.subtime).ToString("yyyy-MM-dd HH:mm:ss"),
                            title = i.title,
                            target = i.target,
                            user_name = i.user_name,
                            comment_count = i.comment_count ?? 0,
                            is_hot = i.comment_count >= CommonDefine.HotCountLimit
                        };
                        if (i.status >= 1 && i.unit_id != null)
                        {
                            i2.unit_name = i.XXXunit.name;
                            i2.unit_logo = i.XXXunit.logo;
                        }
                        rsl.all_list.Add(i2);
                    }
                }

                rsl.success = true;
                return rsl;
            }, new { pindex = pageIndex, psize = pageSize, status = (selStatus) ? (int?)status : null }, selUser);

        }

    }
}

这里返回的rsl,是XXX.Models.Results命名空间下定义的一个类,如下:

namespace XXX.Models.Results
{
    public class ComplaintListResult
    {
        public bool success;
        public string error_msg;
        public class ComplaintListItem
        {
            public int id;
            public string summary;
            public string user_name;
            public string subtime;
            public int? status;
            public string title;
            public string target;
            public int comment_count;
            public bool is_hot;
            public string unit_name;
            public string unit_logo;
        }
        public int all_count;
        public List<ComplaintListItem> all_list;
    }
}

看看参数,这里涉及两个绝对可选参数和两个可选但是互斥参数,绝对可选参数为pindex表示当前页码,默认为0;psize表示页面大小,默认是20; 互斥可选参数为phone和status,这两个可以都没有,但是如果有,status必须是int型,同时phone和status不能同时存在。status和phone均是筛选条件,status存在时为筛选出状态码为status的数据,并分页,phone存在时,表示筛选出所有phone的值为传入的phone的所有数据。 究其用途,status较少,大约只有3-4种,但是phone与用户一一绑定,是海量的。所以,我们希望,对不存在status和phone以及只存在status的情况(这里pindex和psize可以任意,都会缓存)做出缓存,而对提供了phone的不做任何缓存。

具体流程可以不用特别关心,只需关心return ActionInvoker.Invoke()这一句。后面添加了两个可选参数,一个是匿名类型的object,用于传递参数,这里传递pindex,psize和status三个参数,这三个参数都可能是null,如果是null,从invoker的处理代码可以看到,不会添加作为key的标识。 但是也只是这3个字段,如果phone不为null,也就是selUser为true,则不作任何缓存。所以这里在参数列表对象后面又添加了一个bool类型的取消缓存标记selUser。告诉Invoker如果有phone参数提供,则不作缓存。


不妨亲自一试。代码表直接粘,直接粘是跑8起来滴。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值