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
在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代码都属于同步执行的。
异步执行就是说这个任务我委派给其他线程去运行,我自己继续往下执剩余代码,其实就是我们平时用的多线程编程。
我们写程序,干脆把同步和异步发送新订单消息的方法都给写出来,至于说某种场景用同步还是异步,由开发者决定。
在com.example.hxds.snm.task
包中创建NewOrderMassageTask.java
类,在里面定义同步发送消息的代码。
@Component
@Slf4j
public class NewOrderMassageTask {
@Resource
private ConnectionFactory factory;
/**
* 同步发送新订单消息
*/
public void sendNewOrderMessage(ArrayList 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("新订单消息发送失败");
}
}
}
在主类中我们已经添加了支持异步执行的注解,然后我们又创建了Java线程池,你可以确认一下。如果哪个方法需要异步执行,我们就在方法声明加上@Async
注解。那么该方法执行的时候,就会自动委派给线程池的空闲线程,当前主线程会继续往下执行。
@Component
@Slf4j
public class NewOrderMassageTask {
……
/**
* 异步发送新订单消息
*/
@Async
public void sendNewOrderMessageAsync(ArrayList list) {
sendNewOrderMessage(list);
}
}
发送新订单消息给适合接单的司机,我倾向于是用异步发送的方式。这是因为有可能附近适合接单的司机比较多,Java程序给这些司机的队列发送消息可能需要一定的耗时,这就会导致createNewOrder()
执行时间太长,乘客端迟迟得不到响应,也不知道订单创建成功没有。如果采用异步发送消息就好多了,createNewOrder()
函数把发送新订单消息的任务委派给某个空闲线程,自己可以继续往下执行,这样就不会让乘客端小程序等待太长时间,用户体验更好。
接收新订单消息这块,我们决不能搞异步接收。主线程已经返回R对象了,你这个异步接收才完事,你说接收到的消息怎么发送给移动端?所以接收消息,我们必须用同步方式。
@Component
@Slf4j
public class NewOrderMassageTask {
……
/**
* 同步接收新订单消息
*/
public List receiveNewOrderMessage(long userId) {
String exchangeName = "new_order_private"; //交换机名字
String queueName = "queue_" + userId; //队列名字
String routingKey = userId + ""; //routing key
List 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 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 list = task.receiveNewOrderMessage(9527);
list.forEach(one->{
System.out.println(one.getFrom());
System.out.println(one.getTo());
});
}
}