开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 13 天,点击查看活动详情
1. RabbitMQ的五种模式
RabbitMQ中对消息的收发设定了六种模式
(1) 简单模式
简单模式的中没有交换机,只有队列。生产者发送消息,消费者接收消息,完全是一对一的关系。
(2) 工作队列模式
与简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。 这些消费者是竞争关系,也就是一条消息只能被其中一个消费者消费。
(3) 发布订阅模式
该模式必须用到交换机, 交换机需要与队列进行绑定。绑定之后,一个消息可以被多个消费者都收到。交换机可以控制消息到底是发送给所有绑定的队列,还是发送给特定的队列,我们可以自己设置。
交换机分为以下3种,本项目选择Direct这种类型。
- Fanout:广播,将消息交给所有绑定到交换机的队列
- Direct:定向,把消息交给符合指定routing key 的队列
- Topic:通配符,把消息交给符合routing pattern(路由模式)的队列
(4) 路由模式
这是发布订阅模式的增强版,可以把多个routing key分配给同一个消息队列,只要其中的一个routing key满足,交换机就会转发消息。
(5) 通配符模式
这是路由模式的争强版,我们给routing key设置了通配符。只要符合某个通配符,交换机就会转发消息。例如a.#这个消息就会被转发给两个消息队列,如果是b.#这个消息只能发送给消息队列2,这就是通配符的效果。
2. 订单队列的设计方案
本项目采用的是发布订阅模式,每个司机都有自己的消息队列,绑定的名字就是司机的driverId,当需要发送抢单消息给某个司机的时候,交换机会根据driverId把消息路由给对应的消息队列。
3. 阻塞还是非阻塞
RabbitMQ提供了阻塞和非阻塞两种收发消息的模式,默认在SpringBoot上面配置的都是非阻塞的模式。非阻塞模式不适合用本项目。非阻塞模式是服务端依靠线程主动轮询消息队列,并不是移动端主动发起的请求。如果Java程序从RabbitMQ中获取到抢单消息,而移动端根本就没运行,那么抢单消息就无法发送给移动端。
用阻塞式来接收RabbitMQ的消息,服务端没有收发完消息,不会往下执行其他代码。直到收完消息,然后把消息对象返回给移动端。
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);
}
}