本篇文档所有内容,都是基于上一篇文章的进行创作的。
单线程版
我们一个业务场景,课程培训中心无法接入内网的MQ,导致我们需要对标准的本地消息表方案进行修改。
这里说一下和标准的本地消息表实现的区别:此时本地事务完成后,没有办法向MQ发送消息了,此时需要依赖调度任务中心来调度。调度任务定时触发,会捞起消息状态为"处理中"的消息,然后安全培训服务调用课程中心接口推送数据,等待课程中心返回数据处理结果,推送过程是同步的,当收到课程培训中心的响应后,如果数据推送成功,那么更新本地消息表的状态为已成功。
异常情况分析
序号 | 安全培训服务处理业务数据 | 消息表 | 课程培训中心处理业务数据 | 说明 |
---|---|---|---|---|
1 | 失败 | - | - | 安全培训服务回滚 |
2 | 成功 | 失败 | - | 安全培训服务回滚 |
3 | 成功 | 成功 | 失败 | 调度任务中心定时调度,线程重新处理 |
4 | 成功 | 成功 | 成功 | 数据最终一致 |
5 | 成功 | 成功 | 超时 | 消息超时未被处理,死信补偿 |
优缺点
优点
- 数据推送的流程得到了简化,不需要使用MQ,可以降低系统维护的成本
- 在并发小的场景同样也可以保证数据推送的一致性
缺点
- 对数据库的依赖较高
- 单线程的进行数据推送,性能得不到保证,在高并发场景下极容易造成消息的堆积,从而影响正常的业务
多线程版
和单线程版本的区别:调度任务定时触发,会捞起消息状态为"处理中"的消息,并将消息进行切分,分成不同的数据段提交到线程池中,线程池中不同的线程负责将不同的数据段推送的课程培训中心,推送过程是同步的,当收到课程培训中心的响应后,如果数据推送成功,那么更新本地消息表的状态为已成功。
异常情况分析
序号 | 安全培训服务处理业务数据 | 消息表 | 线程池推送数据 | 课程培训中心处理业务数据 | 说明 |
---|---|---|---|---|---|
1 | 失败 | - | - | - | 安全培训服务回滚 |
2 | 成功 | 失败 | - | - | 安全培训服务回滚 |
3 | 成功 | 成功 | 失败 | - | 调度任务中心定时调度,线程重新处理 |
4 | 成功 | 成功 | 成功 | 失败 | 调度任务中心定时调度,线程重新处理 |
5 | 成功 | 成功 | 成功 | 成功 | 数据最终一致 |
6 | 成功 | 成功 | 超时 | 超时 | 消息超时未被处理,死信补偿 |
这里的异常情况,不再需要考虑MQ出现异常的情况,需要考虑线程池出现异常的情况,因为线程池没有类似MQ重试的功能,所以需要依靠调度任务中心定时调度。
一些思考
定时任务集中式扫表消息有延迟
上述方案中,第3步调度任务把需要处理的数据全部查询出来然后切分后交给线程池处理,像这种集中式的扫表,消息推送会存在延迟。因此可以改成,调度任务每次只是查处需要推送的数据的id起点,然后线程池再根据这个id起点,让不同的线程分别去扫表查询需要处理的消息。
类似这样:
ini
ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) ExecutorServiceUtils.getThreadPool();
int poolSize = threadPoolExecutor.getPoolSize();
// 根据id切分推送任务,并把任务提交到线程池
int minId=(int)idRangeMap.get("minId");
int maxId=(int)idRangeMap.get("maxId");
int startId=minId,endId;
for (int i = 0; i <poolSize; i++) {
endId=minId+THREAD_PUSH_DATA_SEGMENT*10;
if (endId>maxId){
endId=maxId;
}
log.info("第{}个线程处理,id:{}至id:{}的数据",i,startId,endId);
ExecutorServiceUtils.getThreadPool().submit(new PushDataTask(startId,endId));
startId=endId;
}
调度任务多次捞起同一条数据处理如何保证幂等
如果调度任务定时触发的时间间隔设置的比较短,一条数据还没有被推送过去,就又被捞起来处理,这样同样会造成数据的重复推送。此外,调度任务触发后,会加锁,锁定一段待推送的数据(避免因为安全培训服务是多实例,造成数据重复推送),这样还有可能造成,两个锁锁住的数据会有重叠,这样其实也是导致数据重复推送。
因此,有两种策略可以解决这个问题:
- 合理设置调度任务定时触发的时间,可以适当设置的长一些,同时线程推送的数据量也可以设置的小一些,也就是尽量让线程在一个相对较长的时间内完成相对少的数据推送
- 可以增加一个加锁状态:在数据被第一次加锁的时候,把数据的状态修改为"处理中-已加锁",每次调度任务捞取数据加锁的时候都避开这些数据,即可保证每把锁锁定的数据段都是唯一的了。