【分布式】Rabbitmq死信队列模型、实战场景---订单延迟30min支付处理

分布式


RabbitMQ 死信队列/ 延迟队列 — 延迟业务逻辑


最近可能分布式进入Redission扩展一下之前的项目,加上分布式锁,进行压测和性能测试就会很少改版了— 部署在个人云服务器上了,同时接下来可能进入应试阶段 — ms、bg、coding,捋一捋项目, 巩固一下计算机基础,准备高级

延时、延迟处理指定的业务逻辑 ---- 在实际生产中非常常见, 比如商城订单完成之后,用户如果一直没有评价,5天后自动好评,会员到期前15天、某个业务到期前X天提示 【 ECS最近给我提示麻了】, 延迟处理的业务逻辑, 之前就是定时器扫表, 但是这样子性能太低了,数据库压力大,RabbitMQ的延迟队列就可以解决

死信队列

RabbitMQ的几个重要的组件就是交换机、队列、和其对应的绑定,依靠交换机的类似路由转发就可以实现消息从生产者传递给消费者, 交换机可以有Topic通配、Direct路由、Fanout广播, 同时也可以直接选择确认消费, 实现Listener,调用Channel信道就可以手动ACK确认消费

但是上面的都是普通的队列模型 — 消息一旦进入独立额,就会马上被对应的消费者监听消费, 但是有的消息不希望被立刻消费,就上面Cfeng说的场景,所以这个时候就可以使用延迟队列 【 之前就是放数据库中定时器扫描】 ,RabbitMQ也是会定时扫描,可以观察后端控制台发现

死信队列 、延迟队列、延时队列, 是RabbitMQ队列中的一种, 进入这种类型队列的消息会被延迟消费,而普通队列就是立刻消费

下面分析一下 传统方式 和RabbitMQ方式: 春运12306抢票场景 【高并发抢购的Cache减库存处理这里就不再深入, 接口限流这里也不细分】

传统的定时器扫表方式:

开启定时器(单独开一个线程),每10s扫描数据库表,执行业务逻辑【失效的通知,快到期的发消息提示】,轮询的方式一直运行,知道手动关闭,否则线程一直运行

12306抢到火车票之后,官方提示用户抢票成功,并且提醒30s内付款,这个时候会生成一个订单,上面是倒计时,正常情况输入密码支付后成功, 但是用户如果没有支付,那么就需要取消这个订单并且重新放出火车票

简易实现: 用户下单后生成一个订单记录,其一个字段state就是用户支付状态,最开始为未支付, 放入数据库表,同时发送消息给用户提示30分钟内付款 【 可以直接返回一个Reponse即可】,用户方面如果付款,就会立刻将订单的状态变为已支付 — 于此同时,新开一个线程,开启定时器,每10s执行一次,扫描数据库表,查找所有的未支付订单,比较当前时间和下单时间, 如果超过30min, 取消该订单(删除记录或者放到废弃记录表),之后退还该车票

春运抢票是高并发、大数据量,正常情况会有越来越多的人抢票,那么就会产生大量的未支付订单,定时器又需要频繁扫描数据库【cache就是为了减少访问】,那么就会压垮数据库服务器,最终可能宕机,Over

网站崩溃、点击购买无响应 — 并发量过大,数据库压力过大导致内存、CPU、网络、数据库服务等负载过高

RabbitMQ一定程度上解决了业务、应用问题

用户抢票下单后生成的订单信息录入数据库,同时将该订单ID等信息放入RabbitMQ延迟队列 【 时间设置30分钟 — 30分钟后再处理消息,FIFO】,

用户支付后订单的状态变为已支付,更新数据库表

延迟队列30分钟后,相关的消费者消费消息 – 可以依据消息的ID从数据库中查询订单的状态, 如果未支付就代表订单失效,将订单的车票返回

RabbitMQ主要就是优化了定时器的逻辑,不再需要频繁轮询数据库表,减小了数据库压力, 使用延迟队列代替, 死信队列的消息都会在TTL后再进行处理

