RabbitMQ在代驾订单发布上的使用

172 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 13 天,点击查看活动详情

1. RabbitMQ的五种模式

RabbitMQ中对消息的收发设定了六种模式

(1) 简单模式

简单模式的中没有交换机,只有队列。生产者发送消息,消费者接收消息,完全是一对一的关系。 image.png

(2) 工作队列模式

与简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。 这些消费者是竞争关系,也就是一条消息只能被其中一个消费者消费。

image.png

(3) 发布订阅模式

该模式必须用到交换机, 交换机需要与队列进行绑定。绑定之后,一个消息可以被多个消费者都收到。交换机可以控制消息到底是发送给所有绑定的队列,还是发送给特定的队列,我们可以自己设置。

image.png

交换机分为以下3种,本项目选择Direct这种类型。

  • Fanout:广播,将消息交给所有绑定到交换机的队列
  • Direct:定向,把消息交给符合指定routing key 的队列
  • Topic:通配符,把消息交给符合routing pattern(路由模式)的队列

(4) 路由模式

这是发布订阅模式的增强版,可以把多个routing key分配给同一个消息队列,只要其中的一个routing key满足,交换机就会转发消息。

image.png

(5) 通配符模式

这是路由模式的争强版,我们给routing key设置了通配符。只要符合某个通配符,交换机就会转发消息。例如a.#这个消息就会被转发给两个消息队列,如果是b.#这个消息只能发送给消息队列2,这就是通配符的效果。

image.png

2. 订单队列的设计方案

本项目采用的是发布订阅模式,每个司机都有自己的消息队列,绑定的名字就是司机的driverId,当需要发送抢单消息给某个司机的时候,交换机会根据driverId把消息路由给对应的消息队列。

image.png

3. 阻塞还是非阻塞

RabbitMQ提供了阻塞和非阻塞两种收发消息的模式,默认在SpringBoot上面配置的都是非阻塞的模式。非阻塞模式不适合用本项目。非阻塞模式是服务端依靠线程主动轮询消息队列,并不是移动端主动发起的请求。如果Java程序从RabbitMQ中获取到抢单消息,而移动端根本就没运行,那么抢单消息就无法发送给移动端。

image.png

用阻塞式来接收RabbitMQ的消息,服务端没有收发完消息,不会往下执行其他代码。直到收完消息,然后把消息对象返回给移动端。

image.png

4、发送消息

Java程序执行代码可以分成两类:同步和异步。

  • 同步就是由当前线程来执行,平时写的Java代码都属于同步执行的。
  • 异步执行就是说这个任务委派给其他线程去运行,主线程则继续往下执剩余代码。

在项目中,同步和异步执行都会有应用场景,下面阐述一下。

发送新订单消息给适合接单的司机,适合使用异步发送的方式。这是因为有可能附近适合接单的司机比较多,Java程序给这些司机的队列发送消息可能需要一定的耗时,这就会导致创建订单执行时间太长,乘客端迟迟得不到响应,也不知道订单创建成功没有。

如果采用异步发送消息,createNewOrder()函数把发送新订单消息的任务委派给某个空闲线程,自己可以继续往下执行,这样就不会让乘客端小程序等待太长时间,用户体验更好。

4-1. 同步发送消息

@Data
public class NewOrderMessage {
    private String userId; // 司机ID
    private String orderId; // 订单ID
    private String from; // 起始位置
    private String to; // 终点位置
    private String expectsFee; // 预估费用
    private String mileage; // 预估里程
    private String minute; // 预估时间
    private String distance; // 司机距离上车点距离
    private String favourFee; // 顾客小费
​
}
/**
     * 同步发送新订单消息
     */
    public void sendNewOrderMessage(ArrayList<NewOrderMessage> list) {
        int ttl = 1 * 60 * 1000; //新订单消息缓存过期时间1分钟
        String exchangeName = "new_order_private"; //交换机的名字
        try (
                Connection connection = factory.newConnection();
                Channel channel = connection.createChannel();
        ) {
            //定义交换机,根据routing key路由消息
            channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT);
            HashMap param = new HashMap();
            for (NewOrderMessage message : list) {
                //MQ消息的属性信息
                HashMap map = new HashMap();
                map.put("orderId", message.getOrderId());
                map.put("from", message.getFrom());
                map.put("to", message.getTo());
                map.put("expectsFee", message.getExpectsFee());
                map.put("mileage", message.getMileage());
                map.put("minute", message.getMinute());
                map.put("distance", message.getDistance());
                map.put("favourFee", message.getFavourFee());
                //创建消息属性对象
                AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().contentEncoding("UTF-8")
                        .headers(map).expiration(ttl + "").build();
​
                String queueName = "queue_" + message.getUserId(); //队列名字
                String routingKey = message.getUserId(); //routing key
                //声明队列(持久化缓存消息,消息接收不加锁,消息全部接收完并不删除队列)
                channel.queueDeclare(queueName, true, false, false, param);
                channel.queueBind(queueName,exchangeName,routingKey);
                //向交换机发送消息,并附带routing key
                channel.basicPublish(exchangeName, routingKey, properties, ("新订单" + message.getOrderId()).getBytes());
                log.debug(message.getUserId() + "的新订单消息发送成功");
            }
​
        } catch (Exception e) {
            log.error("执行异常", e);
            throw new HxdsException("新订单消息发送失败");
        }
    }

