java 分布式系统 - 依据redis实现session共享机制 - 初级篇
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了java 分布式系统 - 依据redis实现session共享机制 - 初级篇相关的知识,希望对你有一定的参考价值。
一、session的基本应用
大家认识session,可能更多是在java web中浏览器和服务器之间的会话交互。那么为什么要有session?
比如你登录一个系统,每次都要输入个人信息,是不是觉得不方便或者是很烦,首先http协议本身是没有状态可言的(同一个会话的2个请求没有识别能力,2个请求都互相不了解,它们都是由最新实例化的环境进行解析,除了应用本身已经存储在全局对象中所有信息,该环境不会保存与会话相关的任何信息。)所以客户端就使用cookie,服务器使用session,典型的应用是解决重复登录和购物车问题。
二、分布式下的session数据共享方案
大家都知道tomcat的session是依托于web应用的数据信息,Session是一块在服务器开辟的内存空间,其存储结构为ConcurrentHashMap(可以查看源码)那么在分布式系统下,怎么去解决数据在各个客户机之间的数据共享问题,这里给出一个最典型的方案,就是session模块独立出去,再结合redis,把他做成中间件的形式,是要客户机给我一个sessionId就能够获取存储在redis中的数据。
三、实现思路和源码设计
1、session - session对象
1 package com.zhouxi.redis.session;
2 3 import java.io.Serializable; 4 import java.text.DateFormat; 5 import java.text.SimpleDateFormat; 6 import java.util.Date; 7 8 /** 9 * session对象 -- 该对象的创建时间和maxAlive要保存到redis缓存中11 * 12 */ 13 public class Session implements Serializable { 14 15 private static final long serialVersionUID = 1L; 16 17 private String sessionId; // session id用来区分session 18 private String createTime; // 创建时间 19 private boolean isNew; // session对象是否是新创建的 20 private long maxAlive; // 设置session最大的存在时间--<计算方式是从创建到生命周期结束> 21 private boolean invalid; // 设置session是否失效 22 23 public Session(String sessionId){ 24 this(sessionId,SessionUtil.DEFAULT_CYCLE_TIME); // 设置默认的生命周期是30分钟 25 } 26 27 public Session(String sessionId, long maxAlive){ 28 Date date=new Date(); 29 DateFormat format=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 30 this.createTime=format.format(date); 31 this.sessionId = sessionId; 32 this.isNew = true; 33 this.maxAlive = maxAlive; // 默认session最大的存在时间是30分钟 34 this.invalid = false; // 默认情况下session不失效 35 } 36 37 /*---------------------set-get-------------------*/ 38 39 public void setCreateTime(String createTime) { 40 this.createTime = createTime; 41 } 42 43 public boolean isInvalid() { 44 return invalid; 45 } 46 47 public void setInvalid(boolean invalid) { 48 this.invalid = invalid; 49 } 50 51 public long getMaxAlive() { 52 return maxAlive; 53 } 54 55 public String getCreateTime() { 56 return createTime; 57 } 58 59 public boolean isNew() { 60 return isNew; 61 } 62 63 public void setNew(boolean isNew) { 64 this.isNew = isNew; 65 } 66 67 public String getSessionId() { 68 return sessionId; 69 } 70 }
2、HttpSession 接口 - 处理session的方法
package com.zhouxi.redis.session; /** * session api * @see tomcat->HttpSession * * */ public interface HttpSession { /** * Returns a string containing the unique identifier assigned to this * session. The identifier is assigned by the servlet container and is * implementation dependent. * @return a string specifying the identifier assigned to this session * @exception IllegalStateException * if this method is called on an invalidated session */ public String getSessionId(); /** * Returns the object bound with the specified name in this session, or * <code>null</code> if no object is bound under the name. * @param name * a string specifying the name of the object * @return the object with the specified name * @exception IllegalStateException * if this method is called on an invalidated session */ public Object getAttribute(String field); /** * Binds an object to this session, using the name specified. If an object * of the same name is already bound to the session, the object is replaced. * <p> * After this method executes, and if the new object implements * <code>HttpSessionBindingListener</code>, the container calls * <code>HttpSessionBindingListener.valueBound</code>. The container then * notifies any <code>HttpSessionAttributeListener</code>s in the web * application. * <p> * If an object was already bound to this session of this name that * implements <code>HttpSessionBindingListener</code>, its * <code>HttpSessionBindingListener.valueUnbound</code> method is called. * <p> * If the value passed in is null, this has the same effect as calling * <code>removeAttribute()</code>. * @param name * the name to which the object is bound; cannot be null * @param value * the object to be bound * @exception IllegalStateException * if this method is called on an invalidated session */ public long setAttribute(String field, Object value); /** * Removes the object bound with the specified name from this session. If * the session does not have an object bound with the specified name, this * method does nothing. * <p> * After this method executes, and if the object implements * <code>HttpSessionBindingListener</code>, the container calls * <code>HttpSessionBindingListener.valueUnbound</code>. The container then * notifies any <code>HttpSessionAttributeListener</code>s in the web * application. * @param name * the name of the object to remove from this session * @exception IllegalStateException * if this method is called on an invalidated session */ public void removeAttribute(String field); /** * Invalidates this session then unbinds any objects bound to it. * @exception IllegalStateException * if this method is called on an already invalidated session */ public void checkInvalidate(); /** * set session lifeTime * * */ public void setLifeCycleTime(); /** * get session对象 * * */ public Session getSession(); }
3、session工具类 - 用来组合创建时间和最大生命周期
package com.zhouxi.redis.session.util; /** * session的工具类 * @author zhouxi * * */ public final class SessionUtil { public static final String KEYNAME = "sessionInfo"; // 保存session相关信息的key值 public static final int DEFAULT_CYCLE_TIME = 1800; // 默认生命周期限制为30分钟 /** * 把创建时间和最大的存在时间进行组合 * @param createTime * @param maxAlive * @return * * */ public static String createTimeAndMaxAlive(String createTime,long maxAlive){ return createTime + ";" + maxAlive; } /** * 把查找出来的session信息进行分解,分解得到createTime和maxAlive * @param sessionInfo * @return * * */ public static String[] parseSessionInfo(String sessionInfo){ String [] infos = sessionInfo.split(";"); return infos; } }
4、Jedis中设置保存对象生存时间的方法
/** * 设置对应的key的生命周期 * @param key * @param milliseconds * @return * * */ @Override public long pexpireTime(String key, long milliseconds) { return jedis.pexpire(key, milliseconds); }
查看API可以知道,在redis2.1。3之后,只要调用这个方法,每次生命周期都会覆盖更新,这个刚好符合我们的需求。
5、HttpSession接口的具体实现类
package com.zhouxi.redis.session; import org.apache.log4j.Logger; import com.zhouxi.redis.command.JedisCommand; import com.zhouxi.redis.command.jedisCommandImpl; import com.zhouxi.redis.session.util.SessionUtil; /** * 共享session - redis使用hash的数据结构保存数据 * @see tomcat -> session封装 * */ public class DistributedSession implements HttpSession { private Session session; // session对象 private static JedisCommand jedisCommand; // JedisCommand操作对象--单例模式 private static Logger logger = Logger.getLogger(DistributedSession.class); private final String sessionId; public DistributedSession(String sessionId) { this(sessionId, SessionUtil.DEFAULT_CYCLE_TIME); } public DistributedSession(String sessionId, long maxAlive){ jedisCommand = jedisCommandImpl.getInstance(); // 初始化 this.sessionId = sessionId; if (getSessionCheckInfo() == null) { session = new Session(sessionId, maxAlive); // 生成一个session对象 // 每次生成新的对象就向redis保存重要的属性信息,而且这些信息是不能进行修改的 this.setAttribute(SessionUtil.KEYNAME, SessionUtil.createTimeAndMaxAlive(session.getCreateTime(), session.getMaxAlive())); this.setLifeCycleTime(); // 设置session的生命周期 } else { logger.info("这个sessionId已经保存..."); String[] infos = this.getSessionCheckInfo(); session = new Session(sessionId, Long.parseLong(infos[1])); session.setCreateTime(infos[0]); session.setNew(false); // 标记这个session不是最先创建的,之前在redis中已经保存了重要信息。 this.setLifeCycleTime(); // 刷新生命周期 } } /** * 从redis中查找session对象,每次在创建DistributedSession的时候判断redis内部是否 * 已经存在了相同ID的session对象,如果是那样吗,就避免每次都需要创建新的session,无法实现 session共享的功能 * @return * */ public String[] getSessionCheckInfo() { String info = (String) this.getAttribute(SessionUtil.KEYNAME); logger.info("从redis中获取的session保存的创建时间和最大生命周期时间限制为: " + info); if (info == null) return null; return SessionUtil.parseSessionInfo(info); } /** * 获取到redis中保存的sesion对象 * @param field * @return * */ @Override public Object getAttribute(String field){ checkInvalidate(); // 判断session是否失效 return jedisCommand.hashGet(this.sessionId, field); } /** * 将信息存放到redis中 * @param field * @param value * */ @Override public long setAttribute(String field, Object value) { checkInvalidate(); // 判断session是否失效 if(session != null) return jedisCommand.hashSet(session.getSessionId(), field, value); return 0; } /** * 通过session删除保存在redis中的数据 * @param name * */ @Override public void removeAttribute(String field) { checkInvalidate(); // 判断session是否失效 if(session != null) jedisCommand.hashDel(session.getSessionId(), field); else logger.info("session is null ..."); } /** * 设置session存在的生命周期--以秒为单位 * */ @Override public void setLifeCycleTime() { if(session != null){ // 设置对用sessionId的生命周期 jedisCommand.pexpireTime(session.getSessionId(), session.getMaxAlive() * 1000); logger.info("session刷新了生命周期为 " + session.getMaxAlive() + " 秒!"); }else{ logger.info("session is null ..."); } } /** * 判断session是否有效 * @return * */ @Override public void checkInvalidate() { if(session != null){ if (session.isInvalid()) { throw new IllegalStateException("Session is invalid......"); } } } @Override public String getSessionId() { return this.session.getSessionId(); } @Override public Session getSession() { return this.session; } }
6、解决每次调用session之后都要刷新生命周期,实现动态代理(拦截器的实现原理),每次调用HttpSession的方法都需要都动态代理类,然后代理类中在发现调用了HttpSesson的方法之后就会进行刷新生命周期,相当于进行更新pexpire中设置的方法。
package com.zhouxi.redis.session.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import org.apache.log4j.Logger; import com.zhouxi.redis.session.HttpSession; /** * 动态代理处理器 * @author zhouxi * */ public class DynamicProxyHandler implements InvocationHandler { private static Logger logger = Logger.getLogger(DynamicProxyHandler.class); private HttpSession session; // 被代理对象 /** * 实现拦截的方法 * @param obj 代理类对象 * @param method 被代理的接口方法 * @param objs 被代理接口方法的参数 * @return 方法调用返回的结果 * */ @Override public Object invoke(Object obj, Method method, Object[] params) throws Throwable { Object result = null; result=method.invoke(session, params); flushPexpireTime(session); return result; } /** * 创建动态代理对象,并绑定被代理类和代理处理器 * @param object * @return 被代理的类对象 * */ public HttpSession bind(HttpSession session) { this.session = session; return (HttpSession) Proxy.newProxyInstance(session.getClass().getClassLoader(), session.getClass().getInterfaces(), this); } /** * 每次调用HttpSession接口的方法就刷新redis的生命周期 * * */ public void flushPexpireTime(HttpSession session){ logger.info("使用了HttpSession的方法,重整session的生命周期..."); session.setLifeCycleTime(); } }
7、由于jedis对象和JedisCommand方法我都已经进行封装,Jedis我是采用读取配置文件的方法去连接redis,然后通过连接池获取Jedis实例,大概的session封装的思路
就是这样,测试方法如下。
package com.zhouxi.redis.test.session; import org.junit.Test; import com.zhouxi.redis.session.DistributedSession; import com.zhouxi.redis.session.HttpSession; import com.zhouxi.redis.session.proxy.DynamicProxyHandler; /** * 测试类 * * */ public class SessionJuit { /** * 创建2个session对象,都是session-1的ID,判断关键数据是否是一样的 * * */ @SuppressWarnings("unused") @Test public void test2(){ DynamicProxyHandler handler = new DynamicProxyHandler(); HttpSession session = new DistributedSession("session1",100); HttpSession sessionPorxy = handler.bind(session); // 创建动态代理对象 sessionPorxy.setAttribute("name", "zhouxi"); System.out.println("................session1第二个案例的数据................"); DynamicProxyHandler handler2 = new DynamicProxyHandler(); HttpSession session2 = new DistributedSession("session1"); HttpSession sessionPorxy2 = handler.bind(session); // 创建动态代理对象 System.out.println("session存储的数据是: " + sessionPorxy2.getAttribute("name")); System.out.println(sessionPorxy2.getSession().getMaxAlive()); System.out.println("................session2的数据................"); DynamicProxyHandler handler3 = new DynamicProxyHandler(); HttpSession session3 = new DistributedSession("session2"); HttpSession sessionPorxy3 = handler.bind(session3); // 创建动态代理对象 System.out.println("session存储的数据是: " + sessionPorxy3.getAttribute("name")); System.out.println(sessionPorxy3.getSession().getMaxAlive()); } }
package com.zhouxi.redis.test.session; import com.zhouxi.redis.session.DistributedSession; import com.zhouxi.redis.session.HttpSession; import com.zhouxi.redis.session.proxy.DynamicProxyHandler; public class SessionTest { /** * 测试Session中动态代理效果 * 每次调用Http的方法;都需要重整session的生命周期 * * */ public static void main(String[] args) { DynamicProxyHandler handler = new DynamicProxyHandler(); HttpSession session = new DistributedSession("sesson-1",1); HttpSession sessionPorxy = handler.bind(session); // 创建动态代理对象 sessionPorxy.setAttribute("name", "zhouxi"); Thread thread = new Thread(){ public void run(){ while(true){ try { sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("保存的信息是:" + sessionPorxy.getAttribute("sessionInfo")); } } }; thread.start(); } }
以上是关于java 分布式系统 - 依据redis实现session共享机制 - 初级篇的主要内容,如果未能解决你的问题,请参考以下文章