分布式session共享:session+redis+nginx
前一段时间师兄让我接触到session共享这个模块,因为有的项目跑在两台服务器上,有时候负载均衡之后,客户会突然跳到登录的界面,关于为什么要共享session这里就不再做过多赘述。这里记录一下自己的实现过程,方便以后查阅和改进。
项目是maven项目,跑在tomcat服务器上,关于tomcat、maven、redis、nginx也不做过度的介绍。
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-redis</artifactId> <version>1.8.1.RELEASE</version> </dependency>
<!-- redis 客户端配置 --> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="testWhileIdle" value="false"/> <property name="testOnBorrow" value="false"/> <property name="testOnReturn" value="false"/> </bean> <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <property name="hostName" value="${redis.host}" /> <property name="port" value="${redis.port}" /> <property name="password" value="${redis.password}" /> <property name="timeout" value="${redis.timeout}" /> <property name="poolConfig" ref="jedisPoolConfig" /> </bean> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="jedisConnectionFactory" /> <property name="keySerializer"> <bean class="org.springframework.data.redis.serializer.StringRedisSerializer"></bean> </property> <property name="valueSerializer"> <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"></bean> </property> </bean>
这是逻辑实现前的一些相关配置,关于session实现的逻辑,一开始的想法是在登录验证成功的时候,将HttpSession取出来,request.getSession()取到的是一个StandardSessionFacade对象,StandardSessionFacade是一个HttpSession的实现类,在将这个对象序列化,存入redis(同时在HttpSession中存入一个标记)。之后每次的请求,都会在一个Filter里面验证HttpSession中我们存的标记,如果标记不存在,可能访问的不是同一台服务器,再去redis中取。这个思路有一个问题就是序列化存入的redis的时候,序列化总是出错,开始使用Json序列化StandardSessionFacade对象,发现序列化总是不成功(这个时候还没有自己手写Json序列化的意识),后来使用JDK的序列化方式,也是不成功,原因是session中的attribute域中存的很多对象没有实现serializable接口,但是其中对象太繁杂,不好手动去改,就放弃了这种想法。
在后来的一种实现思路就是,自己写一个HttpSession的实现类UserSession,同时还要实现HttpServletRequestWrapper,自己写一个SessionFilter,在所有Filter之前就把request换成我们实现的类,同时这个类getSession调用的是我们写的UserSession,在UserSession中用自己定义的Hashmap来存储session的内容,并实时的在redis中进行存取。同样这样的方式也产生了不可序列化的问题,后来发现是关于用户权限的对象不可序列化,最终的解决办法是,将用户权限的关键信息用string类型存入redis,需要从redis中取出关于权限的信息时,再通过有效信息还原成关于权限的相关对象。
public class UserSession implements HttpSession { private String sid = ""; private HttpSession session; private HttpServletRequest request; private HttpServletResponse response; private Map<String, Object> map = null; private SessionService sessionService = (SessionService) SpringInit.getSpringContext().getBean("sessionService"); public UserSession() { } public UserSession(HttpSession session) { this.session = session; } public UserSession(String sid, HttpSession session) { this(session); this.sid = sid; } public UserSession(String sid, HttpSession session, HttpServletRequest request, HttpServletResponse response) { this(sid, session); this.request = request; this.response = response; } private Map<String, Object> getSessionMap() { if (this.map == null) { this.map = sessionService.getSession(this.sid , new HttpServletRequestWrapper(sid, request,response)); } return this.map; } @Override public Object getAttribute(String name) { if (this.getSessionMap() != null) { Object value = this.getSessionMap().get(name); return value; } return null; } @Override public void setAttribute(String name, Object value) { this.getSessionMap().put(name, value); sessionService.saveSession(this.sid, this.getSessionMap()); } @Override public void invalidate() { this.getSessionMap().clear(); sessionService.removeSession(this.sid); CookieUtil.removeCookieValue(this.request,this.response, GlobalConstant.JSESSIONID); } @Override public void removeAttribute(String name) { this.getSessionMap().remove(name); sessionService.saveSession(this.sid, this.getSessionMap()); } @Override public Object getValue(String name) { return this.session.getValue(name); } @SuppressWarnings("unchecked") @Override public Enumeration getAttributeNames() { return (new Enumerator(this.getSessionMap().keySet(), true)); } @Override public String[] getValueNames() { return this.session.getValueNames(); } @Override public void putValue(String name, Object value) { this.session.putValue(name, value); } @Override public void removeValue(String name) { this.session.removeValue(name); } @Override public long getCreationTime() { return this.session.getCreationTime(); } @Override public String getId() { return this.sid; } @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 boolean isNew() { return this.session.isNew(); } }
public class HttpServletRequestWrapper extends javax.servlet.http.HttpServletRequestWrapper{ private HttpSession session; private HttpServletRequest request; private HttpServletResponse response; private String sid = ""; public HttpServletRequestWrapper(HttpServletRequest request) { super(request); } public HttpServletRequestWrapper(String sid, HttpServletRequest request) { super(request); this.sid = sid; } public HttpServletRequestWrapper(String sid, HttpServletRequest request, HttpServletResponse response) { super(request); this.request = request; this.response = response; this.sid = sid; if (this.session == null) { this.session = new UserSession(sid, super.getSession(false), request, response); } } @Override public HttpSession getSession(boolean create) { if (this.session == null) { if (create) { this.session = new UserSession(this.sid, super.getSession(create), this.request, this.response); return this.session; } else { return null; } } return this.session; } @Override public HttpSession getSession() { if (this.session == null) { this.session = new UserSession(this.sid, super.getSession(), this.request, this.response); } return this.session; } }
public class SessionFilter extends OncePerRequestFilter implements Filter { // private static final Logger LOG = Logger.getLogger(SessionFilter.class); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String sid; if (request.getRequestURI().equals("/") || request.getRequestURI().equals("/merchant/login")){ filterChain.doFilter(request,response); }else { sid = CookieUtil.getCookieValue(request,GlobalConstant.COOKID); if (StringUtil.isEmpty(sid)){ sid = StringUtil.getUuid(); CookieUtil.setCookie(request, response, GlobalConstant. COOKID, sid, 60 * 60 ); } //交给自定义的HttpServletRequestWrapper处理 filterChain.doFilter(new HttpServletRequestWrapper(sid, request, response), response); } } }
@Service public class SessionService { private final static Logger LOG = Logger.getLogger(SessionService.class); private JdkSerializationRedisSerializer jdkSerializer = new JdkSerializationRedisSerializer(); @Autowired private RedisTemplate<String,Serializable> redisTemplate ; @Autowired private MyUserDetailsService myUserDetailsService; @SuppressWarnings("unchecked") public Map<String, Object> getSession(String sid , HttpServletRequest request) { Map<String, Object> session = new HashMap<String, Object>(); try { Object obj = redisTemplate.opsForValue() .get(RedisKeyUtil.SESSION_DISTRIBUTED_SESSIONID+sid); if(obj != null){ obj = jdkSerializer.deserialize((byte[])obj); session = (Map<String, Object>) obj; } } catch (Exception e) { LOG.error("Redis获取session异常" + e.getMessage(), e.getCause()); } return session; } public void saveSession(String sid, Map<String, Object> session) { try { redisTemplate.opsForValue() .set(RedisKeyUtil.SESSION_DISTRIBUTED_SESSIONID + sid, jdkSerializer.serialize(session), RedisKeyUtil.SESSION_TIMEOUT, TimeUnit.HOURS); } catch (Exception e) { LOG.error("Redis保存session异常" + e.getMessage(), e.getCause()); } } public void removeSession(String sid) { try { redisTemplate.delete( RedisKeyUtil.SESSION_DISTRIBUTED_SESSIONID + sid); } catch (Exception e) { LOG.error("Redis删除session异常" + e.getMessage(), e.getCause()); } } }
最后来看一下redis里的存储结果: