Shiro学习(七):总结Shiro 与Redis 整合过程中的2个问题及解决方案

一、频繁从Redis 中读取Session 的问题说明:针对这个问题

问题描述:

每次请求需要访问多次Redis服务,即使是同一个请求,每次也要从Redis 读取Session,

这个访问的频次会出现很长时间的IO等待,对每次请求的性能减低了,并且对Redis的压力

也提高了。

针对这个问题一般有2个方案来解决,下边分别来看下这2个方案,并对比下优劣

1、方案一

说明:该方案前边"Shiro学习(四)" 已经给出了一种解决方案,为了2中方案的对比,

现在把"Shiro学习(四)" 中的方案也拿过来

Shiro 在实际工作中常常用于Web 项目,通过我们使用的SessionManager对象

DefaultWebSessionManager 也可以确定是Web 项目;

在Web 中,在一个请求处于活动中时,ServletRequest中会缓存用户的很多信息,其中

就包括 Session 信息,那么我们可不可以先从 ServletRequest 读取用户的Session,

ServletRequest 中读取不到我们再去Redis 中读取Session

通过Debug 发现,Shiro是在 DefaultWebSessionManager.retrieveSession() 中调用

SessionDAO 的 readSession() 方法去读取Session 的。

我们可以重写 DefaultWebSessionManager.retrieveSession() 方法,先从ServletRequest 中

读取Session,ServletRequest 中没有 再去Redis 中读取,实现步骤如下:

1)自定义 SessionManager 继承 DefaultWebSessionManager,并重写 retrieveSession() 方法

java 复制代码
/****************************************************
 * 解决单次请求多次访问Redis 的问题
 * @author lbf
 * @date 2025/4/28 15:08
 ****************************************************/
public class ShiroSessionManager extends DefaultWebSessionManager {

    private static Logger logger = LoggerFactory.getLogger(DefaultWebSessionManager.class);

    /**
     * 优化读取 Session
     *  先从 ServletRequest 请求域中读取,ServletRequest 中不存在再去Redis 中读取
     *
     * @param sessionKey the session key to use to look up the target session.
     * @return
     * @throws UnknownSessionException
     */
    @Override
    protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {

        Serializable sessionId = getSessionId(sessionKey);

        ServletRequest request = null;
        if (sessionKey instanceof WebSessionKey) {
            request = ((WebSessionKey) sessionKey).getServletRequest();
        }

        if (request != null && null != sessionId) {
            Object sessionObj = request.getAttribute(sessionId.toString());
            if (sessionObj != null) {
                logger.debug("read session from request");
                return (Session) sessionObj;
            }
        }

        Session session = super.retrieveSession(sessionKey);
        if (request != null && null != sessionId) {
            request.setAttribute(sessionId.toString(), session);
        }
        return session;
    }
}

RedisSessionDAO代码如下:

java 复制代码
/****************************************************
 * 自定义 SeesionDAO,用于将session 数据保存到 Redis
 * 参考
 * @author lbf
 * @date 2025/4/23 13:45
 ****************************************************/
@Slf4j
//@Component
public class RedisSessionDAO extends AbstractSessionDAO {

    /**
     * 常量,key前缀
     */
    private static final String DEFAULT_SESSION_KEY_PREFIX = "shiro:session:";
    /**
     * key前缀,可以手动指定
     */
    private String keyPrefix = DEFAULT_SESSION_KEY_PREFIX;


    /**
     * 超时时间
     */
    private static final int DEFAULT_EXPIRE = -2;//默认的过期时间,即session.getTimeout() 中的过期时间
    private static final int NO_EXPIRE = -1;//没有过期时间

    /**
     * 请保确session在redis中的过期时间长于sesion.getTimeout(),
     * 否则会在session还没过期,redis存储session已经过期了会把session自动删除
     */
    private int expire = DEFAULT_EXPIRE;

    /**
     * 毫秒与秒的换算单位
     */
    private static final int MILLISECONDS_IN_A_SECOND = 1000;

    private RedisManager redisManager;

    @Override
    protected Serializable doCreate(Session session) {

        if(session == null){
            log.error("session is null");
            throw new UnknownSessionException("session is null");
        }
        //1、根据Seesion 生成 SeesionId
        Serializable sessionId = this.generateSessionId(session);
        //2、关联sessionId 与 session,可以基于sessionId拿到session
        this.assignSessionId(session,sessionId);
        //3、保存session
        saveSession(session);

        return sessionId;
    }

    private void saveSession(Session session){
        if(session == null || session.getId() == null){
            log.error("session or sessionId is null");
            throw new UnknownSessionException("session or sessionId is null");
        }
        //获取 redis 中的 sessionKey
        String redisSessionKey = getRedisSessionKey(session.getId());
        if(expire == DEFAULT_EXPIRE){
            this.redisManager.set(redisSessionKey,session,(int)session.getTimeout()/MILLISECONDS_IN_A_SECOND);
            return;
        }
        if(expire != NO_EXPIRE && expire*MILLISECONDS_IN_A_SECOND < session.getTimeout()){
            log.warn("Redis session expire time: "
                    + (expire * MILLISECONDS_IN_A_SECOND)
                    + " is less than Session timeout: "
                    + session.getTimeout()
                    + " . It may cause some problems.");
        }

        this.redisManager.set(redisSessionKey,session,expire);


    }

    @Override
    protected Session doReadSession(Serializable sessionId) {

        if(sessionId == null){
            log.error("sessionId is null !");
        }

        Session session = null;
        try{
            
            //从Redis 中读取session
            String redisKey = getRedisSessionKey(sessionId);
            session = (Session)redisManager.get(redisKey);
            
        }catch (Exception e){
            log.error(" get session fail sessionId = {}",sessionId);
        }

        return session;
    }

    
    /**
     * 更新Session
     * @param session the Session to update
     * @throws UnknownSessionException
     */
    @Override
    public void update(Session session) throws UnknownSessionException {

        if(session == null || session.getId() == null){
            log.error(" session is null or sessionId is null");
            throw new UnknownSessionException("session is null or sessionId is null");
        }

        //如果会话过期/停止 没必要再更新了
        try {
            if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
                return;
            }           

            this.saveSession(session);
        } catch (Exception e) {
            log.warn("update Session is failed", e);
        }



    }

    @Override
    public void delete(Session session) {
        if(session == null || session.getId() == null){
            log.error(" session is null or sessionId is null");
            throw new UnknownSessionException("session is null or sessionId is null");
        }

        String redisSessionKey = getRedisSessionKey(session.getId());
        redisManager.del(redisSessionKey);
    }

    @Override
    public Collection<Session> getActiveSessions() {

        Set<Session> sessions = new HashSet<>();
        try{
            Set<String> keySets = redisManager.scan(this.keyPrefix+"*");

            for(String key:keySets){
                Session session = (Session)redisManager.get(key);
                sessions.add(session);
            }
        }catch (Exception e){
            log.error(" get active session fail !",e);
        }

        return sessions;
    }

    /**
     * 获取session存储在Redis 中的key
     * @param sessionId
     * @return
     */
    public String getRedisSessionKey(Serializable sessionId){
        return this.keyPrefix+sessionId;
    }

    public RedisManager getRedisManager() {
        return redisManager;
    }

    public void setRedisManager(RedisManager redisManager) {
        this.redisManager = redisManager;
    }

    public String getKeyPrefix() {
        return keyPrefix;
    }

    public void setKeyPrefix(String keyPrefix) {
        this.keyPrefix = keyPrefix;
    }

    public long getSessionInMemoryTimeout() {
        return sessionInMemoryTimeout;
    }

    public void setSessionInMemoryTimeout(long sessionInMemoryTimeout) {
        this.sessionInMemoryTimeout = sessionInMemoryTimeout;
    }

    public int getExpire() {
        return expire;
    }

    public void setExpire(int expire) {
        this.expire = expire;
    }
}

2)在Shiro 配置类 ShiroConfig 用自定义的 SessionManager 替代 DefaultWebSessionManager

java 复制代码
@Bean
    public SessionManager sessionManager(){
        //DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();

        ShiroSessionManager sessionManager = new ShiroSessionManager();

        //设置session过期时间为1小时(单位:毫秒),默认为30分钟
        sessionManager.setGlobalSessionTimeout(30 * 60 * 1000);
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        sessionManager.setDeleteInvalidSessions(true);



        //注入会话监听器
        sessionManager.setSessionListeners(Collections.singleton(new MySessionListener1()));

        //可以不配置,默认有实现
        SimpleCookie simpleCookie = new SimpleCookie();
        simpleCookie.setName("BASIC_WEBSID");
        simpleCookie.setHttpOnly(true);
        sessionManager.setSessionIdCookie(simpleCookie);


        //设置SessionDAO
        //todo 在配置类中Bean的另一种注入方式
        sessionManager.setSessionDAO(sessionDAO());


        return sessionManager;
    }

