用阻塞式来接收RabbitMQ的消息

目录

一、阻塞式

二、连接RabbitMQ

三、封装新订单消息 

四、发送消息

1. 同步发送消息

2. 异步发送消息

五、接收消息

六、删除与清空队列

七、程序测试


一、阻塞式

        RabbitMQ提供了阻塞和非阻塞两种收发消息的模式,默认在SpringBoot上面配置的都是非阻塞的模式。非阻塞模式不适合用在本案例中。因为非阻塞模式是后端Java程序依靠线程主动轮询消息队列,并不是移动端主动发起的请求。如果Java程序从RabbitMQ中获取到抢单消息,而移动端根本就没运行,你说这个抢单消息怎么发送给移动端?

        所以正确的做法是用阻塞式来接收RabbitMQ的消息,阻塞式顾名思义就是Java没收发完消息,绝对不往下执行其他代码。直到收完消息,然后把消息打包成R对象返回给移动端。

        虽然SpringBoot里面YML文件可以配置RabbitMQ,但是配置出来的是非阻塞的形式,这一点我们不能接受。所以我们在hxds-snm子系统的application.yml文件中,只定义了值注入的信息,然后手工编码的方式去连接RabbitMQ,这样就能使用阻塞式读写RabbitMQ的消息了。

rabbitmq:
  host: localhost
  port: 5672
  username: guest
  password: guest

二、连接RabbitMQ

        在com.example.hxds.snm.config包中创建RabbitMQConfig类,里面封装创建RabbitMQ连接的方法。因为我提供给大家的RabbitMQ没有设置密码,所以创建连接的时候没用上用户名和密码。

package com.example.hxds.snm.config;

import com.rabbitmq.client.ConnectionFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {
    @Value("${rabbitmq.host}")
    private String host;

    @Value("${rabbitmq.port}")
    private int port;

    @Value("${rabbitmq.username}")
    private String username;

    @Value("${rabbitmq.password}")
    private String password;

    @Bean
    public ConnectionFactory getFactory() {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(host);
        factory.setPort(port);
//        factory.setUsername(username);
//        factory.setPassword(password);
        return factory;
    }
}

三、封装新订单消息 

        因为新订单消息里面包含了很多内容,例如订单编号、上车点地址、终点地址、总里程、预估金额、上车点距离代驾司机的距离等等,我们在Web层和业务层之间传递新订单需要给方法定义很多参数,这太麻烦了,不如我们创建一个新订单消息的封装类。

        在com.example.hxds.snm.entity包中创建NewOrderMessage.java类,封装新订单消息。

@Data
public class NewOrderMessage {
    private String userId;
    private String orderId;
    private String from;
    private String to;
    private String expectsFee;
    private String mileage;
    private String minute;
    private String distance;
    private String favourFee;

}

四、发送消息

        我们知道RabbitMQ收发消息可以分成阻塞和非阻塞,那么Java程序执行代码也可以分成两类:同步和异步。

        同步就是由当前线程来执行,我们平时写的Java代码都属于同步执行的。

        异步执行就是说这个任务我委派给其他线程去运行,我自己继续往下执剩余代码,其实就是我们平时用的多线程编程。

        我们写程序,干脆把同步和异步发送新订单消息的方法都给写出来,至于说某种场景用同步还是异步,由开发者决定。

1. 同步发送消息

        在com.example.hxds.snm.task包中创建NewOrderMassageTask.java类,在里面定义同步发送消息的代码。

@Component
@Slf4j
public class NewOrderMassageTask {
    @Resource
    private ConnectionFactory factory;

    /**
     * 同步发送新订单消息
     */
    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());
                //创建消息属性对象
                //RabbitMQ的消息除了正文之外,还可以包含很多属性(可自定义)
                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("新订单消息发送失败");
        }
    }
}

2. 异步发送消息

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

@Component
@Slf4j
public class NewOrderMassageTask {
    ……
        
    /**
     * 异步发送新订单消息
     */
    @Async
    public void sendNewOrderMessageAsync(ArrayList<NewOrderMessage> list) {
        sendNewOrderMessage(list);
    }
}

        发送新订单消息给适合接单的司机,我倾向于是用异步发送的方式。这是因为有可能附近适合接单的司机比较多,Java程序给这些司机的队列发送消息可能需要一定的耗时,这就会导致createNewOrder()执行时间太长,乘客端迟迟得不到响应,也不知道订单创建成功没有。如果采用异步发送消息就好多了,createNewOrder()函数把发送新订单消息的任务委派给某个空闲线程,自己可以继续往下执行,这样就不会让乘客端小程序等待太长时间,用户体验更好。

五、接收消息

        接收新订单消息这块,我们决不能搞异步接收。主线程已经返回R对象了,你这个异步接收才完事,你说接收到的消息怎么发送给移动端?所以接收消息,我们必须用同步方式。

@Component
@Slf4j
public class NewOrderMassageTask {
    ……
        
    /**
     * 同步接收新订单消息
     */
    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("接收新订单失败");
        }
    }
}

六、删除与清空队列

        最后我们把删除队列和清空队列消息的代码给写一下,都有同步和异步两种方式。

@Component
@Slf4j
public class NewOrderMassageTask {
    ……
        
    /**
     * 同步删除新订单消息队列
     */
    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);
    }
}

七、程序测试

        因为乘客下单和司机接单业务流程中,需要用到发送消息和接收消息,那么咱们就来测试一下刚才封装的代码。在test目录之下创建测试类。

package com.example.hxds.snm;

import com.example.hxds.snm.entity.NewOrderMessage;
import com.example.hxds.snm.task.NewOrderMassageTask;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;

@SpringBootTest
public class Demo {
    @Resource
    private NewOrderMassageTask task;

    @Test
    public void send() {
        NewOrderMessage message = new NewOrderMessage();
        message.setUserId("9527");
        message.setFrom("沈阳北站");
        message.setTo("沈阳站");
        message.setDistance("3.2");
        message.setExpectsFee("46.0");
        message.setMileage("18.6");
        message.setMinute("18");
        message.setFavourFee("0.0");
        ArrayList list = new ArrayList() {{
            add(message);
        }};
        task.sendNewOrderMessageAsync(list);
    }

    @Test
    public void recieve() {
        List<NewOrderMessage> list = task.receiveNewOrderMessage(9527);
        list.forEach(one->{
            System.out.println(one.getFrom());
            System.out.println(one.getTo());
        });
    }
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chengbo_eva

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值