分布式中使用Redis實(shí)現(xiàn)Session共享
Session實(shí)現(xiàn)原理
session和cookie是我們做web開(kāi)發(fā)中常用到的兩個(gè)對(duì)象,它們之間會(huì)不會(huì)有聯(lián)系呢?
Cookie是什么? Cookie 是一小段文本信息,伴隨著用戶(hù)請(qǐng)求和頁(yè)面在 Web 服務(wù)器和瀏覽器之間傳遞。Cookie 包含每次用戶(hù)訪問(wèn)站點(diǎn)時(shí) Web 應(yīng)用程序都可以讀取的信息。(Cookie 會(huì)隨每次HTTP請(qǐng)求一起被傳遞服務(wù)器端,排除js,css,image等靜態(tài)文件,這個(gè)過(guò)程可以從fiddler或者ie自帶的網(wǎng)絡(luò)監(jiān)控里面分析到,考慮性能的化可以從盡量減少cookie著手)
Cookie寫(xiě)入瀏覽器的過(guò)程:我們可以使用如下代碼在Asp.net項(xiàng)目中寫(xiě)一個(gè)Cookie 并發(fā)送到客戶(hù)端的瀏覽器(為了簡(jiǎn)單我沒(méi)有設(shè)置其它屬性)。
- HttpCookie cookie = new HttpCookie("RedisSessionId", "string value");Response.Cookies.Add(cookie);

我們可以看到在服務(wù)器寫(xiě)的cookie,會(huì)通過(guò)響應(yīng)頭Set-Cookie的方式寫(xiě)入到瀏覽器。
Session是什么? Session我們可以使用它來(lái)方便地在服務(wù)端保存一些與會(huì)話相關(guān)的信息。比如常見(jiàn)的登錄信息。
Session實(shí)現(xiàn)原理? HTTP協(xié)議是無(wú)狀態(tài)的,對(duì)于一個(gè)瀏覽器發(fā)出的多次請(qǐng)求,WEB服務(wù)器無(wú)法區(qū)分 是不是來(lái)源于同一個(gè)瀏覽器。所以服務(wù)器為了區(qū)分這個(gè)過(guò)程會(huì)通過(guò)一個(gè)sessionid來(lái)區(qū)分請(qǐng)求,而這個(gè)sessionid是怎么發(fā)送給服務(wù)端的呢?前面說(shuō)了cookie會(huì)隨每次請(qǐng)求發(fā)送到服務(wù)端,并且cookie相對(duì)用戶(hù)是不可見(jiàn)的,用來(lái)保存這個(gè)sessionid是最好不過(guò)了,我們通過(guò)下面過(guò)程來(lái)驗(yàn)證一下。
- Session["UserId"] = 123;

