高扩展Web应用HTTP Session共享方案
在构建能够灵活地进行水平扩展、高可用性的Java Web应用程序时候,对http session的处理策略很大程度决定了应用程序的扩展性、可用性。一般而言对http session有如下的处理方案:
1、在服务器端不保存Session,完全无状态
对于不需要保持用户状态的Web应用,采用Stateless是最为恰当的,因此就不存在Session共享的问题。REST (Representational State Transfer) 算是最为典型的例子。
2、基于浏览器Cookie的Session共享
此种方案把用户相关的Session信息存储到浏览器的Cookie中,也称为客户端Session。
采用Flash Cookie、URL重写的方式传递Session信息的方案也可以归为此类。
缺点:只能够存储字符串、数值等基本类型的数据;Cookie大小存在限制;安全性;带宽及数据解压缩、网络传输性能问题。
3、基于数据库的Session共享,实现分布式应用间Session共享
此种方案把Session信息存储到数据库表,这样实现不同应用服务器间Session信息的共享。诸如Websphere Portal、Weblogic Portal都采用了类似的方案。
Tomcat Persistent Manager 的JDBC Based Store 提供了类似实现机制,表结构如下:
create table tomcat_sessions (
session_id varchar(100) not null primary key,
valid_session char(1) not null,
max_inactive int not null,
last_access bigint not null,
app_name varchar(255),
session_data mediumblob,
KEY kapp_name(app_name)
);
优点:实现简单
缺点:由于数据库服务器相对于应用服务器更难扩展且资源更为宝贵,在高并发的Web应用中,最大的性能瓶颈通常在于数据库服务器。因此如果将Session存储到数据库表,频繁的增加、删除、查询操作很容易造成数据库表争用及加锁,最终影响业务。
4、基于应用服务器/Servlet容器的Clustering机制
一些常用的应用服务器及Servlet容器的Clustering机制可以实现Session Replication的功能,例如Tomcat Clustering/Session Replication、Jboss buddy replication。
缺点:基于Clustering的Session复制性能很差,扩展性也很不行。
5、基于NFS的Session共享
通过NFS方式来实现各台服务器间的Session共享,各台服务器只需要mount共享服务器的存储Session的磁盘即可,实现较为简单。但NFS对高并发读写的性能并不高,在硬盘I/O性能和网络带宽上存在较大瓶颈,尤其是对于Session这样的小文件的频繁读写操作。
基于磁盘阵列/SAN/NAS等共享存储的方案道理也类似。
6、基于Terracotta、Ehcache、JBossCache等Java Caching方案实现Session共享
如果系统架构是Java体系,可以考虑采用Terracotta、Ehcache、JbossCache、Oscache等Java Caching方案来实现Session 共享。
缺点:架构用于非java体系很不方便;对于是诸如静态页面之类的缓存,采用Memcached的方案比Java更为高效
7、基于Memcached/Tokyo Tyrant 等Key-Value DB的Session共享
整体说来此种方案扩展性最好,推荐使用。
原理:Tomcat 服务器提供了org.apache.catalina.session.StandardManager 和org.apache.catalina.session.PersistentManager用于Session对象的管理,可以自定义PersistentManager的
Store 类来实现自己Memcached、Tokyo Tyrant、Redis等Key-Value DB的客户端。
以Memcached的客户端为例(摘自Use MemCacheStore in Tomcat):
package com.yeeach;import com.danga.MemCached.MemCachedClient; import com.danga.MemCached.SockIOPool;public class MemCacheStore extends StoreBase implements Store {/** * The descriptive information about this implementation. */protected static String info = "MemCacheStore/1.0";/** * The thread safe and thread local memcacheclient instance. */private static final ThreadLocal<MemCachedClient> memclient = new ThreadLocal<MemCachedClient>();/** * The server list for memcache connections. */private List<String> servers = new ArrayList<String>();/** * all keys for current request session. */private List<String> keys = Collections.synchronizedList(new ArrayList<String>());/** * Return the info for this Store. */public String getInfo() {return (info);}/** * Clear all sessions from the cache. */public void clear() throws IOException {getMemcacheClient().flushAll();keys.clear();}/** * Return local keyList size. */public int getSize() throws IOException {return getKeyList().size();}/** * Return all keys */public String[] keys() throws IOException {return getKeyList().toArray(new String[] {});}/** * Load the Session from the cache with given sessionId. * */public Session load(String sessionId) throws ClassNotFoundException,IOException {try {byte[] bytes = (byte[]) getMemcacheClient().get(sessionId);if (bytes == null)return null;ObjectInputStream ois = bytesToObjectStream(bytes);StandardSession session = (StandardSession) manager.createEmptySession();session.setManager(manager);session.readObjectData(ois);if (session.isValid() && !keys.contains(sessionId)) {keys.add(sessionId);}return session;} catch (Exception e) {return (null);}}/** * transform a vaild Session from objectinputstream. * Check which classLoader is responsible for the current instance. * * @param bytes * @return ObjectInputStream with the Session object. * @throws IOException */private ObjectInputStream bytesToObjectStream(byte[] bytes)throws IOException {ByteArrayInputStream bais = new ByteArrayInputStream(bytes);ObjectInputStream ois = null;Loader loader = null;ClassLoader classLoader = null;Container container = manager.getContainer();if (container != null)loader = container.getLoader();if (loader != null)classLoader = loader.getClassLoader();if (classLoader != null)ois = new CustomObjectInputStream(bais, classLoader);elseois = new ObjectInputStream(bais);return ois;}/** * remove the session with given sessionId */public void remove(String sessionId) throws IOException {getMemcacheClient().delete(sessionId);List<String> keyList = getKeyList();keyList.remove(sessionId);}/** * Store a objectstream from the session into the cache. */public void save(Session session) throws IOException {ByteArrayOutputStream baos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(baos);StandardSession standard = (StandardSession) session;standard.writeObjectData(oos);getMemcacheClient().add(session.getId(), baos.toByteArray());Object ob = getMemcacheClient().get(session.getId());List<String> keyList = getKeyList();keyList.add(session.getId());}/** * * @return */private List<String> getKeyList() {return keys;}/** * Simple instanc of the Memcache client and SockIOPool. * @return memchacheclient */private MemCachedClient getMemcacheClient() {if (memclient == null) {Integer[] weights = { 1 };// grab an instance of our connection poolSockIOPool pool = SockIOPool.getInstance();if (!pool.isInitialized()) {String[] serverlist = servers.toArray(new String[] {});// set the servers and the weightspool.setServers(serverlist);pool.setWeights(weights);// set some basic pool settings// 5 initial, 5 min, and 250 max conns// and set the max idle time for a conn// to 6 hourspool.setInitConn(5);pool.setMinConn(5);pool.setMaxConn(250);pool.setMaxIdle(1000 * 60 * 60 * 6);// set the sleep for the maint thread// it will wake up every x seconds and// maintain the pool sizepool.setMaintSleep(30);// set some TCP settings// disable nagle// set the read timeout to 3 secs// and don't set a connect timeoutpool.setNagle(false);pool.setSocketTO(3000);pool.setSocketConnectTO(0);// initialize the connection poolpool.initialize();}// lets set some compression on for the client// compress anything larger than 64kmemclient.get().setCompressEnable(true);memclient.get().setCompressThreshold(64 * 1024);}return memclient.get();}public List<String> getServers() {return servers;}public void setServers(String serverList) {StringTokenizer st = new StringTokenizer(serverList, ", ");servers.clear();while (st.hasMoreTokens()) {servers.add(st.nextToken());}}}<Context path="/test" docBase="test.war"><Manager className="org.apache.catalina.session.PersistentManager"distributable="true"><Store className="com.yeeach.MemcachedStore"servers="192.168.0.111:11211,192.168.0.112:11211" /></Manager></Context>