Cache实战

JerryXia 发表于 , 阅读 (2,061)

开始

做过Web站点性能优化的人,应该都用过缓存这种技术。而在这篇文章中,我所说的Cache是狭义的,仅仅指的是Web站点开发使用到的ASP.NET的Cache,是使用HttpRuntime.Cache访问到的那个Cache,而不是其它的缓存技术或广义的缓存。

介绍

ASP.NET本身提供了一个强大的、便于使用的缓存机制,用于将需要大量服务器资源来创建的对象存储在内存中。缓存这些类型的资源会大大改进应用程序的性能。缓存实例是每个应用程序专用的,其生存期依赖于应用程序的生存期,重新启动应用程序后,将重新创建Cache对象。

Cache类的功能

Cache类提供了强大的功能,允许您自定义如何缓存项以及将它们缓存多长时间,这些功能一点都不比Memcached、Redis弱。

  • 缓存项的移除优先级。当缺乏系统内存时,缓存会自动移除很少使用的或优先级较低的项以释放内存。该技术也称为清理,这是缓存确保过期数据不使用宝贵的服务器资源的方式之一。当执行清理时,您可以指示Cache给予某些项比其他项更高的优先级。若要指示项的重要性,可以在使用Add或Insert方法添加项时指定一个CacheItemPriority枚举值。
  • 缓存项的过期时间。ASP.NET支持二种缓存项的过期策略:绝对过期和滑动过期。当使用Add或Insert方法将项添加到缓存时,您还可以建立项的过期策略。您可以通过使用DateTime值指定项的确切过期时间(绝对过期时间),来定义项的生存期。也可以使用TimeSpan值指定一个弹性过期时间,弹性过期时间允许您根据项的上次访问时间来指定该项过期之前的运行时间。一旦项过期,便将它从缓存中移除。试图检索它的值的行为将返回null,除非该项被重新添加到缓存中。
  • 缓存项的依赖。ASP.NET允许您根据外部文件、目录(文件依赖项)或另一个缓存项(键依赖项)来定义缓存项的有效性。如果具有关联依赖项的项发生更改,缓存项便会失效并从缓存中移除。您可以使用该技术在项的数据源更改时从缓存中移除这些项。

Cache类的使用

在程序中我们可以通过以下几种方式访问到Cache对象

System.Web.Caching.Cache c0 = new AccountController().HttpContext.Cache;
System.Web.Caching.Cache c1 = new System.Web.UI.Page().Cache;
System.Web.Caching.Cache c2 = System.Web.HttpContext.Current.Cache; // new HttpContext().Cache
System.Web.Caching.Cache c3 = System.Web.HttpRuntime.Cache;

c0, c1, c2对应的方式其实都是c3的快捷方式,跟踪ASP.NET的源代码最终都会定位到这个System.Web.HttpRuntime.Cache,这个Cache类不能在ASP.NET应用程序外使用。它是为在ASP.NET中用于为Web应用程序提供缓存而设计和测试的。对于其他类型的应用程序,如控制台应用程序或Windows窗体应用程序,则可以使用高版本中的ObjectCache类。

我们使用Cache时,一般使用三个操作:读、写、删除,分别是调用Get、Add/Insert、Remove这几个方法。

说下读/写操作,平时我们一般为了贪图方便,会使用它的索引器,我们来看下源代码:

public object this[string key]
{
    [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
    get
    {
        return this.Get(key);
    }
    [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
    set
    {
        this.Insert(key, value);
    }
}

跟一些核心数据结构是KV形式的类一样,它的索引器其实是在调用内部的Get和Insert方法。作为一个强迫症患者,我一般都会
用Get、Insert来替换索引器的使用,虽然少一次的Call调用其实可以忽略不计。

这里要注意的点是:如果Key之前不存在,则调用Add和Insert是一样的效果;如果相应的Key已经存在,调用Add方法时,你这次的Value则不会覆盖原有的缓存项的值,而调用Insert方法,则永远会覆盖之前的值。

MSDN上对Cache的介绍中,描述Insert有几个重载方法,重载方法的最后两个中有一个难得的优点:缓存项的移除通知。我们利用Cache的这个特性可以做一些事情。

public void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemUpdateCallback onUpdateCallback);
       
public void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback);

场景一:数据计数器

