上一篇介紹了一些redis的安裝及使用步驟,本篇開始將介紹redis的實際應用場景,先從最多見的session開始,恰好也從新學習一遍session的實現原理。在閱讀以前假設你已經會使用nginx+iis實現負載均衡搭建負載均衡站點了,這裏咱們會搭建兩個站點來驗證redis實現的session是否能共享。css
閱讀目錄html
session和cookie是咱們作web開發中經常使用到的兩個對象,它們之間會不會有聯繫呢?nginx
Cookie是什麼? Cookie 是一小段文本信息,伴隨着用戶請求和頁面在 Web 服務器和瀏覽器之間傳遞。Cookie 包含每次用戶訪問站點時 Web 應用程序均可以讀取的信息。(Cookie 會隨每次HTTP請求一塊兒被傳遞服務器端,排除js,css,image等靜態文件,這個過程能夠從fiddler或者ie自帶的網絡監控裏面分析到,考慮性能的化能夠從儘可能減小cookie着手)web
Cookie寫入瀏覽器的過程:咱們可使用以下代碼在Asp.net項目中寫一個Cookie 併發送到客戶端的瀏覽器(爲了簡單我沒有設置其它屬性)。redis
HttpCookie cookie = new HttpCookie("RedisSessionId", "string value");Response.Cookies.Add(cookie);
咱們能夠看到在服務器寫的cookie,會經過響應頭Set-Cookie的方式寫入到瀏覽器。算法
Session是什麼? Session咱們可使用它來方便地在服務端保存一些與會話相關的信息。好比常見的登陸信息。chrome
Session實現原理? HTTP協議是無狀態的,對於一個瀏覽器發出的屢次請求,WEB服務器沒法區分 是否是來源於同一個瀏覽器。因此服務器爲了區分這個過程會經過一個sessionid來區分請求,而這個sessionid是怎麼發送給服務端的呢?前面說了cookie會隨每次請求發送到服務端,而且cookie相對用戶是不可見的,用來保存這個sessionid是最好不過了,咱們經過下面過程來驗證一下。數據庫
Session["UserId"] = 123;
經過上圖再次驗證了session和cookie的關係,服務器產生了一次設置cookie的操做,這裏的sessionid就是用來區分瀏覽器的。爲了實驗是區分瀏覽器的,能夠實驗在IE下進行登陸,而後在用chrome打開相同頁面,你會發如今chrome仍是須要你登陸的,緣由是chrome這時沒有sessionid。httpOnly是表示這個cookie是不會在瀏覽器端經過js進行操做的,防止人爲串改sessionid。api
asp.net默認的sessionid的鍵值是ASP.NET_SessionId,能夠在web.config裏面修改這個默認配置瀏覽器
<sessionState mode="InProc" cookieName="MySessionId"></sessionState>
服務器端Session讀取? 服務器端是怎麼讀取session的值呢 ,Session["鍵值"]。那麼問題來了,爲何在Defaule.aspx.cs文件裏能夠獲取到這個Session對象,這個Session對象又是何時被初始化的呢。
爲了弄清楚這個問題,咱們能夠經過轉到定義的方式來查看。
System.Web.UI.Page ->HttpSessionState(Session)
protected internal override HttpContext Context { [System.Runtime.TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] get { if (_context == null) { _context = HttpContext.Current; } return _context; } } public virtual HttpSessionState Session { get { if (!_sessionRetrieved) { /* try just once to retrieve it */ _sessionRetrieved = true; try { _session = Context.Session; } catch { // Just ignore exceptions, return null. } } if (_session == null) { throw new HttpException(SR.GetString(SR.Session_not_enabled)); } return _session; } }
上面這一段是Page對象初始化Session對象的,能夠看到Session的值來源於HttpContext.Current,而HttpContext.Current又是何時被初始化的呢,咱們接着往下看。
public sealed class HttpContext : IServiceProvider, IPrincipalContainer { internal static readonly Assembly SystemWebAssembly = typeof(HttpContext).Assembly; private static volatile bool s_eurlSet; private static string s_eurl; private IHttpAsyncHandler _asyncAppHandler; // application as handler (not always HttpApplication) private AsyncPreloadModeFlags _asyncPreloadModeFlags; private bool _asyncPreloadModeFlagsSet; private HttpApplication _appInstance; private IHttpHandler _handler; [DoNotReset] private HttpRequest _request; private HttpResponse _response; private HttpServerUtility _server; private Stack _traceContextStack; private TraceContext _topTraceContext; [DoNotReset] private Hashtable _items; private ArrayList _errors; private Exception _tempError; private bool _errorCleared; [DoNotReset] private IPrincipalContainer _principalContainer; [DoNotReset] internal ProfileBase _Profile; [DoNotReset] private DateTime _utcTimestamp; [DoNotReset] private HttpWorkerRequest _wr; private VirtualPath _configurationPath; internal bool _skipAuthorization; [DoNotReset] private CultureInfo _dynamicCulture; [DoNotReset] private CultureInfo _dynamicUICulture; private int _serverExecuteDepth; private Stack _handlerStack; private bool _preventPostback; private bool _runtimeErrorReported; private PageInstrumentationService _pageInstrumentationService = null; private ReadOnlyCollection<string> _webSocketRequestedProtocols; }
我這裏只貼出了一部分源碼,HttpContext包含了咱們經常使用的Request,Response等對象。HttpContext得從ASP.NET管道提及,以IIS 6.0爲例,在工做進程w3wp.exe中,利用Aspnet_ispai.dll加載.NET運行時(若是.NET運行時還沒有加載)。IIS 6.0引入了應用程序池的概念,一個工做進程對應着一個應用程序池。一個應用程序池能夠承載一個或多個Web應用,每一個Web應用映射到一個IIS虛擬目錄。與IIS 5.x同樣,每個Web應用運行在各自的應用程序域中。若是HTTP.SYS接收到的HTTP請求是對該Web應用的第一次訪問,在成功加載了運行時後,會經過AppDomainFactory爲該Web應用建立一個應用程序域(AppDomain)。隨後,一個特殊的運行時IsapiRuntime被加載。IsapiRuntime定義在程序集System.Web中,對應的命名空間爲System.Web.Hosting。IsapiRuntime會接管該HTTP請求。IsapiRuntime會首先建立一個IsapiWorkerRequest對象,用於封裝當前的HTTP請求,並將該IsapiWorkerRequest對象傳遞給ASP.NET運行時:HttpRuntime,今後時起,HTTP請求正式進入了ASP.NET管道。根據IsapiWorkerRequest對象,HttpRuntime會建立用於表示當前HTTP請求的上下文(Context)對象:HttpContext。
至此相信你們對Session初始化過程,session和cookie的關係已經很瞭解了吧,下面開始進行Session共享實現方案。
一.StateServer方式
這種是asp.net提供的一種方式,還有一種是SQLServer方式(不必定程序使用的是SQLServer數據庫,因此通用性不高,這裏就不介紹了)。也就是將會話數據存儲到單獨的內存緩衝區中,再由單獨一臺機器上運行的Windows服務來控制這個緩衝區。狀態服務全稱是「ASP.NET State Service 」(aspnet_state.exe)。它由Web.config文件中的stateConnectionString屬性來配置。該屬性指定了服務所在的服務器,以及要監視的端口。
<sessionState mode="StateServer" stateConnectionString="tcpip=127.0.0.1:42424" cookieless="false" timeout="20" />
在這個例子中,狀態服務在當前機器的42424端口(默認端口)運行。要在服務器上改變端口和開啓遠程服務器的該功能,可編輯HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\aspnet_state\Parameters註冊表項中的Port值和AllowRemoteConnection修改爲1。 顯然,使用狀態服務的優勢在於進程隔離,並可在多站點中共享。 使用這種模式,會話狀態的存儲將不依賴於iis進程的失敗或者重啓,然而,一旦狀態服務停止,全部會話數據都會丟失(這個問題redis不會存在,從新了數據不會丟失)。
這裏提供一段bat文件幫助修改註冊表,能夠複製保存爲.bat文件執行
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\aspnet_state\Parameters" /v "AllowRemoteConnection" /t REG_DWORD /d 1 /f reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\aspnet_state\Parameters" /v "Port" /t REG_DWORD /d 42424 /f net stop aspnet_state net start aspnet_state pause
完成這些配置之後仍是不能實現共享,雖然站點間的SessionId是一致的,但只有一個站點可以讀取的到值,而其它站點讀取不到。下面給出解決方案,在Global文件裏面添加下面代碼
public override void Init() { base.Init(); foreach (string moduleName in this.Modules) { string appName = "APPNAME"; IHttpModule module = this.Modules[moduleName]; SessionStateModule ssm = module as SessionStateModule; if (ssm != null) { FieldInfo storeInfo = typeof(SessionStateModule).GetField("_store", BindingFlags.Instance | BindingFlags.NonPublic); FieldInfo configMode = typeof(SessionStateModule).GetField("s_configMode", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static); SessionStateMode mode = (SessionStateMode)configMode.GetValue(ssm); if (mode == SessionStateMode.StateServer) { SessionStateStoreProviderBase store = (SessionStateStoreProviderBase)storeInfo.GetValue(ssm); if (store == null)//In IIS7 Integrated mode, module.Init() is called later { FieldInfo runtimeInfo = typeof(HttpRuntime).GetField("_theRuntime", BindingFlags.Static | BindingFlags.NonPublic); HttpRuntime theRuntime = (HttpRuntime)runtimeInfo.GetValue(null); FieldInfo appNameInfo = typeof(HttpRuntime).GetField("_appDomainAppId", BindingFlags.Instance | BindingFlags.NonPublic); appNameInfo.SetValue(theRuntime, appName); } else { Type storeType = store.GetType(); if (storeType.Name.Equals("OutOfProcSessionStateStore")) { FieldInfo uribaseInfo = storeType.GetField("s_uribase", BindingFlags.Static | BindingFlags.NonPublic); uribaseInfo.SetValue(storeType, appName); object obj = null; uribaseInfo.GetValue(obj); } } } break; } } }
二.redis實現session共享
下面咱們將使用redis來實現共享,首先要弄清楚session的幾個關鍵點,過時時間,SessionId,一個SessionId裏面會存在多組key/value數據。基於這個特性我將採用Hash結構來存儲,看看代碼實現。用到了上一篇提供的RedisBase幫助類。
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.SessionState; using ServiceStack.Redis; using Com.Redis; namespace ResidSessionDemo.RedisDemo { public class RedisSession { private HttpContext context; public RedisSession(HttpContext context, bool IsReadOnly, int Timeout) { this.context = context; this.IsReadOnly = IsReadOnly; this.Timeout = Timeout; //更新緩存過時時間 RedisBase.Hash_SetExpire(SessionID, DateTime.Now.AddMinutes(Timeout)); } /// <summary> /// SessionId標識符 /// </summary> public static string SessionName = "Redis_SessionId"; // // 摘要: // 獲取會話狀態集合中的項數。 // // 返回結果: // 集合中的項數。 public int Count { get { return RedisBase.Hash_GetCount(SessionID); } } // // 摘要: // 獲取一個值,該值指示會話是否爲只讀。 // // 返回結果: // 若是會話爲只讀,則爲 true;不然爲 false。 public bool IsReadOnly { get; set; } // // 摘要: // 獲取會話的惟一標識符。 // // 返回結果: // 惟一會話標識符。 public string SessionID { get { return GetSessionID(); } } // // 摘要: // 獲取並設置在會話狀態提供程序終止會話以前各請求之間所容許的時間(以分鐘爲單位)。 // // 返回結果: // 超時期限(以分鐘爲單位)。 public int Timeout { get; set; } /// <summary> /// 獲取SessionID /// </summary> /// <param name="key">SessionId標識符</param> /// <returns>HttpCookie值</returns> private string GetSessionID() { HttpCookie cookie = context.Request.Cookies.Get(SessionName); if (cookie == null || string.IsNullOrEmpty(cookie.Value)) { string newSessionID = Guid.NewGuid().ToString(); HttpCookie newCookie = new HttpCookie(SessionName, newSessionID); newCookie.HttpOnly = IsReadOnly; newCookie.Expires = DateTime.Now.AddMinutes(Timeout); context.Response.Cookies.Add(newCookie); return "Session_"+newSessionID; } else { return "Session_"+cookie.Value; } } // // 摘要: // 按名稱獲取或設置會話值。 // // 參數: // name: // 會話值的鍵名。 // // 返回結果: // 具備指定名稱的會話狀態值;若是該項不存在,則爲 null。 public object this[string name] { get { return RedisBase.Hash_Get<object>(SessionID, name); } set { RedisBase.Hash_Set<object>(SessionID, name, value); } } // 摘要: // 判斷會話中是否存在指定key // // 參數: // name: // 鍵值 // public bool IsExistKey(string name) { return RedisBase.Hash_Exist<object>(SessionID, name); } // // 摘要: // 向會話狀態集合添加一個新項。 // // 參數: // name: // 要添加到會話狀態集合的項的名稱。 // // value: // 要添加到會話狀態集合的項的值。 public void Add(string name, object value) { RedisBase.Hash_Set<object>(SessionID, name, value); } // // 摘要: // 從會話狀態集合中移除全部的鍵和值。 public void Clear() { RedisBase.Hash_Remove(SessionID); } // // 摘要: // 刪除會話狀態集合中的項。 // // 參數: // name: // 要從會話狀態集合中刪除的項的名稱。 public void Remove(string name) { RedisBase.Hash_Remove(SessionID,name); } // // 摘要: // 從會話狀態集合中移除全部的鍵和值。 public void RemoveAll() { Clear(); } } }
下面是實現相似在cs文件中能直接使用Session["UserId"]的方式,個人MyPage類繼承Page實現了本身的邏輯主要作了兩件事 1:初始化RedisSession 2:實現統一登陸認證,OnPreInit方法裏面判斷用戶是否登陸,若是沒有登陸了則跳轉到登錄界面
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.UI; namespace ResidSessionDemo.RedisDemo { /// <summary> /// 自定義Page 實現如下功能 /// 1.初始化RedisSession /// 2.實現頁面登陸驗證,繼承此類,則能夠實現全部頁面的登陸驗證 /// </summary> public class MyPage:Page { private RedisSession redisSession; /// <summary> /// RedisSession /// </summary> public RedisSession RedisSession { get { if (redisSession == null) { redisSession = new RedisSession(Context, true, 20); } return redisSession; } } protected override void OnPreInit(EventArgs e) { base.OnPreInit(e); //判斷用戶是否已經登陸,若是未登陸,則跳轉到登陸界面 if (!RedisSession.IsExistKey("UserCode")) { Response.Redirect("Login.aspx"); } } } }
咱們來看看Default.aspx.cs是如何使用RedisSession的,至此咱們實現了和Asp.netSession如出一轍的功能和使用方式。
RedisSession.Remove("UserCode");
相比StateServer,RedisSession具備如下優勢
1.redis服務器重啓不會丟失數據 2.可使用redis的讀寫分離個集羣功能更加高效讀寫數據
測試效果,使用nginx和iis部署兩個站點作負載均衡,iis1地址127.0.0.1:8002 iis2地址127.0.0.1:9000 nginx代理服務地址127.0.0.1:8003,不懂如何配置的能夠去閱讀個人nginx+iis實現負載均衡這篇文章。咱們來看一下測試結果。
訪問127.0.0.1:8003 須要進行登陸 用戶名爲admin 密碼爲123
登陸成功之後,重點關注端口號信息
刷新頁面,重點關注端口號信息
能夠嘗試直接訪問iis1地址127.0.0.1:8002 iis2地址127.0.0.1:9000 這兩個站點,你會發現都不須要登陸了。至此咱們的redis實現session功能算是大功告成了。
使用redis實現session告一段落,下面留個問題討論一下方案。微信開發提供了不少接口,參考下面截圖,能夠看到獲取access_token接口每日最多調用2000次,如今大公司提供的不少接口針對不對級別的用戶接口訪問次數限制都是不同的,至於作這個限制的緣由應該是防止惡意攻擊和流量限制之類的。那麼個人問題是怎麼實現這個接口調用次數限制功能。你們能夠發揮想象力參與討論哦,或許你也會碰到這個問題。
先說下我知道的兩種方案:
1.使用流量整形中的令牌桶算法,大小固定的令牌桶可自行以恆定的速率源源不斷地產生令牌。若是令牌不被消耗,或者被消耗的速度小於產生的速度,令牌就會不斷地增多,直到把桶填滿。後面再產生的令牌就會從桶中溢出。最後桶中能夠保存的最大令牌數永遠不會超過桶的大小。
說淺顯點:好比上面的獲取access_token接口,一天2000次的頻率,即1次/分鐘。咱們令牌桶容量爲2000,可使用redis 最簡單的key/value來存儲 ,key爲用戶id,value爲整形存儲還可以使用次數,而後使用一個定時器1分鐘調用client.Incr(key) 實現次數自增;用戶每訪問一次該接口,相應的client.Decr(key)來減小使用次數。
可是這裏存在一個性能問題,這僅僅是針對一個用戶來講,假設有10萬個用戶,怎麼使用定時器來實現這個自增操做呢,難道是循環10萬次分別調用client.Incr(key)嗎?這一點沒有考慮清楚。
2.直接用戶訪問一次 先進行總次數判斷,符合條件再就進行一次自增
兩種方案優缺點比較 | ||
優勢 | 缺點 | |
令牌桶算法 | 流量控制精確 | 實現複雜,而且因爲控制精確反而在實際應用中有麻煩,極可能用戶在晚上到凌晨期間訪問接口次數很少,白天訪問次數多些。 |
簡單算法 | 實現簡單可行,效率高 | 流量控制不精確 |
本篇從實際應用講解了redis,後面應該還會有幾篇繼續介紹redis實際應用,敬請期待!
本篇文章用到的資源打包下載地址:redis_demo
svn下載地址:http://code.taobao.org/svn/ResidSessionDemo/