通過(guò)上圖再次驗(yàn)證了session和cookie的關(guān)系,服務(wù)器產(chǎn)生了一次設(shè)置cookie的操作,這里的sessionid就是用來(lái)區(qū)分瀏覽器的。為了實(shí)驗(yàn)是區(qū)分瀏覽器的,可以實(shí)驗(yàn)在IE下進(jìn)行登錄,然后在用chrome打開(kāi)相同頁(yè)面,你會(huì)發(fā)現(xiàn)在chrome還是需要你登錄的,原因是chrome這時(shí)沒(méi)有sessionid。httpOnly是表示這個(gè)cookie是不會(huì)在瀏覽器端通過(guò)js進(jìn)行操作的,防止人為串改sessionid。
asp.net默認(rèn)的sessionid的鍵值是ASP.NET_SessionId,可以在web.config里面修改這個(gè)默認(rèn)配置
- <sessionState mode="InProc" cookieName="MySessionId"></sessionState>
服務(wù)器端Session讀取? 服務(wù)器端是怎么讀取session的值呢 ,Session["鍵值"]。那么問(wèn)題來(lái)了,為什么在Defaule.aspx.cs文件里可以獲取到這個(gè)Session對(duì)象,這個(gè)Session對(duì)象又是什么時(shí)候被初始化的呢。
為了弄清楚這個(gè)問(wèn)題,我們可以通過(guò)轉(zhuǎn)到定義的方式來(lái)查看。
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對(duì)象初始化Session對(duì)象的,可以看到Session的值來(lái)源于HttpContext.Current,而HttpContext.Current又是什么時(shí)候被初始化的呢,我們接著往下看。
- 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等對(duì)象。HttpContext得從ASP.NET管道說(shuō)起,以IIS 6.0為例,在工作進(jìn)程w3wp.exe中,利用Aspnet_ispai.dll加載.NET運(yùn)行時(shí)(如果.NET運(yùn)行時(shí)尚未加載)。IIS 6.0引入了應(yīng)用程序池的概念,一個(gè)工作進(jìn)程對(duì)應(yīng)著一個(gè)應(yīng)用程序池。一個(gè)應(yīng)用程序池可以承載一個(gè)或多個(gè)Web應(yīng)用,每個(gè)Web應(yīng)用映射到一個(gè)IIS虛擬目錄。與IIS 5.x一樣,每一個(gè)Web應(yīng)用運(yùn)行在各自的應(yīng)用程序域中。如果HTTP.SYS接收到的HTTP請(qǐng)求是對(duì)該Web應(yīng)用的第一次訪問(wèn),在成功加載了運(yùn)行時(shí)后,會(huì)通過(guò)AppDomainFactory為該Web應(yīng)用創(chuàng)建一個(gè)應(yīng)用程序域(AppDomain)。隨后,一個(gè)特殊的運(yùn)行時(shí)IsapiRuntime被加載。IsapiRuntime定義在程序集System.Web中,對(duì)應(yīng)的命名空間為System.Web.Hosting。IsapiRuntime會(huì)接管該HTTP請(qǐng)求。IsapiRuntime會(huì)首先創(chuàng)建一個(gè)IsapiWorkerRequest對(duì)象,用于封裝當(dāng)前的HTTP請(qǐng)求,并將該IsapiWorkerRequest對(duì)象傳遞給ASP.NET運(yùn)行時(shí):HttpRuntime,從此時(shí)起,HTTP請(qǐng)求正式進(jìn)入了ASP.NET管道。根據(jù)IsapiWorkerRequest對(duì)象,HttpRuntime會(huì)創(chuàng)建用于表示當(dāng)前HTTP請(qǐng)求的上下文(Context)對(duì)象:HttpContext。
至此相信大家對(duì)Session初始化過(guò)程,session和cookie的關(guān)系已經(jīng)很了解了吧,下面開(kāi)始進(jìn)行Session共享實(shí)現(xiàn)方案。
Session共享實(shí)現(xiàn)方案
一.StateServer方式
這種是asp.net提供的一種方式,還有一種是SQLServer方式(不一定程序使用的是SQLServer數(shù)據(jù)庫(kù),所以通用性不高,這里就不介紹了)。也就是將會(huì)話數(shù)據(jù)存儲(chǔ)到單獨(dú)的內(nèi)存緩沖區(qū)中,再由單獨(dú)一臺(tái)機(jī)器上運(yùn)行的Windows服務(wù)來(lái)控制這個(gè)緩沖區(qū)。狀態(tài)服務(wù)全稱(chēng)是“ASP.NET State Service ”(aspnet_state.exe)。它由Web.config文件中的stateConnectionString屬性來(lái)配置。該屬性指定了服務(wù)所在的服務(wù)器,以及要監(jiān)視的端口。
- <sessionState mode="StateServer" stateConnectionString="tcpip=127.0.0.1:42424" cookieless="false" timeout="20" />
在這個(gè)例子中,狀態(tài)服務(wù)在當(dāng)前機(jī)器的42424端口(默認(rèn)端口)運(yùn)行。要在服務(wù)器上改變端口和開(kāi)啟遠(yuǎn)程服務(wù)器的該功能,可編輯HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\aspnet_state\Parameters注冊(cè)表項(xiàng)中的Port值和AllowRemoteConnection修改成1。 顯然,使用狀態(tài)服務(wù)的優(yōu)點(diǎn)在于進(jìn)程隔離,并可在多站點(diǎn)中共享。 使用這種模式,會(huì)話狀態(tài)的存儲(chǔ)將不依賴(lài)于iis進(jìn)程的失敗或者重啟,然而,一旦狀態(tài)服務(wù)中止,所有會(huì)話數(shù)據(jù)都會(huì)丟失(這個(gè)問(wèn)題redis不會(huì)存在,重新了數(shù)據(jù)不會(huì)丟失)。
這里提供一段bat文件幫助修改注冊(cè)表,可以復(fù)制保存為.bat文件執(zhí)行
- 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
二.redis實(shí)現(xiàn)session共享
下面我們將使用redis來(lái)實(shí)現(xiàn)共享,首先要弄清楚session的幾個(gè)關(guān)鍵點(diǎn),過(guò)期時(shí)間,SessionId,一個(gè)SessionId里面會(huì)存在多組key/value數(shù)據(jù)。基于這個(gè)特性我將采用Hash結(jié)構(gòu)來(lái)存儲(chǔ),看看代碼實(shí)現(xiàn)。用到了上一篇提供的RedisBase幫助類(lèi)。
- 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;
- //更新緩存過(guò)期時(shí)間
- RedisBase.Hash_SetExpire(SessionID, DateTime.Now.AddMinutes(Timeout));
- }
- /// <summary>
- /// SessionId標(biāo)識(shí)符
- /// </summary>
- public static string SessionName = "Redis_SessionId";
- //
- // 摘要:
- // 獲取會(huì)話狀態(tài)集合中的項(xiàng)數(shù)。
- //
- // 返回結(jié)果:
- // 集合中的項(xiàng)數(shù)。
- public int Count
- {
- get
- {
- return RedisBase.Hash_GetCount(SessionID);
- }
- }
- //
- // 摘要:
- // 獲取一個(gè)值,該值指示會(huì)話是否為只讀。
- //
- // 返回結(jié)果:
- // 如果會(huì)話為只讀,則為 true;否則為 false。
- public bool IsReadOnly { get; set; }
- //
- // 摘要:
- // 獲取會(huì)話的唯一標(biāo)識(shí)符。
- //
- // 返回結(jié)果:
- // 唯一會(huì)話標(biāo)識(shí)符。
- public string SessionID
- {
- get
- {
- return GetSessionID();
- }
- }
- //
- // 摘要:
- // 獲取并設(shè)置在會(huì)話狀態(tài)提供程序終止會(huì)話之前各請(qǐng)求之間所允許的時(shí)間(以分鐘為單位)。
- //
- // 返回結(jié)果:
- // 超時(shí)期限(以分鐘為單位)。
- public int Timeout { get; set; }
- /// <summary>
- /// 獲取SessionID
- /// </summary>
- /// <param name="key">SessionId標(biāo)識(shí)符</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;
- }
- }
- //
- // 摘要:
- // 按名稱(chēng)獲取或設(shè)置會(huì)話值。
- //
- // 參數(shù):
- // name:
- // 會(huì)話值的鍵名。
- //
- // 返回結(jié)果:
- // 具有指定名稱(chēng)的會(huì)話狀態(tài)值;如果該項(xiàng)不存在,則為 null。
- public object this[string name]
- {
- get
- {
- return RedisBase.Hash_Get<object>(SessionID, name);
- }
- set
- {
- RedisBase.Hash_Set<object>(SessionID, name, value);
- }
- }
- // 摘要:
- // 判斷會(huì)話中是否存在指定key
- //
- // 參數(shù):
- // name:
- // 鍵值
- //
- public bool IsExistKey(string name)
- {
- return RedisBase.Hash_Exist<object>(SessionID, name);
- }
- //
- // 摘要:
- // 向會(huì)話狀態(tài)集合添加一個(gè)新項(xiàng)。
- //
- // 參數(shù):
- // name:
- // 要添加到會(huì)話狀態(tài)集合的項(xiàng)的名稱(chēng)。
- //
- // value:
- // 要添加到會(huì)話狀態(tài)集合的項(xiàng)的值。
- public void Add(string name, object value)
- {
- RedisBase.Hash_Set<object>(SessionID, name, value);
- }
- //
- // 摘要:
- // 從會(huì)話狀態(tài)集合中移除所有的鍵和值。
- public void Clear()
- {
- RedisBase.Hash_Remove(SessionID);
- }
- //
- // 摘要:
- // 刪除會(huì)話狀態(tài)集合中的項(xiàng)。
- //
- // 參數(shù):
- // name:
- // 要從會(huì)話狀態(tài)集合中刪除的項(xiàng)的名稱(chēng)。
- public void Remove(string name)
- {
- RedisBase.Hash_Remove(SessionID,name);
- }
- //
- // 摘要:
- // 從會(huì)話狀態(tài)集合中移除所有的鍵和值。
- public void RemoveAll()
- {
- Clear();
- }
- }
- }
下面是實(shí)現(xiàn)類(lèi)似在cs文件中能直接使用Session["UserId"]的方式,我的MyPage類(lèi)繼承Page實(shí)現(xiàn)了自己的邏輯主要做了兩件事 1:初始化RedisSession 2:實(shí)現(xiàn)統(tǒng)一登錄認(rèn)證,OnPreInit方法里面判斷用戶(hù)是否登錄,如果沒(méi)有登錄了則跳轉(zhuǎn)到登陸界面
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Web;
- using System.Web.UI;
- namespace ResidSessionDemo.RedisDemo
- {
- /// <summary>
- /// 自定義Page 實(shí)現(xiàn)以下功能
- /// 1.初始化RedisSession
- /// 2.實(shí)現(xiàn)頁(yè)面登錄驗(yàn)證,繼承此類(lèi),則可以實(shí)現(xiàn)所有頁(yè)面的登錄驗(yàn)證
- /// </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);
- //判斷用戶(hù)是否已經(jīng)登錄,如果未登錄,則跳轉(zhuǎn)到登錄界面
- if (!RedisSession.IsExistKey("UserCode"))
- {
- Response.Redirect("Login.aspx");
- }
- }
- }
- }
我們來(lái)看看Default.aspx.cs是如何使用RedisSession的,至此我們實(shí)現(xiàn)了和Asp.netSession一模一樣的功能和使用方式。
- RedisSession.Remove("UserCode");
相比StateServer,RedisSession具有以下優(yōu)點(diǎn)
1.redis服務(wù)器重啟不會(huì)丟失數(shù)據(jù) 2.可以使用redis的讀寫(xiě)分離個(gè)集群功能更加高效讀寫(xiě)數(shù)據(jù)
測(cè)試效果,使用nginx和iis部署兩個(gè)站點(diǎn)做負(fù)載均衡,iis1地址127.0.0.1:8002 iis2地址127.0.0.1:9000 nginx代理服務(wù)地址127.0.0.1:8003,不懂如何配置的可以去閱讀我的nginx+iis實(shí)現(xiàn)負(fù)載均衡這篇文章。我們來(lái)看一下測(cè)試結(jié)果。
訪問(wèn)127.0.0.1:8003 需要進(jìn)行登錄 用戶(hù)名為admin 密碼為123

