上一篇使用Redis實現Session共享方式雖然可行,但是實際操作起來卻很麻煩,現有程式碼已經是這個樣子了,總不可能全部換掉吧!好吧,這是個很實際的問題,那麼能不能實現無侵入式的分散式Session共享方案呢?mode=”InProc”這是web.config裡面使用iis程式儲存Session的配置,不知你注意過沒,mode除了InProc,SQLServer,StateServer這幾個常用的,還有一個Custom。這裡我要使用的是網友提供給的一種方自定義Session,需要繼承System.Web.SessionState.SessionStateStoreProviderBase實現自己的SessionStateStoreProvide,下面講解如何實現自定義的RedisSessionStateStore。
SessionStateStoreProviderBase
SessionStateStoreProviderBase是asp.net提供的定義資料儲存區的會話狀態提供程式所需的成員。像常用InProcSessionStateStore(mode=”InProc”),SqlSessionStateStore(mode=”SQLServer”),都是繼承SessionStateStoreProviderBase實現的儲存。我們來看看msdn對其成員的定義
成員 | 說明 |
---|---|
InitializeRequest 方法 | 執行會話狀態儲存提供程式必需的所有初始化操作。 |
EndRequest 方法 | 執行會話狀態儲存提供程式必需的所有清理操作。 |
Dispose 方法 | 釋放會話狀態儲存提供程式不再使用的所有資源。 |
GetItemExclusive 方法 | 從會話資料儲存區中檢索會話的值和資訊,並在請求持續期間鎖定資料儲存區中的會話項資料。GetItemExclusive 方法設定幾個輸出引數值,這些引數值將資料儲存區中當前會話狀態項的狀態通知給執行呼叫的 SessionStateModule。如果資料儲存區中未找到任何會話項資料,則GetItemExclusive 方法將 locked 輸出引數設定為false,並返回 null。這將導致 SessionStateModule呼叫 CreateNewStoreData 方法來為請求建立一個新的SessionStateStoreData 物件。如果在資料儲存區中找到會話項資料但該資料已鎖定,則GetItemExclusive 方法將 locked 輸出引數設定為true,將 lockAge 輸出引數設定為當前日期和時間與該項鎖定日期和時間的差,將 lockId 輸出引數設定為從資料儲存區中檢索的鎖定識別符號,並返回 null。這將導致SessionStateModule 隔半秒後再次呼叫GetItemExclusive 方法,以嘗試檢索會話項資訊和獲取對資料的鎖定。如果 lockAge 輸出引數的設定值超過ExecutionTimeout 值,SessionStateModule 將呼叫ReleaseItemExclusive 方法以清除對會話項資料的鎖定,然後再次呼叫 GetItemExclusive 方法。
如果 regenerateExpiredSessionId 屬性設定為 true,則 actionFlags 引數用於其 Cookieless 屬性為 true 的會話。actionFlags 值設定為 InitializeItem (1) 則指示會話資料儲存區中的項是需要初始化的新會話。通過呼叫CreateUninitializedItem 方法可以建立會話資料儲存區中未初始化的項。如果會話資料儲存區中的項已經初始化,則 actionFlags 引數設定為零。 如果提供程式支援無 Cookie 會話,請將 actionFlags 輸出引數設定為當前項從會話資料儲存區中返回的值。如果被請求的會話儲存項的 actionFlags 引數值等於InitializeItem 列舉值 (1),則 GetItemExclusive 方法在設定 actionFlags |
GetItem 方法 | 除了不嘗試鎖定資料儲存區中的會話項以外,此方法與GetItemExclusive 方法執行的操作相同。GetItem 方法在 EnableSessionState 屬性設定為 ReadOnly 時呼叫。 |
SetAndReleaseItemExclusive 方法 | 採用當前請求的 HttpContext 例項、當前請求的SessionID 值、包含要儲存的當前會話值的SessionStateStoreData 物件、當前請求的鎖定識別符號以及指示要儲存的資料是屬於新會話還是現有會話的值作為輸入。如果 newItem 引數為 true,則SetAndReleaseItemExclusive 方法使用提供的值將一個新項插入到資料儲存區中。否則,資料儲存區中的現有項使用提供的值進行更新,並釋放對資料的任何鎖定。請注意,只有與提供的 SessionID 值和鎖定識別符號值匹配的當前應用程式的會話資料才會更新。呼叫 SetAndReleaseItemExclusive 方法後,SessionStateModule 呼叫 ResetItemTimeout 方法來更新會話項資料的過期日期和時間。 |
ReleaseItemExclusive 方法 | 採用當前請求的 HttpContext 例項、當前請求的SessionID 值以及當前請求的鎖定識別符號作為輸入,並釋放對會話資料儲存區中的項的鎖定。在呼叫 GetItem 或GetItemExclusive 方法,並且資料儲存區指定被請求項已鎖定,但鎖定時間已超過 ExecutionTimeout 值時會呼叫此方法。此方法清除鎖定,釋放該被請求項以供其他請求使用。 |
RemoveItem 方法 | 此方法在 Abandon 方法被呼叫時呼叫。 |
CreateUninitializedItem 方法 | 採用當前請求的 HttpContext 例項、當前請求的SessionID 值以及當前請求的鎖定識別符號作為輸入,並向會話資料儲存區新增一個 actionFlags 值為InitializeItem 的未初始化項。如果 regenerateExpiredSessionId 屬性設定為 true,則 CreateUninitializedItem 方法用於無 Cookie 會話,這將導致遇到過期會話 ID 時,SessionStateModule 會生成一個新的 SessionID值。生成新的 SessionID 值的過程需要瀏覽器重定向到包含新生成的會話 ID 的 URL。在包含過期的會話 ID 的初始請求期間,會呼叫 CreateUninitializedItem 方法。SessionStateModule 獲取一個新的 SessionID 值來替換過期的會話 ID 之後,它會呼叫CreateUninitializedItem 方法以將一個未初始化項新增到會話狀態資料儲存區中。然後,瀏覽器重定向到包含新生成的 SessionID 值的 URL。如果會話資料儲存區中存在未初始化項,則可以確保包含新生成的 SessionID 值的重定向請求被視為新的會話,而不會被誤認為是對過期會話的請求。
會話資料儲存區中未初始化的項與新生成的 SessionID值關聯,並且僅包含預設值,其中包括到期日期和時間以及與 GetItem 和 GetItemExclusive 方法的actionFlags 引數相對應的值。會話狀態儲存區中的未初始化項應包含一個與 InitializeItem 列舉值 (1) 相等的actionFlags 值。此值由 GetItem 和GetItemExclusive 方法傳遞給SessionStateModule,並針對 SessionStateModule指定當前會話是新會話。然後,SessionStateModule將初始化該新會話,並引發 Session_OnStart 事件。 |
CreateNewStoreData 方法 | 採用當前請求的 HttpContext 例項和當前會話的Timeout 值作為輸入,並返回帶有空ISessionStateItemCollection 物件的新的SessionStateStoreData 物件、一個HttpStaticObjectsCollection 集合和指定的 Timeout值。使用 GetSessionStaticObjects 方法可以檢索 ASP.NET 應用程式的 HttpStaticObjectsCollection 例項。 |
上面的定義有點長,其實很多都在說明一點那就是原生了Session是單執行緒的方式實現的,當多個進行讀的時候會加鎖後面的會等待。我們下面實現的去掉了這些鎖,加快併發讀寫。
實現自己的RedisSessionStateStore
繼承SessionStateStoreProviderBase實現自己的RedisSessionStateStore也很簡單,只需繼承SessionStateStoreProviderBase重寫CreateNewStoreData,CreateUninitializedItem,GetItem等幾個方法即可,下面貼出程式碼,參考InProcSessionStateStore實現。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
/// <summary> /// 使用Redis實現SessionStateStoreProviderBase /// </summary> public class RedisSessionStateStore : SessionStateStoreProviderBase { /// <summary> /// 建立新的Session執行 /// </summary> public override SessionStateStoreData CreateNewStoreData(HttpContext context, int timeout) { return CreateLegitStoreData(context, null, null, timeout); } internal static SessionStateStoreData CreateLegitStoreData(HttpContext context, ISessionStateItemCollection sessionItems, HttpStaticObjectsCollection staticObjects, int timeout) { if (sessionItems == null) sessionItems = new SessionStateItemCollection(); if (staticObjects == null && context != null) staticObjects = SessionStateUtility.GetSessionStaticObjects(context); return new SessionStateStoreData(sessionItems, staticObjects, timeout); } public override void CreateUninitializedItem(HttpContext context, string id, int timeout) { RedisSessionState state = new RedisSessionState(null, null, timeout); RedisBase.Item_Set<string>(id, state.ToJson(),timeout); } private SessionStateStoreData DoGet(HttpContext context, string id, bool exclusive, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actionFlags) { locked = false; lockId = null; lockAge = TimeSpan.Zero; actionFlags = SessionStateActions.None; RedisSessionState state = RedisSessionState.FromJson(RedisBase.Item_Get<string>(id)); if (state == null) { return null; } RedisBase.Item_SetExpire(id, state._timeout); return CreateLegitStoreData(context, state._sessionItems, state._staticObjects, state._timeout); } /// <summary> /// 取值的時候執行 /// </summary> public override SessionStateStoreData GetItem(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actionFlags) { return this.DoGet(context, id, false, out locked, out lockAge, out lockId, out actionFlags); } public override SessionStateStoreData GetItemExclusive(HttpContext context, string id, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actionFlags) { return this.DoGet(context, id, true, out locked, out lockAge, out lockId, out actionFlags); } /// <summary> /// 新增 修改 移除鍵值時執行 /// </summary> /// <param name="item">item.Items為當前所有的鍵值對</param> public override void SetAndReleaseItemExclusive(HttpContext context, string id, SessionStateStoreData item, object lockId, bool newItem) { ISessionStateItemCollection sessionItems = null; HttpStaticObjectsCollection staticObjects = null; if (item.Items.Count > 0) sessionItems = item.Items; if (!item.StaticObjects.NeverAccessed) staticObjects = item.StaticObjects; RedisSessionState state2 = new RedisSessionState(sessionItems, staticObjects, item.Timeout); RedisBase.Item_Set<string>(id, state2.ToJson(), item.Timeout); } #region "未實現方法" public override void Dispose() { } public override void EndRequest(HttpContext context) { } public override void InitializeRequest(HttpContext context) { } public override void ReleaseItemExclusive(HttpContext context, string id, object lockId) { } public override void RemoveItem(HttpContext context, string id, object lockId, SessionStateStoreData item) { RedisBase.Item_Remove(id); } public override void ResetItemTimeout(HttpContext context, string id) { } public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback) { return true; } #endregion } internal sealed class SessionStateItem { public Dictionary<string, object> Dict; public int Timeout; } internal sealed class RedisSessionState { internal ISessionStateItemCollection _sessionItems; internal HttpStaticObjectsCollection _staticObjects; internal int _timeout; internal RedisSessionState(ISessionStateItemCollection sessionItems, HttpStaticObjectsCollection staticObjects, int timeout) { this.Copy(sessionItems, staticObjects, timeout); } internal void Copy(ISessionStateItemCollection sessionItems, HttpStaticObjectsCollection staticObjects, int timeout) { this._sessionItems = sessionItems; this._staticObjects = staticObjects; this._timeout = timeout; } public string ToJson() { // 這裡忽略_staticObjects這個成員。 if (_sessionItems == null || _sessionItems.Count == 0) { return null; } Dictionary<string, object> dict = new Dictionary<string, object>(_sessionItems.Count); string key; NameObjectCollectionBase.KeysCollection keys = _sessionItems.Keys; for (int i = 0; i < keys.Count; i++) { key = keys[i]; dict.Add(key, _sessionItems[key]); } SessionStateItem item = new SessionStateItem { Dict = dict, Timeout = this._timeout }; return JsonConvert.SerializeObject(item); } public static RedisSessionState FromJson(string json) { if (string.IsNullOrEmpty(json)) { return null; } try { SessionStateItem item = JsonConvert.DeserializeObject<SessionStateItem>(json); SessionStateItemCollection collections = new SessionStateItemCollection(); foreach (KeyValuePair<string, object> kvp in item.Dict) { collections[kvp.Key] = kvp.Value; } return new RedisSessionState(collections, null, item.Timeout); } catch { return null; } } } |
使用也很簡單,修改web.config,如下圖
然後可以保持原先程式碼不變,像Session[“UserCode”]=”admin”方式進行使用,但是現在的Session已經具備了分散式的特徵,支援跨域。這裡得說一下該方式的缺點,在GetItem和SetAndReleaseItemExclusive時需要對鍵值對進行反序列化和序列化操作,對於儲存資料量大的情況反而效能相對於系統提供的方式大打折扣,所以使用的時候需要考慮自己的實際場景。
總結
本來分散式Session共享到上篇就完結了,但是由於方案的可行性差,還有更好的方案,所以花了點時間參考了前面MSND中的說明,和ASP.net原始碼中InProcSessionStateStore的實現,解決GetItemExclusive等方法的併發鎖定問題,最終實現了Redis的儲存方式,更加靈活方便。這裡要感謝大家提的意見,讓我又學會了一個知識點!
資源下載地址:redis_demo
svn下載地址:http://code.taobao.org/svn/ResidSessionDemo/
本文參考:
sessionstatestoreproviderbase定義:https://msdn.microsoft.com/zh-cn/library/system.web.sessionstate.sessionstatestoreproviderbase(VS.80).aspx
sessionstatestoreproviderbase成員:https://msdn.microsoft.com/zh-cn/library/ms178587(v=vs.80).aspx