there is no session with sessionId XXX 异步线程导致shiro报错

异步线程采用 ThreadPoolTaskExecutor

先说下大概的业务:

①采用定时器每天从服务器的某个文件夹下获取txt数据,然后将数据处理存放到一临时表。
②开启一个新的线程,将临时表数据同步到正式表中。
③再开启一个线程对正式表进行分析处理,生成用户能看懂的报表或业务数据。
一共是2个异步线程!

为什么要开启多次线程呢 ?

答案是: 如果上面三个步骤放到一个事务中的话,那么如果前面2个没问题,就第三个报错了,回滚起来就直接回滚到第一步了。
如果发现前面2步没问题,那么可以写一个功能从前台直接触发第三步。不用重新跑前面的数据了。

异步线程可以保证每个线程都是一个独立的事务

最开始想的是直接要不new一个thread来实现异步线程,但是这种放在代码里面看的不是很简洁。
往上搜了下,比较合适的是spring里面自带的一个ThreadPoolTaskExecutor。没了解太多直接就用上去了,然后也没出啥问题,就部署到生成环境了。

@Bean(name="processExecutor")
	    public TaskExecutor workExecutor() {
	        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
	        threadPoolTaskExecutor.setThreadNamePrefix("Async-");
	        threadPoolTaskExecutor.setCorePoolSize(10);//核心线程池数  满10个后又从1开始
	        threadPoolTaskExecutor.setMaxPoolSize(20);// 最大线程
	        threadPoolTaskExecutor.setQueueCapacity(600);//队列容量
	        threadPoolTaskExecutor.afterPropertiesSet();
	        return threadPoolTaskExecutor;   
	    }

在系统启动类里面添加上面这个bean并且@EnableAsync 在类上允许我们系统启用异步,然后在定义个异步方法上面增加 @Async(“processExecutor”) ,这个方法便变成了异步方法。

系统上去后也没问题 ,晚上定时器也正常运转。

大概隔了一周,定时器执行报错了

There is no session with id [007983ef-7039-4f76-8429-6f5f599f71e1]
1
搜了下这个是shiro底层的一个报错。。。 意思从字面就能看出来:通过这个id找不到session。网上有好多情况,但是我的情况跟他们任何一个都不一样。

首先我得在我本机上重现这个问题。。就瞎猫乱抓,
出现找不到session可能是session过时了吧?
但是我们系统是前后端分离系统。。。。登录和校验都是通过token来进行。为什么这里又出现session了呢,这块我也搞不清楚。

我就找到系统shiroConfig的配置
并且添加了一句话 sessionManager.setGlobalSessionTimeout(20000);
让一个session的时长只能保持20s,因为我的异步线程大概要跑1分钟。所以
等执行到最后一个异步线程的时候大概已经过了30s,那么原来的sessionId肯定过期了。

  @Bean("sessionManager")
    public SessionManager sessionManager(Collection<SessionListener> listeners){
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionValidationSchedulerEnabled(true);
        sessionManager.setSessionIdCookieEnabled(true);
       sessionManager.setGlobalSessionTimeout(20000);//session 20s后过期。
        return sessionManager;
    }

前台不停去调用 还真的。。。重现这个问题了。。。

我还是去跟了下源码:
报错是从org.apache.shiro.session.mgt.eis.AbstractSessionDAO

  public Session readSession(Serializable sessionId) throws UnknownSessionException {
        Session s = doReadSession(sessionId);
        if (s == null) {
            throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
        }
        return s;
    }

这里引发的 继续往上跟。。最终跟到我们系统

	public static SysUserEntity getUserEntity() {
		return (SysUserEntity)SecurityUtils.getSubject().getPrincipal();
	}

在调用getPrincipal()的时候去底层去readSession了
如果我直接return null 那么还真的就不报上面那个错了。。。。
好像问题解决了????
但是又不对呀,我这里这个静态方法是获取系统当前用户的,return null了。取不到当前用户也不合适呀。
而且为什么这里通过sessionid取的时候又取不到当前session呢?定时器这个sessionid 是怎么产生的?

我就又写了一个监听session的方法。。。

@Configuration
public class SessionConfig implements  SessionListener  {
	private static final Map<Serializable, SimpleSession> sessions = new HashMap<>();

	public List<Session> getActiveSessions() {
		System.out.println(new ArrayList<>(sessions.values()));
		return new ArrayList<>(sessions.values());
	}
  
	 @Override  
	    public void onStart(Session session) {//会话创建触发 已进入shiro的过滤连就触发这个方法  
	        // TODO Auto-generated method stub  
	        Subject currentUser = SecurityUtils.getSubject();	
		   sessions.put(session.getId(), (SimpleSession)session);
	       LogUtils.getLog().info(new ArrayList<>(sessions.values()));
	       System.out.println("session总数为:"+sessions.values().size());
	      for( SimpleSession s:sessions.values())
	    	  {
	    	  System.out.println("sessionId为:"+s.getId()+"______"+s.isExpired());
	    	  }

	    }  
	  
	    @Override  
	    public void onStop(Session session) {//退出  
	        // TODO Auto-generated method stub  
	        System.out.println("退出会话:" + session.getId());  
	    }  
	  
