分布式session基于redis的简单实现

公司的web服务器经过分布式之后,对于会话保持的功能完全依赖于nginx的ip会话保持,然而生产上仍然会出现突然会话失效导致用户被踢出的问题,,经过多次问题分析初步怀疑nginx会话保持功能偶尔失效的可能。所以做一个分布式session的组件来保证即使路由到不同服务器也不会被强制下线。

整体思路采用所有redis的字段全部存于redis上,本机只保留sessionid指针的方式,并不会把整个session拉下来缓存,而是以redis的hash结构,对于需要的sessionkey做单点查询。

实现方式是在web.xml中定义第一拦截器将Request对象替换为自定义的request,采用装饰模式重写构造方法与getSession方法。采用的是隐性替换的方式。。这里说一下装饰模式的原因是源码追踪时候发现原本过来的servletRequest就已经有了一个装饰器总线HttpServletRequestWrapper,那么还是延用了之前的设计思想。

先上一些代码 : 注:Ballon是公司封装了一层redis的外观,其实就是jedis连接池。到时候替换下就行了。

首先在web.xml中加入第一拦截器

 
    RedisSessionFilter
    cn.com.yitong.ares.filter.RedisSessionFilter
  

拦截器RedisSessionFilter.java 

注* :登录时重新刷新session是因为要保证即使用户退出时并未成功销毁session,也会写到不同session进行下次登录。因为是采用的会话作为用户身份标识,所以对会话安全要求较高

package cn.com.yitong.ares.filter;

import java.io.IOException;
import java.util.regex.Pattern;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import cfca.org.slf4j.MDC;
import cn.com.yitong.ares.consts.AresR;
import cn.com.yitong.modules.session.RedisHttpServletRequestWrapper;
import cn.com.yitong.modules.session.RedisHttpSession;
/**
 * 转换request为对象类型为redis依赖request
 * @author Mordred
 *
 */
public class RedisSessionFilter implements Filter{
  private Logger logger = LoggerFactory.getLogger(getClass());
  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
     logger.info("redis session filter init");
  }

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
    MDC.clear();
    HttpServletRequest re = (HttpServletRequest)request;  
    HttpServletResponse res = (HttpServletResponse)response;  
    RedisHttpServletRequestWrapper wrapper = new RedisHttpServletRequestWrapper(re,res); 
    RedisHttpSession session = (RedisHttpSession)wrapper.getSession();
    String sessionId = session.getId();
    String url = wrapper.getRequestURL().toString();
    //登录时刷新session
    if(checkUrlPattern(url, Pattern.compile("/login/HTLogin.do"))){
      String loginName = session.getAttributeObject(AresR.MDC_USER_ID, String.class);
      if(StringUtils.isNotBlank(loginName)){
        logger.info("登录时携带上一次会话,给予新sessionId loginName={}", loginName);
        session.invalidate();
        sessionId = wrapper.getSession().getId();
      }
    }
    logger.info("request : url={}, sessionId={}", url, sessionId);
    chain.doFilter(wrapper, response);  
  }

  @Override
  public void destroy() {
    logger.info("redis session filter destroy");
  }
  private boolean checkUrlPattern(String url, Pattern pattern) {
    java.util.regex.Matcher m = pattern.matcher(url);
    return m.find();
  }
}

RedisHttpServletRequestWrapper.java

package cn.com.yitong.modules.session;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import cn.com.yitong.ares.channel.Resource;
import cn.com.yitong.modules.dmb.util.CommonSeqUtil;
import cn.com.yitong.modules.reload.ReloadUtil;
import cn.com.yitong.util.security.MD5Encrypt;
/**
 * redis存session入口request类,负责创建与刷新session
 * @author Mordred
 *
 */
