分布式Session

一、场景

在集群条件下,比如利用NGINX转发HTTP请求到多个TOMCAT,需要保证每个用户的SESSION在每个TOMCAT下都是一样的。否则会出现需要重复登录的情况,影响用户体验和浪费服务器性能。

二、方案

1、利用Ngnix的ip_hash负载均衡算法每次都将同一用户的所有请求转发至同一台服务器上。优点是简便快捷,缺点是如果那台服务器挂了,所有被hash到那台服务器的ip地址都将无法访问该网站。
2、利用tomcat自带的集群session复制共享,即每次session发生变化时,就广播给所有集群中的服务器,使所有的服务器上的session保持相同。配置不是很难,缺点是会消耗更多内存和带宽,tomcat官方推荐在集群比较小时采用此方案。
3、利用第三方开源组件Tomcat-redis-session-manager来实现session共享。这个方法需要在Tomcat下添加依赖jar包,简单配置context.xml,无代码侵入,但是依赖redis数据库。

三、Shiro实现Session共享

项目中的身份认证、权限管理经常要用到Shiro框架,Shiro也有一套自己的Session管理机制。shiro提供的会话可以用于JavaSE/JavaEE环境,不依赖于任何底层容器,可以独立使用,是完整的会话模块。即可以直接使用shiro的会话管理替换如web容器的会话管理。

Shiro会话管理器 sessionManager

shiro提供了三个默认实现:

  • DefaultSessionManager:DefaultSecurityManager使用的默认实现,用于JavaSE环境;
  • ServletContainerSessionManager:DefaultWebSecurityManager使用的默认实现,用于Web环境,其直接使用Servlet容器的会话;
  • DefaultWebSessionManager:用于Web环境的实现,可以替代ServletContainerSessionManager,自己维护着会话,直接废弃了Servlet容器的会话管理。

单机环境下我们使用DefaultWebSecurityManager的默认会话管理器ServletContainerSessionManager,使用的仍然是Tomcat底层的会话机制。

集群环境下要实现Session共享就要用到DefaultWebSessionManager,自己编写Dao来处理Session在Redis中的增删改查。

结合Spring的配置依赖注入如下:

    <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxIdle" value="1" />
        <property name="maxTotal" value="5" />
        <property name="blockWhenExhausted" value="true" />
        <property name="maxWaitMillis" value="30000" />
        <property name="testOnBorrow" value="true" />
    </bean>

    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="hostName" value="127.0.0.1" />
        <property name="port" value="6379"/>
        <property name="poolConfig" ref="jedisPoolConfig" />
        <property name="usePool" value="true"/>
    </bean>
        <!-- 利用springframework.data.redis操作redis -->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory"   ref="jedisConnectionFactory" />
    </bean>

    <bean id="redisManager" class="com.java1234.utils.RedisManager">
        <property name="redisTemplate" ref="redisTemplate"/>
    </bean>
    <bean id="shiroRedisSessionDAO" class="com.java1234.dao.RedisSessionDao">
        <property name="redisManager" ref="redisManager"/>
    </bean>
    <!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->
    <bean id="shareSession" class="org.apache.shiro.web.servlet.SimpleCookie">
        <!-- cookie的name,对应的默认是 JSESSIONID -->
        <constructor-arg name="name" value="SHAREJSESSIONID" />
        <!-- jsessionId的path为 / 用于多个系统共享jsessionId -->
        <property name="path" value="/" />
        <property name="httpOnly" value="true"/>
    </bean>

    <bean id="defaultWebSessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">

        <!-- session存储的实现 -->
        <property name="sessionDAO" ref="shiroRedisSessionDAO" />

        <!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->
        <property name="sessionIdCookie" ref="shareSession" />

        <!-- 设置全局会话超时时间,默认30分钟(1800000) -->
        <property name="globalSessionTimeout" value="1800000" />

        <!-- 是否在会话过期后会调用SessionDAO的delete方法删除会话 默认true -->
        <property name="deleteInvalidSessions" value="true" />

        <!-- 会话验证器调度时间 -->
        <property name="sessionValidationInterval" value="1800000" />

        <!-- 定时检查失效的session -->
        <property name="sessionValidationSchedulerEnabled" value="true" />
    </bean>


    <!-- 安全管理器 -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">  
      <property name="realm" ref="myRealm"/>
        <property name="sessionManager" ref="defaultWebSessionManager" />
    </bean>
//继承AbstractSessionDAO重写Session的怎能增删改查方法
public class RedisSessionDao extends AbstractSessionDAO {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private RedisManager redisManager;

    /**
     * The Redis key prefix for the sessions
     */
    private static final String KEY_PREFIX = "shiro_redis_session:";

    @Override
    public void update(Session session) throws UnknownSessionException {
        this.saveSession(session);
    }

    @Override
    public void delete(Session session) {
        if (session == null || session.getId() == null) {
            logger.error("session or session id is null");
            return;
        }
        redisManager.del(KEY_PREFIX + session.getId());
    }

