分布式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代码的侵入性较大,自己需要编写的代码较多。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

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

友情链接更多精彩内容