背景:最近接手一个新的应用-商机快递,主要是给用户发送营销邮件,由于不定期会有漏发的情况,所以在里面加了一些逻辑来修复这个问题,由于系统采用了多线程的方式,改之前考虑的不周全,最后会导致重发的现象。
// 创建一个阻塞队列,容量为maxThread*2
ArrayBlockingQueue blockQueue = new ArrayBlockingQueue(maxThread * 2);
exec = new BizExpressThreadPoolExecutor(maxThread, maxThread, 60, TimeUnit.SECONDS, blockQueue,
maxThread * 2);
exec.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 该方法可以给当前的进程注册一个清理线程,当进程退出的时候,会执行线程中的代码。
Runtime.getRuntime().addShutdownHook(new Thread(new ShutdownHook()));
while (!isShutDown) {
exec.execute(new BizExpressMailRun());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
/**
* @param corePoolSize 线程池维护线程的最少数量
* @param maximumPoolSize 线程池维护线程的最大数量
* @param keepAliveTime 线程池维护线程所允许的空闲时间
* @param unit 线程池维护线程所允许的空闲时间的单位
* @param workQueue 线程池所使用的缓冲队列
* @param semaphore
*/
public BizExpressThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, int semaphore){
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
available = new Semaphore(semaphore, true);
}
系统初始时会实例一个ThreadPoolExecutor对象(exec)。ThreadPoolExecutor是并发包中一个提供线程池的服务,可以很容易将一个实现了Runnable接口的任务放入线程池中执行。具体的execute(Runnable)方法执行过程为:
首先判断传入的Runnable对象是否为null,如果为null直接抛出NullPointException异常。如果不为空执行下面步骤
如果当前的线程数小于配置的corePoolSize,则调用addIfUnderCorePoolSize方法进而会调用mainLock锁。
如果当前的线程数小于配置的corePoolSize并且线程处于RUNNING状态,调用addThread增加线程,
addThread方法首先创建Worker对象,然后调用threadFactory( Thread newThread(Runnable r); )创建新的线程,如果创建新的线程不为null时,将Worker对象的thread属性指向此创建出来的线程,并将此Worker对象放入到workers中,最后增加当前线程池中的线程数。
用代码描述为:
private List workers;
private int count;//当前线程池中的线程数
public void addThread(Runnable r){
Worker worker=new Worker();
Thread tt=threadFactory.newThread(r);
if(rr!=null){
worker.setThread(tt);
}
workers.add(worker);
count++;
}
--------------------------
while (!isShutDown) {
exec.execute(new BizExpressMailRun());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
}
这是阻塞队列的执行入口,是一个循环过程,中间会休眠2秒,队列的长度是初始化时的corePoolSize,消费和生产无序进行。
原来的做方法是
public void doPerform() {
List<BizExpressDailyDO> bizexpresses = bizExpressDailyDAO.fetchSomeBizExpressDaily(
BizExpressConfig.getServerIp(),
BizExpressConfig.getBuildEachFetchNum());
// 如果没有取到数据,则休眠5秒,避免反复读取数据库。
if (bizexpresses == null || bizexpresses.size() == 0) {
try {
Thread.currentThread().sleep(5000);
// 补发发送失败邮件
doContinue();
} catch (InterruptedException e) {
}
return;
}
takeCareOf(bizexpresses);
}
一个线程执行doPerform方法,会改biz_express_daily表中的记录的ip,然后再执行takeCareOf方法来发邮件,发送成功后会将记录的status字段值改掉,但是关键的是在完成status改掉之前会有一段时间。
另外的线程调用doPerform方法,此时bizexpresses 为空,进入到if分支,调用doContinue方法。
public void doContinue() {
// 获取当日被选出,但未被处理过的纪录,最多取100条
List<BizExpressDailyDO> bizexpresses = bizExpressDailyDAO.fetchLastUnbuiltBizExpressDaily(
BizExpressConfig.getServerIp(),
BizExpressConfig.getBuildEachFetchNum());
if (bizexpresses.size() > 0) {
takeCareOf(bizexpresses);
} else {
return;
}
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
doContinue();
}
此时的bizexpresses 是有值的,而且取出来的正好是刚才第一个线程里面的值,然后同样执行takeCareOf方法发送邮件。
这样就会造成邮件的重发,当然根据线程抢占的激烈程度,会导致重发邮件的数量也不一致。
修复邮件重发问题
本文介绍了一个使用线程池发送营销邮件的应用遇到的邮件重发问题及其解决方案。通过改进线程池管理和数据库交互逻辑,确保了邮件的准确发送。
710

被折叠的 条评论
为什么被折叠?