2、方案二

参考开源项目 shiro-redis,使用ThreadLocal 来缓存Session,每次RedisSessionDAO读取

Session 时,先从ThrealLocal 中读取,若 ThreadLocal 中的Session 不存在或已经过期,则

再去Redis 中读取,实现步骤如下:

1)因为ThreadLocal 只能存储对象,而我们需要判断Session是否过期,所以需要对Session包

装一下,不能直接存储;

自定义用于存储Session到 ThreadLocal 的对象 SessionInMemory,代码如下:

java 复制代码
/****************************************************
 * 参考开源项目 shiro-redis 解决频繁从redis 中读取session 的问题
 *
 * 该类是为了方便存储带有超时时间的Session
 *
 * @author lbf
 * @date 2025/4/28 10:22
 ****************************************************/
public class SessionInMemory {

    /**
     * Session
     */
    private Session session;
    /**
     * 创建时间
     */
    private Date  createTime;

    public Session getSession() {
        return session;
    }

    public void setSession(Session session) {
        this.session = session;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
}

2)修改 RedisSessionDAO 中的 doReadSession() 方法

java 复制代码
//添加的字段
private static final long DEFAULT_SESSION_IN_MEMORY_TIMEOUT = 5000L;
 /*
  * session存储在内存的时间
  */
 private long sessionInMemoryTimeout = DEFAULT_SESSION_IN_MEMORY_TIMEOUT;
 private static ThreadLocal sessionsInThread = new ThreadLocal();

@Override
    protected Session doReadSession(Serializable sessionId) {

        if(sessionId == null){
            log.error("sessionId is null !");
        }

        Session session = null;
        try{
            //1、先从内存中读取Session,即从 ThreadLocal 中读取Session
            session = getSessionFromThreadLocal(sessionId);
            if(session != null){
                return session;
            }
            //2、ThreadLocal中Session 不存在,然后再从Redis 中读取session
            String redisKey = getRedisSessionKey(sessionId);
            session = (Session)redisManager.get(redisKey);
            if(session != null){
                //将session 保存到内存ThreadLocal 中
                setSessionToThreadLocal(sessionId,session);
            }
        }catch (Exception e){
            log.error(" get session fail sessionId = {}",sessionId);
        }

        return session;
    }

    /**
     * 基于 ThreadLocal 缓存一个请求的Session,避免频繁的从Redis 中读取Session
     *
     * @param sessionId
     * @return
     */
    private Session getSessionFromThreadLocal(Serializable sessionId){

        Session session = null;
        if(sessionsInThread.get() == null){
            return null;
        }
        Map<Serializable,SessionInMemory> memoryMap = (Map<Serializable,SessionInMemory>)sessionsInThread.get();
        SessionInMemory sessionInMemory = memoryMap.get(sessionId);
        if(sessionInMemory == null){
            return null;
        }
        //若Session存在于ThreadLocal 中,则判断Session 是否过期
        Date now =  new Date();
        long duration = now.getTime() - sessionInMemory.getCreateTime().getTime();
        if(duration <= MILLISECONDS_IN_A_SECOND){//未过期
            session = sessionInMemory.getSession();
            log.debug("read session from memory");
        }else {
            //Session 在ThreadLocal 过期了,则删除
            memoryMap.remove(sessionId);
        }

        return session;

    }

    private void setSessionToThreadLocal(Serializable sessionId,Session session){

        Map<Serializable,SessionInMemory> memoryMap = (Map<Serializable,SessionInMemory>)sessionsInThread.get();
        if(memoryMap == null){
            memoryMap = new ConcurrentHashMap<>();
            //将 memoryMap 存放到 ThreadLocal 中
            sessionsInThread.set(memoryMap);
        }

        //构建SessionInMemory
        SessionInMemory sessionInMemory = new SessionInMemory();
        sessionInMemory.setSession(session);
        sessionInMemory.setCreateTime(new Date());

        memoryMap.put(sessionId,sessionInMemory);

    }

3)配置类ShiroConfig中 SessionManager 使用默认的 DefaultWebSessionManager

