1. 背景
在日常开发中,延时任务是一个无法避免的话题。为了达到延时这一目的,在不同场景下会有不同的解决方案,对各个方案优缺点的认知程度决定了架构决策的有效性。
本文章,以电商订单超时未支付为业务场景,推导多种解决方案,并对每个方案的优缺点进行分析,所涉及的方案包括:
1.数据库轮询方案。2.单机内存解决方案。3.分布式延时队列方案。
最后,为了提升研发效率,我们将使用声明式编程思想,对分布式延时队列方案进行封装,有效的分离 业务 与 技术。
1.1 业务场景
业务场景非常简单,就是大家最熟悉的电商订单,相信很多细心的小伙伴都发现,我们在电商平台下单后,如果超过一定的时间还未支付,系统自动将订单设置为超时自动取消,从而释放绑定的资源。
核心流程如下:
1.在电商平台下单,生成待支付订单;2.在规定的时间内没有完成支付,系统将自动取消订单,订单状态变成“超时取消”;3.在规定的时间内完成支付,订单将变成“已支付”
订单状态机如下:
状态机
1.2 基础组件简介
整个 Demo 采用 DDD 的设计思路,为了便于理解,先介绍所涉及的基础组件:
1.2.1. OrderInfo
订单聚合根,提供构建和取消等业务方法。具体的代码如下:
@Data
@Entity
@Table(name = "order_info")
public class OrderInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "status")
@Enumerated(EnumType.STRING)
private OrderInfoStatus orderStatus;
@Column(name = "create_time")
private Date createTime = new Date();
/**
* 取消订单
*/
public void cancel() {
setOrderStatus(OrderInfoStatus.CANCELLED);
}
/**
* 创建订单
* @param createDate
* @return
*/
public static OrderInfo create(Date createDate){
OrderInfo orderInfo = new OrderInfo();
orderInfo.setCreateTime(createDate);
orderInfo.setOrderStatus(OrderInfoStatus.CREATED);
return orderInfo;
}
}
1.2.2 OrderInfoRepository
基于 Spring Data Jpa 实现,主要用于数据库访问,代码如下:
public interface OrderInfoRepository extends JpaRepository<OrderInfo, Long> {
List<OrderInfo> getByOrderStatusAndCreateTimeLessThan(OrderInfoStatus created, Date overtime);
}
Spring Data 会根据 方法签名 或 @Query 注解生成代理对象,无需我们写任何代码,便能实现基本的数据库访问。
1.2.3. OrderInfoService
应用服务层,面向 User Case,主要完成业务流程编排,核对代码如下:
@Service
@Slf4j
public class OrderInfoService {
@Autowired
private ApplicationEventPublisher eventPublisher;
@Autowired
private OrderInfoRepository orderInfoRepository;
/**
* 生单接口 <br />
* 1. 创建订单,保存至数据库
* 2. 发布领域事件,触发后续处理
* @param createDate
*/
@Transactional(readOnly = false)
public void create(Date createDate){
OrderInfo orderInfo = OrderInfo.create(createDate);
this.orderInfoRepository.save(orderInfo);
eventPublisher.publishEvent(new OrderInfoCreateEvent(orderInfo));
}
/**
* 取消订单
* @param orderId
*/
@Transactional(readOnly = false)
public void cancel(Long orderId){
Optional<OrderInfo> orderInfoOpt = this.orderInfoRepository.findById(orderId);
if (orderInfoOpt.isPresent()){
OrderInfo orderInfo = orderInfoOpt.get();
orderInfo.cancel();
this.orderInfoRepository.save(orderInfo);
log.info("success to cancel order {}", orderId);
}else {
log.info("failed to find order {}", orderId);
}
}
/**
* 查找超时未支付的订单
* @return
*/
@Transactional(readOnly = true)
public List<OrderInfo> findOvertimeNotPaidOrders(Date deadLine){
return this.orderInfoRepository.getByOrderStatusAndCreateTimeLessThan(OrderInfoStatus.CREATED, deadLine);
}
}
1.2.4. OrderController
对外暴露的 Web 接口,提供接口创建订单,主要用于测试,代码如下:
@RestController
@RequestMapping("order")
public class OrderController {
@Autowired
private OrderInfoService orderInfoService;
/**
* 生成新的订单,主要用于测试
*/
@PostMapping("insertTestData")
public void createTestOrder(){
Date date = DateUtils.addMinutes(new Date(), -30);
date = DateUtils.addSeconds(date, 10);
this.orderInfoService.create(date);
}
}
所依赖的组件介绍完了,让我们进入第一个方案。
2. 数据库轮询方案
这是最简单的方案,每个订单都保存了创建时间,只需要写个定时任务,从数据库中查询出已经过期但是尚未支付的订单,依次执行订单取消即可。
2.1. 方案实现
核心流程如下:
数据库轮询方案
1.用户创建订单,将订单信息保存到数据库;2.设定一个定时任务,每一秒触发一次检查任务;3.任务按下面步骤执行•先从数据库中查找 超时未支付 的订单;•依次执行定的 Cancel 操作;•将变更保存到数据库;
核心代码如下:
@Service
@Slf4j
public class DatabasePollStrategy {
@Autowired
private OrderInfoService orderInfoService;
/**
* 每隔 1S 运行一次 <br/>
* 1. 从 DB 中查询过期未支付订单(状态为 CREATED,创建时间小于 deadLintDate)
* 2. 依次执行 取消订单 操作
*/
@Scheduled(fixedDelay = 1 * 1000)
public void poll(){
Date now = new Date();
Date overtime = DateUtils.addMinutes(now, -30);
List<OrderInfo> overtimeNotPaidOrders = orderInfoService.findOvertimeNotPaidOrders(overtime);
log.info("load overtime Not paid orders {}", overtimeNotPaidOrders);
overtimeNotPaidOrders.forEach(orderInfo -> this.orderInfoService.cancel(orderInfo.getId()));
}
}
2.2. 方案小结
1.优点:简单•开发简单。系统复杂性低,特别是在 Spring Schedule 帮助下;•测试简单。没有外部依赖,逻辑集中,方便快速定位问题;•上线简单。没有繁琐的配置,复杂的申请流程;2.缺点:•数据库负担重。不停的轮询,会加重数据库的负载;•时效性不足。任务最高延时为轮询时间,不适合时效要求高的场景(在订单场景已经足够);•