延时任务从入门到精通

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.缺点:•数据库负担重。不停的轮询,会加重数据库的负载;•时效性不足。任务最高延时为轮询时间,不适合时效要求高的场景(在订单场景已经足够);•

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值