最近因为业务需求,需要做一个定时汇总数据的功能,因此使用到了ServletContextListener和timer定时器,今天来总结一下这次的踩坑之旅。
程序发布到生产环境后没有产生新的汇总数据,日志上的现象是,两个server同时启动线程,并且同时开始汇总数据(真的是同时,毫秒数是不是一致没注意,但是秒数真的是一样的),没有输出汇总结束的日志,因为生产环境使用websphere,部署模式是单个node两个server,也就是两个server同时使用一个war包,所以我断定是两个相同的程序同时对数据库进行操作(查增改都有)产生了死锁。
核心的破解方法一定是只让其中一个server启动这个线程,为此我想了如下几个方法来控制。
方法一:通过IP区分,但因为都是在一个node上,所以两个server的IP相同,卒
方法二:通过服务端口区分,但因为在ServletContextListener中,不能通过request获取服务端口,而获取容器端口的方法又因为容器不同而不同,websphere在网上这方面的资料几!乎!没!有!方法二又卒
方法三:通过插表判断,新建一张表table,只有一个字段,比如叫flag,varchar类型,在表里预先插入一条数据值为’1’,
使用的SQL: update table set flag=varchar_format(current timestamp,‘YYYY-MM-DD’) where DATE_TIME != varchar_format(current timestamp,‘YYYY-MM-DD’)
然后程序里通过判断返回的更新数量来决定这个server是不是启动timer,更新成了会返回1,不成功返回0(持久层是mybatis)
附上代码:
ServletContextListener
public class TimerListener implements ServletContextListener{
private Timer timer = null;
@Override
public void contextInitialized(ServletContextEvent event) {
ServletContext servletContext = event.getServletContext();
WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
DataProcessingService dataProcessingService = webApplicationContext.getBean(DataProcessingService.class);
boolean flag = dataProcessingService.timerListenerMark();
if(!flag){
System.out.println("-------------------------------不执行TimerListener-----------------------------------");
return;
}
}
@Override
public void contextDestroyed(ServletContextEvent event) {
}
}
DataProcessingService
@Service
public class DataProcessingServiceImpl implements DataProcessingService {
@Autowired
private TimerListenerMarkMapper timerListenerMarkMapper;
@Override
public boolean timerListenerMark() {
boolean flag = false;
//成功将字段update成当前日期,则返回true以执行timer,否则返回false不执行timer
int i = timerListenerMarkMapper.updateTimerLisMark();
if(i > 0){
flag = true;
}
return flag;
}
}
TimerListenerMarkMapper
<update id="updateTimerLisMark">
update TIMERLISTENERMARK
set DATE_TIME = varchar_format(current timestamp,'YYYY-MM-DD')
where DATE_TIME != varchar_format(current timestamp,'YYYY-MM-DD')
</update>
方法三成功了
本以为这个问题解决了就全解决了,然而,生活不止眼前的苟且,还有对你无穷无尽的虐。
单独执行timer的server日志上依旧只有汇总数据的开始,看不到结束,至此隐隐的感觉上边做的那些事都白干了,于是我就想会不会是线程内程序异常终止了,所以做了个测试,将里面用到的表名改成了没有的表的名字,在本地启动程序,果然现象和生产环境的现象一致,代码:
TimerListener
public class TimerListener implements ServletContextListener{
private Timer timer = null;
@Override
public void contextInitialized(ServletContextEvent event) {
ServletContext servletContext = event.getServletContext();
WebApplicationContext webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
DataProcessingService dataProcessingService = webApplicationContext.getBean(DataProcessingService.class);
timer = new Timer(true);
event.getServletContext().log("定时器已启动");
System.out.println("-------------------------------TimerListener executing-----------------------------------");
//设置执行时间
Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);//每天
//定制每天的11:00:00执行
calendar.set(year, month, day, 11, 30, 00);
java.util.Date date = calendar.getTime();
//每天定时执行一次
int period = 1000 * 60 * 60 * 24;//执行周期一天
timer.schedule(new DataProcessingTask(dataProcessingService), date, period);
}
}
DataProcessingTask
public class DataProcessingTask extends TimerTask{
protected final Log logger = LogFactory.getLog(getClass());
private static boolean isRunning = false;
private DataProcessingService dataProcessingService;
public DataProcessingTask(DataProcessingService dataProcessingService){
this.dataProcessingService = dataProcessingService;
}
public DataProcessingTask(){
super();
}
@Override
@Transactional
public void run() {
if (!isRunning) {
isRunning = true;
logger.info("开始执行任务。");
//开始处理数据
dataProcessingService.processingData();
logger.info("任务执行结束。");
isRunning = false;
} else {
logger.info("上一次任务执行还未结束,本次任务不能执行。");
}
}
}
总结下来就是多线程内有异常了不会输出到日志上,就那么悄无声息的停止
解决办法是在方法上加了异常捕获,修改后的run方法如下:
@Override
@Transactional
public void run() {
if (!isRunning) {
isRunning = true;
logger.info("开始执行任务。");
//开始处理数据
try{
dataProcessingService.processingData();
}catch(Exception e){
e.printStackTrace();
}
logger.info("任务执行结束。");
isRunning = false;
} else {
logger.info("上一次任务执行还未结束,本次任务不能执行。");
}
}
再次打到生产环境运行,这次终于把错误信息打印出来了,结果就是因为生产环境少了一张表,程序异常导致线程终止,将表导给生产环境,程序终于正常运行,至此,填完收工