shiro之redis頻繁訪問問題

目前安全框架shiro使用較爲普遍,其功能也比較強大。爲了分佈式session共享,一般的作法是將session存儲在redis中,實現多個節點獲取同一個session。此實現能夠實現session共享,但session的特色是內存存儲,就是爲了高速頻繁訪問,每一個請求都必須驗證session是否存在是否過時,也從session中獲取數據。這樣致使一個頁面刷新過程當中的數十個請求會同時訪問redis,在幾毫秒內同時操做session的獲取,修改,更新,保存,刪除等操做,從而形成redis的併發量飆升,刷新一個頁面操做redis幾十到幾百次。java

爲了解決因爲session共享形成的redis高併發問題,很明顯須要在redis以前作一次短暫的session緩存,若是該緩存存在就不用從redis中獲取,從而減小同時訪問redis的次數。若是作session緩存,主要有兩種種方案,其實原理都相同:jquery

  1>重寫sessionManager的retrieveSession方法。首先從request中獲取session,若是request中不存在再走原來的從redis中獲取。這樣可讓一個請求的屢次訪問redis問題獲得解決,由於request的生命週期爲瀏覽器發送一個請求到接收服務器的一次響應完成,所以,在一次請求中,request中的session是一直存在的,而且不用擔憂session超時過時等的問題。這樣就能夠達到有多少次請求就幾乎有多少次訪問redis,大大減小單次請求,頻繁訪問redis的問題。大大減小redis的併發數量。此實現方法最爲簡單。web

 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比較合適,而且存活時間固定不能刷新,不能隨着訪問而延長存活。redis

/**
 * 
 */
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必定過時spring

  3.redis中的session更新會清楚cache中的session保證session一直性數據庫

相關文章
相關標籤/搜索