Shiro 性能優化:解決 Session 頻繁讀寫問題


背景

Shiro 提供了強大的 Session 管理功能,基於 Shiro 實現 Session 共享非常方便,只需要定制一個我們自己的SessionDAO,並將它綁定給 SessionManager 即可。在我們的 SessionDAO 中,通常會將 Session 保存到 Redis,那么 Shiro 對 Session 的增刪改查,都會直接操作 Redis。

但是由於 Shiro 對 Session 的訪問非常頻繁,用戶的一次請求,可能就會觸發幾十次的 Session 訪問操作,在 Session 共享的場景下,如果每次都訪問 Redis,勢必會影響性能。

應對思路

本地緩存 Session

將 Session 對象緩存於本地內存中,能夠有效減少從 Redis 中讀取 Session 的次數。

最簡單的方案,就是將 Session 對象保存到 request 域中,那么在一次請求內,只需要從 Redis 中獲取一次,之后就可以直接從當前 request 域中獲取,並且當請求結束后緩存會自動銷毀,不用擔心內存泄漏。

避免不必要的 Session 更新

ShiroFilter 對每個請求都會檢查 Session 是否存在,如果存在,則調用 SessionManager 的 touch() 方法,將 Session 的 lastAccessTime 屬性值更新為當前時間,並調用 SessionDAO 的 update() 方法保存更新。

由此可見,當 Session 被創建出來之后,用戶的每個請求都會使 SessionDAO 的 update() 方法至少被調用一次。

那么 Session 的 lastAccessTime 屬性是干嘛用的呢?有必要每個請求都去更新一下嗎?

lastAccessTime 屬性記錄的是用戶的上次訪問時間,它主要用於驗證 Session 是否超時,當用戶訪問系統時,如果本次訪問的時間距離上次訪問時間超過了 timeout 閾值,則判定 Session 超時。如果 lastAccessTime 的值不斷更新,那么 Session 就有可能永不超時。因此,更新 lastAccessTime 屬性值的操作可以認為是給 Session “續命”。

既然是“續命”,沒必要每次都“續”(除非命真的很短)。我們可以重寫 SessionManager 的 touch() 方法,在更新過 lastAccessTime 屬性的值后,先不急着保存更新,而是計算一下兩次訪問的時間間隔,只有當它大於某個閾值時,才去主動調用 SessionDAO 的 update() 方法來保存更新。這樣也就大大降低了 Session 更新的頻率。

代碼實現

ShiroSessionDAO.java

@Repository
public class ShiroSessionDAO extends AbstractSessionDAO {

    private static final String SESSION_REDIS_KEY_PREFIX = "session:";

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = generateSessionId(session);
        assignSessionId(session, sessionId);
        redisTemplate.boundValueOps(SESSION_REDIS_KEY_PREFIX + session.getId().toString()).set(session);
        return sessionId;
    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        redisTemplate.boundValueOps(SESSION_REDIS_KEY_PREFIX + session.getId().toString()).set(session);
    }

    @Override
    public void delete(Session session) {
        redisTemplate.delete(SESSION_REDIS_KEY_PREFIX + session.getId().toString());
        HttpServletRequest request = getRequest();
        if (request != null) { // 一定要進行空值判斷,因為SessionValidationScheduler的線程也會調用這個方法,而在那個線程中是不存在Request對象的
            request.removeAttribute(session.getId().toString());
        }
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        HttpServletRequest request = getRequest();
        if (request != null) {
            Session sessionObj = (Session) request.getAttribute(sessionId.toString());
            if (sessionObj != null) {
                return sessionObj;
            }
        }

        Session session = (Session) redisTemplate.boundValueOps(SESSION_REDIS_KEY_PREFIX + sessionId).get();
        if (session != null && request != null) {
            request.setAttribute(sessionId.toString(), session);
        }
        return session;
    }

    @Override
    public Collection<Session> getActiveSessions() {
        Set<String> keys = redisTemplate.keys(SESSION_REDIS_KEY_PREFIX + "*");
        if (keys != null && !keys.isEmpty()) {
            List<Object> sessions = redisTemplate.opsForValue().multiGet(keys);
            if (sessions != null) {
                return sessions.stream().map(o -> (Session) o).collect(Collectors.toList());
            }
        }
        return Collections.emptySet();
    }

    private HttpServletRequest getRequest() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return requestAttributes != null ? requestAttributes.getRequest() : null;
    }

}

ShiroConfig.java

@Configuration
public class ShiroConfig {

    @Bean
    public SessionManager sessionManager(SessionDAO sessionDAO) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager() {
            @Override // 重寫touch()方法,降低Session更新的頻率
            public void touch(SessionKey key) throws InvalidSessionException {
                Session session = doGetSession(key);
                if (session != null) {
                    long oldTime = session.getLastAccessTime().getTime();
                    session.touch(); // 更新訪問時間
                    long newTime = session.getLastAccessTime().getTime();
                    if (newTime - oldTime > 300000) { // 如果兩次訪問的時間間隔大於5分鍾,主動持久化Session
                        onChange(session);
                    }
                }
            }
        };
        
        sessionManager.setSessionDAO(sessionDAO); // 綁定SessionDAO

        SimpleCookie sessionIdCookie = new SimpleCookie("sessionId");
        sessionIdCookie.setPath("/");
        sessionIdCookie.setMaxAge(8 * 60 * 60); // 單位:秒數
        sessionManager.setSessionIdCookie(sessionIdCookie); // 綁定Cookie模版
        
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        sessionManager.setGlobalSessionTimeout(60 * 60 * 1000);
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setSessionValidationInterval(2 * 60 * 60 * 1000);
        sessionManager.setDeleteInvalidSessions(true);
        
        return sessionManager;
    }

    ... 略 ...

}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM