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共享机制 - 初级篇的主要内容,如果未能解决你的问题,请参考以下文章

使用Spring Session做分布式会话管理

使用Spring Session做分布式会话管理

蚂蚁花呗4面:Redis+分布式架构+MySQL+linux+红黑树

三种状态保持机制

Java实现Redis分布式锁

Java应用XXRedis进阶