团队开发框架实战—缓存管理

团队开发框架实战—缓存管理

WEB 缓存

缓存它是一种空间换取时间的一种技术
Web缓存(或HTTP缓存)是用于Web文档,如HTML页面和图像,减少带宽的使用,服务器的负载的一种信息技术。一个Web缓存系统存储通过Cache来传递的文件的副本;如果满足某些条件,则可以从缓存中得到后续的请求。

WEb缓存有几种方式

  • 服务端缓存
    利用 Memcached,Redis,In-Memery 等缓存技术实现对数据的缓存。

Reids

Redis是一个开源,先进的key-value(键/值对)存储,并且勇于构建高性能,可扩展的Web应用程序的完美解决方案
Redis和Memcached的对比

  1. Redis数据库完全在内存中,使用磁盘仅用于持久性
  2. 相比较许多键值对存储,redis拥有更加丰富的数据类型,Redis提供的五种数据类型: strings、map、 list、sets、 sorted sets
  3. Redis可以将数据复制到任意数量的从服务器
    Redis拥有的优势
  4. Redis的执行响应速度非常快
  5. 支持丰富的数据类型
  6. 原子性,保证了如果两个客户端同事访问的Redis服务器将获得更新后的值
  7. 多功能实用工具,Redis是一个多实用的工具,可以在多个用例如缓存,消息,队列实用,任何短暂的数据,应用程序。

Memcached

  1. Memcached是一个高性能的分布式缓存系统,用于Web应用减轻数据库负载,它通过在内存中缓存数据和对象来减少读取数据库的次数,从而提高网站的访问速度。
  1. Memcached是一个给予存储键/值对的HashMap,底层使用C语言完成,但是客户端可以使用任何语言来编写(Java,.NET,PHP).
  2. Memcached中的数据都是存储在memcached内置的内存存储空间中,由于数据仅存在于内存中,因此当某个服务器停止运行或者出现问题之后,所有存放在服务器上的键/值对都会丢失
  3. 下面这幅图可以说明Memcached的工作过程,当首次访问系统的时候,系统从数据库中取得数据保存到Memcached中,在第二次访问的时候则直接从Memcached中读取出来需要的值信息。
    Memcached.png

    Memcached的特征
    Memcached作为高速运行的分布式缓存服务器,具有以下的特点:
  4. 协议简单(客户端通信并不是使用复杂的XML格式,而是使用简单的基于文本行的协议)
  5. 基于libevent的事件处理(libevent是一个程序库,即使服务器的连接数增加,也能发挥o(1)的性能)
  6. 内置内存存储方式(c中已经描述)
  7. Memcached不互相通信的分布式。
  • 代理服务器缓存
    利用类似nginx的反向代理服务器,对请求的url对应的输出的进行缓存。这个缓存和应用程序实现的动态页面缓存类似,只不过用反向代理充当了应用程序的缓存实现。
  • 客户端缓存
    浏览器缓存,其实主要就是HTTP协议定义的缓存机制(如: Last-Modified,If-Modified-Since,Expires; Cache-control等)。

ASP.NET Core 缓存

  • .NET数据缓存框架
支持 功能 应用场景
System.HttpRuntime.Cache、System.Web.Caching.Cache、System.Web.Context.Current.Cache 全部 网站中缓存对象实例,HTML等 web
System.Runtime.Caching .NET 4.0 各种对象 web / non-web
System.Web.Caching.OutputCacheProvider .NET 4.0 网站中缓存HTML页面 web

System.Runtime.Caching,这是在.NET 4.0中新增的缓存框架,存在于程序集System.Runtime.Caching.dll。它是一个可扩展的数据缓存框架,内置提供了内存缓存的实现MemoryCache。HttpRuntime.Cache 获取当前应用程序的Cache,通俗来说就是此缓存信息虽然被放在了System.Web命名空间下,但是非Web程序也可以使用此缓存

