【原创】Tomcat集群环境下对session进行外部缓存的方法(1)
BJJC网改版,
计划将应用部署在tomcat集群上,集群的部署方案为Apache+Tomcat6,连接件为mod_jk,其中开启了session复制和粘性session。计划节点数为3个。
到这,或许就可以中止了,tomcat集群谁不会建啊?实现了fail-over,当节点1处理会话时如果突然宕掉,那么其他节点会迅速接管而且不停顿的执行服务,对客户端完全透明,apache也很好的执行了lb,虽然还没有进行性能测试,但是起码横向扩展是没有问题的。但是,仔细想想,觉得还是有些问题。
为了实现fail-over,启用了session复制,这每个节点都会留一份session的副本,对于大规模的访问,tomcat能否撑住?假如集群面临1000个并发访问,虽然这1000个请求的压力会分散到3个节点上,但是实际上每个节点都有1000个session数,从资源的消耗上并没有节省多少,这样的话,再大的访问量并不一定撑得住。其实session复制的主要目的就是为了某节点宕掉后其他节点能迅速的接管请求,其实就是一个替补的作用。存在于其他节点中的session其实大部分都是空闲并且高度冗余。也就是说,session复制在节点数增多或者访问量激增时是很耗费资源占用中间件内存的。集群虽然提高了可用性,但是性能没有大的提升,尤其集群节点增多时。每当一个session创建后,节点都要向集群分发session,这样,节点都忙着传播session去了,集群的吞吐量会下降。
所以,一种做法就是将session外部存储或者cache,也就是说,将分散在各个节点中的会话信息拿出来,集中式存储,每个节点接收到客户端的会话请求时都去session池中查找session。这其实并不是什么很时髦的做法,大型的网站很多都采用这种办法,加上前端的页面缓存,提高网站的并发访问量和可用性,只是,在tomcat下如何做?
通过查看tomcat源码,发现可以重写tomcat的会话管理器,自定义类接管该服务,对应session的创建、管理任务进行接管,将session对象从中间件内存中剥离出来进行外部存储。同时对于静态页面或者个性化信息极少的页面,进行页面级cache。
因此,对于外部缓存,我选择的是MemCache,我首先在MemCache上进行了试验。MemCache将是Session和页面的缓存地,不过后来我放弃使用MemCache来缓存Session,原因后面会说明。
首先,我定义了以下类结构来完成这个工作,包括接管Tomcat的session管理以及缓存session对象等一系列操作:
类说明:
CachedSessionManager:该类继承自ManagerBase类,后者为Tomcat的会话管理器。
CachedSession:自定义的Session类,继承自StandardSession,为自定义的一个Tomcat的Session对象。
通过以上两个类,首先将Tomcat的会话管理器架空,其次,对Tomcat处理的Session对象进行了重写,这样,就完全将Session从Tomcat中剥离出来了,Session管理器和被管理的对象都是我自定义的了。
ISessionCaching:接口,抽象了session缓存的各种操作接口,该接口的实现类具体将决定如何对提供的Session进行缓存,我分别实现了四种缓存方案,Map、MemCache、Oracle、TimeSten。
SessionCacheDb:ISessionCaching接口的实现类,提供了数据库缓存session的解决方案,该类继承自DbCacheSession,后者具体决定如何缓存Session至db。
SessionCacheMap:ISessionCaching接口的实现类,提供了JVM内部Map缓存,该方法主要用来测试是否正确的接管了Tomcat的Session管理并能完全的拦截Session对象,无实际意义。
SessionCacheMemCache:ISessionCaching接口的实现类,提供了MemCache缓存Session的解决方案,其中该类依赖于MemCachedManager类,后者具体决定将如何缓存Session至MemCache.
TimeStenCacheSession:ISessionCaching接口的实现类,提供了TimeSten的存储方案,其实该类和SessionCacheDb没有什么区别,就是数据源来源不同。
核心的类:
CachedSessionManager:
package com.thunisoft.session; import java.io.IOException; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import org.apache.catalina.Session; import org.apache.catalina.session.ManagerBase; import org.apache.catalina.session.StandardSession; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import com.thunisoft.cache.ISessionCaching; /** * SessionCacheManager,Session自定义管理器 * @author zhangxsh * */ public class CachedSessionManager extends ManagerBase { private static String cachepath; public String getCachepath() { return cachepath; } public void setCachepath(String cachepath) { this.cachepath = cachepath; } protected Log log = LogFactory.getLog(CachedSessionManager.class); /** * 定义如何将Session缓存 */ private ISessionCaching sessionCache; @Override public void add(Session session) { if (log.isDebugEnabled()) { log.debug("===================" + this.getSessionMaxAliveTime()); } initCache(); if (session != null) { sessionCache.addSession(session.getId(), (CachedSession) session, new Date(getExpireDate())); } } /** * 初始化Cache缓存,通过manager节点配置提供类名加载类 */ private synchronized void initCache() { if (sessionCache == null) { try { sessionCache = (ISessionCaching) Class.forName(cachepath) .newInstance(); sessionCache.setManager(this); } catch (InstantiationException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IllegalAccessException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } /** * 获取session超时时间 * * @return 时间毫秒数 */ private long getExpireDate() { return getCurrentTime() + 15 * 60 * 1000; } private long getCurrentTime() { return System.currentTimeMillis(); } @Override public Session createEmptySession() { System.out.println("createEmptySession"); return new CachedSession(this, sessionCache); } @Override public Session createSession() { if (log.isDebugEnabled()) { log.debug("createEmptySession:null"); } return createSession(null); } @Override public Session createSession(String sessionId) { if (log.isDebugEnabled()) { log.debug(sessionId + "--Session create"); } Session session = createEmptySession(); session.setNew(true); session.setValid(true); session.setCreationTime(System.currentTimeMillis()); session.setMaxInactiveInterval(this.maxInactiveInterval); if (sessionId == null) { sessionId = generateSessionId(); } session.setId(sessionId); return (session); } @Override public void expireSession(String sessionId) { initCache(); sessionCache.removeSession(sessionId); } @Override public Session findSession(String sessionId) throws IOException { initCache(); if (sessionId == null) { return null; } return sessionCache.findSession(sessionId); } @Override public Session[] findSessions() { // TODO Auto-generated method stub return super.findSessions(); } @Override protected synchronized String generateSessionId() { String sid = super.generateSessionId(); if (log.isDebugEnabled()) { log.debug("generateSessionId--" + sid); } // TODO Auto-generated method stub return sid; } @Override protected StandardSession getNewSession() { if (log.isDebugEnabled()) { log.debug("getNewSession"); } // TODO Auto-generated method stub return new CachedSession(this, sessionCache); } @Override public HashMap getSession(String sessionId) { Session s = (Session) sessionCache.getSession(sessionId); if (s == null) { if (log.isInfoEnabled()) { log.info("Session not found " + sessionId); } return null; } Enumeration ee = s.getSession().getAttributeNames(); if (ee == null || !ee.hasMoreElements()) { return null; } HashMap map = new HashMap(); while (ee.hasMoreElements()) { String attrName = (String) ee.nextElement(); map.put(attrName, getSessionAttribute(sessionId, attrName)); } return map; } @Override public String getSessionAttribute(String sessionId, String key) { initCache(); Session s = (Session) sessionCache.getSession(sessionId); if (s == null) { if (log.isInfoEnabled()) log.info("Session not found " + sessionId); return null; } Object o = s.getSession().getAttribute(key); if (o == null) return null; return o.toString(); } @Override public int getSessionMaxAliveTime() { // TODO Auto-generated method stub return super.getSessionMaxAliveTime(); } private int sessionAliveTime; public void setSessionAliveTime(int sessionAliveTime) { // TODO Auto-generated method stub if (log.isInfoEnabled()) log.info("sessionMaxAliveTime" + sessionMaxAliveTime); super.setSessionMaxAliveTime(sessionAliveTime); } @Override public void remove(Session session) { if (log.isInfoEnabled()) log.info("removeSession" + session.getId()); sessionCache.removeSession(session.getId()); } @Override public void setSessionIdLength(int idLength) { // TODO Auto-generated method stub super.setSessionIdLength(idLength); } public int getRejectedSessions() { // TODO Auto-generated method stub return 0; } @Override public void load() throws ClassNotFoundException, IOException { // TODO Auto-generated method stub } @Override public void setRejectedSessions(int arg0) { // TODO Auto-generated method stub } @Override public void unload() throws IOException { // TODO Auto-generated method stub } }
CachedSession:
package com.thunisoft.session; import java.io.Serializable; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.servlet.http.HttpSession; import org.apache.catalina.Manager; import org.apache.catalina.SessionListener; import org.apache.catalina.session.StandardSession; import com.thunisoft.cache.ISessionCaching; public class CachedSession extends StandardSession implements Serializable{ private static final long serialVersionUID = 1L; /** * session缓存方法接口 */ private ISessionCaching sessionCache; Map sessionMap=new ConcurrentHashMap(); /** * 重写默认构造方法,提供session管理器和缓存接口 * @param manager session管理器 * @param sessionCache session缓存 */ public CachedSession(Manager manager,ISessionCaching sessionCache){ super(manager); this.sessionCache=sessionCache; } @Override public void expire() { // TODO Auto-generated method stub super.expire(); } @Override public void setAttribute(String arg0, Object arg1, boolean arg2) { // TODO Auto-generated method stub /** * 每当修改了一次对象类型,便重新往manager中set一次 */ super.setAttribute(arg0, arg1, arg2); getManager().add(this); } @Override public void expire(boolean arg0) { // TODO Auto-generated method stub super.expire(arg0); } @Override public long getCreationTime() { // TODO Auto-generated method stub return super.getCreationTime(); } @Override public String getId() { // TODO Auto-generated method stub return super.getId(); } @Override public Manager getManager() { // TODO Auto-generated method stub return super.getManager(); } @Override public HttpSession getSession() { // TODO Auto-generated method stub return super.getSession(); } @Override public Object getValue(String name) { // TODO Auto-generated method stub return super.getValue(name); } @Override public String[] getValueNames() { // TODO Auto-generated method stub return super.getValueNames(); } @Override protected String[] keys() { // TODO Auto-generated method stub return super.keys(); } @Override public void putValue(String name, Object value) { // TODO Auto-generated method stub super.putValue(name, value); } @Override public void removeAttribute(String name, boolean notify) { // TODO Auto-generated method stub super.removeAttribute(name, notify); } @Override public void removeAttribute(String name) { // TODO Auto-generated method stub super.removeAttribute(name); } @Override protected void removeAttributeInternal(String arg0, boolean arg1) { // TODO Auto-generated method stub super.removeAttributeInternal(arg0, arg1); } @Override public void removeNote(String name) { // TODO Auto-generated method stub super.removeNote(name); } @Override public void removeSessionListener(SessionListener listener) { // TODO Auto-generated method stub super.removeSessionListener(listener); } @Override public void removeValue(String name) { // TODO Auto-generated method stub super.removeValue(name); } @Override public void setAttribute(String arg0, Object arg1) { // TODO Auto-generated method stub super.setAttribute(arg0, arg1); } @Override public void setId(String id) { // TODO Auto-generated method stub super.setId(id); } @Override public void setManager(Manager manager) { // TODO Auto-generated method stub super.setManager(manager); } @Override public String toString() { // TODO Auto-generated method stub return "session"; } }
DbCacheSession:
1 package com.thunisoft.cache.impl.dbimpl; 2 3 import java.io.ByteArrayInputStream; 4 import java.io.ByteArrayOutputStream; 5 import java.io.IOException; 6 import java.io.ObjectInputStream; 7 import java.io.ObjectOutputStream; 8 import java.sql.Connection; 9 import java.sql.DriverManager; 10 import java.sql.PreparedStatement; 11 import java.sql.ResultSet; 12 import java.sql.SQLException; 13 14 import org.apache.commons.logging.Log; 15 import org.apache.commons.logging.LogFactory; 16 17 import com.thunisoft.session.CachedSession; 18 19 /** 20 * 数据库缓存Session 21 * 22 * @author zhangxsh 23 * 24 */ 25 public class DbCacheSession { 26 27 private static final Log log = LogFactory.getLog(DbCacheSession.class); 28 29 private byte[] writeObject(CachedSession session) { 30 ByteArrayOutputStream byteout = new ByteArrayOutputStream(); 31 ObjectOutputStream objout = null; 32 try { 33 objout = new ObjectOutputStream(byteout); 34 session.writeObjectData(objout); 35 } catch (IOException e) { 36 log.error("get bytes from session failed!", e); 37 } 38 39 return byteout.toByteArray(); 40 } 41 42 /** 43 * 根据sessionId和Session空对象构造完整的session对象 44 * @param sessionId sessionid 45 * @param session 空session对象 46 * @return 反序列化后的session对象 47 */ 48 public CachedSession getSessionObject(String sessionId, 49 CachedSession session) { 50 51 return readObjectFromDb(sessionId, session); 52 } 53 54 /** 55 * 根据sessionId和Session空对象构造完整的session对象 56 * @param sessionId sessionid 57 * @param session 空session对象 58 * @return 反序列化后的session对象 59 */ 60 private CachedSession readObjectFromDb(String sessionId, 61 CachedSession session) { 62 PreparedStatement stat = null; 63 byte[] sessionBytes = null; 64 try { 65 stat = getConnection().prepareStatement( 66 "select c_session from t_session where c_sid=?"); 67 stat.setString(1, sessionId); 68 ResultSet rus = stat.executeQuery(); 69 while (rus.next()) { 70 sessionBytes = rus.getBytes(1); 71 } 72 } catch (SQLException e) { 73 // TODO Auto-generated catch block 74 e.printStackTrace(); 75 } 76 77 return readObject(sessionBytes, session); 78 } 79 80 /** 81 * 将Session对象序列化为二进制数据然后保存入库 82 * @param session session对象 83 */ 84 public void setSession(CachedSession session) { 85 byte[] sessionBytes = writeObject(session); 86 writeObjectIntoDb(session.getId(), sessionBytes); 87 88 } 89 90 public static void main(String[] args) { 91 // Session s=new Session(); 92 // s.setId("AERDS122223"); 93 DbCacheSession sess = new DbCacheSession(); 94 // sess.setSession(s); 95 String s = "AERDS122223"; 96 // System.out.println(sess.getSessionObject(s).getId()); 97 98 } 99 100 /** 101 * 将session保存入库,先删再插 102 * @param sessionId sid 103 * @param sessionBytes session对象二进制数据 104 */ 105 private void writeObjectIntoDb(String sessionId, byte[] sessionBytes) { 106 PreparedStatement stat = null; 107 Connection con = getConnection(); 108 try { 109 stat = con.prepareStatement("delete from t_session where c_sid=?"); 110 stat.setString(1, sessionId); 111 stat.execute(); 112 stat = con.prepareStatement("insert into t_session values(?,?)"); 113 stat.setString(1, sessionId); 114 stat.setBytes(2, sessionBytes); 115 stat.execute(); 116 } catch (SQLException e) { 117 e.printStackTrace(); 118 } finally { 119 try { 120 con.close(); 121 stat.close(); 122 } catch (SQLException e) { 123 // TODO Auto-generated catch block 124 e.printStackTrace(); 125 } 126 127 } 128 129 } 130 131 private CachedSession readObject(byte[] sessionBytes, CachedSession session) { 132 if (sessionBytes == null) { 133 return session; 134 } 135 ByteArrayInputStream ins = null; 136 ObjectInputStream objipt = null; 137 138 try { 139 ins = new ByteArrayInputStream(sessionBytes); 140 objipt = new ObjectInputStream(ins); 141 session.readObjectData(objipt); 142 ins.close(); 143 objipt.close(); 144 } catch (IOException e) { 145 log.error("get session from bytes failed!", e); 146 } catch (ClassNotFoundException e) { 147 log.error("sesializable session failed!", e); 148 } 149 System.out.println(session.getId() + "-session is found"); 150 return session; 151 152 } 153 154 protected Connection getConnection() { 155 Connection con = null; 156 try { 157 Class.forName("oracle.jdbc.driver.OracleDriver"); 158 con = DriverManager.getConnection( 159 "jdbc:oracle:thin:@127.0.0.1:1521:ORCL", "zhangxsh", 160 "zhangxsh"); 161 162 } catch (ClassNotFoundException e1) { 163 // TODO Auto-generated catch block 164 e1.printStackTrace(); 165 } 166 // Context ctx; 167 // DataSource ds = null; 168 // Connection con = null; 169 // try { 170 // ctx = new InitialContext(); 171 // ds = (DataSource) ctx.lookup("jdbc/oracle"); 172 // con = ds.getConnection(); 173 // 174 // } catch (NamingException e) { 175 // log.error("can not find jndi:" + "jdbc/oracle", e); 176 // } catch (SQLException e) { 177 // // TODO Auto-generated catch block 178 // e.printStackTrace(); 179 // } 180 // return con; 181 catch (SQLException e) { 182 // TODO Auto-generated catch block 183 e.printStackTrace(); 184 } 185 return con; 186 } 187 }
将该类定义为一个manager加入context.xml中,启动tomcat,不用对应用做任何修改,因为修改的是tomcat。
以Oracle数据库缓存(暂时将session保存到数据库中)为例,部署到Tomcat集群下面(2个节点)测试效果:
1.首先打开测试页面:
可见请求被lb至s1节点服务,sessionid为:
E4ACDD8588CBCC0BD41B1789E23F1E5F.s1,
新建会话打开相同链接:
发现被lb至s2节点,sessionid为:
8D1037E94D95E162179921AB7D8CEA80.s2
查询数据库缓存表:
发现这两个会话均被保存至表中。
下面提交一些信息至session看效果:
分别提交了三次,发现都可以正常的读取并显示出来,说明会话可以正确的被修改,并且不会丢失更改。
同时在会话2也做几次修改session的操作:
发现session之间互不影响,是正常的隔离的。
s1页面的session来自于节点s1,如果关闭s1,会怎么样呢?下面关闭s1节点并刷新s1页面,此时只有节点2存活:
发现一样可以正常读取session,该请求被lb至节点2,节点2正常接管服务,并正常的拿到该会话的session信息。
如果把两个节点都重启呢?发现结果都一样,session信息一样可以读取,如果把数据库中的session删除,刷新页面,session立刻就变了,这就验证了session信息已经完全脱离了中间件了。
请继续浏览后半部分