登錄成功以后,重點(diǎn)關(guān)注端口號(hào)信息

刷新頁(yè)面,重點(diǎn)關(guān)注端口號(hào)信息

可以嘗試直接訪問(wèn)iis1地址127.0.0.1:8002 iis2地址127.0.0.1:9000 這兩個(gè)站點(diǎn),你會(huì)發(fā)現(xiàn)都不需要登錄了。至此我們的redis實(shí)現(xiàn)session功能算是大功告成了。
回到頂部
問(wèn)題拓展
使用redis實(shí)現(xiàn)session告一段落,下面留個(gè)問(wèn)題討論一下方案。微信開(kāi)發(fā)提供了很多接口,參考下面截圖,可以看到獲取access_token接口每日最多調(diào)用2000次,現(xiàn)在大公司提供的很多接口針對(duì)不對(duì)級(jí)別的用戶(hù)接口訪問(wèn)次數(shù)限制都是不一樣的,至于做這個(gè)限制的原因應(yīng)該是防止惡意攻擊和流量限制之類(lèi)的。那么我的問(wèn)題是怎么實(shí)現(xiàn)這個(gè)接口調(diào)用次數(shù)限制功能。大家可以發(fā)揮想象力參與討論哦,或許你也會(huì)碰到這個(gè)問(wèn)題。

