團隊開發(fā)框架實戰(zhàn)—緩存管理
WEB 緩存
緩存它是一種空間換取時間的一種技術。
Web緩存(或HTTP緩存)是用于Web文檔,如HTML頁面和圖像,減少帶寬的使用,服務器的負載的一種信息技術。一個Web緩存系統(tǒng)存儲通過Cache來傳遞的文件的副本;如果滿足某些條件,則可以從緩存中得到后續(xù)的請求。
WEb緩存有幾種方式:
- 服務端緩存
利用 Memcached,Redis,In-Memery 等緩存技術實現(xiàn)對數(shù)據(jù)的緩存。
Reids
Redis是一個開源,先進的key-value(鍵/值對)存儲,并且勇于構建高性能,可擴展的Web應用程序的完美解決方案
Redis和Memcached的對比
- Redis數(shù)據(jù)庫完全在內存中,使用磁盤僅用于持久性
- 相比較許多鍵值對存儲,redis擁有更加豐富的數(shù)據(jù)類型,Redis提供的五種數(shù)據(jù)類型: strings、map、 list、sets、 sorted sets
- Redis可以將數(shù)據(jù)復制到任意數(shù)量的從服務器
Redis擁有的優(yōu)勢 - Redis的執(zhí)行響應速度非常快
- 支持豐富的數(shù)據(jù)類型
- 原子性,保證了如果兩個客戶端同事訪問的Redis服務器將獲得更新后的值
- 多功能實用工具,Redis是一個多實用的工具,可以在多個用例如緩存,消息,隊列實用,任何短暫的數(shù)據(jù),應用程序。
Memcached
- Memcached是一個高性能的分布式緩存系統(tǒng),用于Web應用減輕數(shù)據(jù)庫負載,它通過在內存中緩存數(shù)據(jù)和對象來減少讀取數(shù)據(jù)庫的次數(shù),從而提高網(wǎng)站的訪問速度。
- Memcached是一個給予存儲鍵/值對的HashMap,底層使用C語言完成,但是客戶端可以使用任何語言來編寫(Java,.NET,PHP).
- Memcached中的數(shù)據(jù)都是存儲在memcached內置的內存存儲空間中,由于數(shù)據(jù)僅存在于內存中,因此當某個服務器停止運行或者出現(xiàn)問題之后,所有存放在服務器上的鍵/值對都會丟失。
- 下面這幅圖可以說明Memcached的工作過程,當首次訪問系統(tǒng)的時候,系統(tǒng)從數(shù)據(jù)庫中取得數(shù)據(jù)保存到Memcached中,在第二次訪問的時候則直接從Memcached中讀取出來需要的值信息。
Memcached.png
Memcached的特征
Memcached作為高速運行的分布式緩存服務器,具有以下的特點: - 協(xié)議簡單(客戶端通信并不是使用復雜的XML格式,而是使用簡單的基于文本行的協(xié)議)
- 基于libevent的事件處理(libevent是一個程序庫,即使服務器的連接數(shù)增加,也能發(fā)揮o(1)的性能)
- 內置內存存儲方式(c中已經(jīng)描述)
- Memcached不互相通信的分布式。
- 代理服務器緩存
利用類似nginx的反向代理服務器,對請求的url對應的輸出的進行緩存。這個緩存和應用程序實現(xiàn)的動態(tài)頁面緩存類似,只不過用反向代理充當了應用程序的緩存實現(xiàn)。 - 客戶端緩存
瀏覽器緩存,其實主要就是HTTP協(xié)議定義的緩存機制(如: Last-Modified,If-Modified-Since,Expires; Cache-control等)。
ASP.NET Core 緩存
- .NET數(shù)據(jù)緩存框架
類 | 支持 | 功能 | 應用場景 |
---|---|---|---|
System.HttpRuntime.Cache、System.Web.Caching.Cache、System.Web.Context.Current.Cache | 全部 | 網(wǎng)站中緩存對象實例,HTML等 | web |
System.Runtime.Caching | .NET 4.0 | 各種對象 | web / non-web |
System.Web.Caching.OutputCacheProvider | .NET 4.0 | 網(wǎng)站中緩存HTML頁面 | web |
System.Runtime.Caching,這是在.NET 4.0中新增的緩存框架,存在于程序集System.Runtime.Caching.dll。它是一個可擴展的數(shù)據(jù)緩存框架,內置提供了內存緩存的實現(xiàn)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供開發(fā)人員進行擴展,但是它默認情況下,仍然使用System.Web.Caching.Cache來做做緩存。
- 內存緩存
最簡單的一種緩存,ASP.NET Core 提供了** IMemoryCache 接口來供我們使用。它存儲在本地的 WEB 服務器內容中,注意是單機的 WEB 服務器,如果你需要部署的是一個服務器集群的話,那么你應該用分布式緩存**,而不是選擇這個。就不詳細介紹了,想了解的可以直接看官方文檔。
- 分布式緩存
隨著云應用和服務器集群以及 docker 等技術的成熟,越來越多的應用程序開始考慮集群部署,因為它具有更好的性能和可伸縮可擴展性。那么這個時候就需要用到分布式緩存了。在 ASP.NET Core應用中,已經(jīng)對分布式緩存做了抽象,提供了 IDistributedCache 接口,該接口提供了添加,檢索,刪除等的同步和異步的方法。并且還默認提供了 Redis 和 SQLServer 的分布式緩存實現(xiàn),我們也可以實現(xiàn) IDistributedCache 接口來擴展自己的緩存系統(tǒng)。
需要說明的是Get,GetAsync和Set,SetAsync。 這兩個接口方法默認是使用的byte[],之所以沒有提供直接存儲對象的方法是因為微軟想把這個默認序列化的選擇交給用戶,因為每一個團隊的偏好是不一樣的,有些團隊喜歡使用 XML,有些喜歡使用 JSON,有些喜歡使用 Protobuf 等,所以在 項目中,你可以根據(jù)自己的偏好來擴展想要的方法。
具體使用方法還是直接看官方文檔好了。
- Response 緩存
在 ASP.NET Core中,有一種緩存叫做Response緩存,這個緩存主要是用來做代理服務器的緩存。它主要原理是在輸出的HTTP Response的header里面添加指定的緩存標記。這些緩存標記用來讓客戶端或者代理服務器來識別需要緩存的內容。然后當客戶端有請求到代理服務器的時候,代理服務器可以識別出一部分請求,然后直接把結果返回給瀏覽器,從而提高后端應用程序的性能和吞吐。
從這個圖中看出來,在第一次的時候,一個客戶端請求經(jīng)過代理服務器請求的我們后端的WEB服務器上,然后WEB服務器在返回結果的META上添加了cache-control標簽,它的值為public。
下面是cache-control標簽一些值的說明:
- public
指示響應可被任何緩存區(qū)緩存。 - private
指示對于單個用戶的整個或部分響應消息,不能被共享緩存處理。這允許服務器僅僅描述當用戶的部分響應消息,此響應消息對于其他用戶的請求無效。 - no-cache
指示請求或響應消息不能緩存(HTTP/1.0用Pragma的no-cache替換)根據(jù)什么能被緩存 - max-age
指示客戶機可以接收生存期不大于指定時間(以秒為單位)的響應。 - min-fresh
指示客戶機可以接收響應時間小于當前時間加上指定時間的響應。 - max-stale
指示客戶機可以接收超出超時期間的響應消息。如果指定max-stale消息的值,那么客戶機可以接收超出超時期指定值之內的響應消息。
Expires 表示存在時間,允許客戶端在這個時間之前不去檢查(發(fā)請求),等同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
在過濾器中的排序。
現(xiàn)在,我們已經(jīng)知道了如果在Action中設置緩存標記了。
Nginx 緩存
對于一些靜態(tài)文件,比如程序用到的圖片,css,js等,Nginx是可以直接處理的,只需要配置一下。如果使用Nginx來處理靜態(tài)文件的話,那么程序中startup.cs就可以不用添加app.UseStaticFiles();中間件了。
-
配置
打開nginx.conf文件,在ubuntu系統(tǒng)下位于/etc/nginx/conf.d/nginx.conf
沒有的話就新建一個。內容如下:
proxy_temp_path /usr/local/nginx/proxy_temp_dir 1 2; #注:proxy_temp_path和proxy_cache_path指定的路徑必須在同一分區(qū)
#keys_zone=cache1:100m 表示這個zone(緩存區(qū)域)名稱為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表示資源緩存的狀態(tài),有HIT MISS EXPIRED三種狀態(tài)
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、執(zhí)行超時等錯誤,自動將請求轉發(fā)到upstream負載均衡池中的另一臺服務器,實現(xiàn)故障轉移。
#proxy_next_upstream http_502 http_504 error timeout invalid_header;
#設置資源緩存的zone
proxy_cache cache1;
#設置緩存的key
proxy_cache_key $host$uri$is_args$args;
#設置狀態(tài)碼為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節(jié)點是用來配置負載均衡的服務器的,proxy_pass用來設置代理到upstream節(jié)點,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也為提供了很多種緩存方法。但是,在使用之前一定要了解每一種緩存的技術實現(xiàn),切不可盲目使用。 -
關于部署
個人認為,在 ASP.NET Core 理想的分布式部署環(huán)境有兩種:
第一種是基于云的部署,比如使用Azure,AWS,阿里云等,那么我們可以使用他們提供的負載均衡器來幫助我們攔截洪水般的請求,然后借助于云提供的高可用的實例集群或者Docker集群來降低應用程序的壓力,提升吞吐。
比如我們項目現(xiàn)在使用的AWS的部署環(huán)境,借助于AWS來實現(xiàn)企業(yè)的私有云,包括高可用的Redis集群,彈性EC2集群,RDS集群,S3等,這個時候只需要專注于業(yè)務。
第二種是自己搭建集群環(huán)境,可以在服務器前端使用Nginx的負載均衡和緩存來攔截大部分的HTTP請求,然后后端使用Docker集群來做部署。