	    @Override  
	    public void onExpiration(Session session) {//会话过期时触发  
	        // TODO Auto-generated method stub  
	    	sessions.remove(session.getId());
	        System.out.println("会话过期:" + session.getId());   
	    }  
	  

}

在上面的sessionManager里面
// sessionManager.setSessionListeners(listeners);

这里可以监听到前台的方法。。。。定时器跑的监听不到。 说明定时器就没有session?
答案就是定时器不会有session。。

继续说:
我到前台也写了一个按钮,来实现业务。
大概就是:
①点击按钮直接返回已后台执行,然后新线程开始从服务器取数据
②开启一个新的线程,将临时表数据同步到正式表中。
③再开启一个线程对正式表进行分析处理,生成用户能看懂的报表或业务数据。

从前台点击的话便是3个线程。

这篇文章确实写得有点乱,因为基本上都是一会一个想法,我想把我的想法都写出来。

不知道怎么的我就各种乱尝试,前面说了 我对ThreadPoolTaskExecutor 不是很熟悉,继续瞎猫乱抓, 我把核心线程改成100个。。。竟然不报那个错了。

而且我又到网上看到以及结合debug发现。 线程池里面的异步线程执行完后是不会消失的。。。。而且执行顺序大概是 1-2-3-4-5-6-7-8-9-10-1-2-3-4-5·····
执行到10后又回到1

这让我突然想到是不是会有这么一种情况:

线程池里面的线程包含session的信息,比如async-1线程,上次执行完后里面还包含着老的sessionId,但是这个session 30分钟后就过期了。下次再用刀async-1的时候用老的sessionId去获取session肯定是拿不到。

但是上面我在定时器启动的时候 并没有监听到任何session的创建呀。

突然听到同事说了这么一句话,新创建的线程会继承原来有的主线程的session信息

我大概理了下:恍然大悟,最终的推断也和我想的如出一辙:

目前系统能触发到异步线程的路径有2条: 后台定时器 和 前台手工按钮触发。

定时器执行的话会执行2个异步线程
前台调用的话会执行3个异步线程。

①如果从前台跑的话。。。 那三个异步线程会继承主线程的sessionId.
②如果从后台跑的话。。。 那2个线程没有sessioid。但是也不会报错。
(sessionId是空的,所以这句话SecurityUtils.getSubject().getPrincipal()应该不会继续往下执行)

今天是4.25号
我们服务器22号启动了一次。。。
然后当天手动执行了一次
线程池里面存在 Async-1,Async-2,Async-3.并且这三个是带走主线程的sesionId的。
4.23号和4.24都是自动跑 4.25早上发现由于服务器的文件为空,所以没跑,便手动执行了下。

日期 线程 是否有sessionid 发起方式
4-22 Async-1,Async-2,Async-3 有 手动发起
4-23 Async-4,Async-5 无 定时器
4-24 Async-6,Async-7 无 定时器
4-25 Async-8,Async-9,Async-10 有 手动发起
--------------------- 我当时已经就推断:如果再执行一次,就会报错。 执行两次也会报错。 执行三次也会报错。 但是执行第四次就会成功。。。
为什么我会这么推论?
目前线程刚好执行到Async-10。那么下次肯定又是1了
但是Async-1是有sessionId的。而且Async-1里面的sessionId 在4.22号已经过期了。所以会报错。
Async-2,Async-3同理。

等到Async-4的时候,因为这个线程是无sessionId的 那么前台调用到他后会正常执行。
Async-5,Async-6也是无sessionId的,那么Async-4,Async-5,Async-6 会正常执行,不会报错。

完事后,再调用一次。Async-7因为没有sessionId会直接成功。Async-8因为带有sessionId 所以会报错。。。

最终实践下来跟我猜测得一模一样。

怎么解决呢?

其实很简单,只要每次调用线程完后释放掉即可。下次重新开辟新的线程肯定不会带有已经过时的sessionId。

@Bean(name="processExecutor")
	    public TaskExecutor workExecutor() {
	        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
	        threadPoolTaskExecutor.setThreadNamePrefix("Async-");
	        threadPoolTaskExecutor.setCorePoolSize(10);//核心线程池数  满10个后又从1开始
	        threadPoolTaskExecutor.setMaxPoolSize(20);// 最大线程
	        threadPoolTaskExecutor.setQueueCapacity(600);//队列容量
	        threadPoolTaskExecutor.afterPropertiesSet();
	        threadPoolTaskExecutor.setAllowCoreThreadTimeOut(true);//允许核心线程空闲时,过了一定的时间自动销毁
	        threadPoolTaskExecutor.setKeepAliveSeconds(1);//设置线程空闲时间为1秒  1秒后便销毁
	        return threadPoolTaskExecutor;
	    }

添加2行代码threadPoolTaskExecutor.setAllowCoreThreadTimeOut(true);
threadPoolTaskExecutor.setKeepAliveSeconds(1) 即可!!!!

其实这个出错最大的就是我盲目使用了ThreadPoolTaskExecutor 线程池!
其实直接new thread 就完事了。。。不会出现这么多问题。
但是报错也好。让我了解到了更多的东西。。。
嘿嘿,人不犯错怎么会成长呢?

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值