先說(shuō)下我知道的兩種方案:
1.使用流量整形中的令牌桶算法,大小固定的令牌桶可自行以恒定的速率源源不斷地產(chǎn)生令牌。如果令牌不被消耗,或者被消耗的速度小于產(chǎn)生的速度,令牌就會(huì)不斷地增多,直到把桶填滿(mǎn)。后面再產(chǎn)生的令牌就會(huì)從桶中溢出。最后桶中可以保存的最大令牌數(shù)永遠(yuǎn)不會(huì)超過(guò)桶的大小。
說(shuō)淺顯點(diǎn):比如上面的獲取access_token接口,一天2000次的頻率,即1次/分鐘。我們令牌桶容量為2000,可以使用redis 最簡(jiǎn)單的key/value來(lái)存儲(chǔ) ,key為用戶(hù)id,value為整形存儲(chǔ)還可使用次數(shù),然后使用一個(gè)定時(shí)器1分鐘調(diào)用client.Incr(key) 實(shí)現(xiàn)次數(shù)自增;用戶(hù)每訪問(wèn)一次該接口,相應(yīng)的client.Decr(key)來(lái)減少使用次數(shù)。
但是這里存在一個(gè)性能問(wèn)題,這僅僅是針對(duì)一個(gè)用戶(hù)來(lái)說(shuō),假設(shè)有10萬(wàn)個(gè)用戶(hù),怎么使用定時(shí)器來(lái)實(shí)現(xiàn)這個(gè)自增操作呢,難道是循環(huán)10萬(wàn)次分別調(diào)用client.Incr(key)嗎?這一點(diǎn)沒(méi)有考慮清楚。
2.直接用戶(hù)訪問(wèn)一次 先進(jìn)行總次數(shù)判斷,符合條件再就進(jìn)行一次自增
兩種方案優(yōu)缺點(diǎn)比較 | ||
優(yōu)點(diǎn) | 缺點(diǎn) | |
令牌桶算法 | 流量控制精確 | 實(shí)現(xiàn)復(fù)雜,并且由于控制精確反而在實(shí)際應(yīng)用中有麻煩,很可能用戶(hù)在晚上到凌晨期間訪問(wèn)接口次數(shù)不多,白天訪問(wèn)次數(shù)多些。 |
簡(jiǎn)單算法 | 實(shí)現(xiàn)簡(jiǎn)單可行,效率高 | 流量控制不精確 |
總結(jié)
本篇從實(shí)際應(yīng)用講解了redis,后面應(yīng)該還會(huì)有幾篇繼續(xù)介紹redis實(shí)際應(yīng)用,敬請(qǐng)期待!
本篇文章用到的資源打包下載地址:redis_demo
svn下載地址:http://code.taobao.org/svn/ResidSessionDemo/