.NET 4.0的缓存功能主要由三部分组成:System.Runtime.Caching,System.Web.Caching.Cache和Output Cache。
System.Runtime.Caching这是在.NET 4.0中新增的缓存框架,主要是使用MemoryCache对象,该对象存在于程序集System.Runtime.Caching.dll。
System.Web.Caching.Cache这个则是在.NET2.0开始就一直存在的缓存对象,一般主要用在Web中,当然也可以用于Winform里面,不过要引用System.Web.dll。
Output Cache则是Asp.NET里面使用的,在ASP.NET 4.0之前的版本都是直接使用System.Web.Caching.Cache来缓存HTML片段。在ASP.NET 4.0中对它进行了重新设计,提供了一个OutputCacheProvider供开发人员进行扩展,但是它默认情况下,仍然使用System.Web.Caching.Cache来做做缓存。

  • 内存缓存

最简单的一种缓存,ASP.NET Core 提供了** IMemoryCache 接口来供我们使用。它存储在本地的 WEB 服务器内容中,注意是单机的 WEB 服务器,如果你需要部署的是一个服务器集群的话,那么你应该用分布式缓存**,而不是选择这个。就不详细介绍了,想了解的可以直接看官方文档

  • 分布式缓存

随着云应用和服务器集群以及 docker 等技术的成熟,越来越多的应用程序开始考虑集群部署,因为它具有更好的性能和可伸缩可扩展性。那么这个时候就需要用到分布式缓存了。在 ASP.NET Core应用中,已经对分布式缓存做了抽象,提供了 IDistributedCache 接口,该接口提供了添加,检索,删除等的同步和异步的方法。并且还默认提供了 Redis 和 SQLServer 的分布式缓存实现,我们也可以实现 IDistributedCache 接口来扩展自己的缓存系统。
需要说明的是Get,GetAsync和Set,SetAsync。 这两个接口方法默认是使用的byte[],之所以没有提供直接存储对象的方法是因为微软想把这个默认序列化的选择交给用户,因为每一个团队的偏好是不一样的,有些团队喜欢使用 XML,有些喜欢使用 JSON,有些喜欢使用 Protobuf 等,所以在 项目中,你可以根据自己的偏好来扩展想要的方法。
具体使用方法还是直接看官方文档好了。

  • Response 缓存

在 ASP.NET Core中,有一种缓存叫做Response缓存,这个缓存主要是用来做代理服务器的缓存。它主要原理是在输出的HTTP Response的header里面添加指定的缓存标记。这些缓存标记用来让客户端或者代理服务器来识别需要缓存的内容。然后当客户端有请求到代理服务器的时候,代理服务器可以识别出一部分请求,然后直接把结果返回给浏览器,从而提高后端应用程序的性能和吞吐。

proxy-and-cache.png

从这个图中看出来,在第一次的时候,一个客户端请求经过代理服务器请求的我们后端的WEB服务器上,然后WEB服务器在返回结果的META上添加了cache-control标签,它的值为public。
下面是cache-control标签一些值的说明:

  • public
    指示响应可被任何缓存区缓存。
  • private
    指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当用户的部分响应消息,此响应消息对于其他用户的请求无效。
  • no-cache
    指示请求或响应消息不能缓存(HTTP/1.0用Pragma的no-cache替换)根据什么能被缓存
  • max-age
    指示客户机可以接收生存期不大于指定时间(以秒为单位)的响应。
  • min-fresh
    指示客户机可以接收响应时间小于当前时间加上指定时间的响应。
  • max-stale
    指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。

Expires 表示存在时间,允许客户端在这个时间之前不去检查(发请求),等同max-age的效果。但是如果同时存在,则被Cache-Control的max-age覆盖。
格式:Expires = "Expires" ":" HTTP-date
通过HTTP的META设置expires和cache-control