public class RedisHttpServletRequestWrapper extends HttpServletRequestWrapper{
  private RedisHttpSession session;
  private HttpServletResponse response;  
  private HttpServletRequest request;
  private Logger logger = LoggerFactory.getLogger(getClass());
  /**
   * 存于cookie中的sessionId键值
   */
  public static final String session_key = "JSESSIONID";//redisSessionId
  /**
   * 存于redis中的sessionId前缀
   */
  public static final String session_prefix = "sys_session:";
  public static final String session_exist_key = "sys_exist_symbol";
  public RedisHttpServletRequestWrapper(HttpServletRequest request, HttpServletResponse response) {
    super(request);
    this.request = request;
    this.response = response;
  }
  @Override  
  public HttpSession getSession(boolean create) {
    if(session != null  && session.isExist()){
      return session;
    }
    Cookie[] cookies = request.getCookies();
    String redisSessionId = null;
    if(cookies != null){
      for(Cookie cookie : cookies){
        if(session_key.equals(cookie.getName())){
          redisSessionId = cookie.getValue();
          logger.info("cookie : {}--{} ", session_key, redisSessionId);
          break;
        }
      }
    }
    if(create){
      if(StringUtils.isBlank(redisSessionId)){
        String newSessionId = saveNewSession();
        writeSidToCookie(newSessionId);
      }else if(isExist(redisSessionId)){
        refreshSession(redisSessionId);
      }else{
        String newSessionId = saveNewSession();
        writeSidToCookie(newSessionId);
      }
    }else{
      if(!StringUtils.isBlank(redisSessionId)){
        if(isExist(redisSessionId)){
          refreshSession(redisSessionId);
        }
      }
    }
    return session;  
  }  

  @Override  
  public HttpSession getSession() {  
      return getSession(true);  
  }  
  
  /**
   * 保存新session
   * @return
   */
  protected String saveNewSession(){
    String redisSessionId = MD5Encrypt.MD5(CommonSeqUtil.getSeqNo());
    saveSession(redisSessionId);
    return redisSessionId;
  }
  /**
   * 判断session是否存在于redis
   * @param redisSessionId
   * @return
   */
  protected boolean isExist(String redisSessionId){
    return "Y".equals(ReloadUtil.getBallon().hget(session_prefix + redisSessionId, session_exist_key));
  }
  /**
   * 保存session于redis
   * @param redisSessionId
   */
  protected void saveSession(String redisSessionId){
    ReloadUtil.getBallon().hset(session_prefix + redisSessionId, session_exist_key, "Y");
    refreshSession(redisSessionId);
  }
  /**
   * 刷新session失效时间
   * @param redisSessionId
   */
  protected void refreshSession(String redisSessionId){
    ReloadUtil.getBallon().expire(session_prefix + redisSessionId, Resource.getInt("SESSION_TIME_OUT"));
    setSession(redisSessionId);
  }
  /**
   * 设置session于类内部,只记录sessionId,内存中不存其余任何属性
   * @param redisSessionId
   */
  protected void setSession(String redisSessionId){
    session = new RedisHttpSession(null, redisSessionId);
  }
  /**
   * 返回sessionId给浏览器
   * @param sid
   */
  protected void writeSidToCookie(String sid) {  
      Cookie mycookies = new Cookie(session_key, sid); 
      String project = request.getContextPath();
      if(StringUtils.isNotBlank(project)){
        mycookies.setPath(project);
      }
      mycookies.setMaxAge(-1);  //60*60*24*365
      response.addCookie(mycookies);  
  }  
}

RedisHttpSession.java

其实只是一个壳子,里面只缓存了sessionid,剩下的字段都没有。所有数据都是去redis获取

注* :有的地方没有做assertExist校验是因为跑起来会报错,进去看了一下是一个listener类,会间歇性的调用我重写的方法,但是本身这个类就不存在实体字段所以过不去校验,会抛出异常。

package cn.com.yitong.modules.session;

import java.lang.reflect.ParameterizedType;
import java.util.Enumeration;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import javax.servlet.http.HttpSession;

import org.springframework.util.Assert;

import com.alibaba.fastjson.JSON;

import cn.com.yitong.modules.reload.ReloadUtil;
/**
 * session存入redis核心类,重写session中取值及赋值逻辑
 * @author Mordred
 *
 */
public class RedisHttpSession extends HttpSessionWrapper {
  private String sid;
  public static boolean invalidate(String sid){
    long flag = ReloadUtil.getBallon().del(RedisHttpServletRequestWrapper.session_prefix + sid);
    return flag == 1;
  }
  public RedisHttpSession(HttpSession session, String sid) {
    super(session);
    this.sid = sid;
  }
  @Override  
  public Enumeration getAttributeNames() {  
    assertNotExist();
    Vector keys = new Vector(ReloadUtil.getBallon().hkeys(RedisHttpServletRequestWrapper.session_prefix + sid));
    keys.remove(RedisHttpServletRequestWrapper.session_exist_key);
    return keys.elements();
  }  

  @Override  
  public void setAttribute(String name, Object value) {  
    ReloadUtil.getBallon().hset(RedisHttpServletRequestWrapper.session_prefix + sid, name, JSON.toJSONString(value));
  }  