java 复制代码
@Bean
    public SessionManager sessionManager(){
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();


        //设置session过期时间为1小时(单位:毫秒),默认为30分钟
        sessionManager.setGlobalSessionTimeout(30 * 60 * 1000);
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        sessionManager.setDeleteInvalidSessions(true);



        //注入会话监听器
        sessionManager.setSessionListeners(Collections.singleton(new MySessionListener1()));

        //可以不配置,默认有实现
        SimpleCookie simpleCookie = new SimpleCookie();
        simpleCookie.setName("BASIC_WEBSID");
        simpleCookie.setHttpOnly(true);
        sessionManager.setSessionIdCookie(simpleCookie);


        //设置SessionDAO
        //todo 在配置类中Bean的另一种注入方式
        sessionManager.setSessionDAO(sessionDAO());


        return sessionManager;
    }

二、频繁的将Session 更新到Redis的问题

问题描述:

通过Debug 发现,Shiro默认创建的Session是 SimpleSession,且每次访问Session

后,SimpleSession 的 lastAccessTime(最后访问时间) 就会发生改变,lastAccessTime

改变会触发将Session 更新到 Redis,如下图所示:

针对该问题有3种解决方案,下边分别看下这3种方案:

1、方案一

参考开源项目shiro-redis,新定义一个ShiroSession 并继承 SimpleSession,在 ShiroSession

中定义一个标志位 isChange,只有 lastAccessTime 之外的属性发生改变时,isChange被设置

为 True,只有 isChange 发生改变时 isChange 被设置为false;然后在RedisSessionDAO中

的update() 方法中先判断标志位 isChange 的值,isChange=true才执行更新操作,

实现步骤如下:

1)定义ShiroSession

java 复制代码
/****************************************************
 * 在前边 8001 模块中存在一个问题,即:
 * 每次操作系统时,SimpleSession(Session 的实现)的 lastAccessTime(最后一次被访问时间)就会被改变,
 * 此时就会触发 SessionDAO.update 去更新Redis 中的Session,会频繁的向Redis 写数据
 *
 * 针对这个问题,我们自定义Session(如:ShiroSession),继承 SimpleSession;在 ShiroSession 中
 * 添加标志位属性 isChanged,除了 lastAccessTime 之外的字段发生改变时,isChanged 被设置为true,
 * 否则 isChanged 被设置为false;然后在 RedisSessionDAO 的update() 方法中先判断 isChanged
 * 的值是否为true,isChanged==true才执行更新操作(参考开源项目 shiro-redis)
 *
 *
 * @author lbf
 * @date 2025/4/25 14:25
 ****************************************************/
public class ShiroSession extends SimpleSession implements Serializable {

    // 除lastAccessTime以外其他字段发生改变时为true
    private boolean isChanged = false;

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

    /**
     * 停止
     */
    @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();
    }
}

2)修改 RedisSessionDAO 中 的 update()

java 复制代码
 @Override
    public void update(Session session) throws UnknownSessionException {

        if(session == null || session.getId() == null){
            log.error(" session is null or sessionId is null");
            throw new UnknownSessionException("session is null or sessionId is null");
        }

        //如果会话过期/停止 没必要再更新了
        try {
            if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
                return;
            }
            
            if (session instanceof ShiroSession) {
                // 如果没有主要字段(除lastAccessTime以外其他字段)发生改变
                ShiroSession ss = (ShiroSession) session;
                if (!ss.isChanged()) {
                    return;
                }
                //如果没有返回 证明有调用 setAttribute往redis 放的时候永远设置为false
                ss.setChanged(false);
            }
             

            this.saveSession(session);
        } catch (Exception e) {
            log.warn("update Session is failed", e);
        }



    }

3)模仿 SimpleSessionFactory 创建 ShiroSessionFactory,用于创建 ShiroSession

java 复制代码
/****************************************************
 * 仿造 SimpleSessionFactory 创建 ShiroSessionFactory 用于创建 ShiroSession
 * 将 ShiroSessionFactory 注入到  SessionManager 才能生效
 *
 * @author lbf
 * @date 2025/4/25 16:29
 ****************************************************/
public class ShiroSessionFactory implements SessionFactory {

    /**
     * 创建 ShiroSession
     * @param initData the initialization data to be used during {@link Session} creation.
     * @return
     */
    @Override
    public Session createSession(SessionContext initData) {
        if (initData != null) {
            String host = initData.getHost();
            if (host != null) {
                return new ShiroSession(host);
            }
        }
        return new ShiroSession();
    }
}

4)将 SimpleSessionFactory 注入到 SessionManager 中,

