生产环境一个十分不好重现的报错
系统环境:springboot mysql mybatis shiro linux
定时器 extends SchedulingConfigurer
异步线程采用 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]
搜了下这个是shiro底层的一个报错。。。 意思从字面就能看出来:通过这个id找不到session。网上有好多情况,但是我的情况跟他们任何一个都不一样。
首先我得在我本机上重现这个问题。。就瞎猫乱抓,
出现找不到session可能是session过时了吧?
但是我们系统是前后端分离系统。。。。登录和校验都是通过token来进行。为什么这里又出现session了呢,这块我也搞不清楚。
我就找到系统shiroConfig的配置
并且添加了一句话 sessionManager.setGlobalSessionTimeout(20000);
让一个session的时长只能保持20s,因为我的异步线程大概要跑1分钟。所以
等执行到最后一个异步线程的时候大概已经过了30s,那么原来的sessionId肯定过期了。
@Bean("sessionManager")
public SessionManager sessionManager(Collection 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 sessions = new HashMap<>();
public List 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 就完事了。。。不会出现这么多问题。
但是报错也好。让我了解到了更多的东西。。。
嘿嘿,人不犯错怎么会成长呢?