【摘】Memcached(会话)

来源:互联网 发布:小米2s能用4g网络吗 编辑:程序博客网 时间:2024/06/14 08:13
 

环境: 一台apache和两台tomcat或resin做负载均衡,要求session跨二级域名。


 
如果用tomcat之间集群来session共享或者存储到数据库中效率都较低,还有一种方法是在apache中设置同一个session的连续访问都放到同一台tomcat上,名叫apache session sticky,这样避免做session共享也可以,但却没到真正的"负载均衡",那有什么意义呢?我现在使用的系统中JVM不定时会Crash,怀疑和几个GC参数有关,感觉Java/Tomcat真还不是个特健壮的平台,不敢把宝都押上。以上的几种方法并没有亲自试过,有实际运用经验的朋友欢迎多多交流。Google知道许多国外高流量的网站都使用memcached做session,在javaeye上也看过许多帖子,很受启发,但没有一个系统的解决方案,去年简单review过它的代码(看见C就来劲),就准备用它了!


memcached的官方网址是http://www.danga.com/memcached

  


有三种数据将会放入memcached

 


1. 全局配置信息。
比如树形的产品分类,因为要实现点击某分类可以看到该分类下所有子孙分类的所属产品,如果每次用SQL查询来解决这问题DB负载就太大了,所以在tomcat初始化后我们会把DB数据加载到内存,然后组织好便于查询。这些数据是多台tomcat共享的,也存在修改的可能,如果数据不放到一个服务上每次修改就需要同步数据,这样的系统就弱智了。


2. Session信息
上面已经提到过,同样是因为多台tomcat的缘故,干脆将数据放到一个服务中。


3. SQL查询结果集
每次执行查询都会拿该SQL语句去掉中间的空格后的字符串作为Key去memcached中查是否已经有过cache,如果有就避免了操作DB,大大提升系统性能,如果没有则去DB查询一次然后将结果放入memcached以便下次廉价使用。这些cache的数据记录可以设置一个过期时间戳,像SQL这种我想一般在短短几分钟之内吧,具体参数可以根据产品的需要来指定。


 
本文主要谈第二种情况Session共享的解决方法。


 
tomcat中session工作原理

 

先谈谈tomcat中session工作原理,其他web服务器大致相同。每来一次http请求,tomcat都会判断http header中是否有名字为JSESSIONID的cookie,如果没有则分配一个16字节的随机数,用session.getID()可以获得,最后写入response,同时把该JSESSIONID对应的session对象保存至系统内置的一个session table中,该cookie没有设置过期时间,于是保存在浏览器的内存中并没有写入系统cookie文件,所以直到用户关闭浏览器之前每次请求都会带有这个cookie,而每次重新打开一个新的浏览器窗口都会有不同的JSESSIONID也是这个缘故。这时候当用户下次再来请求便已经有了该JSESSIONID,倘若系统发现session table中没有该值对应的session对象,则意味着session已经过期或者用户调用过session.invalidate()方法。
伪代码如下(注意:这里忽略某些jsp或servlet指定不需要建立session的情况,如<%@ page session="false" %>):

Cookie cookie = request.getCookie("JSESSIONID");
Boolean newSession = false;
If (cookie == null)
{
 // 不带名为JSESSIONID的cookie
 newSession = true;
}
Else
{
 // 已经有JSESSIONID
 Session session = Session_table.gut(session_id);
 If (session == null)
 {
  // session已经过期或者用户调用过session.invalidate()方法
  newSession = true;
 }
}

 

If (newSession)
{
 // 随机生成session id
 String session_id = java.util.UUID.randomUUID().toString();
 // 放入系统内置表,哈希
 session_table.put(session_id, new Session(session_id));
 // 写入用户浏览器
 response.addCookie(new Cookie("JESESSIONID", session_id));
}

 

因为session的设计有连续一段时间没有访问就过期,所以在每次访问<%@ page session="true" %>的页面时都会更新该请求对应session的最终过期时间,而且tomcat默认该值就是true。同时tomcat将会有一个thread定期扫描session table,将那些已到达最后过期时间的session清除,这个就不敲伪代码了。

 

Session跨二级域名

 

这个好办,假设你的网站域名是www.my.com,只需设置cookie.setDomain(".my.com");,便可实现无论访问xxx.my.com还是yyy.my.com都可以共享cookie。如果你要集成其他系统,比如开源的BBS/Blog/Wiki,剩下的一步就是把各系统间的user表关联上即可,不过工作量似乎也不少,呵呵!

 

多台tomcat要求登录状态穿越

 

首先假象下一台tomcat或apache session sticky使用memcached的方式,每台tomcat独立生成session id,独立维护session table。这时在用户登录后便存储User对象到对应的memcached对象中即可,如memcachedClient.set(session.getID(), user);,但如果是多台tomcat非sticky模式理论上存在session id冲突的情形,一旦发生则系统及其难堪。如:

String session_id = session.getID();
// 如果两台tomcat的session_id相同则下面取出的user对象是最后使用这个相同的session_id调用memcachedClient.set登录的用户信息
User user = (User) memcachedClient.get(session_id);

结果不妙。并且当某用户点击退出后其他使用这个session_id的用户也被系统踢出。

解决办法很简单,既然每个村庄的每条河流都有一个惟一的名字,我们给每台tomcat取个惟一的名字就好了,在session_id这个作为memcached的key的字符串中含带该名即可,不过这样一来就用不上tomcat自动生成的JSESSIONID这个cookie了,于此同时可能会出现同一个用户同时登录,所以还是需要一个大的随机数来辨别。于是在用户登录的时候变成这样:

// 根据request的内容组织好user对象
User user = ……

