最近做一个线上新闻的app系统后台,涉及到一个推送的功能。开始设计时没想太多,以为非常的容易,无非定时的把未处理的数据进行处理不就可以了吗?
于是写了个这样的代码,
@Scheduled
void work() {
for (Message message : queryNotHander()) {
push(message);
}
}
List<Message> queryNotHander() {return new ArrayList<Message>();};
void push(Message message) {
System.out.println(message);
}
发现没,简单定时批量任务处理逻辑骨架。可是总觉得,有问题,心里不踏实,不敢保证代码不出异常啊?要是出异常了,那线程不就终止了吗,后续不就不能继续处理了吗?肯定不行。既然每条记录的处理需要单独的隔离,那么必然单条记录的异常必须不能影响到后续其他的处理逻辑,改代码,如下:
@Scheduled
void work() {
for (Message message : queryNotHander()) {
push(message);
}
}
List<Message> queryNotHander() {return new ArrayList<Message>();};
void push(Message message) {
try {
doPush(message);
changeStatusToSuccess(message);
} catch (Exception e) {
log.info("", e);
}
}
void doPush(Message message) {}
/**
* 把状态改变为成功状态,不然下次还会处理相同的数据
* @param message
*/
void changeStatusToSuccess(Message message){}
多了一个具体处理方法,在push中进行了异常的捕获,并对于异常进行了日志记录,方便后面的异常追查,并且还做了状态的控制,成功处理的改变状态为success。
但是,仔细分析,还有很大的问题,如果定时任务查询出来的一批数据,并且没有到下一次定时启动时处理完成,也就是没有打上成功的标记,会如何?那么下一次就会同样查出相同的数据,也就是重复执行,尤其对于高频率的定时来说更是如此,怎么解决?看下面代码:
@Scheduled
void work() {
for (Message message : queryNotHander()) {
process(message);
}
}
List<Message> queryNotHander() {return new ArrayList<Message>();};
void process(Message message) {
try {
if(!changeStatusByCASToHandering(message))
return;
doProcess(message);
changeStatusFromHandingToSuccess(message);
} catch (Exception e) {
log.info("", e);
}
}
void doProcess(Message message) {}
/**
* 把处理中的状态变为处理成功
* @param message
*/
void changeStatusFromHandingToSuccess(Message message){}
/**
* 通过cas比较将状态从未处理到处理中
* @param message
* @return true 状态改变成功 fasle 失败(此时说明有其他线程已经占用了资源,也就是说其他线程正在处理)
*/
boolean changeStatusByCASToHandering(Message message) {return true;}
上面流程,好似没问题了,循环资源,处理资源前进行cas操作(改变新值时比较原值)进行资源的占用(保证不会有其他线程会查出),然后处理,最后改变资源成功处理的标记。流程上是没问题了,不过代码去有些问题,异常处理不够完善,进行简单的修改,最后代码为:
@Scheduled
void work() {
for (Message message : queryNotHander()) {
process(message);
}
}
List<Message> queryNotHander() {return new ArrayList<Message>();};
void process(Message message) {
boolean successFlag = false;
try {
if(!changeStatusByCASToHandering(message))
return;
doProcess(message);
successFlag = true;
} catch (Exception e) {
log.info("", e);//此处为进行失败处理的地方,进行日志记录或者数据记录,或者邮件通知等等操作。
} finally {
if(successFlag)
changeStatusFromHandingToSuccess(message);
else
changeStatusFromHandingToFailure(message);
}
}
void doProcess(Message message) {}
void changeStatusFromHandingToFailure(Message message) {};
/**
* 把处理中的状态变为处理成功
* @param message
*/
void changeStatusFromHandingToSuccess(Message message){}
/**
* 通过cas比较将状态从未处理到处理中
* @param message
* @return true 状态改变成功 fasle 失败(此时说明有其他线程已经占用了资源)
*/
boolean changeStatusByCASToHandering(Message message) {return true;}
此流程用于频率很高的定时,应该没问题了,但是具体的场景,doProcess方法需要不同的处理。因为doProcess失败可能会在核心的业务处理完成后,也许是掉用了远程服务成功的后续处理异常,也许是数据更新完成后续处理异常(当然事务回滚除外)等等。处理失败也有很简单的一种情况,业务数据没有改变或者远程服务没有调用前的异常。
所以对于失败的处理,需要根据业务数据是否改变进行具体的处理,如何定义,需要看具体的业务,或者此方法中,就应该只处理核心的业务,保证失败的原子性(失败后业务数据并不会改变)。
总之,定时任务尤其是频率很高的定时,需要有非常严谨的思路以及细心的设计。