死信队列的典型应用场景 就是需要延迟处理的业务逻辑, 比如下单后30分钟内必须付款,或者30分钟没有进行邮箱验证发送提醒, 而预警功能【比如是否过期,还是使用传统过的定时器任务即可,SpirngBoot简化后就一个@EnableScheduling和 @Schedule就可以实现】

RabbitMQ的死信队列引入的目的就是用于 “ 延迟” 一段时间再执行,延迟是自动化的,不需要人为干涉

死信队列demo

传统的定时器轮询处理方式, 死信队列占用的系统资源少 【不需要轮询数据库获得数据,减少了DB压力】、 人为干涉少,放入延迟队列即可,搭建好模型后可以不需要过多关注、 自动消费处理【延迟的时间到达时,消息会自动路由到实际的队列处理】

死信队列消息模型

设置参数args: x-dead-letter-exchange x-dead-letter-routing-key x-message-ttl

普通的队列就是 Producer发送消息携带Routing key到达交换机,交换机查询Binding,后将消息发送到key同的Queue中,由监听该Queue的消费者消费

而死信队列同样具有这些组件,同时还有DLX,DLK、TTL,DLX和DLK必须, TTL可选

  • DLX : Dead Letter Exchagne: 死信交换机, 特殊的一种交换机
  • DLK: Dead Letter Routing-Key: 死信路由, 特殊的路由,和DLX组成死信队列
  • TTL : Time to Live【redis也有】,进入死信队列的消息存活时间,TTL后,消息Dead,进入下一个中转站,等待消费

消息变为Dead的情况:

  • 消息被拒绝: basic.reject, basic.nack; 消息被拒绝后,不会再重新投递,requeue的参数取值false
  • 消息TTL过: 消息的设置的存活时间过了,自然dead – 可以在消息属性中设置消息的存活时间,messageProperties.setExpiration()
  • 队列达到最大长度: 队列达到最大长度,不能再投放新的消息

当消息Dead后,消息将会重新投递publish到另外一个Exchange,也就是DLX,死信交换机,由DLK分发到真正的队列,最终消费,没有被死信队列消费的消息将会换一个地方消费 — 实现延迟消费,死信交换机

在这里插入图片描述

简单来说: 就是两个交换机,两个路由,两个队列,基本Exchange绑定死信Queue, 死信Queue的死信交换机依靠DLK绑定到 消费者队列Queue

可以看到死信队列和普通的队列模型的差异就是在基本消息模型绑定的真正队列中间先绑定到死信队列(DLX — dead后下一个中转站、DLK、TTL)

Producer发送message给基本Exchange,由Exhange以及Binding Key,将消息放到死信队列【第一个暂存区】,进入后TTL开始倒计时,TTL到,消息进入DLX,由DLK指定发送到指定的真正的Queue中, 消费者监听消费 【进入死信队列的Message等待TTL后由DLX和DLK转发到Queue】

TTL既可以设置为死信队列一部分,也可以在MessageProperties中设置,当同时设置的时候,消息的最大存活时间 取二者最小

为什么说死信队列由DLX、DLK、TTL组成呢?

其实就是这三个部分为死信队列一部分,可以查看Queue的源码,重载的构造方法中其中一个为:

public Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, @Nullable Map<String, Object> arguments) {

其中DLX、DLK就是在Map arguments中声明

各种技术都先来个demo演示: 就是延迟30分钟处理MessageEntity

MessageEntity随意封装,主要是在Config中搭建死信队列消息模型

##自定义配置死信交换机名称
    queue-dead-name: middleware.mq.dead.queue
    exhange-dead-name: middleware.mq.dead.exchange
    route-dead-name: middleware.mq.dead.route
    exchange-basic-name: middleware.producer.basic.exchange
    route-basic-name: middleware.producer.basic.route
    queue-basic-name: middleware.producer.basic.queue

先配置一下交换机和rotue等的名称,实际上一个系统就一个交换机即可

/**
     * 死信队列消息模型
     */
    @Bean
    public Queue basicDeadQueue() {
        //死信队列,需要指定Map
        Map<String,Object> arguments = new HashMap<>();
        //创建死信交换机
        arguments.put("x-dead-letter-exchange",Objects.requireNonNull(environment.getProperty("spring.rabbitmq.exhange-dead-name")));
        //拆功能键死信路由
        arguments.put("x-dead-letter-routing-key",Objects.requireNonNull(environment.getProperty("spring.rabbitmq.route-dead-name")));
        //设置TTL, 10 000ms, 10s
        arguments.put("x-message-ttl",10000);

        //创建死信队列
        return new Queue(Objects.requireNonNull(environment.getProperty("spring.rabbitmq.queue-dead-name")),true,false,false,arguments);
    }

    @Bean
    public TopicExchange basicProducerExchange() {
        return new TopicExchange(Objects.requireNonNull(environment.getProperty("spring.rabbitmq.exchange-basic-name")),true,false);
    }

    // 创建基本交换机和死信队列的绑定,并指定binding key
    @Bean
    public Binding basicProducerBinding() {
        return BindingBuilder.bind(basicDeadQueue()).to(basicProducerExchange()).with(Objects.requireNonNull(environment.getProperty("spring.rabbitmq.route-basic-name")));
    }

    //消费者消费的真正队列
    @Bean
    public Queue realConsumerQueue() {
        return new Queue(Objects.requireNonNull(environment.getProperty("spring.rabbitmq.queue-basic-name")),true);
    }
    
    //上面的死信队列只是指定了死信交换机和死信路由的名称,还需要创建出来
    @Bean
    public TopicExchange basicDeadExchange() {
        return new TopicExchange(Objects.requireNonNull(environment.getProperty("spring.rabbitmq.exhange-dead-name")),true,false);
    }
    
    @Bean
    public Binding basicDeadBindign() {
        return BindingBuilder.bind(realConsumerQueue()).to(basicDeadExchange()).with(Objects.requireNonNull(environment.getProperty("spring.rabbitmq.route-dead-name")));
    }

死信Queue就是一个中转, 所以会涉及到两组组件, 既包括基本交换机和基本路由的绑定,也有死信交换机和死信路由和TTL

接下来就创建一个死信模型的消息发送Demo, 首先是生产者DeadProducer

 * 创建rabbitMQ死信队列生产者
 */

@Component
@Slf4j
@RequiredArgsConstructor
public class DeadProducer {

    private final ObjectMapper objectMapper;

    private final RabbitTemplate rabbitTemplate;

    private final Environment environment;

    public void sendDeadMessage(MessageEntity messageEntity) {
        try {
            rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
            //指定路由的交换机和routing key
            rabbitTemplate.setExchange(Objects.requireNonNull(environment.getProperty("spring.rabbitmq.exchange-basic-name")));
            rabbitTemplate.setRoutingKey(Objects.requireNonNull(environment.getProperty("spring.rabbitmq.route-basic-name")));
            //将消息发送到死信队列
            rabbitTemplate.convertAndSend(messageEntity,message -> {
                MessageProperties properties = message.getMessageProperties();
                properties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                properties.setHeader(AbstractJavaTypeMapper.DEFAULT_KEY_CLASSID_FIELD_NAME,MessageEntity.class);
                //同时设置TTL,以较短的为准,在配置时设置的死信队列TTL 10s
                properties.setExpiration(String.valueOf(10000));
                return message;
            });
            log.info("死信队列消息模型,消息发送成功: {}",messageEntity);
        } catch (Exception e) {
            log.error("死信对垒模型: 消息发送异常: {}",messageEntity,e.fillInStackTrace());
        }
    }
}

相比之前的基本的模型没有太大的不同,只是这里可以设置一个TTL,当同时设置时,以较短的时间为准,这里都是10s

@RequiredArgsConstructor
@Component
@Slf4j
public class DeadConsumer {

    private final ObjectMapper objectMapper;

    //这里就自动确认消费即可
    @RabbitListener(queues = "${spring.rabbitmq.queue-basic-name}",containerFactory = "singleListenerContainer")
    public void consumeMessage(@Payload MessageEntity messageEntity) {
        try {
            log.info("死信队列消息模型 消费者 监听到消息 : {}", messageEntity);
            //业务逻辑,处理消息
        } catch (Exception e) {
            log.info("死信队列消息模型 消费者 消费异常: {}",messageEntity,e.fillInStackTrace());
        }
    }
}

之后调用生产者发送消息即可,就像之前的用户登录写日志,写入数据库操作异步进行,所以就直接使用Rabbit的生产者发送消息,加快登录流程, 提升用户体验

@Test
    public void testDead() throws Exception {
        deadProducer.sendDeadMessage(new MessageEntity(1,"我上线测试Dead",new Student(1,"zs","miss")));
        //这里只是将消息投递到私信队列,为了看到结果,不能只是投递了之后就结束进程,先sleep一下
        Thread.sleep(20000);
    }

执行的结果就可以看到消费者成功消费消息

2022-09-18 21:12:40.770  INFO 15164 --- [           main] c.server.rabbitmq.producer.DeadProducer  : 死信队列消息模型,消息发送成功: MessageEntity(id=1, message=我上线测试Dead, sender=Student(id=1, realName=zs, username=miss))
2022-09-18 21:12:40.774  INFO 15164 --- [nectionFactory1] c.server.config.RabbitmqConfig           : 消息发送成功:correlationData(null),ack(true),cause(null)
2022-09-18 21:12:50.805  INFO 15164 --- [ntContainer#0-1] c.server.rabbitmq.consumer.DeadConsumer  : 死信队列消息模型 消费者 监听到消息 : MessageEntity(id=1, message=我上线测试Dead, sender=Student(id=1, realName=zs, username=miss))

观察监听器收到消息的时间确实少了10s

平台订单支付超时 — 演示

RabbitMQ死信队列应用广泛,其主要就是在基本模型基础上加上死信Queue暂存,其占用系统资源更少、自动实现监听【自动化操作】

电商大火的当下,秒杀和延迟时常见的两个场景

在这里插入图片描述

【we 在正式coding前要做好分析可行性、需求分析、总体设计、结构设计, 像这样将业务流程表现清楚,编码才有理有据】

这就按照这个流程实现用户下单, 用户选购商品加入购物车之后,用户下单之后生成订单,需要提示用户30分钟内完成支付,记录进入订单表,同时将下单的记录编号当作消息压入延迟队列, 30min后自动放入监听队列,监听器依靠该id查询数据库中的订单,如果支付就提示,没有支付则订单失效,商品返回

业务分析

用户下单支付超时业务 场景, 这个场景主要是三个流程组成: 用户下单,延迟队列发送、延迟监听下单记录状态、更新用户下单状态

  • 用户下单业务 : 用户点击 去支付、结算 按钮,在数据库中插入一条订单的记录,设置支付状态为 已保存
  • 死信队列发送、延迟队列监听: 用户下单功能之后,将下单的id作为message放入延迟队列,并且在最后的队列中进行监听
  • 更新下单状态: 监听到之后,查询数据库,如果为 已保存 那么说明还没有支付、订单失效

代码实现

由于使用的mybatis-plus,基本的依照ID进行CRUD就不编写了,设计数据库表直接逆向工程,这里就只需要设计一张秒杀后的支付订单类和传递的消息【包含订单的ID】

use db_middleware;

DROP TABLE IF EXISTS `user_order`;
CREATE TABLE `user_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_no` varchar(255) NOT NULL COMMENT '订单编号',
  `user_id` int(11) NOT NULL COMMENT '用户id',
  `status` int(11) DEFAULT NULL COMMENT '支付状态(1=已保存;2=已付款;3=已取消)',
  `is_active` int(255) DEFAULT '1' COMMENT '是否有效(1=有效;0=失效)',
  `create_time` datetime DEFAULT NULL COMMENT '下单时间',
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8 COMMENT='用户下单记录表';

DROP TABLE IF EXISTS `mq_order`;
CREATE TABLE `mq_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_id` int(11) NOT NULL COMMENT '下单记录id',
  `business_time` datetime DEFAULT NULL COMMENT '失效下单记录的时间',
  `memo` varchar(255) DEFAULT NULL COMMENT '备注信息',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='RabbitMQ失效下单记录的历史记录表';

这里只是一个部分的业务场景,不是完整的业务场景,所以这里就不展示所有的记录了,后面Cfeng.net的源码开放的时候会展示完整的数据库表【当然是其他延迟处理业务,不是下单延迟支付】

@Data
@EqualsAndHashCode(callSuper = false)
public class UserOrder extends Model<UserOrder> {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 订单编号
     */
    private String orderNo;

    /**
     * 用户id
     */
    private Integer userId;

    /**
     * 支付状态(1=已保存;2=已付款;3=已取消)
     */
    private Integer status;

    /**
     * 是否有效(1=有效;0=失效)
     */
    private Integer isActive;

    /**
     * 下单时间
     */
    private LocalDateTime createTime;

    private LocalDateTime updateTime;

随着业务的开展,重复的简单的业务确实没必要花费时间做重复运动,持久层框架逐步自动化(mybatis-plus、JPA),Lombok插件都是基于这进行设计, we也需要思考,重复的东西如何优化,封装一个更加方便的插件

业务逻辑中需要使用延迟队列, Cfeng刚刚已经演示过死信队列,所以这里我不再做重复工作(费时无用),直接复用

这里为了方便测试,TTL就设置为10s

 @Bean
    public Queue basicDeadQueue() {
        //死信队列,需要指定Map
        Map<String,Object> arguments = new HashMap<>();
        //创建死信交换机
        arguments.put("x-dead-letter-exchange",Objects.requireNonNull(environment.getProperty("spring.rabbitmq.exchange-dead-name")));
        //拆功能键死信路由
        arguments.put("x-dead-letter-routing-key",Objects.requireNonNull(environment.getProperty("spring.rabbitmq.route-dead-name")));
        //设置TTL, 10 000ms, 10s
        arguments.put("x-message-ttl",10000);

        //创建死信队列
        return new Queue(Objects.requireNonNull(environment.getProperty("spring.rabbitmq.queue-dead-name")),true,false,false,arguments);
    }

    @Bean
    public TopicExchange basicProducerExchange() {
        return new TopicExchange(Objects.requireNonNull(environment.getProperty("spring.rabbitmq.exchange-basic-name")),true,false);
    }

    // 创建基本交换机和死信队列的绑定,并指定binding key
    @Bean
    public Binding basicProducerBinding() {
        return BindingBuilder.bind(basicDeadQueue()).to(basicProducerExchange()).with(Objects.requireNonNull(environment.getProperty("spring.rabbitmq.route-basic-name")));
    }

    //消费者消费的真正队列
    @Bean
    public Queue realConsumerQueue() {
        return new Queue(Objects.requireNonNull(environment.getProperty("spring.rabbitmq.queue-basic-name")),true);
    }

    //上面的死信队列只是指定了死信交换机和死信路由的名称,还需要创建出来
    @Bean
    public TopicExchange basicDeadExchange() {
        return new TopicExchange(Objects.requireNonNull(environment.getProperty("spring.rabbitmq.exchange-dead-name")),true,false);
    }

    @Bean
    public Binding basicDeadBinding() {
        return BindingBuilder.bind(realConsumerQueue()).to(basicDeadExchange()).with(Objects.requireNonNull(environment.getProperty("spring.rabbitmq.route-dead-name")));
    }

这里参数设置就是DLX、DLK、TTL, 配置之后,如果出现了406的相关ERROR提示,直接删除队列重建即可,因为可能Queue存在不能强加参数

接下来简单开发一下Controller,MVCM实现即可,Middleware主要辅助实现应用系统的核心的业务逻辑

controller在没有引入中间件层之前只是接收处理前端请求、并且处理调用和兴业务逻辑层服务

引入之后 还需要提供额外服务的中间件处理层Middleware

先简单封装前后台数据交互的DTO

@Data
@ToString
public class UserOderDto implements Serializable {

    private static final long serialVersionUID = 3941276201724001282L;
    //订单编号
    @NotNull
    private String orderNo;

    //用户编号
    @NotNull
    private Integer userId;
}

之后编写核心业务逻辑类: 完成用户下单功能和 更新用户下单状态

public interface DeadUserOrderService {

    //用户下单,将下单记录id压入死信队列
    void pushUserOrder(UserOderDto userOderDto) throws Exception;

    //更新用户下单状态,修改数据库表
    void updateUserOrderRecord(UserOrder userOrder);
}


@Service
@Slf4j
@RequiredArgsConstructor
public class DeadUserOrderServiceImpl implements DeadUserOrderService {

    //死信队列生产者,用于将消息发布到死信队列处理
    private final DeadProducer deadProducer;

    //数据库表的dao
    private final UserOrderMapper userOrderMapper;

    private final MqOrderMapper mqOrderMapper;

    @Override
    public void pushUserOrder(UserOderDto userOderDto) throws Exception {
        //用户下单
        UserOrder userOrder = new UserOrder();
        //对象复制,属性复制
        BeanUtils.copyProperties(userOderDto,userOrder);
        //设置支付状态为已保存
        userOrder.setStatus(1);
        //设置下单时间
        userOrder.setCreateTime(LocalDateTime.now());
        //插入下单记录,这里应该是同步的,必须要将记录插入数据库
        userOrderMapper.insert(userOrder);
        log.info("用户下单成功,下单信息: {}",userOrder);
        //压入死信队列,默认就是异步的, 上面Order生成了一个默认的ID
        Integer orderId = userOrder.getId();
        //将该ID压入死信队列,这里Cfeng为了复用直接修改MessageEntity
        deadProducer.sendDeadMessage(new MessageEntity(orderId));
    }

    //商品失效的处理
    @Override
    public void updateUserOrderRecord(UserOrder userOrder) {
        try {
            if(!Objects.isNull(userOrder)) {
                //更新失效用户的下单记录
                userOrder.setIsActive(0);
                //更新时间
                userOrder.setUpdateTime(LocalDateTime.now());
                //下单实体更新
                userOrderMapper.updateById(userOrder);

                //下单历史记录,mqOrder
                MqOrder mqOrder = new MqOrder();
                mqOrder.setBusinessTime(LocalDateTime.now());
                mqOrder.setOrderId(userOrder.getId());
                //信息
                mqOrder.setMemo("更新失效的当前用户下单的记录ID,orderID= " + userOrder.getId());
                mqOrderMapper.insert(mqOrder);
            }
        } catch (Exception e) {
            log.error("用户下单支付超时业务: 更新失效订单异常 {}",e.fillInStackTrace());
        }
    }
}

之后复用之前的Dead消息的生产者和消费者

@Component
@Slf4j
@RequiredArgsConstructor
public class DeadProducer {

    private final ObjectMapper objectMapper;

    private final RabbitTemplate rabbitTemplate;

    private final Environment environment;

    public void sendDeadMessage(MessageEntity messageEntity) {
        try {
            rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
            //指定路由的交换机和routing key
            rabbitTemplate.setExchange(Objects.requireNonNull(environment.getProperty("spring.rabbitmq.exchange-basic-name")));
            rabbitTemplate.setRoutingKey(Objects.requireNonNull(environment.getProperty("spring.rabbitmq.route-basic-name")));
            //将消息发送到死信队列
            rabbitTemplate.convertAndSend(messageEntity,message -> {
                MessageProperties properties = message.getMessageProperties();
                properties.setDeliveryMode(MessageDeliveryMode.PERSISTENT);
                properties.setHeader(AbstractJavaTypeMapper.DEFAULT_KEY_CLASSID_FIELD_NAME,MessageEntity.class);
                //同时设置TTL,以较短的为准,在配置时设置的死信队列TTL 10s
                properties.setExpiration(String.valueOf(10000));
                return message;
            });
            log.info("死信队列消息模型,消息发送成功: {}",messageEntity);
        } catch (Exception e) {
            log.error("死信对垒模型: 消息发送异常: {}",messageEntity,e.fillInStackTrace());
        }
    }
}

消费者接收到消息需要对消息进行处理,查询状态,如果没有支付,那么就需要让订单失效【更新订单信息,同时插入mq失效历史订单表】

@RequiredArgsConstructor
@Component
@Slf4j
public class DeadConsumer {

    private final ObjectMapper objectMapper;

    private final UserOrderMapper userOrderMapper;

    private final DeadUserOrderService userOrderService;

    //这里就自动确认消费即可
    @RabbitListener(queues = "${spring.rabbitmq.queue-basic-name}",containerFactory = "singleListenerContainer")
    public void consumeMessage(@Payload MessageEntity messageEntity) {
        try {
            log.info("死信队列消息模型 消费者 监听到消息 : {}", messageEntity);
            //业务逻辑,处理消息, orderID在messageEntity中, 本来可以编写更加精确的sql,但这里cfeng就直接使用生成的处理一下
            UserOrder userOrder = userOrderMapper.selectById(messageEntity.getId());
            if(!Objects.isNull(userOrder)) {
                if(Objects.equals(userOrder.getStatus(),1)) {
                    //未支付
                    userOrderService.updateUserOrderRecord(userOrder);
                }
            }
        } catch (Exception e) {
            log.info("死信队列消息模型 消费者 消费异常: {}",messageEntity,e.fillInStackTrace());
        }
    }
}

处理器简单调用service处理即可,只是注意返回的ResponseEntity

@RestController
@RequestMapping("/order")
@RequiredArgsConstructor
public class UserOrderController {

    private DeadUserOrderService deadUserOrderService;

    // BindingResult就是验证的结果
    @PostMapping("/purchase")
    public ResponseEntity<String> purchaseGoods(@Validated @RequestBody UserOderDto userOderDto, BindingResult bindingResult) {
        //验证参数的合法性
        if(bindingResult.hasErrors()) {
            //参数不合法
            return new ResponseEntity(HttpStatus.BAD_REQUEST);
        }
        try {
            deadUserOrderService.pushUserOrder(userOderDto);
        } catch (Exception e) {
            return new ResponseEntity(HttpStatus.BAD_REQUEST);
        }
        return ResponseEntity.ok("请求成功");
    }

接下来就可以直接使用PostMan进行测试,测试之后的其中一个结果

2022-09-18 22:12:46.668  INFO 13568 --- [nio-8081-exec-4] c.s.s.impl.DeadUserOrderServiceImpl      : 用户下单成功,下单信息: UserOrder(id=8, orderNo=109978871, userId=110, status=1, isActive=null, createTime=2022-09-18T22:12:46.337671500, updateTime=null)
2022-09-18 22:12:46.689  INFO 13568 --- [nio-8081-exec-4] c.server.rabbitmq.producer.DeadProducer  : 死信队列消息模型,消息发送成功: MessageEntity(id=8)
2022-09-18 22:12:46.694  INFO 13568 --- [nectionFactory1] c.server.config.RabbitmqConfig           : 消息发送成功:correlationData(null),ack(true),cause(null)
2022-09-18 22:12:56.708  INFO 13568 --- [ntContainer#0-1] c.server.rabbitmq.consumer.DeadConsumer  : 死信队列消息模型 消费者 监听到消息 : MessageEntity(id=8)

可以看到10s后消费者监听到消息,因为这个过程中用户没有支付【没有修改状态,所以失效】, 数据库失效订单表中已经有数据

在实际项目中,可以将时间设置为30min

Refreshed 2022-09-18 22:17:07 Virtual host

其实rabbitmq也是定时刷新、轮询,只是不需要轮询数据库,而是轮询的队列,和redis一样,将工作台换了一个地方,减轻数据库的压力

死信队列创建成功之后,严格要求不再修改,修改会报错,除非删除队列重建,严格要求是为了保证安全,如果线上环境修改可能导致数据丢失,重复消费等

在绑定组件时需要思考清楚,修改可以直接在rabbitMQ后端控制台调整(不建议) ,或者修改config的组件绑定关系,删除后台控制台组件,重新运行项目在Rabbit后端控制台生成新的组件

这里最后提一句: 延时功能的实现不是只有RabbitMQ, 还可以借助Redis的驻网格内存综合中间件Redission实现、 时间轮、soted set等都可以… 只是MQ使用更广泛(毕竟是专业做Message的】🎄

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值