java 复制代码
 @Bean
    public SessionManager sessionManager(){
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();

        //设置session过期时间为1小时(单位:毫秒),默认为30分钟
        sessionManager.setGlobalSessionTimeout(30 * 60 * 1000);
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        sessionManager.setDeleteInvalidSessions(true);



        //注入会话监听器
        sessionManager.setSessionListeners(Collections.singleton(new MySessionListener1()));

        //可以不配置,默认有实现
        SimpleCookie simpleCookie = new SimpleCookie();
        simpleCookie.setName("BASIC_WEBSID");
        simpleCookie.setHttpOnly(true);
        sessionManager.setSessionIdCookie(simpleCookie);

        //设置SessionFactory
        sessionManager.setSessionFactory(new ShiroSessionFactory());

        //设置SessionDAO
        //todo 在配置类中Bean的另一种注入方式
        sessionManager.setSessionDAO(sessionDAO());


        return sessionManager;
    }


/**
     * 使用自定义的 RedisSessionDAO,用于将session存储到Redis 中
     *
     * @return
     */
    @Bean("sessionDAO")
    public SessionDAO sessionDAO(){
        RedisSessionDAO sessionDAO = new RedisSessionDAO();
        RedisManager redisManager = redisManager();
        sessionDAO.setRedisManager(redisManager);
        return sessionDAO;
    }

2、方案二

上边的"方案一"存在一个问题,即:lastAccessTime 发生改变一直不更新Redis,可能会导

致用户在线,但Redis 中的Session已经过期的问题;

针对这个问题,我们可以采用定时更新的方案,即Session 任何属性字段发生改变时,不立即

更新,而是每隔一段时间批量更新一次,实现步骤如下:

1)定义基于批量更新Session 的类 BufferUpdateRedisSessionDAO,继承 RedisSessionDAO

并重写 update 方法,实现代码如下:

java 复制代码
/****************************************************
 * 在前边 8001 模块中存在一个问题,即:
 *    每次操作系统时,SimpleSession(Session 的实现)的 lastAccessTime(最后一次被访问时间)就会被改变,
 *    此时就会触发 SessionDAO.update 去更新Redis 中的Session,会频繁的向Redis 写数据,会导致一些性能问题。
 *
 * 针对这个问题网上解决方案有很多,下边列出几个常用的方案:
 *     1)关闭 lastAccessTime 的自动更新,sessionManager.setUpdateLastAccessEnabled(false); // 默认已开启
 *          但有些版本的 DefaultWebSessionManager 已经没有这个属性了
 *     2)自定义Session,如 ShiroSession ,继承 SimpleSession,如 ShiroSession ,并定义一个boolean类型标志位,如 isChange,
 *           该标志位初始值是 false,只有除了lastAccessTime 之外的字段发生改变时,isChanged 被设置为true,
 *    3)批量更新Session到Redis,不用每次 Session发生改变时(包括 lastAccessTime 发生改变)就立即更新Session,而是
 *         每隔一段时间(如:每隔5s)更新一次Session 到 Redis,通过 ScheduledExecutorService 来实现定时更新Session
 *
 *  todo 注意:
 *      上边方案 1 和 2 ,都可能存在在线用户的Session 与 Redis 中的Session 数据不一致的情况;
 *      如果用户的Session 只有 lastAccessTime 发生改变,可能会存在 用户一直在线,但Redis 中的 Session 已经过期的情况
 *
 * @author lbf
 * @date 2025/4/25 15:51
 ****************************************************/
public class BufferUpdateRedisSessionDAO extends RedisSessionDAO {

    /**
     * 定义基于定时任务的线程池,用于定时更新Session
     * 更新 Session 到Redis 需要单线程执行,所以核心线程数为1
     */
    private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

    /**
     * 最后一次需要更新的Session,更新之后 lastSession 需要设置为空
     *
     * 分布式下,lastSession 需要能让其他线程看到
     */
    private volatile Session lastSession = null;


    @Override
    public void update(Session session) throws UnknownSessionException {

        this.lastSession = session;
        //将更新任务提交到线程池
        //设置每隔5s执行一次
        /**
         * 这里用lambda而不用 this.flush(),是因为 scheduler.schedule() 方法
         * 第一个参数是 Runnable类型的,lambda 表达式 this::flush 会把 flush 包装到 Runnable 中,
         *  this::flush 相当于:
         *      Runnable r = new Runnable() {
         *             @Override
         *             public void run() {
         *                 flush();
         *             }
         *         };
         */
        
        scheduler.schedule(this::flush,1, TimeUnit.SECONDS);



    }

    private void flush(){

        if(this.lastSession != null){
            //调用父类方法,真正执行
            super.update(lastSession);
            this.lastSession = null;
        }
    }
}

RedisSessionDAO 的update 方法如下:

java 复制代码
@Override
    public void update(Session session) throws UnknownSessionException {

        if(session == null || session.getId() == null){
            log.error(" session is null or sessionId is null");
            throw new UnknownSessionException("session is null or sessionId is null");
        }

        //如果会话过期/停止 没必要再更新了
        try {
            if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
                return;
            }


            this.saveSession(session);
        } catch (Exception e) {
            log.warn("update Session is failed", e);
        }



    }

2)在配置类ShiroConfig 中使用 BufferUpdateRedisSessionDAO 替代 RedisSessionDAO

java 复制代码
@Bean("sessionDAO")
    public SessionDAO sessionDAO(){
        //RedisSessionDAO sessionDAO = new RedisSessionDAO();

        BufferUpdateRedisSessionDAO sessionDAO = new BufferUpdateRedisSessionDAO();

        RedisManager redisManager = redisManager();
        sessionDAO.setRedisManager(redisManager);

        return sessionDAO;
    }

3、方案三

上边方案二也存在一个问题,更新间隔时间不好把握,若时间太短,可能会增加redis的压力;

若时间太长,可能会存在SimpleSession lastAccessTime 之外的属性发生改变时,Session

没有及时更新到redis中而导致Session不一致用户认证失败的问题;

可不可以把"方案一" 与 "方案二" 整合一起,当 SimpleSession lastAccessTime 之外的属性

发生改变时立即将Session 更新到 Redis 中,否则定时批量更新,实现步骤如下

1)定义 ShiroSession,代码请参考"方案一"

2)模仿 SimpleSessionFactory 创建 ShiroSessionFactory,用于创建 ShiroSession;

代码请参考"方案一"

3)修改RedisSession 中的update 方法,代码如下:

java 复制代码
public class RedisSessionDAO extends AbstractSessionDAO {

    /**
     * 常量,key前缀
     */
    private static final String DEFAULT_SESSION_KEY_PREFIX = "shiro:session:";
    /**
     * key前缀,可以手动指定
     */
    private String keyPrefix = DEFAULT_SESSION_KEY_PREFIX;

    private static final long DEFAULT_SESSION_IN_MEMORY_TIMEOUT = 5000L;
    /**
     * 
     * session存储在内存的时间
     */
    private long sessionInMemoryTimeout = DEFAULT_SESSION_IN_MEMORY_TIMEOUT;

    /**
     * 过期时间
     */
    private static final int DEFAULT_EXPIRE = -2;//默认的过期时间,即session.getTimeout() 中的过期时间
    private static final int NO_EXPIRE = -1;//没有过期时间

    /**
     * 请保确session在redis中的过期时间长于sesion.getTimeout(),
     * 否则会在session还没过期,redis存储session已经过期了会把session自动删除
     */
    private int expire = DEFAULT_EXPIRE;

    /**
     * 毫秒与秒的换算单位
     */
    private static final int MILLISECONDS_IN_A_SECOND = 1000;

    private RedisManager redisManager;
    private static ThreadLocal sessionsInThread = new ThreadLocal();

    @Override
    protected Serializable doCreate(Session session) {

        if(session == null){
            log.error("session is null");
            throw new UnknownSessionException("session is null");
        }
        //1、根据Seesion 生成 SeesionId
        Serializable sessionId = this.generateSessionId(session);
        //2、关联sessionId 与 session,可以基于sessionId拿到session
        this.assignSessionId(session,sessionId);
        //3、保存session
        saveSession(session);

        return sessionId;
    }

    private void saveSession(Session session){
        if(session == null || session.getId() == null){
            log.error("session or sessionId is null");
            throw new UnknownSessionException("session or sessionId is null");
        }
        //获取 redis 中的 sessionKey
        String redisSessionKey = getRedisSessionKey(session.getId());
        if(expire == DEFAULT_EXPIRE){
            this.redisManager.set(redisSessionKey,session,(int)session.getTimeout()/MILLISECONDS_IN_A_SECOND);
            return;
        }
        if(expire != NO_EXPIRE && expire*MILLISECONDS_IN_A_SECOND < session.getTimeout()){
            log.warn("Redis session expire time: "
                    + (expire * MILLISECONDS_IN_A_SECOND)
                    + " is less than Session timeout: "
                    + session.getTimeout()
                    + " . It may cause some problems.");
        }

        this.redisManager.set(redisSessionKey,session,expire);


    }

