当前位置: 首页 > news >正文

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() 方法

          

/***************************************************** 解决单次请求多次访问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*/@Overrideprotected 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代码如下:

             

/***************************************************** 自定义 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;@Overrideprotected Serializable doCreate(Session session) {if(session == null){log.error("session is null");throw new UnknownSessionException("session is null");}//1、根据Seesion 生成 SeesionIdSerializable sessionId = this.generateSessionId(session);//2、关联sessionId 与 session,可以基于sessionId拿到sessionthis.assignSessionId(session,sessionId);//3、保存sessionsaveSession(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 中的 sessionKeyString 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);}@Overrideprotected Session doReadSession(Serializable sessionId) {if(sessionId == null){log.error("sessionId is null !");}Session session = null;try{//从Redis 中读取sessionString 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*/@Overridepublic 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);}}@Overridepublic 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);}@Overridepublic 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

@Beanpublic 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,代码如下:

       

/***************************************************** 参考开源项目 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() 方法

//添加的字段
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();@Overrideprotected Session doReadSession(Serializable sessionId) {if(sessionId == null){log.error("sessionId is null !");}Session session = null;try{//1、先从内存中读取Session,即从 ThreadLocal 中读取Sessionsession = getSessionFromThreadLocal(sessionId);if(session != null){return session;}//2、ThreadLocal中Session 不存在,然后再从Redis 中读取sessionString 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);}//构建SessionInMemorySessionInMemory sessionInMemory = new SessionInMemory();sessionInMemory.setSession(session);sessionInMemory.setCreateTime(new Date());memoryMap.put(sessionId,sessionInMemory);}

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

        

@Beanpublic 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

/***************************************************** 在前边 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以外其他字段发生改变时为trueprivate boolean isChanged = false;public ShiroSession() {super();this.setChanged(true);}public ShiroSession(String host) {super(host);this.setChanged(true);}@Overridepublic void setId(Serializable id) {super.setId(id);this.setChanged(true);}@Overridepublic void setStopTimestamp(Date stopTimestamp) {super.setStopTimestamp(stopTimestamp);this.setChanged(true);}@Overridepublic void setExpired(boolean expired) {super.setExpired(expired);this.setChanged(true);}@Overridepublic void setTimeout(long timeout) {super.setTimeout(timeout);this.setChanged(true);}@Overridepublic void setHost(String host) {super.setHost(host);this.setChanged(true);}@Overridepublic void setAttributes(Map<Object, Object> attributes) {super.setAttributes(attributes);this.setChanged(true);}@Overridepublic void setAttribute(Object key, Object value) {super.setAttribute(key, value);this.setChanged(true);}@Overridepublic Object removeAttribute(Object key) {this.setChanged(true);return super.removeAttribute(key);}/*** 停止*/@Overridepublic void stop() {super.stop();this.setChanged(true);}/*** 设置过期*/@Overrideprotected void expire() {this.stop();this.setExpired(true);}public boolean isChanged() {return isChanged;}public void setChanged(boolean isChanged) {this.isChanged = isChanged;}@Overridepublic boolean equals(Object obj) {return super.equals(obj);}@Overrideprotected boolean onEquals(SimpleSession ss) {return super.onEquals(ss);}@Overridepublic int hashCode() {return super.hashCode();}@Overridepublic String toString() {return super.toString();}
}

     2)修改 RedisSessionDAO 中 的 update()

 @Overridepublic 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 放的时候永远设置为falsess.setChanged(false);}this.saveSession(session);} catch (Exception e) {log.warn("update Session is failed", e);}}

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

/***************************************************** 仿造 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*/@Overridepublic Session createSession(SessionContext initData) {if (initData != null) {String host = initData.getHost();if (host != null) {return new ShiroSession(host);}}return new ShiroSession();}
}

     4)将 SimpleSessionFactory 注入到 SessionManager 中,

 @Beanpublic 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);//设置SessionFactorysessionManager.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 方法,实现代码如下:   

           

/***************************************************** 在前边 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;@Overridepublic 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 方法如下:

@Overridepublic 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

@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 方法,代码如下:

          

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();@Overrideprotected Serializable doCreate(Session session) {if(session == null){log.error("session is null");throw new UnknownSessionException("session is null");}//1、根据Seesion 生成 SeesionIdSerializable sessionId = this.generateSessionId(session);//2、关联sessionId 与 session,可以基于sessionId拿到sessionthis.assignSessionId(session,sessionId);//3、保存sessionsaveSession(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 中的 sessionKeyString 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);}@Overrideprotected Session doReadSession(Serializable sessionId) {if(sessionId == null){log.error("sessionId is null !");}Session session = null;try{//1、先从内存中读取Session,即从 ThreadLocal 中读取Sessionsession = getSessionFromThreadLocal(sessionId);if(session != null){return session;}//2、ThreadLocal中Session 不存在,然后再从Redis 中读取sessionString 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);}//构建SessionInMemorySessionInMemory sessionInMemory = new SessionInMemory();sessionInMemory.setSession(session);sessionInMemory.setCreateTime(new Date());memoryMap.put(sessionId,sessionInMemory);}/*** 更新Session* @param session the Session to update* @throws UnknownSessionException*/@Overridepublic 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);}}@Overridepublic 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);}@Overridepublic 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,代码如下:

         

public class BufferUpdateRedisSessionDAO extends RedisSessionDAO {/*** 定义基于定时任务的线程池,用于定时更新Session* 更新 Session 到Redis 需要单线程执行,所以核心线程数为1*/private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);/*** 最后一次需要更新的Session,更新之后 lastSession 需要设置为空** 分布式下,lastSession 需要能让其他线程看到*/private volatile Session lastSession = null;@Overridepublic 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,代码如下:

@Beanpublic 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);//设置SessionFactorysessionManager.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;}

相关文章:

  • Kotlin DSL 深度解析:从 Groovy 迁移的困惑与突破
  • 加密算法:ed25519和RSA
  • 如何搭建spark yarn 模式的集群集群。
  • 快速搭建对象存储服务 - Minio,并解决临时地址暴露ip、短链接请求改变浏览器地址等问题
  • Matlab自学笔记五十二:变量名称:检查变量名称是否存在或是否与关键字冲突
  • 如何创建并使用极狐GitLab 受保护分支?
  • 第二十节:编码实操题-实现图片懒加载指令
  • Milvus(9):字符串字段、数字字段
  • Linux查看文件列表并按修改时间降序排序
  • Sql刷题日志(day6)
  • QTableView复选框居中
  • K8S学习笔记01
  • uniapp+vue3+ts 使用canvas实现安卓端、ios端及微信小程序端二维码生成及下载
  • 线性代数的本质大白话理解
  • 分布式链路追踪理论
  • [ACTF2020 新生赛]Include [ACTF2020 新生赛]Exec
  • Ubuntu深度学习革命:NVIDIA-Docker终极指南与创新实践
  • python练习:求数字的阶乘
  • Ubuntu 20.04 上安装 最新版CMake 3.31.7 的详细步骤
  • Spring Boot定时任务
  • 保利发展去年净利润约50亿元,在手现金1342亿元
  • 传智教育连续3个交易日跌停:去年净利润由盈转亏
  • 中日友好医院通报“医师肖某被举报”:基本属实,开除党籍并解聘
  • CSR周刊:李宁打造世界地球日特别活动,珀莱雅发布2024年度可持续发展报告
  • 教育强国建设基础教育综合改革试点来了!改什么?怎么改?
  • 央媒谈多地景区试水“免费开放”:盲目跟风会顾此失彼