在很多系统中都会有计数器的功能存在,比如CMS系统中我们要对文章的访问量、赞/踩等等来基于某种条件进行记录。假设需求是这样的:每次用户访问这篇文章,访问量加1。常规的做法是:我们在程序里根据参数中的文章ID进行对应数据库中表的Views字段的更新,只要在执行一次sql就能完成这个功能了,update t_Article set Views = Views + 1 where ArticleId = 10001;,很简单是吧?但是如果这篇文章恰好被编辑在后台加精置顶出现在首页了,那么在这篇文章发布后,短时间内可能会造成被点击非常频繁,要知道网站上肯定不止一篇文章,那么这时候如果程序还是以之前的方式工作的话,DB被占用的connection就会非常多,结果是DBA有可能会来找你了。

为了解决这个问题,我想到了ASP.NET Cache,它能帮我完成这个功能,下面我来说说在服务端整个实现的思路。

  1. 针对要更新访问量的文章,根据它的ID生成一个Key, 这个key大概是这样子的:prefix_ + {Id}, prefix是根据系统自己定义的业务前缀
  2. 一旦用户点击了某篇文章,其ID为10010,我们就往缓存中key是prefix_10010的值推+1,过期时间设置为1分钟。
  3. 等到key为prefix_10010的缓存过期时,调用回调通知函数, 回调函数有三个参数,分别是缓存的键、值、移除原因,根据参数解析出相应的文章ID和更新数量,得到sql如下:update t_Article set Views = Views + 22 where ArticleId = 10001;

场景二:配置功能的优化加强:文件监视和自动加载

相信任何网站都有自己的配置文件,在.net Web Application Site中默认使用的是Web.config文件,我们一般都会将应用中使用的一些配置存放到appSettings配置节里,但是它会有些缺点。在一个没有做好持续集成和部署的公司,发布应用程序的时候,Web.config文件一般是作为忽略文件,一旦涉及到Web.config改动的时候,在权限松的公司,开发可以远程更改拷贝,在权限管理较严格的公司,还要邮件相关人士申请。

可能有些人会利用config的transform功能来做发布,在Web.Debug.config和Web.Release.config文件里面写入相应环境的配置之后,msbuild工具支持传入build参数来合并转换Web.config文件以达到目的。但是Web.config文件的最终还是会引起站点的重启,.Net应用都知道的,第一次So Slow。在公司没有用zookeeper之类做配置中心的情况下,我希望应用程序的配置文件能够简单,配置支持多环境自动切换覆盖,修改配置文件同步生产环境不要导致站点重启。

为了解决自己开发的需求,可以使用Cache提供的文件依赖以及缓存移除通知功能。我先写了一个简单的Demo:

public class ConfigDemo
{
    private static readonly string DemoCacheKey = Guid.NewGuid().ToString();
    private static readonly string ConfigFilePath = FileHelper.CombineAppDataPaths(new string[]
    {
        "Configs", "config.json"
    });

    public static void Init()
    {
        _content = Load();
    }

    private static string _content;
    public static string Current
    {
        get { return _content; }
    }

    public static string Load()
    {
        string content = null;
        if (File.Exists(ConfigFilePath))
        {
            content = FileHelper.ReadFromFile(ConfigFilePath);
        }
        else
        {
            throw new FileNotFoundException("~/App_Data/Configs/config.json Not Found");
        }
        var dep = new CacheDependency(ConfigFilePath);
        HttpRuntime.Cache.Insert(DemoCacheKey, content, dep, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration,
            CacheItemPriority.NotRemovable, CacheRemovedCallback);

        return content;
    }

    private static void CacheRemovedCallback(string key, object value, CacheItemRemovedReason reason)
    {
        System.Threading.Thread.Sleep(50);
        // 重新加载配置参数
        _content = Load();
    }
}

说明:上面的代码演示了在配置文件config.json修改后,自动更新运行配置内容的实现方式。详细请参考:项目各环境配置文件的读取

场景三:Cron定时任务

记得遇到过有个这样子需求:在类似于订单表中,一个新创建的订单存在业务有效期,比如过了半小时后,这条数据需要更新它的业务状态,并作一些业务操作,如:发送通知提醒等等。

正常情况下一般会写一个Job去执行,但是系统中Job的增加终归是不太利于一套系统的维护,对于这种简单的计划任务我们使用缓存的过期时间+缓存的移除后通知机制去实现。实现方式跟场景二中的代码类似,我们需要做的是在CacheRemovedCallback方法中执行我们需要做的Job任务。

最后

我认为缓存只是一个概念,static变量也是一种缓存,一个static的Dictionary就是一个缓存容器了,这种缓存比ASP.NET Cache更快,但是ASP.NET Cache的上面列举的一些高级功能是它不具备的。

ASP.NET Cache不能取代以memcached、redis为代表的分布式缓存技术,但它由于是不需要跨进程访问,效率也比分布式缓存的速度更快。各种缓存技术各有优缺点,合理使用它们的特性才是关键。

添加新评论