    @Override
    protected Session doReadSession(Serializable sessionId) {

        if(sessionId == null){
            log.error("sessionId is null !");
        }

        Session session = null;
        try{
            //1、先从内存中读取Session,即从 ThreadLocal 中读取Session
            session = getSessionFromThreadLocal(sessionId);
            if(session != null){
                return session;
            }
            //2、ThreadLocal中Session 不存在,然后再从Redis 中读取session
            String redisKey = getRedisSessionKey(sessionId);
            session = (Session)redisManager.get(redisKey);
            if(session != null){
                //将session 保存到内存ThreadLocal 中
                setSessionToThreadLocal(sessionId,session);
            }
        }catch (Exception e){
            log.error(" get session fail sessionId = {}",sessionId);
        }

        return session;
    }

    /**
     * 基于 ThreadLocal 缓存一个请求的Session,避免频繁的从Redis 中读取Session
     *
     * @param sessionId
     * @return
     */
    private Session getSessionFromThreadLocal(Serializable sessionId){

        Session session = null;
        if(sessionsInThread.get() == null){
            return null;
        }
        Map<Serializable,SessionInMemory> memoryMap = (Map<Serializable,SessionInMemory>)sessionsInThread.get();
        SessionInMemory sessionInMemory = memoryMap.get(sessionId);
        if(sessionInMemory == null){
            return null;
        }
        //若Session存在于ThreadLocal 中,则判断Session 是否过期
        Date now =  new Date();
        long duration = now.getTime() - sessionInMemory.getCreateTime().getTime();
        if(duration <= MILLISECONDS_IN_A_SECOND){//未过期
            session = sessionInMemory.getSession();
            log.debug("read session from memory");
        }else {
            //Session 在ThreadLocal 过期了,则删除
            memoryMap.remove(sessionId);
        }

        return session;

    }

    private void setSessionToThreadLocal(Serializable sessionId,Session session){

        Map<Serializable,SessionInMemory> memoryMap = (Map<Serializable,SessionInMemory>)sessionsInThread.get();
        if(memoryMap == null){
            memoryMap = new ConcurrentHashMap<>();
            //将 memoryMap 存放到 ThreadLocal 中
            sessionsInThread.set(memoryMap);
        }

        //构建SessionInMemory
        SessionInMemory sessionInMemory = new SessionInMemory();
        sessionInMemory.setSession(session);
        sessionInMemory.setCreateTime(new Date());

        memoryMap.put(sessionId,sessionInMemory);

    }

    /**
     * 更新Session
     * @param session the Session to update
     * @throws UnknownSessionException
     */
    @Override
    public void update(Session session) throws UnknownSessionException {

        if(session == null || session.getId() == null){
            log.error(" session is null or sessionId is null");
            throw new UnknownSessionException("session is null or sessionId is null");
        }

        //如果会话过期/停止 没必要再更新了
        try {
            if (session instanceof ValidatingSession && !((ValidatingSession) session).isValid()) {
                return;
            }


            this.saveSession(session);
        } catch (Exception e) {
            log.warn("update Session is failed", e);
        }



    }

    @Override
    public void delete(Session session) {
        if(session == null || session.getId() == null){
            log.error(" session is null or sessionId is null");
            throw new UnknownSessionException("session is null or sessionId is null");
        }

        String redisSessionKey = getRedisSessionKey(session.getId());
        redisManager.del(redisSessionKey);
    }

    @Override
    public Collection<Session> getActiveSessions() {

        Set<Session> sessions = new HashSet<>();
        try{
            Set<String> keySets = redisManager.scan(this.keyPrefix+"*");

            for(String key:keySets){
                Session session = (Session)redisManager.get(key);
                sessions.add(session);
            }
        }catch (Exception e){
            log.error(" get active session fail !",e);
        }

        return sessions;
    }

    /**
     * 获取session存储在Redis 中的key
     * @param sessionId
     * @return
     */
    public String getRedisSessionKey(Serializable sessionId){
        return this.keyPrefix+sessionId;
    }

    public RedisManager getRedisManager() {
        return redisManager;
    }

    public void setRedisManager(RedisManager redisManager) {
        this.redisManager = redisManager;
    }

    public String getKeyPrefix() {
        return keyPrefix;
    }

    public void setKeyPrefix(String keyPrefix) {
        this.keyPrefix = keyPrefix;
    }

    public long getSessionInMemoryTimeout() {
        return sessionInMemoryTimeout;
    }

    public void setSessionInMemoryTimeout(long sessionInMemoryTimeout) {
        this.sessionInMemoryTimeout = sessionInMemoryTimeout;
    }

