本節(jié)討論問題Memcached緩存有效期及SesstionStateProvider管理Session。
- DefaultExpireTime 和 對(duì)象序列化存儲(chǔ)
- SesstionStateProvider
MemcachedProvider是如何控制存儲(chǔ)數(shù)據(jù)的有效期的
一、DefaultExpireTime 和 對(duì)象序列化存儲(chǔ)
配置文件方式
View Code <?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="cacheProvider" type="MemcachedProviders.Cache.CacheProviderSection, MemcachedProviders"
allowDefinition="MachineToApplication" restartOnExternalChanges="true"/>
<sectionGroup name="enyim.com">
<section name="memcached" type="Enyim.Caching.Configuration.MemcachedClientSection, Enyim.Caching" />
</sectionGroup>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
</configSections>
<enyim.com>
<memcached>
<servers>
<!-- put your own server(s) here-->
<add address="127.0.0.1" port="11211" />
</servers>
<socketPool minPoolSize="10" maxPoolSize="100"
connectionTimeout="00:00:10" deadTimeout="00:02:00" />
</memcached>
</enyim.com>
<cacheProvider defaultProvider="MemcachedCacheProvider">
<providers>
<add name="MemcachedCacheProvider"
type="MemcachedProviders.Cache.MemcachedCacheProvider, MemcachedProviders" keySuffix="_MySuffix_" defaultExpireTime="2000"/>
</providers>
</cacheProvider>
<log4net>
<!-- Define some output appenders -->
<appender name="ConsoleAppender" type="log4net.Appender.ConsoleAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" />
</layout>
</appender>
<!--<threshold value="OFF" />-->
<!-- Setup the root category, add the appenders and set the default priority -->
<root>
<priority value="WARN"/>
<appender-ref ref="ConsoleAppender">
<filter type="log4net.Filter.LevelRangeFilter">
<levelMin value="WARN"/>
<levelMax value="FATAL"/>
</filter>
</appender-ref>
</root>
</log4net>
</configuration>
在配置文件中可以看到使用了DistCache使用用的緩存策略是MemcachedCacheProvider,如果不需要分布式緩存我們利用WebCache可以繼承CacheProvider重寫一個(gè)緩存Provider注入到Discache中。
keySuffix代表 Key后綴 即給你的Key加后綴,以方便使用MemcachedCacheProvider的不同客戶端區(qū)分各自的緩存數(shù)據(jù)
DefaultExpireTime 緩存數(shù)據(jù)有效期 毫秒為單位,雖然匹配了但默認(rèn)不使用的,必須通過指定的方法實(shí)現(xiàn)
看一下測(cè)試代碼
View Code using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MemcachedProviders.Cache;
using MemcachedProviders.Session;
using System.Threading;
namespace DemoTest
{
class Program
{
static void Main(string[] args)
{
//服務(wù)端緩存通行證數(shù)據(jù)10秒
DistCache.DefaultExpireTime = 10000;
//假如用戶登錄成功
User user = new User();
user.Name = "小憐香";
user.Age = 20;
user.Birthday = Convert.ToDateTime("1991-1-1");
user.Sex = false;
user.IsLoginSucess = true;
//生成通過證
string passCode = Guid.NewGuid().ToString();
Console.WriteLine("首次緩存數(shù)據(jù)-------------------8秒后讀取");
//緩存用戶信息(10秒) ,并將PassCode保存到Cookie中
DistCache.Add(passCode, user, true);
//如何讓用戶在操作期間 緩存不過期呢?
Thread.Sleep(8000);
Console.WriteLine(DistCache.Get<User>(passCode).Name);
Console.WriteLine("重新緩存--------------9秒后再讀取");
//從Cookie中獲取PassCode ,通過PassCode從緩存中讀取用戶數(shù)據(jù)后重新緩存
//User newUser = DistCache.Get<User>(passCode);
//DistCache.Remove(passCode);
DistCache.Add(passCode, user, DistCache.DefaultExpireTime); //Set方式重新緩存 (MemcacheProvider封裝不是很好)用第一方式設(shè)置還是Add
Thread.Sleep(8000); //如果上面的代碼沒有刷新緩存至10秒 那下面的代碼肯定會(huì)超時(shí)
if(DistCache.Get<User>(passCode)!=null)
Console.WriteLine(DistCache.Get<User>(passCode).Name);
else
Console.WriteLine("因?yàn)闆]有操作,緩存未刷新,導(dǎo)致數(shù)據(jù)過期");
Console.WriteLine("3秒后再次讀取數(shù)據(jù)----------");
Thread.Sleep(3000);
if (DistCache.Get<User>(passCode) != null)
Console.WriteLine(DistCache.Get<User>(passCode).Name);
else
Console.WriteLine("因?yàn)闆]有操作,緩存未刷新,導(dǎo)致數(shù)據(jù)過期");
//以上服務(wù)端的過程 即服務(wù)端用戶登錄數(shù)據(jù)先過期,PassCode取不到用戶登錄數(shù)據(jù),可以判定為登錄過期
//還有一種情況客戶端Cookie先過期 導(dǎo)致無法取到PassCode,因此服務(wù)端就無法獲取用戶數(shù)據(jù),可以判定為登錄過期。
Console.ReadLine();
}
[Serializable]
public class User
{
public string Name { set; get; }
public bool Sex { set; get; }
public int Age { set; get; }
public DateTime Birthday { set; get; }
public bool IsLoginSucess { set; get; }
}
}
}
結(jié)果
可以看到通過DistCache.Add("Name2", "小鳳仙我不認(rèn)識(shí)", true); 存儲(chǔ)的數(shù)據(jù)在3秒后再取時(shí)已經(jīng)過期清除了。
另外我們?cè)谑褂肕emcachedCacheProvider方法存儲(chǔ)對(duì)象時(shí),該對(duì)象一定要標(biāo)記為可序列化Serializable。當(dāng)然你也可以不使用MemcachedCacheProvider,自己先將對(duì)象先序列化再存儲(chǔ),取出來的時(shí)候自己在反序列化。(沒有壓縮功能)
既然MemcachedCacheProvider有緩存過期功能,是不是可以利用這一點(diǎn)+Cookie實(shí)現(xiàn)一個(gè)自定義Session功能呢
當(dāng)用戶登錄了系統(tǒng),這個(gè)時(shí)候生成一個(gè)GUID的PassCode通過行證做為緩存Key,并將用戶登錄信息保存到緩存中,同時(shí)PassCode根據(jù)請(qǐng)求輸出到客戶端Cookie中保存。此時(shí)Cookie也設(shè)置了有效期,服務(wù)端緩存也設(shè)置了有效期,因此可通過這兩種情況來驗(yàn)證用戶是否登錄過期。
當(dāng)Cookie中PassCode未過期,服務(wù)端可以從Cookie中獲取PassCode 去緩存中讀取用戶登錄數(shù)據(jù),如果用戶登錄數(shù)據(jù)有效,刷重新設(shè)置用戶數(shù)據(jù)有效期,并充許執(zhí)行相關(guān)操作。當(dāng)用戶數(shù)據(jù)過期時(shí)則認(rèn)為用戶登錄過期需重新登錄。
當(dāng)Cookie中PassCode過期,則服務(wù)端無法讀取PassCode,則認(rèn)為用戶登錄過期。 看一下服務(wù)端示例代碼,因?yàn)榭蛻舳酥灰袛嘁幌翽assCode是否存在就可以了。
View Code using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MemcachedProviders.Cache;
using MemcachedProviders.Session;
using System.Threading;
namespace DemoTest
{
class Program
{
static void Main(string[] args)
{
//服務(wù)端緩存通行證數(shù)據(jù)10秒
DistCache.DefaultExpireTime = 10000;
//假如用戶登錄成功
User user = new User();
user.Name = "小憐香";
user.Age = 20;
user.Birthday = Convert.ToDateTime("1991-1-1");
user.Sex = false;
user.IsLoginSucess = true;
//生成通過證
string passCode = Guid.NewGuid().ToString();
Console.WriteLine("首次緩存數(shù)據(jù)-------------------9秒后讀取");
//緩存用戶信息(10秒) ,并將PassCode保存到Cookie中
DistCache.Add(passCode, user, true);
//如何讓用戶在操作期間 緩存不過期呢?當(dāng)用戶每一次操作或訪問就刷新一下服務(wù)端的緩存
Thread.Sleep(9000);
Console.WriteLine(DistCache.Get<User>(passCode).Name);
Console.WriteLine("取出數(shù)據(jù)重新緩存--------------9秒后再讀取");
//從Cookie中獲取PassCode ,通過PassCode從緩存中讀取用戶數(shù)據(jù)后重新緩存
User newUser = DistCache.Get<User>(passCode);
DistCache.Remove(passCode);
DistCache.Add(passCode, newUser, true);
Thread.Sleep(9000); //如果上面的代碼沒有刷新緩存至10秒 那下面的代碼肯定會(huì)超時(shí)
if(DistCache.Get<User>(passCode)!=null)
Console.WriteLine(DistCache.Get<User>(passCode).Name);
else
Console.WriteLine("因?yàn)闆]有操作,緩存未刷新,導(dǎo)致數(shù)據(jù)過期");
Console.WriteLine("持續(xù)11秒沒有刷新操作,再次讀取數(shù)據(jù)----------");
Thread.Sleep(2000);
if (DistCache.Get<User>(passCode) != null)
Console.WriteLine(DistCache.Get<User>(passCode).Name);
else
Console.WriteLine("因?yàn)闆]有操作,緩存未刷新,導(dǎo)致數(shù)據(jù)過期");
//以上服務(wù)端的過程 即服務(wù)端用戶登錄數(shù)據(jù)先過期,PassCode取不到用戶登錄數(shù)據(jù),可以判定為登錄過期
//還有一種情況客戶端Cookie先過期 導(dǎo)致無法取到PassCode,因此服務(wù)端就無法獲取用戶數(shù)據(jù),可以判定為登錄過期。
Console.ReadLine();
}
[Serializable]
public class User
{
public string Name { set; get; }
public bool Sex { set; get; }
public int Age { set; get; }
public DateTime Birthday { set; get; }
public bool IsLoginSucess { set; get; }
}
}
}
上面處理實(shí)現(xiàn)Session會(huì)話(不是ASP.NET的Session)的優(yōu)點(diǎn):
1.解決跨域,跨窗口的會(huì)話認(rèn)證(針對(duì)ASP.NET的Session不跨窗口),解決A站點(diǎn)轉(zhuǎn)B站點(diǎn)或重新打開瀏覽器訪問的Session認(rèn)證問題。
2.分布式緩存Session或PassCode,實(shí)現(xiàn)認(rèn)證服務(wù)器負(fù)載均橫。
3.效率要高于數(shù)據(jù)庫(kù)。
缺點(diǎn):
1.一臺(tái)緩存服務(wù)器Down掉后,該服務(wù)器上所有Session或PassCode丟失,導(dǎo)致用戶訪問過期。
2.受分布式緩存讀取數(shù)據(jù)命中率影響,一旦未命中則導(dǎo)致用戶訪問過期。
3.分布式緩存效率要低于傳統(tǒng)緩存。
4.Cookie欺騙,偽造在有效期內(nèi)的PassCode去騙取服務(wù)器認(rèn)證。(這個(gè)問題無法避免,即使你用ASP.NET Session)
缺點(diǎn)的第1,2兩條,我們可以通過重寫MemcachedCacheProvider,增加寫入數(shù)據(jù)庫(kù)功能,同時(shí)這樣做了也會(huì)因反復(fù)讀寫更新數(shù)據(jù)庫(kù)而導(dǎo)致性能降低。當(dāng)用戶Cookie的PassCode還在有效期內(nèi),通過PassCode去找查緩存服務(wù)器中的用戶登錄信息,如果未查到(數(shù)據(jù)已過期或未命中亦或服務(wù)器Down掉),則去數(shù)據(jù)庫(kù)中查找并確認(rèn)是否已過期,如果未過期則讀取并寫入緩存中(針對(duì)服務(wù)器Down掉和丟失兩種情況)。第3條是沒有辦法,實(shí)現(xiàn)分布式總是損失部分性能的。至于第4條可以通過加密解密的方式處理。 個(gè)人覺得在不是特別的情況下沒必要增加數(shù)據(jù)庫(kù)備份功能,因?yàn)槟阋磸?fù)讀寫更新數(shù)據(jù)庫(kù)中這個(gè)緩存的有效期,以及過期后刪除等操作。如果一定要保證Session不丟失,那建議還是直接保存數(shù)據(jù)庫(kù)中,并在每次操作時(shí)更新數(shù)據(jù)的有效期。
Cookies跨域:正常的cookie只能在一個(gè)應(yīng)用中共享,即一個(gè)cookie只能由創(chuàng)建它的應(yīng)用獲得。關(guān)于如何跨域讀取Cookie方法很多,這里不介紹了。
URL后綴跨域:可將PassCode或SessionID通過URL后綴轉(zhuǎn)到其它站點(diǎn)。(相比Cookie就暴露了,雖然可以加密彌補(bǔ)但是還是無法防止別人借用)
下面來看一下Memcahed實(shí)現(xiàn)Asp.net的Session緩存功能,這個(gè)是即實(shí)現(xiàn)了寫入緩存中,又寫入數(shù)據(jù)庫(kù)中并實(shí)時(shí)更新有效期。
二、SesstionStateProvider
Session是由應(yīng)用服務(wù)器維持的一個(gè)服務(wù)器端的存儲(chǔ)空間,用戶在連接服務(wù)器時(shí),會(huì)由服務(wù)器生成一個(gè)唯一的SessionID,用該SessionID 為標(biāo)識(shí)符來存取服務(wù)器端的Session存儲(chǔ)空間。而SessionID這一數(shù)據(jù)則是保存到客戶端,用Cookie保存的,用戶提交頁(yè)面時(shí),會(huì)將這一 SessionID提交到服務(wù)器端,來存取Session數(shù)據(jù)。這一過程,是不用開發(fā)人員干預(yù)的。所以一旦客戶端禁用Cookie,那么Session可能會(huì)失效,也可能會(huì)傳Url傳遞即xxx.aspx?SessionID=xxxxxxxxx的形式。
這么看來,Session的原理其實(shí)和我們前面的設(shè)計(jì)的Cookie+PassCode+緩存功能是一致的。即打開瀏覽器請(qǐng)求后會(huì)產(chǎn)生一個(gè)Session會(huì)話,該會(huì)話有一個(gè)唯一標(biāo)識(shí)SessionID(相當(dāng)于PassCode),當(dāng)用戶登錄后成功后,將用戶登錄信息緩存至對(duì)應(yīng)用SessionID的Session存儲(chǔ)中(相當(dāng)于緩存)。當(dāng)響應(yīng)返回客戶端后,SessionID被保存在Cookie中。當(dāng)下一次請(qǐng)求回來時(shí),服務(wù)端會(huì)根據(jù)Cookie中的SessionID從Session存儲(chǔ)中取出對(duì)應(yīng)的Session,綁到上下文中。然后從上下文中的Session獲取相應(yīng)的用戶信息。
既然原理相同,那不可避免的都會(huì)碰到同樣類似的問題:
1.Session丟失
2.Cookie或URL后綴參數(shù)SessionID一樣可以欺騙
3.跨站點(diǎn),跨瀏覽器,跨窗口等等訪問一樣無法通行。
有人說利用Cookie+Session ,即Session存儲(chǔ)的用戶信息及其它可能更多的數(shù)據(jù)保存到Cookie中。
1.在Cookie有效期內(nèi)Session丟失了,即從Cookie中取回所有數(shù)據(jù)綁回HttpContext的Session中。
2.跨站點(diǎn),跨瀏覽器,跨窗口時(shí),在Cookie有效期內(nèi)將Session綁至其它站點(diǎn)。
3.加密解決欺騙問題
這個(gè)時(shí)候Cookie除了傳遞認(rèn)證標(biāo)識(shí),還兼具了彌補(bǔ)Session丟失和跨域的一個(gè)存儲(chǔ)載體了,即取代了Session備份數(shù)據(jù)庫(kù)的功能。那誰主導(dǎo)數(shù)據(jù)有效期控制呢?Session本身過期了,而Cookie沒有過期,這樣導(dǎo)致了客戶端一直有權(quán)限訪問站點(diǎn)。并且大量的信息被保存在客戶端中,雖然加密了,但仍可借用騙取通行。因此不提倡這種做法,Cookie本身是輕量型的,最好只擔(dān)當(dāng)SessionID或PassCode等標(biāo)識(shí)或一些客戶端自定義性的數(shù)據(jù)的保存和傳遞工作。所歸根到底,關(guān)于會(huì)話憑據(jù)及會(huì)話信息的備份工作是由誰來做?無疑這一塊還是交給數(shù)據(jù)庫(kù)來做最放心了,但實(shí)時(shí)的驗(yàn)證工作還是交給緩存或Session存儲(chǔ)本身來處理。
當(dāng)然你可以選擇使用Cookie+asp.net Session+SQL備份兼跨域,或直接使用Cookie+PassCode+分布式緩存跨域+SQL備份的方案。
甚至也可以兩者接合或 Cookie+asp.net Session+分布式緩存跨域+SQL備份。終于扯到了下面的內(nèi)容了:
Asp.net 本身提供了sessionState的策略注入方案,看一下web.config的配置文件
View Code <system.web>
<sessionState cookieless="false" regenerateExpiredSessionId="true" mode="Custom"
customProvider="MemcachedSessionProvider">
<providers>
<add name="MemcachedSessionProvider"
type="MemcachedProviders.Session.SessionStateProvider,MemcachedProviders"
connectionStringName="SqlSessionServices" dbType="SQL" writeExceptionsToEventLog="false" />
</providers>
</sessionState>
</system.web>
通過繼承SessionStateStoreProviderBase重寫Provider可以實(shí)現(xiàn)對(duì)Session的進(jìn)一步處理。即HttpApplication管線進(jìn)入HttpHandler的時(shí)候,SessionStateStoreProviderBase會(huì)重新創(chuàng)建Session綁定到HttpContext中,并根據(jù)SessionID從對(duì)應(yīng)的存儲(chǔ)體讀取數(shù)據(jù)初始化這個(gè)Session,當(dāng)HttpHandler結(jié)束請(qǐng)求時(shí),又會(huì)調(diào)用SessionStateStoreProviderBase將從HttpContext中讀取新進(jìn)Session的數(shù)據(jù)保存到對(duì)應(yīng)的存儲(chǔ)體(字典集合,緩存,數(shù)據(jù)庫(kù),分布式緩存等),并且清除掉過期的數(shù)據(jù),至此Session清除,整個(gè)過程是循環(huán)往復(fù)的,也就是說Session只存在HttpHandler請(qǐng)求期間。
MemcachedProviders/SessionStateProvider在HttpHandler處理請(qǐng)求結(jié)束前,從HttpContext讀取Session并寫入或更新到Memcahed緩存服務(wù)器中,同時(shí)清除掉Memcahed中過期的數(shù)據(jù),同時(shí)也觸發(fā)了數(shù)據(jù)庫(kù)相應(yīng)的操作(更新緩存有效期,刪除過期數(shù)據(jù))。當(dāng)下次請(qǐng)求至HttpHandler階段,Memcached又根SessionID讀取緩存中的Session綁回到HttpContext中。所以我們從HttpContext讀取的Session,都是從Memcahed緩存中讀取的。
可以看到providers的節(jié)點(diǎn)中MemcachedSessionProvider的屬性connectionStringName="SqlSessionServices" 指向了一個(gè)數(shù)據(jù)庫(kù)連接。
View Code <connectionStrings>
<add name="SqlSessionServices" connectionString="Data Source= LIULJ2576\SQL2008;Initial Catalog = Session;User Id = sa;Password = sa;"/>
</connectionStrings>
MemcachedProvider提供該數(shù)據(jù)庫(kù)的腳本,一張表tblSessions和相關(guān)操作存儲(chǔ)過程
proc_CleanExpiredData是清除過期數(shù)據(jù)的存儲(chǔ)過程。由于過期的數(shù)據(jù)或者直接關(guān)閉瀏覽器導(dǎo)致Session沒有清除,而MemcachedSessionProvider也沒有自動(dòng)清除這些Session,所以只能通過調(diào)用此存儲(chǔ)過程定期清除掉一些過期的會(huì)話。我們也可以通過SQL代理實(shí)現(xiàn)這一功能。
看一下Web訪問之后的數(shù)據(jù)表結(jié)果
當(dāng)同一個(gè)Session會(huì)話持續(xù)不斷,Expires和LockDate會(huì)不斷的刷新,以防止過期。Timeout為默認(rèn)或設(shè)置的過期時(shí)間(分鐘)。
規(guī)則是這樣,當(dāng)存儲(chǔ)的數(shù)據(jù)過了有效期后,當(dāng)有訪問這些數(shù)據(jù)發(fā)生時(shí),則MemcahedSesstionProvider會(huì)從表里清除這條對(duì)應(yīng)的SessionID記錄。同時(shí)系統(tǒng)彈出登錄過期,要求用戶重新登錄,這個(gè)時(shí)候只要用戶不是關(guān)閉瀏覽器登錄進(jìn)來的,還是會(huì)用之前的SessionID再寫入tblSessions表中。關(guān)閉瀏覽器IE會(huì)自動(dòng)清除Cookie上的SessionID,所以這個(gè)時(shí)候再打開瀏覽器就新產(chǎn)生了一個(gè)SessionID。通過SessionStateProvider在Memcahed中保存了Session,同時(shí)也會(huì)實(shí)時(shí)更新至數(shù)據(jù)庫(kù)。
既然Session都寫入數(shù)據(jù)庫(kù)了,完全可以在跨域時(shí)或者丟失從數(shù)據(jù)庫(kù)里讀取在有效期內(nèi)的Session,而沒必要通過分布式緩存讀取。當(dāng)不是跨域時(shí)訪問時(shí),又可以直接利用Session存儲(chǔ)而沒有必要通過Memcached讀取。(以上是誤解)事實(shí)是這樣的嗎? 通過對(duì)SessionStateProvider源碼分析,SessionStateProvider繼承重寫了SessionStateStoreProviderBase,使用Memcached已經(jīng)替代了原先Session的存儲(chǔ)機(jī)制,而數(shù)據(jù)庫(kù)則是充當(dāng)Memcahed的備份。SessionStateProvider并沒有畫蛇添足,并且確實(shí)考慮了所有情況,確實(shí)是個(gè)很完美的方案。
相反另一個(gè)問題就來了,為什么說Session會(huì)丟失,Session是在什么情況下丟失的?高負(fù)載,高并發(fā)?我沒有碰到這些情況,假如這些情況容易發(fā)生,相信放到Memcached中也會(huì)碰到這種問題,如果是這樣那在不注重效率的情況下,看來只有直接利數(shù)據(jù)庫(kù)存儲(chǔ)Session是最安全的了。web.config SessionState默認(rèn)支持存入數(shù)據(jù)庫(kù)存中的,只要配置一下就可以了。
View Code <sessionState mode="SQLServer" allowCustomSqlDatabase="true"
sqlConnectionString="server=.;uid=sa;password=;initial catalog=sd"
cookieless="false"
timeout="20">
</sessionState>
另外微軟也提供了一種會(huì)話分布式緩存解決方案: