zoukankan      html  css  js  c++  java
  • 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;
        }

        ... 略 ...

    }

  • 相关阅读:
    hdu 5007 水题 (2014西安网赛A题)
    hdu 1698 线段树(成段替换 区间求和)
    poj 3468 线段树 成段增减 区间求和
    hdu 2795 公告板 (单点最值)
    UVaLive 6833 Miscalculation (表达式计算)
    UVaLive 6832 Bit String Reordering (模拟)
    CodeForces 124C Prime Permutation (数论+贪心)
    SPOJ BALNUM (数位DP)
    CodeForces 628D Magic Numbers (数位DP)
    POJ 3252 Round Numbers (数位DP)
  • 原文地址:https://www.cnblogs.com/yonghengzh/p/13725249.html
Copyright © 2011-2022 走看看