    public int getExpire() {
        return expire;
    }

    public void setExpire(int expire) {
        this.expire = expire;
    }
}

4)修改 BufferUpdateRedisSessionDAO,代码如下:

java 复制代码
public class BufferUpdateRedisSessionDAO extends RedisSessionDAO {

    /**
     * 定义基于定时任务的线程池,用于定时更新Session
     * 更新 Session 到Redis 需要单线程执行,所以核心线程数为1
     */
    private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

    /**
     * 最后一次需要更新的Session,更新之后 lastSession 需要设置为空
     *
     * 分布式下,lastSession 需要能让其他线程看到
     */
    private volatile Session lastSession = null;


    @Override
    public void update(Session session) throws UnknownSessionException {

        this.lastSession = session;
        //将更新任务提交到线程池
        //设置每隔5s执行一次
        /**
         * 这里用lambda而不用 this.flush(),是因为 scheduler.schedule() 方法
         * 第一个参数是 Runnable类型的,lambda 表达式 this::flush 会把 flush 包装到 Runnable 中,
         *  this::flush 相当于:
         *      Runnable r = new Runnable() {
         *             @Override
         *             public void run() {
         *                 flush();
         *             }
         *         };
         */
        if(this.lastSession instanceof ShiroSession){
            ShiroSession shiroSession = (ShiroSession) this.lastSession;
            if(shiroSession.isChanged()){
                // 除了 SimpleSession lastAccessTime 之外的字段发生改变时,立即个更新
                super.update(this.lastSession);
                //Session 更新后标志位归位
                shiroSession.setChanged(false);
                this.lastSession = null;
                return;
            }
        }
        //其他情况定时更新,每隔5s更新一次
        scheduler.schedule(this::flush,1, TimeUnit.SECONDS);



    }

    private void flush(){

        if(this.lastSession != null){
            //调用父类方法,真正执行
            super.update(lastSession);
            this.lastSession = null;
        }
    }
}

5)修改配置类ShiroConfig,代码如下:

java 复制代码
@Bean
    public SessionManager sessionManager(){
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();

        //设置session过期时间为1小时(单位:毫秒),默认为30分钟
        sessionManager.setGlobalSessionTimeout(30 * 60 * 1000);
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setSessionIdUrlRewritingEnabled(false);
        sessionManager.setDeleteInvalidSessions(true);



        //注入会话监听器
        sessionManager.setSessionListeners(Collections.singleton(new MySessionListener1()));

        //可以不配置,默认有实现
        SimpleCookie simpleCookie = new SimpleCookie();
        simpleCookie.setName("BASIC_WEBSID");
        simpleCookie.setHttpOnly(true);
        sessionManager.setSessionIdCookie(simpleCookie);


        //设置SessionFactory
        sessionManager.setSessionFactory(new ShiroSessionFactory());

        //设置SessionDAO
        //todo 在配置类中Bean的另一种注入方式
        sessionManager.setSessionDAO(sessionDAO());


        return sessionManager;
    }


    /**
     * 使用自定义的 RedisSessionDAO,用于将session存储到Redis 中
     *
     * @return
     */
    @Bean("sessionDAO")
    public SessionDAO sessionDAO(){
        //RedisSessionDAO sessionDAO = new RedisSessionDAO();

        BufferUpdateRedisSessionDAO sessionDAO = new BufferUpdateRedisSessionDAO();

        RedisManager redisManager = redisManager();
        sessionDAO.setRedisManager(redisManager);

        return sessionDAO;
    }
相关推荐
说码解字39 分钟前
如何系统学习音视频
学习·音视频
Lester_11011 小时前
嵌入式学习笔记 - 关于STM32 SPI控制器读取以及写入时,标志位TXE, RXNE的变化
笔记·学习
红烧柯基3 小时前
解决redis序列号和反序列化问题
java·数据库·redis
DDDiccc3 小时前
黑马Redis(四)
数据库·redis·mybatis
weixin_486281454 小时前
FFmpeg源码学习---ffmpeg
学习·ffmpeg
CodeCipher6 小时前
Java后端程序员学习前端之html
学习·html5
Dr_Zobot6 小时前
SLAM学习系列——ORB-SLAM3安装(Ubuntu20-ROS/Noetic)
学习·ubuntu·软件安装
枫叶20007 小时前
OceanBase数据库-学习笔记5-用户
数据库·笔记·学习·oceanbase
Nuyoah.8 小时前
《Vue3学习手记7》
javascript·vue.js·学习