zoukankan      html  css  js  c++  java
  • shiro之redis频繁访问问题

    目前安全框架shiro使用较为广泛,其功能也比较强大。为了分布式session共享,通常的做法是将session存储在redis中,实现多个节点获取同一个session。此实现可以实现session共享,但session的特点是内存存储,就是为了高速频繁访问,每个请求都必须验证session是否存在是否过期,也从session中获取数据。这样导致一个页面刷新过程中的数十个请求会同时访问redis,在几毫秒内同时操作session的获取,修改,更新,保存,删除等操作,从而造成redis的并发量飙升,刷新一个页面操作redis几十到几百次。

    为了解决由于session共享造成的redis高并发问题,很明显需要在redis之前做一次短暂的session缓存,如果该缓存存在就不用从redis中获取,从而减少同时访问redis的次数。如果做session缓存,主要有两种种方案,其实原理都相同:

      1>重写sessionManager的retrieveSession方法。首先从request中获取session,如果request中不存在再走原来的从redis中获取。这样可以让一个请求的多次访问redis问题得到解决,因为request的生命周期为浏览器发送一个请求到接收服务器的一次响应完成,因此,在一次请求中,request中的session是一直存在的,并且不用担心session超时过期等的问题。这样就可以达到有多少次请求就几乎有多少次访问redis,大大减少单次请求,频繁访问redis的问题。大大减少redis的并发数量。此实现方法最为简单。

     1 package cn.uce.web.login.filter;
     2 
     3 import java.io.Serializable;
     4 
     5 import javax.servlet.ServletRequest;
     6 
     7 import org.apache.shiro.session.Session;
     8 import org.apache.shiro.session.UnknownSessionException;
     9 import org.apache.shiro.session.mgt.SessionKey;
    10 import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
    11 import org.apache.shiro.web.session.mgt.WebSessionKey;
    12 
    13 public class ShiroSessionManager extends DefaultWebSessionManager {
    14      /**
    15      * 获取session
    16      * 优化单次请求需要多次访问redis的问题
    17      * @param sessionKey
    18      * @return
    19      * @throws UnknownSessionException
    20      */
    21     @Override
    22     protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
    23         Serializable sessionId = getSessionId(sessionKey);
    24 
    25         ServletRequest request = null;
    26         if (sessionKey instanceof WebSessionKey) {
    27             request = ((WebSessionKey) sessionKey).getServletRequest();
    28         }
    29 
    30         if (request != null && null != sessionId) {
    31             Object sessionObj = request.getAttribute(sessionId.toString());
    32             if (sessionObj != null) {
    33                 return (Session) sessionObj;
    34             }
    35         }
    36 
    37         Session session = super.retrieveSession(sessionKey);
    38         if (request != null && null != sessionId) {
    39             request.setAttribute(sessionId.toString(), session);
    40         }
    41         return session;
    42     }
    43 }
     <!-- session管理器 -->
        <bean id="sessionManager" class="cn.uce.web.login.filter.ShiroSessionManager">
          <!-- 超时时间 -->
          <property name="globalSessionTimeout" value="${session.global.timeout}" />
          <!-- session存储的实现 -->
          <property name="sessionDAO" ref="redisSessionDAO" />
          <!-- <property name="deleteInvalidSessions" value="true"/> -->
          <!-- 定时检查失效的session -->
          <!-- <property name="sessionValidationSchedulerEnabled" value="true" /> -->
          <!-- <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
          <property name="sessionIdCookieEnabled" value="true"/> -->
          <property name="sessionIdCookie" ref="sessionIdCookie" />  
        </bean>

      2>session缓存于本地内存中。自定义cacheRedisSessionDao,该sessionDao中一方面注入cacheManager用于session缓存,另一方面注入redisManager用于session存储,当createSession和updateSession直接使用redisManager操作redis.保存session.当readSession先用cacheManager从cache中读取,如果不存在再用redisManager从redis中读取。注意:该方法最大的特点是session缓存的存活时间必须小于redis中session的存活时间,就是当redus的session死亡,cahe中的session一定死亡,为了保证这一特点,cache中的session的存活时间应该设置为s级,设置为1s比较合适,并且存活时间固定不能刷新,不能随着访问而延长存活。

    /**
     * 
     */
    package com.uc56.web.omg.authentication;
    
    
    import java.io.Serializable;
    import java.util.Collection;
    import java.util.Date;
    import java.util.HashSet;
    import java.util.Set;
    
    import org.apache.shiro.session.ExpiredSessionException;
    import org.apache.shiro.session.Session;
    import org.apache.shiro.session.UnknownSessionException;
    import org.apache.shiro.session.mgt.ValidatingSession;
    import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
    import org.apache.shiro.subject.support.DefaultSubjectContext;
    import org.crazycake.shiro.SerializeUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import com.uc56.web.omg.shiroredis.CustomRedisManager;
    
    
    
    /**
     * 将从redis读取的session进行本地缓存,本地缓存失效时重新从redis读取并更新最后访问时间,解决shiro频繁读取redis问题
     */
    public class CachingShiroSessionDao extends CachingSessionDAO {
    
        private static final Logger logger = LoggerFactory.getLogger(CachingShiroSessionDao.class);
        
        /** 保存到Redis中key的前缀 */
        private String keyPrefix = "";
    
        /**
         * jedis  操作redis的封装 
         */
        private CustomRedisManager redisManager;
        
    
        /**
         * 如DefaultSessionManager在创建完session后会调用该方法;
         * 如保存到关系数据库/文件系统/NoSQL数据库;即可以实现会话的持久化;
         * 返回会话ID;主要此处返回的ID.equals(session.getId());
         */
        @Override
        protected Serializable doCreate(Session session) {
            // 创建一个Id并设置给Session
            Serializable sessionId = this.generateSessionId(session);
            assignSessionId(session, sessionId);
            this.saveSession(session);
            return sessionId;
        }
        
        /**
         * 重写CachingSessionDAO中readSession方法,如果Session中没有登陆信息就调用doReadSession方法从Redis中重读
         */
        @Override
        public Session readSession(Serializable sessionId) throws UnknownSessionException {
            Session session = getCachedSession(sessionId);
            if (session == null
                    || session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) == null) {
                session = this.doReadSession(sessionId);
                if (session == null) {
                    throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
                } else {
                    // 缓存
                    cache(session, session.getId());
                }
            }
            return session;
        }
        
    
        /**
         * 根据会话ID获取会话
         *
         * @param sessionId 会话ID
         * @return 
         */
        @Override
        protected Session doReadSession(Serializable sessionId) {
            ShiroSession shiroSession = null;
            try {
                shiroSession = (ShiroSession)SerializeUtils.deserialize(redisManager.get(this.getByteKey(sessionId)));
                if (shiroSession != null 
                        && shiroSession.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY) != null) {
                    //检查session是否过期
                    shiroSession.validate();
                    // 重置Redis中Session的最后访问时间
                    shiroSession.setLastAccessTime(new Date());
                    this.saveSession(shiroSession);
                    logger.info("sessionId {} name {} 被读取并更新访问时间", sessionId, shiroSession.getClass().getName());
                }
            } catch (Exception e) {
                if (!(e instanceof ExpiredSessionException)) {
                    logger.warn("读取Session失败", e);
                }else {
                    logger.warn("session已失效:{}", e.getMessage());
                }
            }
    
            return shiroSession;
        }
        
        //扩展更新缓存机制,每次请求不重新更新session,更新session会延长session的失效时间
        @Override
        public void update(Session session) throws UnknownSessionException {
            doUpdate(session);
            if (session instanceof ValidatingSession) {
                if (((ValidatingSession) session).isValid()) {
                    //不更新ehcach中的session,使它在设定的时间内过期
                    //cache(session, session.getId());
                } else {
                    uncache(session);
                }
            } else {
                cache(session, session.getId());
            }
        }
        
        /**
         * 更新会话;如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用
         */
        @Override
        protected void doUpdate(Session session) {
            //如果会话过期/停止 没必要再更新了
            try {
                if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
                    return;
                }
            } catch (Exception e) {
                logger.error("ValidatingSession error");
            }
    
            try {
                if (session instanceof ShiroSession) {
                    // 如果没有主要字段(除lastAccessTime以外其他字段)发生改变
                    ShiroSession shiroSession = (ShiroSession) session;
                    if (!shiroSession.isChanged()) {
                        return;
                    }
                    shiroSession.setChanged(false);
                    this.saveSession(session);
                    logger.info("sessionId {} name {} 被更新", session.getId(), session.getClass().getName());
    
                } else if (session instanceof Serializable) {
                    this.saveSession(session);
                    logger.info("sessionId {} name {} 作为非ShiroSession对象被更新, ", session.getId(), session.getClass().getName());
                } else {
                    logger.warn("sessionId {} name {} 不能被序列化 更新失败", session.getId(), session.getClass().getName());
                }
            } catch (Exception e) {
                logger.warn("更新Session失败", e);
            }
        }
    
        /**
         * 删除会话;当会话过期/会话停止(如用户退出时)会调用
         */
        @Override
        protected void doDelete(Session session) {
            try {
                redisManager.del(this.getByteKey(session.getId()));
                logger.debug("Session {} 被删除", session.getId());
            } catch (Exception e) {
                logger.warn("修改Session失败", e);
            }
        }
        
        /**
         * 删除cache中缓存的Session
         */
        public void uncache(Serializable sessionId) {
            Session session = this.readSession(sessionId);
            super.uncache(session);
            logger.info("取消session {} 的缓存", sessionId);
        }
        
        /**
         * 
         *  统计当前活动的session
         */
        @Override
        public Collection<Session> getActiveSessions() {
            Set<Session> sessions = new HashSet<Session>();
            
            Set<byte[]> keys = redisManager.keys(this.keyPrefix + "*");
            if(keys != null && keys.size()>0){
                for(byte[] key:keys){
                    Session s = (Session)SerializeUtils.deserialize(redisManager.get(key));
                    sessions.add(s);
                }
            }
            
            return sessions;
        }
        
        /**
         * save session
         * @param session
         * @throws UnknownSessionException
         */
        private void saveSession(Session session) throws UnknownSessionException{
            if(session == null || session.getId() == null){
                logger.error("session or session id is null");
                return;
            }
            
            byte[] key = getByteKey(session.getId());
            byte[] value = SerializeUtils.serialize(session);
            session.setTimeout(redisManager.getExpire() * 1L);        
            this.redisManager.set(key, value, redisManager.getExpire());
        }
        
        /**
         * 将key转换为byte[]
         * @param key
         * @return
         */
        private byte[] getByteKey(Serializable sessionId){
            String preKey = this.keyPrefix + sessionId;
            return preKey.getBytes();
        }
        
        public CustomRedisManager getRedisManager() {
            return redisManager;
        }
    
        public void setRedisManager(CustomRedisManager redisManager) {
            this.redisManager = redisManager;
            
            /**
             * 初使化RedisManager
             */
            this.redisManager.init();
        }
    
        /**
         * 获取 保存到Redis中key的前缀
         * @return keyPrefix
         */
        public String getKeyPrefix() {
            return keyPrefix;
        }
    
        /**
         * 设置 保存到Redis中key的前缀
         * @param keyPrefix 保存到Redis中key的前缀
         */
        public void setKeyPrefix(String keyPrefix) {
            this.keyPrefix = keyPrefix;
        }
    
    }

    /**
     * 
     */
    package com.uc56.web.omg.authentication;
    
    import java.io.Serializable;
    import java.util.Date;
    import java.util.Map;
    
    import org.apache.shiro.session.mgt.SimpleSession;
    
    
    /**
     * 由于SimpleSession lastAccessTime更改后也会调用SessionDao update方法,
     * 增加标识位,如果只是更新lastAccessTime SessionDao update方法直接返回
     */
    public class ShiroSession extends SimpleSession implements Serializable {
        /**
         * 
         */
        private static final long serialVersionUID = 1L;
        
        // 除lastAccessTime以外其他字段发生改变时为true
        private boolean isChanged;
    
        public ShiroSession() {
            super();
            this.setChanged(true);
        }
    
        public ShiroSession(String host) {
            super(host);
            this.setChanged(true);
        }
    
    
        @Override
        public void setId(Serializable id) {
            super.setId(id);
            this.setChanged(true);
        }
    
        @Override
        public void setStopTimestamp(Date stopTimestamp) {
            super.setStopTimestamp(stopTimestamp);
            this.setChanged(true);
        }
    
        @Override
        public void setExpired(boolean expired) {
            super.setExpired(expired);
            this.setChanged(true);
        }
    
        @Override
        public void setTimeout(long timeout) {
            super.setTimeout(timeout);
            this.setChanged(true);
        }
    
        @Override
        public void setHost(String host) {
            super.setHost(host);
            this.setChanged(true);
        }
    
        @Override
        public void setAttributes(Map<Object, Object> attributes) {
            super.setAttributes(attributes);
            this.setChanged(true);
        }
    
        @Override
        public void setAttribute(Object key, Object value) {
            super.setAttribute(key, value);
            this.setChanged(true);
        }
    
        @Override
        public Object removeAttribute(Object key) {
            this.setChanged(true);
            return super.removeAttribute(key);
        }
        
        //更新最后访问时间不更新redis
        @Override
        public void touch() {
            this.setChanged(false);
            super.touch();
        }
    
        /**
         * 停止
         */
        @Override
        public void stop() {
            super.stop();
            this.setChanged(true);
        }
    
        /**
         * 设置过期
         */
        @Override
        protected void expire() {
            this.stop();
            this.setExpired(true);
        }
    
        public boolean isChanged() {
            return isChanged;
        }
    
        public void setChanged(boolean isChanged) {
            this.isChanged = isChanged;
        }
    
        @Override
        public boolean equals(Object obj) {
            return super.equals(obj);
        }
    
        @Override
        protected boolean onEquals(SimpleSession ss) {
            return super.onEquals(ss);
        }
    
        @Override
        public int hashCode() {
            return super.hashCode();
        }
    
        @Override
        public String toString() {
            return super.toString();
        }
    }
    /**
     * 
     */
    package com.uc56.web.omg.authentication;
    
    import org.apache.shiro.session.Session;
    import org.apache.shiro.session.mgt.SessionContext;
    import org.apache.shiro.session.mgt.SessionFactory;
    
    public class ShiroSessionFactory implements SessionFactory {
    
        @Override
        public Session createSession(SessionContext initData) {
            ShiroSession session = new ShiroSession();
            return session;
        }
    }
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"  
        xmlns:aop="http://www.springframework.org/schema/aop" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
        xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"  
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.0.xsd">  
        
        <!-- 自定义权限定义 -->
        <bean id="permissionsRealm" class="com.uc56.web.omg.realm.PermissionsRealm">
            <!-- 缓存管理器 -->
            <property name="cacheManager" ref="shiroRedisCacheManager" />
        </bean>
        <!-- 安全管理器 -->
        <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
            <!-- 缓存管理器 -->
            <property name="cacheManager" ref="shiroEhcacheManager" />
            <!-- session 管理器 -->
              <property name="sessionManager" ref="sessionManager" />
              <property name="realm" ref="permissionsRealm"/>
        </bean>
        <!-- redis 缓存管理器 -->
        <bean id="shiroRedisCacheManager" class="com.uc56.web.omg.shiroredis.CustomRedisCacheManager">
            <property name="redisManager" ref="shiroRedisManager" />
        </bean>
        <bean id="shiroRedisManager" class="com.uc56.web.omg.shiroredis.CustomRedisManager">
            <property name="host" value="${redis.host}" />
            <property name="port" value="${redis.port}" />
            <property name="password" value="${redis.password}" />
            <property name="expire" value="${session.maxInactiveInterval}" />
            <property name="timeout" value="${redis.timeout}" />
        </bean>
        <!-- 提供单独的redis Dao -->
        <!-- <bean id="redisSessionDAO" class="com.uc56.web.omg.shiroredis.CustomRedisSessionDAO">
          <property name="redisManager" ref="shiroRedisManager" />
          <property name="keyPrefix" value="${session.redis.namespace}"></property>
        </bean> -->
        <bean id="sessionDao" class="com.uc56.web.omg.authentication.CachingShiroSessionDao">
            <property name="keyPrefix" value="${session.redis.namespace}"/>
            <property name="redisManager" ref="shiroRedisManager" />
        </bean>
        <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">  
            <property name="securityManager" ref="securityManager"/>
            <property name="loginUrl" value="/login/loginAuthc.do"></property>
            <property name="successUrl" value="login/loginIndex.do"></property>
            <property name="unauthorizedUrl" value="login/forbidden.do" />
            <property name="filters">  
                <map>  
                   <entry key="authc" value-ref="formAuthenticationFilter"/>
                   <entry key="LoginFailureCheck" value-ref="LoginFailureCheckFilter"/>
                </map>  
            </property> 
            <property name="filterChainDefinitions">  
                <value>
                   /login/login.do=anon
                   /login/loginAuthc.do=anon
                   /login/authCheck.do=anon
                   /login/forbidden.do=anon
                   /login/validateUser.do=anon
                   /city/**=anon
                   /easyui-themes/**=anon
                   /images/**=anon
                   /jquery-easyui-1.5.1/**=anon
                   /scripts/**=anon
                   /users/**=anon
                   /**=LoginFailureCheck,authc,user
                </value>
            </property>  
        </bean>
        <!-- 用户授权信息Cache, 采用EhCache,本地缓存最长时间应比中央缓存时间短一些,以确保Session中doReadSession方法调用时更新中央缓存过期时间 -->
        <bean id="shiroEhcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
            <property name="cacheManagerConfigFile" value="classpath:springShiro/spring-shiro-ehcache.xml"/>
        </bean>
        
        <bean id="formAuthenticationFilter" class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter"/> 
        <bean id="LoginFailureCheckFilter" class="com.uc56.web.omg.filter.LoginFailureCheckFilter">
            <property name="casService" ref="casService"></property>
            <property name="loginUserService" ref="loginUserService"></property>
        </bean>
        <bean id="loginUserService" class="com.uc56.web.omg.control.LoginUserService"/>
        <bean id="passwordEncoder" class="com.uc56.core.security.MD5PasswordEncoder"/>
        
         <!-- session管理器 -->
       <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
          <!-- 超时时间 -->
          <property name="globalSessionTimeout" value="${session.global.timeout}" />
          <property name="sessionFactory" ref="sessionFactory"/>
          <!-- session存储的实现 -->
          <property name="sessionDAO" ref="sessionDao" />
          <!-- 定时检查失效的session -->
          <property name="sessionValidationSchedulerEnabled" value="true" />
          <!-- <property name="sessionValidationInterval" value="180000"/> -->
          <property name="sessionIdCookie" ref="sharesession" />  
          <property name="sessionListeners">
                <list>
                    <bean class="com.uc56.web.omg.authentication.listener.ShiroSessionListener"/>
                </list>
          </property>
       </bean>
       
       <!-- sessionIdCookie的实现,用于重写覆盖容器默认的JSESSIONID -->  
        <bean id="sharesession" class="org.apache.shiro.web.servlet.SimpleCookie">  
            <!-- cookie的name,对应的默认是JSESSIONID -->  
            <constructor-arg name="name" value="redisManager.sessionname" />  
            <!-- jsessionId的path为/用于多个系统共享jsessionId -->  
            <property name="path" value="/" />  
             <property name="httpOnly" value="false"/>  
        </bean>    
        
        <!-- 自定义Session工厂方法 返回会标识是否修改主要字段的自定义Session-->
        <bean id="sessionFactory" class="com.uc56.web.omg.authentication.ShiroSessionFactory"/>  
    </beans>
    <?xml version="1.0" encoding="UTF-8"?>
    <ehcache updateCheck="false"  name="shirocache">
        <!-- <diskStore path="java.io.tmpdir"/>
        登录记录缓存 锁定10分钟
        <cache name="passwordRetryCache"
               maxEntriesLocalHeap="2000"
               eternal="false"
               timeToIdleSeconds="3600"
               timeToLiveSeconds="0"
               overflowToDisk="false"
               statistics="true">
        </cache>
        <cache name="authorizationCache"
               maxEntriesLocalHeap="2000"
               eternal="false"
               timeToIdleSeconds="3600"
               timeToLiveSeconds="0"
               overflowToDisk="false"
               statistics="true">
        </cache>
        <cache name="authenticationCache"
               maxEntriesLocalHeap="2000"
               eternal="false"
               timeToIdleSeconds="3600"
               timeToLiveSeconds="0"
               overflowToDisk="false"
               statistics="true">
        </cache>
        <cache name="shiro-activeSessionCache"
               maxEntriesLocalHeap="2000"
               eternal="false"
               timeToIdleSeconds="3600"
               timeToLiveSeconds="0"
               overflowToDisk="false"
               statistics="true">
        </cache>
        <cache name="shiro_cache"
               maxElementsInMemory="2000"
               maxEntriesLocalHeap="2000"
               eternal="false"
               timeToIdleSeconds="0"
               timeToLiveSeconds="0"
               maxElementsOnDisk="0"
               overflowToDisk="true"
               memoryStoreEvictionPolicy="FIFO"
               statistics="true">
        </cache> -->
        <!-- <defaultCache
            在内存中最大的对象数量
            maxElementsInMemory="10000"
            设置元素是否永久的
            eternal="false"
            设置元素过期前的空闲时间
            timeToIdleSeconds="60"
            缓存数据的生存时间(TTL)
            timeToLiveSeconds="60"
            是否当memory中的数量达到限制后,保存到Disk
            overflowToDisk="false"
            diskPersistent="false"
            磁盘失效线程运行时间间隔,默认是120秒
            diskExpiryThreadIntervalSeconds="10"
            缓存满了之后的淘汰算法: LRU(最近最少使用)、FIFO(先进先出)、LFU(较少使用)
            memoryStoreEvictionPolicy="LRU"
         /> -->
         <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToLiveSeconds="60"
            overflowToDisk="false"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="10"
         />
    </ehcache>

    此设计中最重要的一点就是:

      1.cache中的session只存储不更新,也就是说每次访问不会刷新缓存中的session,cache中的session一定会在设定的时间中过期
      2.cache中设置的session的时间一定要短于redis中存储的session,保证redis中session过期是,cache中的session一定过期

      3.redis中的session更新会清楚cache中的session保证session一直性

      

      

  • 相关阅读:
    FJNU 1151 Fat Brother And Geometry(胖哥与几何)
    FJNU 1157 Fat Brother’s ruozhi magic(胖哥的弱智术)
    FJNU 1159 Fat Brother’s new way(胖哥的新姿势)
    HDU 3549 Flow Problem(最大流)
    HDU 1005 Number Sequence(数列)
    Tickets(基础DP)
    免费馅饼(基础DP)
    Super Jumping! Jumping! Jumping!(基础DP)
    Ignatius and the Princess IV(基础DP)
    Keywords Search(AC自动机)
  • 原文地址:https://www.cnblogs.com/xj-blog/p/8289429.html
Copyright © 2011-2022 走看看