简介
有这样一个业务场景,很多业务需要发邮件,如果失败了要重试,每隔5分钟重试一次,最多12次,如果是12次都失败,就记入数据库。
粗一想,很简单嘛,但是仔细想一想,好像不是那么容易,在想一想,嗯,也不是那么难。还是要亲自试一下,写一下代码才知道有哪些坑。
分析
其实,问题的关键在于时间间隔的处理,是使用定时器,还是队列把时间封装一下。
注意,很容易把这2中情况混在一起了。
下面就来把这两种情况都实现一下。
Timer实现
很多对线程池比较熟悉的朋友可能首先想到的是ScheduledThreadPoolExecutor,但是ScheduledThreadPoolExecutor有一个问题就是没有办法取消。
比如发送到第5次成功了,就需要取消周期任务,避免重复发送,ScheduledThreadPoolExecutor是不好实现的。
所以我们直接使用Timer和TimerTask。
先来一个TimerTask
import java.util.TimerTask;
public class MailSendTimerTask extends TimerTask {
private int exeNum;
private String name;
public MailSendTimerTask(String name) {
this.name = name;
}
@Override
public void run() {
if(exeNum < 12){
try{
if(MailUtil.sendMail(name)){
this.cancel();
}
}finally {
exeNum++;
}
}else {
MailUtil.logError(name);
this.cancel();
}
}
}
如果发送成功或者到了12次都失败了,我们就取消任务。
MailUtil假装发了邮件和失败后记录到数据库了
import java.util.Random;
public class MailUtil {
private static final Random random = new Random();
private static final boolean [] deafult_boolean = {false,false,false,false,false,false,false,false,false,false,false,false,true};
public static boolean sendMail(String name){
System.out.println("send mail:" + name);
return deafult_boolean[random.nextInt(deafult_boolean.length)];
}
public static void logError(String name){
System.out.println("log error:" + name);
}
}
来一个测试类:
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Start {
/**
* 5分钟
*/
// public static final int PERIOD = 1000 * 60 * 5;
/**
* 2秒
*/
public static final int PERIOD = 1000 * 2;
private static final Timer timer = new Timer();
private static final ScheduledThreadPoolExecutor scheduledExe = new ScheduledThreadPoolExecutor(2);
public static void main(String[] args) {
for(int i = 0 ;i<20;i++){
addTask(new MailSendTimerTask(String.valueOf(i)));
// exeTask(new MailSendTimerTask(String.valueOf(i)));
}
}
public static void addTask(TimerTask task){
timer.schedule(task,0, PERIOD);
}
public static void exeTask(TimerTask task){
scheduledExe.scheduleAtFixedRate(task,0,PERIOD, TimeUnit.MILLISECONDS);
}
}
我们可以看到使用ScheduledThreadPoolExecutor是没有办法取消任务的。
Timer有一些问题,首先Timer是单线程的,另外Timer执行任务抛出异常后,后面的任务就都不会执行了。
所以我们来看一下队列方式的实现。
DelayQueue实现
为了方便我们使用DelayQueue阻塞队列。
注意DelayQueue队列的元素需要实现Delayed。
public interface Delayed extends Comparable<Delayed> {
/**
* Returns the remaining delay associated with this object, in the
* given time unit.
*
* @param unit the time unit
* @return the remaining delay; zero or negative values indicate
* that the delay has already elapsed
*/
long getDelay(TimeUnit unit);
}
Delayed主要需要获取的是剩余的延迟时间和比较元素的优先级(继承了Comparable)
当getDelay<=0的时候才能从DelayQueue中获取到元素。
先来一个实现看一下:
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
public class Email implements Delayed{
/**
* 毫秒
*/
public static final int PERIOD = 1000 * 1;
private String title;
private Integer retryTimes;
private long lastSendTime;
public Email(String title, Integer retryTimes, long lastSendTime) {
this.title = title;
this.retryTimes = retryTimes;
this.lastSendTime = lastSendTime;
}
@Override
public long getDelay(TimeUnit unit) {
long delay = lastSendTime + PERIOD - System.currentTimeMillis();
// System.out.println(TimeUnit.SECONDS.convert(delay,TimeUnit.MILLISECONDS));
return unit.convert(delay,TimeUnit.MILLISECONDS);
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public Integer getRetryTimes() {
return retryTimes;
}
public void setRetryTimes(Integer retryTimes) {
this.retryTimes = retryTimes;
}
public long getLastSendTime() {
return lastSendTime;
}
public void setLastSendTime(long lastSendTime) {
this.lastSendTime = lastSendTime;
}
@Override
public int compareTo(Delayed o) {
if(!(o instanceof Email)){
return -1;
}
Email that = (Email)o;
long diff = this.lastSendTime - that.lastSendTime;
if(diff == 0)
return 0;
else if(diff > 0)
return 1;
else
return -1;
}
@Override
public String toString() {
return "Email{" +
"title='" + title + '\'' +
", retryTimes=" + retryTimes +
", lastSendTime=" + lastSendTime +
'}';
}
}
注意:getDelay必须小于等于0才能够从队列里面获取到,所以getDelay返回值应该是动态变化的,一般是和当前时间相关的。
getDelay的返回值必须的时间单位必须是纳秒。可以看一下DelayQueue的take中的实现
available.awaitNanos(delay);
这里简单说一下这个思路,我们知道DelayQueue中的队列可能有元素但是没有到延迟时间是取不到的。
如果没有元素,我们可以在加入元素的时候通知,但是有元素,时间没到怎么处理呢?
让while循环一直取?显然这是不好的,消耗cpu,DelayQueue解决方式是,使用延迟时间优先级队列,这样取出来的就是延迟时间最短的,然后再等待一个延迟时间。这样就可以在等待的时候让出cpu,避免无用的消耗。
compareTo也大(返回值>0),表示延迟时间也长,优先级也低。
我们有了Delayed就可以使用DelayQueue来存放任务了,然后就可以开线程池来执行任务了。
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MailHandler {
private static final Random random = new Random();
private static final DelayQueue<Email> failQueue = new DelayQueue<Email>();
private static final ExecutorService service = Executors.newFixedThreadPool(2);
private static final MailHandler instance = new MailHandler();
private static final boolean [] deafult_boolean = {false,false,false,false,false,false,false,false,false,false,false,false,true};
private MailHandler(){
service.submit(new MailSender());
}
public static MailHandler getInstance(){
return instance;
}
public void failHandle(List<Email> mails) {
failQueue.addAll(mails);
}
private static class MailSender implements Runnable{
@Override
public void run() {
while (true){
Email to = null;
try {
to = failQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
if(to == null) {
continue;
}
System.out.println(to);
if(!deafult_boolean[random.nextInt(deafult_boolean.length)]){
Integer retryTimes = to.getRetryTimes();
if(retryTimes < 3) {
to.setRetryTimes(retryTimes + 1);
to.setLastSendTime(System.currentTimeMillis());
failQueue.offer(to);
}else{
System.out.println(to.getTitle() + "----Failure!");
}
}else{
System.out.println(to.getTitle() + "----Success!");
}
}
}
}
public static void main(String[] args) throws InterruptedException {
MailHandler instance = MailHandler.getInstance();
LinkedList<Email> list = new LinkedList<>();
Random random = new Random();
long lastSendTime = System.currentTimeMillis();
for(int i =0;i<20;i++){
list.add(new Email(String.valueOf(i),0, lastSendTime + random.nextInt(5) * 1000));
}
instance.failHandle(list);
}
}
能看出上面的代码有那些问题吗?
其实这就是一个生成消费者模式,我们开了一个2个线程从线程池,但是我们只提交了一个任务。 所以我们可以稍微修改一下构造函数:
private MailHandler(){
service.execute(new MailSender());
service.execute(new MailSender());
}
这里使用Executors.newFixedThreadPool是在适合不过了,因为从开始我们就能决定使用多少个线程来处理任务,和不确定任务数明显不同。
你可以尝试重构一下,也许就能发现这其中的一些微妙的区别。亲自修改一下代码试一下,调试一下,是最容易发现问题的方式。
我们也没有使用submit的方式了,而是使用的execute方式,如果不关心结果的话最好使用execute,如果出错了至少能得到一点堆栈信息,如果使用submit就什么也没有了,除非自定义ThreadPoolExecutor处理了堆栈信息。
使用:
System.out.println(Thread.currentThread().getName()+ ":" + to);
代替:
System.out.println(to);
可以看一下是由哪一个线程处理的任务。