  @Override  
  public Object getAttribute(String name) {  
    String valueString = ReloadUtil.getBallon().hget(RedisHttpServletRequestWrapper.session_prefix + sid, name);
    return JSON.parse(valueString);  
  }  
  /**
   * 外部调用,无需强转
   * @param name
   * @return
   */
  public  T getAttributeObject(String name , Class clazz){
    assertNotExist();
    Assert.notNull(name, "Attribute name must not be null");
    String valueString = ReloadUtil.getBallon().hget(RedisHttpServletRequestWrapper.session_prefix + sid, name);
    return (T)JSON.parseObject(valueString, clazz);
  }
  public void setAttributes(Map map){
    assertNotExist();
    Assert.notNull(map, "Attributes map must not be null");
    Set keys = map.keySet();
    for(String key : keys) {
      setAttribute(key, map.get(key));
    }
//    ReloadUtil.getBallon().hmset(RedisHttpServletRequestWrapper.session_prefix + sid, map);
  }
  @Override
  public void removeAttribute(String name){
    Assert.notNull(name, "Attribute name must not be null");
    ReloadUtil.getBallon().hdel(RedisHttpServletRequestWrapper.session_prefix + sid, name);
  };
  @Override  
  public String getId() {  
      return sid;  
  }  
  @Override
  public void invalidate(){
    RedisHttpSession.invalidate(sid);
  }
  protected boolean isExist(){
    return "Y".equals(ReloadUtil.getBallon().hget(RedisHttpServletRequestWrapper.session_prefix + sid, RedisHttpServletRequestWrapper.session_exist_key));
  }
  protected void assertNotExist(){
    if(!isExist()){
      throw new IllegalStateException("The session does not exist");
    }
  }
}

HttpSessionWrapper.java 模拟requestWrapper,提供装饰器总线

package cn.com.yitong.modules.session;

import java.util.Enumeration;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionContext;
/**
 * 装饰器总类,提供可override入口
 * @author Mordred
 *
 */
public class HttpSessionWrapper implements HttpSession {  
  
  private HttpSession session;  
  public HttpSessionWrapper(HttpSession session) {  
      this.session = session;  
  }  
  @Override  
  public long getCreationTime() {  
      return this.session.getCreationTime();  
  }  

  @Override  
  public String getId() {  
      return this.session.getId();  
  }  

  @Override  
  public long getLastAccessedTime() {  
      return this.session.getLastAccessedTime();  
  }  

  @Override  
  public ServletContext getServletContext() {  
      return this.session.getServletContext();  
  }  

  @Override  
  public void setMaxInactiveInterval(int interval) {  
      this.session.setMaxInactiveInterval(interval);  
  }  

  @Override  
  public int getMaxInactiveInterval() {  
      return this.session.getMaxInactiveInterval();  
  }  

  @Override  
  public HttpSessionContext getSessionContext() {  
      return this.session.getSessionContext();  
  }  

  @Override  
  public Object getAttribute(String name) {  
      return this.session.getAttribute(name);  
  }  

  @Override  
  public Object getValue(String name) {  
      return this.session.getValue(name);  
  }  

  @Override  
  public Enumeration getAttributeNames() {  
      return this.session.getAttributeNames();  
  }  

  @Override  
  public String[] getValueNames() {  
      return this.session.getValueNames();  
  }  

  @Override  
  public void setAttribute(String name, Object value) {  
      this.session.setAttribute(name,value);  
  }  

  @Override  
  public void putValue(String name, Object value) {  
      this.session.putValue(name,value);  
  }  

  @Override  
  public void removeAttribute(String name) {  
      this.session.removeAttribute(name);  
  }  

  @Override  
  public void removeValue(String name) {  
      this.session.removeValue(name);  
  }  

  @Override  
  public void invalidate() {  
      this.session.invalidate();  
  }  

  @Override  
  public boolean isNew() {  
      return this.session.isNew();  
  }  
}  

最后因为只重写了一部分方法,如果需要真正的隐式替换,那么要不保证全部方法重写,要不然就在上层做更高的外观,让业务接口调不到session,只能使用我们外放的方法。项目采用的第二种。

其实最开始上测试环境之前cookie里面定义的sessionid字段是叫做redisSessionId。但是IOS会出现问题,导致非JSESSIONID的cookie无法携带。


只是一个简单的request替换,肯定还是会有一些问题,但是暂时还是可以使用的。


 
  

你可能感兴趣的:(session)