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;
        }

        ... 略 ...

    }

  • 相关阅读:
    c++,不能声明为虚函数的函数
    Abstract
    多态性vptrvtable
    C++的重写,重载,重定义
    final
    scanf()和getchar() 使用
    深入理解C++中的mutable关键字
    equal和==
    MoQ(基于.net3.5,c#3.0的mock框架)简单介绍
    VS2008快捷键
  • 原文地址:https://www.cnblogs.com/yonghengzh/p/13725249.html
Copyright © 2011-2022 走看看