4-2. 异步发送消息

在主类中添加支持异步执行的注解EnableAsync,然后又创建了Java线程池,如果哪个方法需要异步执行,就在方法声明加上@Async注解。那么该方法执行的时候,就会自动委派给线程池的空闲线程,当前主线程会继续往下执行。

/**
     * 异步发送新消息
     */
    @Async
    public void sendNewOrderMessageAsync(ArrayList<NewOrderMessage> list) {
        this.sendNewOrderMessage(list);
    }

5、接收消息

接收新订单消息不能做异步接收。主线程已经返回了,异步可能才刚刚接收完,接收到的消息无法发送给移动端,所以接收消息,必须使用同步方式。

/**
     * 同步接收新订单消息
     */
    public List<NewOrderMessage> receiveNewOrderMessage(long userId) {
        String exchangeName = "new_order_private"; //交换机名字
        String queueName = "queue_" + userId; //队列名字
        String routingKey = userId + ""; //routing key
​
        List<NewOrderMessage> list = new ArrayList();
        try (Connection connection = factory.newConnection();
             Channel privateChannel = connection.createChannel();
        ) {
            //定义交换机,routing key模式
            privateChannel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT);
            //声明队列(持久化缓存消息,消息接收不加锁,消息全部接收完并不删除队列)
            privateChannel.queueDeclare(queueName, true, false, false, null);
            //绑定要接收的队列
            privateChannel.queueBind(queueName, exchangeName, routingKey);
            //为了避免一次性接收太多消息,我们采用限流的方式,每次接收10条消息,然后循环接收
            privateChannel.basicQos(0, 10, true);
​
            while (true) {
                //从队列中接收消息
                GetResponse response = privateChannel.basicGet(queueName, false);
                if (response != null) {
                    //消息属性对象
                    AMQP.BasicProperties properties = response.getProps();
                    Map<String, Object> map = properties.getHeaders();
                    String orderId = MapUtil.getStr(map, "orderId");
                    String from = MapUtil.getStr(map, "from");
                    String to = MapUtil.getStr(map, "to");
                    String expectsFee = MapUtil.getStr(map, "expectsFee");
                    String mileage = MapUtil.getStr(map, "mileage");
                    String minute = MapUtil.getStr(map, "minute");
                    String distance = MapUtil.getStr(map, "distance");
                    String favourFee = MapUtil.getStr(map, "favourFee");
​
                    //把新订单的消息封装到对象中
                    NewOrderMessage message = new NewOrderMessage();
                    message.setOrderId(orderId);
                    message.setFrom(from);
                    message.setTo(to);
                    message.setExpectsFee(expectsFee);
                    message.setMileage(mileage);
                    message.setMinute(minute);
                    message.setDistance(distance);
                    message.setFavourFee(favourFee);
​
                    list.add(message);
​
                    byte[] body = response.getBody();
                    String msg = new String(body);
                    log.debug("从RabbitMQ接收的订单消息:" + msg);
​
                    //确认收到消息,让MQ删除该消息
                    long deliveryTag = response.getEnvelope().getDeliveryTag();
                    privateChannel.basicAck(deliveryTag, false);
                } else {
                    break;
                }
            }
            ListUtil.reverse(list); //消息倒叙,新消息排在前面
            return list;
        } catch (Exception e) {
            log.error("执行异常", e);
            throw new HxdsException("接收新订单失败");
        }
    }

6、删除与清空队列

    /**
     * 同步删除新订单消息队列
     */
    public void deleteNewOrderQueue(long userId) {
        String exchangeName = "new_order_private"; //交换机名字
        String queueName = "queue_" + userId; //队列名字
        try (Connection connection = factory.newConnection();
             Channel privateChannel = connection.createChannel();
        ) {
            //定义交换机
            privateChannel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT);
            //删除队列
            privateChannel.queueDelete(queueName);
            log.debug(userId + "的新订单消息队列成功删除");
        } catch (Exception e) {
            log.error(userId + "的新订单队列删除失败", e);
            throw new HxdsException("新订单队列删除失败");
        }
    }
​
    /**
     * 异步删除新订单消息队列
     */
    @Async
    public void deleteNewOrderQueueAsync(long userId) {
        deleteNewOrderQueue(userId);
    }
​
    /**
     * 同步清空新订单消息队列
     */
    public void clearNewOrderQueue(long userId) {
        String exchangeName =  "new_order_private";
        String queueName = "queue_" + userId;
        try (Connection connection = factory.newConnection();
             Channel privateChannel = connection.createChannel();
        ) {
            privateChannel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT);
            privateChannel.queuePurge(queueName);
            log.debug(userId + "的新订单消息队列清空删除");
        } catch (Exception e) {
            log.error(userId + "的新订单队列清空失败", e);
            throw new HxdsException("新订单队列清空失败");
        }
    }
​
    /**
     * 异步清空新订单消息队列
     */
    @Async
    public void clearNewOrderQueueAsync(long userId) {
        clearNewOrderQueue(userId);
    }
}