集群环境下的Session共享
一、Cookie机制和Session机制回顾
1)定义:Session成为“会话”,具体是指一个终端用户与交互系统进行通信的时间间隔,通常指从注册进入系统到注销退出系统之间所经过的时间。Session实际上是一个特定的时间概念。
2)HTTP协议与状态保持:HTTP 协议本身是无状态的,这与HTTP协议本来的目的是相符的,客户端只需要简单的向服务器请求下载某些文件,无论是客户端还是服务器都没有必要纪录彼此过去的行为,每一次请求之间都是独立的,好比一个顾客和一个自动售货机或者一个普通的(非会员制)大卖场之间的关系一样。
cookie的作用就是为了解决HTTP协议无状态的缺陷。至于后来出现的session机制则是又一种在客户端与服务器之间保持状态的解决方案。
3)Cookie和Session机制的区别和联系(几个有趣的例子):
1、该店的店员很厉害,能记住每位顾客的消费数量,只要顾客一走进咖啡店,店员就知道该怎么对待了。这种做法就是协议本身支持状态。
2、发给顾客一张卡片,上面记录着消费的数量,一般还有个有效期限。每次消费时,如果顾客出示这张卡片,则此次消费就会与以前或以后的消费相联系起来。这种做法就是在客户端保持状态。(Cookie原理)
3、发给顾客一张会员卡,除了卡号之外什么信息也不纪录,每次消费时,如果顾客出示该卡片,则店员在店里的纪录本上找到这个卡号对应的纪录添加一些消费信息。这种做法就是在服务器端保持状态。(Session原理)
由于HTTP协议是无状态的,cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案。同时我们也看到,由于采用服务器端保持状态的方案在客户端也需要保存一个标识,所以session机制可能需要借助于cookie机制来达到保存标识的目的,但实际上它还有其他选择。
二、集群下实现Session共享的几种方案
1.请求精确定位:基于IP地址的Hash策略,将同一用户的请求都集中在一台服务器上,这台服务器上保存了该用户的Session信息。缺点:单点部署发生宕机时,Session丢失。
2.Session复制共享:比如可以用Tomcat自带的插件进行Session同步,使得多台应用服务器之间自动同步Session,保持一致。如果一台发生故障,负载均衡会遍历寻找可用节点,Session也不会丢失。缺点:必须是Tomcat和Tomcat之间,Session的复制也会消耗系统 的性能,使得同步给成员时容易造成内网流量瓶颈。
3.基于cache DB缓存的Session共享(推荐,Spring-Session也是同样的原理,同自定义的JRedis一起配置可以实现目的):使用Redis存取Session信息,应用服务器发生故障时,当Session不在内存中时就会去CacheDB中查找(要求Redis支持持久化),找到则复制到本机,实现Session共享和高可用。
分布式Session配置原理图
其他方式:利用公共的NFS服务器做共享服务器、完全利用Cookie(将Session数据全放在Cookie中)等。
三、基于Redis的Session共享实现(核心代码)
1)原理:写一个Session过滤器拦截每一次请求,在这里检查由Cookie生成的SessionID,进行创建或获取。核心是实现使用装饰类,实现Session在Redis中的存取操作。
2)此处存取方式为 sessionID+sessionKey作为Redis的key ==== sessionValue作为Redis的value,这样保存了每次存取都从Redis中操作,效率更高。
3)注意:序列化方式推荐使用Apache下Commons组件——SerializationUtils 或 org.springframework.util.SerializationUtils
1 //定义请求经过的Session过滤器 2 public class SessionFilter extends OncePerRequestFilter implements Filter { 3 @Override 4 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 5 throws ServletException, IOException { 6 // 从cookie中获取sessionId,如果此次请求没有sessionId,重写为这次请求设置一个sessionId 7 String sid = CookieUtil.getCookieValue(request, GlobalConstant.JSESSIONID); 8 if (StringUtils.isEmpty(sid) || sid.length() != 36) { 9 sid = UUID.randomUUID().toString(); 10 CookieUtil.setCookie(request, response, GlobalConstant.JSESSIONID, sid, 60 * 60); 11 } 12 // 交给自定义的HttpServletRequestWrapper处理 13 filterChain.doFilter(new HttpServletRequestWrapper(sid, request, response), response); 14 } 15 }
1 //Cookie 2 public static void setCookie(HttpServletRequest request, 3 HttpServletResponse response, String name, String value, int seconds) { 4 if (StringUtils.isEmpty(name) || StringUtils.isEmpty(value)) 5 return; 6 Cookie cookie = new Cookie(name, value); 7 //cookie.setDomain(domain); 8 cookie.setMaxAge(seconds); 9 cookie.setPath("/"); 10 response.setHeader("P3P", 11 "CP='IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT'"); 12 response.addCookie(cookie); 13 } 14 15 public String getCookieValue(String name) 16 throws UnsupportedEncodingException { 17 Cookie cookies[] = request.getCookies(); 18 if (cookies != null) { 19 for (int i = 0; i < cookies.length; i++) { 20 if (name.equalsIgnoreCase(cookies[i].getName())) { 21 return cookies[i].getValue(); 22 } 23 } 24 } 25 return ""; 26 }
1 //SessionService实现 sidKey == sessionID+SessionKey 2 public Object getSession(String sidKey) { 3 Object realValue = null; 4 try { 5 String key = “SESSION_DISTRIBUTED_SESSIONID” + sidKey; 6 realValue = SerializeUtil.unserialize(RedisUtils.getInstance().get(key.getBytes())); 7 } catch (Exception e) { 8 LOG.error("Redis获取session异常" + e.getMessage(), e.getCause()); 9 } 10 return realValue; 11 } 12 13 public void saveSession(String sidKey, Object value) { 14 try { 15 String key = “SESSION_DISTRIBUTED_SESSIONID” + sidKey; 16 boolean isSetSuccess = RedisUtils.getInstance().set(key.getBytes(), SerializeUtil.serialize(value)); 17 if (!isSetSuccess) { 18 LOG.error("Redis保存session异常"); 19 } 20 } catch (Exception e) { 21 LOG.error("Redis保存session异常" + e.getMessage(), e.getCause()); 22 } 23 } 24 25 public void removeSession(String sidKey) { 26 try { 27 String key =“SESSION_DISTRIBUTED_SESSIONID”+ sidKey; 28 RedisUtils.getInstance().del(key.getBytes()); 29 } catch (Exception e) { 30 LOG.error("Redis删除session的attribute异常" + e.getMessage(), e.getCause()); 31 } 32 } 33 34 public void removeAllSession(String sid) { 35 try { 36 String keyPattern =“SESSION_DISTRIBUTED_SESSIONID” + sid + "*"; 37 Set<byte[]> keys = RedisUtils.getInstance().keys(keyPattern.getBytes()); 38 for (byte[] key : keys) { 39 RedisUtils.getInstance().del(key); 40 } 41 } catch (Exception e) { 42 LOG.error("Redis删除session异常" + e.getMessage(), e.getCause()); 43 } 44 } 45 46 public Set<String> getAllKeys(String sid) { 47 try { 48 Set<String> keysResult = new HashSet<String>(); 49 String keyPattern =“SESSION_DISTRIBUTED_SESSIONID” + sid + "*"; 50 Set<byte[]> keys = RedisUtils.getInstance().keys(keyPattern.getBytes()); 51 52 for (byte[] key : keys) { 53 keysResult.add(new String(key)); 54 } 55 return keysResult; 56 } catch (Exception e) { 57 LOG.error("Redis删除session异常" + e.getMessage(), e.getCause()); 58 return null; 59 } 60 }
1 HttpServletRequestWrapper extends javax.servlet.http.HttpServletRequestWrapper 2 private HttpSession session; 3 4 private HttpServletRequest request; 5 6 private HttpServletResponse response; 7 8 private String sid = ""; 9 10 public HttpServletRequestWrapper(HttpServletRequest request) { 11 super(request); 12 } 13 14 public HttpServletRequestWrapper(String sid, HttpServletRequest request) { 15 super(request); 16 this.sid = sid; 17 } 18 19 public HttpServletRequestWrapper(String sid, HttpServletRequest request, HttpServletResponse response) { 20 super(request); 21 this.request = request; 22 this.response = response; 23 this.sid = sid; 24 if (this.session == null) { 25 this.session = new HttpSessionWrapper(sid, super.getSession(false), request, response); 26 } 27 } 28 29 @Override 30 public HttpSession getSession(boolean create) { 31 if (this.session == null) { 32 if (create) { 33 this.session = new HttpSessionWrapper(this.sid, super.getSession(create), this.request, this.response); 34 return this.session; 35 } else { 36 return null; 37 } 38 } 39 return this.session; 40 } 41 42 @Override 43 public HttpSession getSession() { 44 if (this.session == null) { 45 this.session = new HttpSessionWrapper(this.sid, super.getSession(), this.request, this.response); 46 } 47 return this.session; 48 }
1 HttpSessionWrapper implements HttpSession{ 2 3 private String sid = ""; 4 5 private HttpSession session; 6 7 private HttpServletRequest request; 8 9 private HttpServletResponse response; 10 11 private SessionService sessionService = (SessionService) SpringContextHolder.getBean("sessionService"); 12 13 public HttpSessionWrapper() { 14 } 15 16 public HttpSessionWrapper(HttpSession session) { 17 this.session = session; 18 } 19 20 public HttpSessionWrapper(String sid, HttpSession session) { 21 this(session); 22 this.sid = sid; 23 } 24 25 public HttpSessionWrapper(String sid, HttpSession session, 26 HttpServletRequest request, HttpServletResponse response) { 27 this(sid, session); 28 this.request = request; 29 this.response = response; 30 } 31 32 33 @Override 34 public Object getAttribute(String name) { 35 return sessionService.getSession(this.sid+"#"+name); 36 } 37 38 @Override 39 public void setAttribute(String name, Object value) { 40 sessionService.saveSession(this.sid+"#"+name, value); 41 } 42 43 @Override 44 public void invalidate() { 45 sessionService.removeAllSession(this.sid); 46 CookieUtil.removeCookieValue(this.request,this.response, GlobalConstant.JSESSIONID); 47 } 48 49 @Override 50 public void removeAttribute(String name) { 51 sessionService.removeSession(this.sid+"#"+name); 52 } 53 54 @Override 55 public Object getValue(String name) { 56 return this.session.getValue(name); 57 } 58 59 @SuppressWarnings("unchecked") 60 @Override 61 public Enumeration getAttributeNames() { 62 return (new Enumerator(sessionService.getAllKeys(this.sid), true)); 63 } 64 65 @Override 66 public String getId() { 67 return this.sid; 68 }
69 }