在电商项目中,我们经常会遇到这样的需求:客户下单成功后,在一定的时间内未能按时(具体时间由业务规则决定)支付,需要将订单自动取消,释放占用的商品库存。类似于这样的需求(延时任务),我们该怎样解决呢?本文主要分享我项目中实际应用的方式
基于RabbitMQ实现
死信队列(Dead Letter Queue)
当消息在队列中变成死信(消费者无法正常处理的消息)之后,它会被重新投递到死信交换机,死信交换机上绑定的消费队列就是死信队列 RabbitMQ的 Queue 可以配置 x-dead-letter-exchange
和 x-dead-letter-routing-key
(可选)两个参数,用来控制队列内出现死信时,则按照这两个参数重新投递
可以看到我们正常的队列,指定了死信交换机和路由Key
核心思路:将设置了过期时间的消息,投放到没有消费者的队列,当消息过期后就会成为死信,死信消息会被投放到死信交换机,我们就可以通过死信队列来消费处理,从而达到延时任务的效果
但是这种方案只适用于过期时间统一的场景(不然实现起来会麻烦,也很复杂),如果过期时间不一样,那么就会出现过期时间长的消息阻塞过期时间短的消息(队列是先进先出的,每次只会判断队头的消息是否过期)。
延时消息插件rabbitmq_delayed_message_exchange
RabbitMQ可以不用死信队列也能实现延迟消息,那就是基于官方出的rabbitmq_delayed_message_exchange
插件 (ps:怎么安装插件,网上有教程可以搜一下)
需要注意的是该插件支持的最大延长时间是(2^32)-1 毫秒,大约49天
首先我们要声明一个x-delayed-message类型的交换机
核心思路:通过这个交换机的消息不会立即进入到x-delayed-message队列中,而是存放到了一个基于Erlang开发的Mnesia数据库中,然后通过一个定时器去查询需要被投递的消息(判断消息过期时间),再把他们投递到x-delayed-message队列中
到这我们已经了解了RabbitMQ实现延时任务的核心,在实际的项目中,我们还需要一系列的手段来保证业务的正常进行
- 消息丢失
通过任务表,来记录我们的延时任务
- 消息重复投递或重复消费
任务表中我们记录了任务的执行状态,可以根据执行状态来避免此类问题
sql
create table job_info
(
id varchar(32) not null comment 'ID'
primary key,
job_no varchar(64) not null comment '任务编号',
tx_no varchar(225) null comment '任务流水号',
job_data json null comment '任务数据',
job_headers varchar(512) null comment '任务数据头',
job_execute_res text null comment '任务执行结果',
job_execute_date datetime null comment '任务执行日期',
job_status varchar(16) null comment '任务状态',
job_execute_status varchar(16) null comment '任务执行状态',
call_url varchar(255) null comment '执行调度地址',
call_type varchar(16) null comment '调度类型',
call_retry varchar(16) null comment '调度重试',
remark text null comment '备注',
status varchar(4) null
)
comment '定时任务表' collate = utf8_bin;
总结
除了基于RabbitMQ实现延时任务,还有其它很多种包括像定时任务、JDK延迟队列、redis、RocketMQ等等来实现,至于选哪种方式,还是应该由具体的业务来决定(适合最重要)。不过如果遇到开头我说的场景,还是推荐基于RabbitMQ插件这种方式。