    @Override
    public Collection<Session> getActiveSessions() {
        Set<Session> sessions = new HashSet<Session>();
        Set<byte[]> keys = redisManager.keys(KEY_PREFIX + "*");
        if(keys != null && keys.size()>0){
            for(byte[] key : keys){
                Session s = (Session) SerializerUtil.deserialize(redisManager.get(SerializerUtil.deserialize(key)));
                sessions.add(s);
            }
        }
        return sessions;
    }

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = this.generateSessionId(session);
        this.assignSessionId(session, sessionId);
        this.saveSession(session);
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        if(sessionId == null){
            logger.error("session id is null");
            return null;
        }

        Session s = (Session)redisManager.get(KEY_PREFIX + sessionId);
        return s;
    }

    private void saveSession(Session session) throws UnknownSessionException{
        if (session == null || session.getId() == null) {
            logger.error("session or session id is null");
            return;
        }
        //设置过期时间
        long expireTime = 18000000;
        session.setTimeout(expireTime);
        redisManager.setEx(KEY_PREFIX + session.getId(), session, expireTime);
    }

    public void setRedisManager(RedisManager redisManager) {
        this.redisManager = redisManager;
    }

    public RedisManager getRedisManager() {
        return redisManager;
    }
}
//统一操作Redis的Manager工具类
public class RedisManager {


    private RedisTemplate<Serializable, Serializable> redisTemplate;


    public <T> void set(String key, T obj) throws DataAccessException{
        final byte[] bkey = key.getBytes();
        final byte[] bvalue = SerializerUtil.serialize(obj);
        redisTemplate.execute(new RedisCallback<Void>() {
            @Override
            public Void doInRedis(RedisConnection connection) throws DataAccessException {
                connection.set(bkey, bvalue);
                return null;
            }
        });
    }


    public <T> boolean setNX(String key, T obj) throws DataAccessException{
        final byte[] bkey = key.getBytes();
        final byte[] bvalue = SerializerUtil.serialize(obj);
        boolean result = redisTemplate.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                return connection.setNX(bkey, bvalue);
            }
        });

        return result;
    }


    public <T> void setEx(String key, T obj, final long expireSeconds) throws DataAccessException{
        final byte[] bkey = key.getBytes();
        final byte[] bvalue = SerializerUtil.serialize(obj);
        redisTemplate.execute(new RedisCallback<Boolean>() {
            @Override
            public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
                connection.setEx(bkey, expireSeconds, bvalue);
                return true;
            }
        });
    }


    public <T> T get(final String key) throws DataAccessException{
        byte[] result = redisTemplate.execute(new RedisCallback<byte[]>() {
            @Override
            public byte[] doInRedis(RedisConnection connection) throws DataAccessException {
                return connection.get(key.getBytes());
            }
        });
        if (result == null) {
            return null;
        }
        return SerializerUtil.deserialize(result);
    }


    public Long del(final String key){
        if (StringUtils.isEmpty(key)) {
            return 0l;
        }
        Long delNum = redisTemplate.execute(new RedisCallback<Long>() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                byte[] keys = key.getBytes();
                return connection.del(keys);
            }
        });
        return delNum;
    }

    public Set<byte[]> keys(final String key){
        if (StringUtils.isEmpty(key)) {
            return null;
        }
        Set<byte[]> bytesSet = redisTemplate.execute(new RedisCallback<Set<byte[]>>() {
            @Override
            public Set<byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
                byte[] keys = key.getBytes();
                return connection.keys(keys);
            }
        });

        return bytesSet;
    }

    public RedisTemplate<Serializable, Serializable> getRedisTemplate() {
        return redisTemplate;
    }

    public void setRedisTemplate(RedisTemplate<Serializable, Serializable> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}

亲测过集群环境下没有问题,但是相对于Tomcat-redis-session-manager代码的侵入性较大,自己需要编写的代码较多。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,036评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,046评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,411评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,622评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,661评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,521评论 1 304
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,288评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,200评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,644评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,837评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,953评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,673评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,281评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,889评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,011评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,119评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,901评论 2 355

推荐阅读更多精彩内容

  • 分布式集群多个tomcat7+redis的session共享实现,科多大数据带你来看看 什么是Session/Co...
    大数据在说话阅读 1,128评论 0 10
  • 在讲解redis分布式session共享之前,我们先聊聊tomcat中session管理机制,包括:请求过程中se...
    48892085f47c阅读 28,969评论 3 43
  • 有时候,我喜欢静静地坐在某一个角落里,什么也不想地发回呆。时间仿佛凝固了,我茫然地融入四周虚无的世界里。 ...
    沽河小白阅读 456评论 0 3
  • 你不是傻子 只是看到了自己的影子 为了虚伪的面子 颠覆经典的案子 抛弃思考的脑子 变成浮夸的骗子 你毫无保留倾其所...
    萤火之枫阅读 207评论 0 1
  • 我喜欢的就是你!不是你的美,不是你的年轻,不是你的身材,不是你的可爱,不是你会做饭,不是你的善良,不是你的温暖。我...
    虚耗阅读 184评论 0 0