<meta http-equiv="Cache-Control" content="max-age=7200" />
<meta http-equiv="Expires" content="Mon, 20 Jul 2016 23:00:00 GMT" />

在 ASP.NET Core MVC 中,提供了ResponseCache
这个特性用来做上面这些事情。它被作为一个Attribute添加的Controller的Action上。

  • Duration
    指示缓存的过期时间,对应到Cache-Control 的 max-age 。
  • Location
    有三个值Any
    ,Client
    ,None
    分别对应到Cache-Control的 public,private,no-cache。
  • NoStore
    设置值是否被存储。如果是true,它将设置Cache-Control为no-store
  • VaryByHeader
    将在header中添加Vary标记。
  • CacheProfileName
    使用的策略,在startup.cs中设置。
  • Order
    在过滤器中的排序。

现在,我们已经知道了如果在Action中设置缓存标记了。

Nginx 缓存

对于一些静态文件,比如程序用到的图片,css,js等,Nginx是可以直接处理的,只需要配置一下。如果使用Nginx来处理静态文件的话,那么程序中startup.cs就可以不用添加app.UseStaticFiles();中间件了。

  • 配置
    打开nginx.conf文件,在ubuntu系统下位于/etc/nginx/conf.d/nginx.conf
    没有的话就新建一个。内容如下:
proxy_temp_path   /usr/local/nginx/proxy_temp_dir 1 2; #注:proxy_temp_path和proxy_cache_path指定的路径必须在同一分区

#keys_zone=cache1:100m 表示这个zone(缓存区域)名称为cache1,分配的内存大小为100MB
#/usr/local/nginx/proxy_cache_dir/cache1 表示cache1这个zone的文件要存放的目录
#levels=1:2 表示缓存目录的第一级目录是1个字符,第二级目录是2个字符,即/usr/local/nginx/proxy_cache_dir/cache1/a/1b这种形式
#inactive=1d 表示这个zone中的缓存文件如果在1天内都没有被访问,那么文件会被cache manager进程删除掉
#max_size=10g 表示这个zone的硬盘容量为10GB
proxy_cache_path  /usr/local/nginx/proxy_cache_dir/cache1  levels=1:2 keys_zone=cache1:100m inactive=1d max_size=10g;

#upstream web-app {
#    server webapp1:5090;
#    server webapp2:5090;
#}

server {
    listen 80;
    server_name *.example.com;

    #在日志格式中加入$upstream_cache_status
    log_format format1 '$remote_addr - $remote_user [$time_local]  '
        '"$request" $status $body_bytes_sent '
        '"$http_referer" "$http_user_agent" $upstream_cache_status';

    #访问日志
    access_log log/access.log fomat1;

    #$upstream_cache_status表示资源缓存的状态,有HIT MISS EXPIRED三种状态
    add_header X-Cache $upstream_cache_status;
    
    #命中的正则表达式
    location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|js|css|html)$ {
        proxy_pass http://127.0.0.1:5000;
        #proxy_pass http://web-app;   
        
        #proxy_set_header Host $host;
        #proxy_set_header X-Real-IP $remote_addr;
        #proxy_set_header X-Forwarded-For $remote_addr;
        #proxy_set_header Accept-Encoding "none";
        #设定proxy_set_header Accept-Encoding '';(或是后台服务器关闭gzip),这样这台机器才不会缓存被压缩的文件,造成乱码
        #proxy_set_header Accept-Encoding ""; 这个也可
        
        #如果后端的服务器返回502、504、执行超时等错误,自动将请求转发到upstream负载均衡池中的另一台服务器,实现故障转移。
        #proxy_next_upstream http_502 http_504 error timeout invalid_header;

        #设置资源缓存的zone
        proxy_cache cache1;

        #设置缓存的key
        proxy_cache_key $host$uri$is_args$args;

        #设置状态码为200和304的响应可以进行缓存,并且缓存时间为10分钟
        proxy_cache_valid 200 304 10m;
        
        # **!!!重要!!!** 这段配置加上后,proxy_cache就能支持后台设定的Cache-Control,Expires。 
        proxy_ignore_headers "Cache-Control" "Expires";

        expires 30d;
    }
}

