詳解集群內Session高可用的實現原理

來源:mp.weixin.qq.com 2016-05-22 17:54:00

在這個互聯網高度發達的時代,許多應用的用戶動輒成百上千萬,甚至上億。為了支持海量用戶的訪問,應用服務器集群這種水平擴展的方式是最常用的。這種情形下,就會涉及到許多單機環境下完全不需要考慮的問題,這其中session的創建、共享和存儲是最常見之一。

在單機環境中,Session的創建和存儲都是由同一個應用服務器實例來完成,而存儲也僅是內存中,最多會在正常的停止服務器的時候,把當前活動的Session鈍化到本地,再次啟動時重新加載。

而多個實例之間,Session數據是完全隔離的。而為了實現Session的高可用,多實例間數據共享是必然的,下麵我們以Redis的SessionManager實現多Tomcat實例Session共享的配置為例,我們來梳理下一般session共享的流程:

添加具體要使用的manager的Jar文件及其依賴

redissessionmanager依賴jedis,commons-pool,commons-pool2

對應版本的redissessionmanager的jar文件

在TOMCAT_HOME/conf/context.xml中增加如下配置

<ValveclassName="com.radiadesign.catalina.session.RedisSessionHandlerValve"/>

<ManagerclassName="com.radiadesign.catalina.session.RedisSessionManager"

host="localhost"

port="6379"database="0"

maxInactiveInterval="30"/>

其中host和port等替換為對應的配置信息

啟動多個Tomcat實例,以自帶的examples應用為例進行驗證

訪問examples應用的servlets/servlet/SessionExample,

在頁麵中添加數據到session中,並查看頁麵上對應的session信息

訪問另一個實例上相同應用的頁麵,查看session信息,兩者應該是一致的

使用redis-cli查看redis中存儲的對應數據,相應的sessionId對應的數據已經保存了下來

以上是一個基本的配置過程,而在這些配置與驗證的步驟中,第二步是核心邏輯實現。前麵的文章,曾介紹過Tomcat的Valve,在請求處理時,Pipeline中的各個Valve的invoke方法會依次執行。Tomcat的AccessLogValve介紹

此處的session處理,就是以一個自定義Valve的形式進行的。關於Session的文章,前麵也寫過幾篇,會附在結尾處

以下是RedisSessionhandlerValve的invoke方法,我們看,主要是在Valve執行後進行Session的存儲或移除。

publicvoidinvoke(Requestrequest,Responseresponse){

try{

getNext().invoke(request,response);

}finally{

finalSessionsession=request.getSessionInternal(false);

storeOrRemoveSession(session);

manager.afterRequest();

}

}

而session的保存和移除又是通過manager執行的。manager.save(session);manager.remove(session);

這裏,manager就是前麵定義的RedisSessionManager。默認單實例情況下,我們使用的都是StandardManager,對比一下兩者,標準的Manager對於session的創建和刪除,都會調到其父類ManagerBase中相應的方法,

publicvoidadd(Sessionsession){

sessions.put(session.getIdInternal(),session);

intsize=getActiveSessions();

if(size>maxActive){

synchronized(maxActiveUpdateLock){

if(size>maxActive){

maxActive=size;

}

}

}

}

publicvoidremove(Sessionsession,booleanupdate){

if(session.getIdInternal()!=null){

sessions.remove(session.getIdInternal());

}

}

我們來看,由於其隻保存在內存的Map中protectedMap<String,Session>sessions=newConcurrentHashMap<>(),每個Tomcat實例都對於不同的map,多個實例間無法共享數據。

對應到RedisSessionManager對於session的處理,都是直接操作redis,基本代碼是下麵這個樣:

publicvoidsave(Sessionsession)throwsIOException{

Jedisjedis=null;

Booleanerror=true;

try{

RedisSessionredisSession=(RedisSession)session;

BooleansessionIsDirty=redisSession.isDirty();

redisSession.resetDirtyTracking();

byte[]binaryId=redisSession.getId().getBytes();

jedis=acquireConnection();

if(sessionIsDirty||currentSessionIsPersisted.get()!=true){

jedis.set(binaryId,serializer.serializeFrom(redisSession));

}

currentSessionIsPersisted.set(true);

jedis.expire(binaryId,getMaxInactiveInterval());

}}

移除時的操作是這樣的

publicvoidremove(Sessionsession,booleanupdate){

Jedisjedis=null;

Booleanerror=true;

log.trace("RemovingsessionID:"+session.getId());

try{

jedis=acquireConnection();

jedis.del(session.getId());

error=false;

}finally{

if(jedis!=null){

returnConnection(jedis,error);

}

}

}

而此時,多個Tomcat實例都讀取相同的Redis,session數據是共享的,其它實例的初始請求過來時,由於會執行findSession的操作,此時會從Redis中加載session,

publicSessionfindSession(Stringid)throwsIOException{

RedisSessionsession;

if(id==null){

session=null;

currentSessionIsPersisted.set(false);

}elseif(id.equals(currentSessionId.get())){

session=currentSession.get();

}else{

session=loadSessionFromRedis(id);//看這裏,會從redis中load

if(session!=null){

currentSessionIsPersisted.set(true);

}

}

currentSession.set(session);

currentSessionId.set(id);

returnsession;

}

從而可以保證在一個實例被切換後,另外的實例可以繼續響應同一個session的請求。

以上即為Redis實現session共享高可用的一些關鍵內容。有興趣的朋友可以看下通過Memcached實現高可用,也是這個原理。順著這個思路,如果你有將Session存儲在其它地方的需求時,完全可以寫一個出來,自己動手,豐衣足食。

總結一下,我們是通過自定義的Valve來實現請求後session的攔截,同時,使用自定義的SessionManager,來滿足不同的session創建與存儲的需求。而至於是存儲在Redis/Memcached中,還是存儲在DB中,隻是位置的區別。原理,是一致的。

點擊查看原文

相關鏈接