// 从一个配置文件中读取当前tomcat的名字,这个有很多策略,还可用当前tomcat的IP+PORT,只要能惟一指定这个tomcat即可
String name = System.getPreperty("tomcat_name");
// 随机生成一个字符串再串联上当前tomcat名字
String key = java.util.UUID.randomUUID().toString() + "_" + name + "_" + user.getID();
// 放入memcached
memcachedClient.put(key, user);

从而保证session状态可以穿越各个tomcat,并且互相之间自由登录/退出并没有冲突。

 

一周不用重新登录

 

现在年轻人都有自己的电脑上网,不用担心以前在网吧一旦未退出某网站会被其他人偷窥隐私,所以现在有些网站提供一周/一月/一年不用重新登录的功能, 要实现这个也得靠cookie,在cookie中保存登录用户名或id,然后再加一个指定的过期时间即可,同时这也牵扯上session过期的问题,老的方式是tomcat来解决每次访问动态页面(访问html/jpg等静态资源除外)刷新session过期时间,现在使用memcached加上没有使用tomcat独立的session管理导致如何解决过期成为问题,虽然memcached支持保存记录时指定过期时间,但不可能每次访问动态资源都去刷新一次这个值吧,cache是为了便于读,倘若经常写就跑题了,如果不去修改记录过期时间又会出现memcached的使用内存长期累积最后爆掉,假如采取定时(每天凌晨3点)定时清除记录也颇觉粗暴。

 

解决办法还是靠cookie,记录加入memcached时给一个通用的过期时间,如1小时(这样一来就必须保证分配给memcached的内存大小必须可以支撑1小时N用户同时在线的情形,估算下一个User对象使用256B,memcached组织数据再花掉128B,就打一共512B,1GB的内存可支撑2097152用户,还不错),在tomcat中设置一个filter监视每次访问动态页面都重写一次已登录状态cookie的过期时间即可,如果真的过期则首先会无法读到cookie,意味着未登录,如果cookie未过期而在memcached中已过期无法命中这时则去DB查询一次再放入memcached即可。在我的设计中忽略所有取消session使用的语法,这一点会带来每次都要写客户端的cookie,但还好,工作放到了客户端,仅仅每次多传输几十个字节而已,这个流量的消耗很容易通过网页瘦身/压缩来交换。

 

cookie安全

 

曾发现过某网站系统中为实现上面讲到的一周不用重新登录将用户id以明文形式保存到cookie文件中,我试过这时候先关闭浏览器(session关闭),然后再打开一个新的浏览器窗口(产生新的session,并且该session对象中并不存在User对象),随意修改该id,再访问该系统,系统发现不存在User对象则根据用户id去DB查询,从而达到成功"易名"登录用户的目的,太危险了,必须对用户id进行加密。选择合适强度的对称加密算法。

 

实现

 

有四处地方需要编程。


1. filter


 配置对所有的.jsp/.do或系统中使用的servlet路径进行监视。
 // 取出你特别写入的cookie
 Cookie cookie = request.getCookie("my_cookie");
 If (cookie == null)
  ;
 Else
 {
  // 假设是一周
  cookie.setMaxAge(7 * 24 * 60 * 60);
  response.addCookie(cookie/*或者重新构造一个Cookie对象*/);
 }

 

2. 登录
 
 Cookie cookie = request.getCookie("my_cookie");
 If (cookie == null)
 {
  // 从request构造User对象
  User user = ……
  // 校验user and pass
  If (Valid(user))
  {
   // 合法用户名 密码
   String name = System.getPreperty("tomcat_name");
   // 随机生成一个字符串再串联上当前tomcat名字
   String key = java.util.UUID.randomUUID().toString() + "_" + name + "_" + user.getID();
   // 加密key
   key = encrypt(key);
   // 保存至memcached,1小时过期
   memcachedClient.set(key, user,1 * 60 * 60);
   // 构造cookie
   cookie = new Cookie("my_cookie", key);
   // 跨二级域名
   cookie.setDomain(".my.com");
   // 最大过期时间
   cookie.setMaxAge(7 * 24 * 60 * 60);
   // 保存至客户端浏览器的cookie
   response.addCookie(cookie);
  }
  Else
   // 非法登录
 }
 Else
  // 已登录
 
这里有一个问题,以上代码不可重入,可能存在这样的情况用户登录时候连续点击两次"登录"按钮可能会导致memcached中保存两条记录,出现memory leak,所以要求网页前端在每次点击后屏蔽该按钮使用。为什么在普通一台tomcat情况没有这个问题呢?因为这种情况两次点击共享了一个JSESSIONID,结果还是同一个session对象,只是一次内容替换而已,并没有带来memory leak。切记这一点。

 

3. 退出


 Cookie cookie = request.getCookie("my_cookie");
 If (cookie == null)
 {
  // 设置立即过期
  Cookie.setMaxAge(0);
  // 保存至客户端浏览器的cookie
  response.addCookie(cookie);

  // 取出加密后的key
  String key = cookie.getValue();
  // 解密
  key = decrypt(key);

  // 从memcached删除记录
  memcachedClient.remove(key);
 }


4. 已登录时取出User对象
 
 这种情形使用最频繁。
 Cookie cookie = request.getCookie("my_cookie");
 If (cookie == null)
  return null;
 // 只要cookie还存在就要理解为已登录状态

 // 取出加密后的key
 String key = cookie.getValue();
 // 解密
 key = decrypt(key);
 // 去memcached查询记录
 User user = (User) memcachedClient.get(key);
 If (user == null)
 {
  // 记录已过期,去DB查询
  user = DB.query(getUserID(key));
  if (user == null)
   return null;
  // 保存至memcached,一小时过期
  memcachedClient.set(key, user, 1 * 60 * 60);
 }

 return user;

 


 

原创粉丝点击