上面有一个配置项在 ASP.NET Core 程序中比较重要,就是proxy_ignore_headers这个配置项,它代表支持后台设定Cache-Control,Expires等。

其中upstream节点是用来配置负载均衡的服务器的,proxy_pass用来设置代理到upstream节点,proxy_next_upstream是用来配置故障转移。

  • 应用配置
    使用sudo nginx -s reload命令来重新加载配置。

ICacheService

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Tdf.CacheManager
{
    public interface ICacheService
    {
        #region 验证缓存项是否存在
        /// <summary>
        ///  验证缓存项是否存在
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <returns></returns>
        bool Exists(string key);
        #endregion

        #region 添加缓存
        /// <summary>
        /// 添加缓存
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <param name="value">缓存Value</param>
        /// <returns></returns>
        bool Add(string key, object value);

        /// <summary>
        /// 添加缓存(异步方式)
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <param name="value">缓存Value</param>
        /// <returns></returns>
        Task<bool> AddAsync(string key, object value);

        /// <summary>
        /// 添加缓存
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <param name="value">缓存Value</param>
        /// <param name="expiresSliding">滑动过期时长(如果在过期时间内有操作,则以当前时间点延长过期时间)</param>
        /// <param name="expiressAbsoulte">绝对过期时长</param>
        /// <returns></returns>
        bool Add(string key, object value, TimeSpan expiresSliding, TimeSpan expiressAbsoulte);

        /// <summary>
        /// 添加缓存(异步方式)
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <param name="value">缓存Value</param>
        /// <param name="expiresSliding">滑动过期时长(如果在过期时间内有操作,则以当前时间点延长过期时间)</param>
        /// <param name="expiressAbsoulte">绝对过期时长</param>
        /// <returns></returns>
        Task<bool> AddAsync(string key, object value, TimeSpan expiresSliding, TimeSpan expiressAbsoulte);

        /// <summary>
        /// 添加缓存
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <param name="value">缓存Value</param>
        /// <param name="expiresIn">缓存时长</param>
        /// <param name="isSliding">是否滑动过期(如果在过期时间内有操作,则以当前时间点延长过期时间)</param>
        /// <returns></returns>
        bool Add(string key, object value, TimeSpan expiresIn, bool isSliding = false);

        /// <summary>
        /// 添加缓存(异步方式)
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <param name="value">缓存Value</param>
        /// <param name="expiresIn">缓存时长</param>
        /// <param name="isSliding">是否滑动过期(如果在过期时间内有操作,则以当前时间点延长过期时间)</param>
        /// <returns></returns>
        Task<bool> AddAsync(string key, object value, TimeSpan expiresIn, bool isSliding = false);
        #endregion

        #region 删除缓存
        /// <summary>
        /// 删除缓存
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <returns></returns>
        bool Remove(string key);

        /// <summary>
        /// 删除缓存(异步方式)
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <returns></returns>
        Task<bool> RemoveAsync(string key);

        /// <summary>
        /// 批量删除缓存
        /// </summary>
        /// <param name="key">缓存Key集合</param>
        /// <returns></returns>
        void RemoveAll(IEnumerable<string> keys);

        /// <summary>
        /// 批量删除缓存(异步方式)
        /// </summary>
        /// <param name="key">缓存Key集合</param>
        /// <returns></returns>
        Task RemoveAllAsync(IEnumerable<string> keys);
        #endregion

        #region 获取缓存
        /// <summary>
        /// 获取缓存
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <returns></returns>
        T Get<T>(string key) where T : class;

        /// <summary>
        /// 获取缓存(异步方式)
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <returns></returns>
        Task<T> GetAsync<T>(string key) where T : class;

        /// <summary>
        /// 获取缓存
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <returns></returns>
        object Get(string key);

        /// <summary>
        /// 获取缓存(异步方式)
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <returns></returns>
        Task<object> GetAsync(string key);

        /// <summary>
        /// 获取缓存集合
        /// </summary>
        /// <param name="keys">缓存Key集合</param>
        /// <returns></returns>
        IDictionary<string, object> GetAll(IEnumerable<string> keys);

        /// <summary>
        /// 获取缓存集合(异步方式)
        /// </summary>
        /// <param name="keys">缓存Key集合</param>
        /// <returns></returns>
        Task<IDictionary<string, object>> GetAllAsync(IEnumerable<string> keys);
        #endregion

        #region 修改缓存 
        /// <summary>
        /// 修改缓存
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <param name="value">新的缓存Value</param>
        /// <returns></returns>
        bool Replace(string key, object value);

        /// <summary>
        /// 修改缓存(异步方式)
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <param name="value">新的缓存Value</param>
        /// <returns></returns>
        Task<bool> ReplaceAsync(string key, object value);

        /// <summary>
        /// 修改缓存
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <param name="value">新的缓存Value</param>
        /// <param name="expiresSliding">滑动过期时长(如果在过期时间内有操作,则以当前时间点延长过期时间)</param>
        /// <param name="expiressAbsoulte">绝对过期时长</param>
        /// <returns></returns>
        bool Replace(string key, object value, TimeSpan expiresSliding, TimeSpan expiressAbsoulte);

        /// <summary>
        /// 修改缓存(异步方式)
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <param name="value">新的缓存Value</param>
        /// <param name="expiresSliding">滑动过期时长(如果在过期时间内有操作,则以当前时间点延长过期时间)</param>
        /// <param name="expiressAbsoulte">绝对过期时长</param>
        /// <returns></returns>
        Task<bool> ReplaceAsync(string key, object value, TimeSpan expiresSliding, TimeSpan expiressAbsoulte);

        /// <summary>
        /// 修改缓存
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <param name="value">新的缓存Value</param>
        /// <param name="expiresIn">缓存时长</param>
        /// <param name="isSliding">是否滑动过期(如果在过期时间内有操作,则以当前时间点延长过期时间)</param>
        /// <returns></returns>
        bool Replace(string key, object value, TimeSpan expiresIn, bool isSliding = false);

        /// <summary>
        /// 修改缓存(异步方式)
        /// </summary>
        /// <param name="key">缓存Key</param>
        /// <param name="value">新的缓存Value</param>
        /// <param name="expiresIn">缓存时长</param>
        /// <param name="isSliding">是否滑动过期(如果在过期时间内有操作,则以当前时间点延长过期时间)</param>
        /// <returns></returns>
        Task<bool> ReplaceAsync(string key, object value, TimeSpan expiresIn, bool isSliding = false);
        #endregion
    }
}

总结

  • 关于缓存
    缓存确实是提升应用程序性能最快也是效果最明显的方式之一,ASP.NET Core也为提供了很多种缓存方法。但是,在使用之前一定要了解每一种缓存的技术实现,切不可盲目使用。
  • 关于部署
    个人认为,在 ASP.NET Core 理想的分布式部署环境有两种:
    第一种是基于云的部署,比如使用Azure,AWS,阿里云等,那么我们可以使用他们提供的负载均衡器来帮助我们拦截洪水般的请求,然后借助于云提供的高可用的实例集群或者Docker集群来降低应用程序的压力,提升吞吐。
    比如我们项目现在使用的AWS的部署环境,借助于AWS来实现企业的私有云,包括高可用的Redis集群,弹性EC2集群,RDS集群,S3等,这个时候只需要专注于业务。
    第二种是自己搭建集群环境,可以在服务器前端使用Nginx的负载均衡和缓存来拦截大部分的HTTP请求,然后后端使用Docker集群来做部署
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容