MQ背景
對於例如發送郵件或短信等行爲,傳統做法往往是自上而下執行,這樣一來,增加用戶等待響應時間,嚴重影響用戶體驗。
之後,開始將這些延時操作放入異步線程去處理,但是這樣會增加CPU的開銷。
故消息中間件橫空出世,很好地解決了這一痛點,實現異步、解耦、流量削峯等功能
市面主流的MQ
ActiveMQ
歷史悠久的開源項目,是Apache下的一個子項目。
已經在很多產品中得到應用,實現了JMS1.1規範,可以和spring-jms輕鬆融合,實現了多種協議,不夠輕巧(源代碼比RocketMQ多),
支持持久化到數據庫,對隊列數較多的情況支持不好。
RabbitMQ
結合erlang語言本身的併發優勢,支持很多的協議:AMQP,XMPP, SMTP, STOMP,也正是如此,使的它變的非常重量級,
更適合於企業級的開發。
RocketMQ
阿里系下開源的一款分佈式、隊列模型的消息中間件,原名Metaq,3.0版本名稱改爲RocketMQ,是阿里參照kafka設計思想使用java
實現的一套mq。同時將阿里系內部多款mq產品(Notify、metaq)進行整合,只維護核心功能,去除了所有其他運行時依賴,保證核心功能
最簡化,在此基礎上配合阿里上述其他開源產品實現不同場景下mq的架構,目前主要多用於訂單交易系統。
Kafka
Apache下的一個子項目,使用scala實現的一個高性能分佈式Publish/Subscribe消息隊列系統,具有以下特性:
高吞吐:在一臺普通的服務器上既可以達到10W/s的吞吐速率;
高堆積:支持topic下消費者較長時間離線,消息堆積量大;
RabitMQ環境的基本安裝 (windows)
1.下載並安裝erlang,下載地址:http://www.erlang.org/download
2.配置erlang環境變量信息
新增環境變量ERLANG_HOME=erlang的安裝地址
將%ERLANG_HOME%\bin加入到path中
3.下載並安裝RabbitMQ,下載地址:http://www.rabbitmq.com/download.html
注意: RabbitMQ 它依賴於Erlang,需要先安裝Erlang。
RabitMQ管理平臺中心
RabbitMQ 管理平臺地址 http://127.0.0.1:15672
默認賬號:guest/guest 用戶可以自己創建新的賬號
Virtual Hosts:
每個VirtualHost相當一個相對獨立的RabbitMQ服務器,VirtualHost之間是相互隔離的。exchange、queue、message不能互通。
默認的端口15672:rabbitmq管理平臺端口號
默認的端口5672: rabbitmq消息中間內部通訊的端口
默認的端口號25672 rabbitmq集羣的端口號
快速入門RabbitMQ簡單隊列
Maven依賴
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>3.6.5 </version>
</dependency>
</dependencies>
獲取連接
public class RabitMQConnection {
/**
* 獲取連接
*
* @return
*/
public static Connection getConnection() throws IOException, TimeoutException {
// 1.創建連接
ConnectionFactory connectionFactory = new ConnectionFactory();
// 2.設置連接地址
connectionFactory.setHost("127.0.0.1");
// 3.設置端口號:
connectionFactory.setPort(5672);
// 4.設置賬號和密碼
connectionFactory.setUsername("yanxiaohui");
connectionFactory.setPassword("yanxiaohui");
// 5.設置VirtualHost
connectionFactory.setVirtualHost("/yxh");
return connectionFactory.newConnection();
}
}
生產者
public class Producer {
private static final String QUEUE_NAME = "rabbit_mq_demo";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
// 1.創建我們的連接
Connection connection = RabitMQConnection.getConnection();
// 2.創建我們通道
Channel channel = connection.createChannel();
// 開啓了確認消息機制
channel.confirmSelect();
String msg = "這是一個mq的入門案例";
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
if (channel.waitForConfirms()) {
System.out.println("發送消息成功");
} else {
System.out.println("發送消息失敗");
}
channel.close();
connection.close();
}
}
消費者
public class Consumer {
private static final String QUEUE_NAME = "rabbit_mq_demo";
private static int serviceTimeOut = 1000;
public static void main(String[] args) throws IOException, TimeoutException {
// 1.創建我們的連接
Connection connection = RabitMQConnection.getConnection();
// 2.創建我們通道
final Channel channel = connection.createChannel();
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消費消息msg:" + msg);
// 手動ack應答模式
channel.basicAck(envelope.getDeliveryTag(), false);
try {
Thread.sleep(serviceTimeOut);
} catch (Exception e) {
e.printStackTrace();
}
}
};
// 3.創建我們的監聽的消息
channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
}
}
RabbitMQ如何保證消息不丟失
生產者:投遞消息時採用消息確認機制confirm,確保消息投遞至消息中間件中,否則進行重新投遞
消費者:採用手動ack的方式通知消息中間件刪除消息,即消息成功消費後再將其刪除
中間件:採用持久化的方式將消息持久化至硬盤中
RabitMQ五種消息模式
點對點
消費者自動確認機制,即生成一個消息,推送一個消息至消費者,不管其是否消費成功,默認自動刪除消息
工作模式
消費者手動ack通知消息的服務器刪除消息,然後推送下一個。
在有多個消費者時,默認輪詢推送,但基於前一個消息刪除後纔會對其推送下個消息,故手動ack可以實現能者多勞的工作模式
發佈訂閱(fanout)
引入交換機的概念,多個隊列綁定至同一交換機上,生產者只需要將消息投放至交換機,交換機就會通過發佈訂閱的方式將
消息廣播至其上隊列。
交換機類型
Fanout exchange(扇型交換機)默認
Direct exchange(直連交換機)
Topic exchange(主題交換機)
Headers exchange(頭交換機)
生產者
public class ProducerFanout {
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 創建Connection
Connection connection = RabitMQConnection.getConnection();
// 創建Channel
Channel channel = connection.createChannel();
// 通道關聯交換機
channel.exchangeDeclare(EXCHANGE_NAME, "fanout", true);
String msg = "發佈訂閱模式";
channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes());
channel.close();
connection.close();
}
}
消費者1
public class Consumer1 {
private static final String QUEUE_NAME = "consumerFanout_1";
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("消費者1...");
// 創建我們的連接
Connection connection = RabitMQConnection.getConnection();
// 創建我們通道
final Channel channel = connection.createChannel();
// 關聯隊列消費者關聯隊列
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消費者1獲取消息:" + msg);
}
};
// 開始監聽消息 自動簽收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
消費者2
public class Consumer2 {
private static final String QUEUE_NAME = "consumerFanout_2";
private static final String EXCHANGE_NAME = "fanout_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
System.out.println("消費者2...");
// 創建我們的連接
Connection connection = RabitMQConnection.getConnection();
// 創建我們通道
final Channel channel = connection.createChannel();
// 關聯隊列消費者關聯隊列
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消費者2獲取消息:" + msg);
}
};
// 開始監聽消息 自動簽收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
路由(direct)
在發佈訂閱的基礎上,引入路由key,每個隊列都可以綁定多個路由key,生產者投遞消息時,可以指定路由key,
交換機根據路由key
查找對應的隊列進行消息投放。
即路由key類似於隊列的一種屬性,方便生成者投放消息時做查詢過濾,又叫direct
生產者
/**
* 定義交換機的名稱
*/
private static final String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
// 1.創建我們的連接
Connection connection = RabitMQConnection.getConnection();
// 2.創建我們通道
Channel channel = connection.createChannel();
// 不需要直接關心隊列,只關心交換機
channel.exchangeDeclare(EXCHANGE_NAME, "direct", true);
String msg = "路由直連的消息"
channel.basicPublish(EXCHANGE_NAME, "key1", null, msg.getBytes());
channel.close();
connection.close();
// 如果交換機沒有綁定隊列,消息可能會丟失
}
消費者1
public class Consumer1 {
private static final String QUEUE_NAME = "consumer_direct_1";
private static final String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 創建我們的連接
Connection connection = RabitMQConnection.getConnection();
// 創建我們通道
final Channel channel = connection.createChannel();
// 關聯隊列消費者關聯隊列
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key1");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消費者1獲取消息:" + msg);
}
};
// 開始監聽消息 自動簽收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
消費者2
public class Consumer2 {
private static final String QUEUE_NAME = "consumer_direct_2";
private static final String EXCHANGE_NAME = "direct_exchange";
public static void main(String[] args) throws IOException, TimeoutException {
// 創建我們的連接
Connection connection = RabitMQConnection.getConnection();
// 創建我們通道
final Channel channel = connection.createChannel();
// 關聯隊列消費者關聯隊列
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key2");
DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("消費者2獲取消息:" + msg);
}
};
// 開始監聽消息 自動簽收
channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
}
}
主題(topic)
在路由的基礎上,實現通配符查詢,即模糊查詢,*代表一個單詞,#代表多個單詞
這樣一來,隊列不用去綁定一個又一個的路由key,直接綁定通配符的路由key即可
SpringBoot整合RabbitMQ
maven依賴
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
<dependencies>
<!-- springboot-web組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 添加springboot對amqp的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
</dependencies>
application.yml
spring:
rabbitmq:
####連接地址
host: 127.0.0.1
####端口號
port: 5672
####賬號
username: yanxiaohui
####密碼
password: yanxiaohui
### 地址
virtual-host: /yxh
配置類
@Component
public class RabbitMQConfig {
/**
* 定義交換機
*/
private String EXCHANGE_SPRINGBOOT_NAME = "springboot_exchange";
/**
* 短信隊列
*/
private String FANOUT_SMS_QUEUE = "fanout_sms_queue";
/**
* 郵件隊列
*/
private String FANOUT_SMS_EMAIL = "fanout_email_queue";
/**
* 創建短信隊列
*/
@Bean
public Queue smsQueue() {
return new Queue(FANOUT_SMS_QUEUE);
}
/**
* 創建郵件隊列
*/
@Bean
public Queue emailQueue() {
return new Queue(FANOUT_SMS_EMAIL);
}
/**
* 創建交換機
*
* @return
*/
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange(EXCHANGE_SPRINGBOOT_NAME);
}
/**
* 定義短信隊列綁定交換機
*/
@Bean
public Binding smsBindingExchange(Queue smsQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(smsQueue).to(fanoutExchange);
}
/**
* 定義郵件隊列綁定交換機
*/
@Bean
public Binding emailBindingExchange(Queue emailQueue, FanoutExchange fanoutExchange) {
return BindingBuilder.bind(emailQueue).to(fanoutExchange);
}
}
生產者
@RestController
public class FanoutProducer {
@Autowired
private AmqpTemplate amqpTemplate;
@RequestMapping("/sendMsg")
public String sendMsg(String msg) {
// 參數1 交換機名稱 、參數2路由key 參數3 消息
amqpTemplate.convertAndSend("springboot_exchange", "", msg);
return "success";
}
}
消費者
@Component
@RabbitListener(queues = "fanout_email_queue")
public class FanoutEmailConsumer {
@RabbitHandler
public void process(String msg) {
System.out.println("郵件消費者消息msg:" + msg);
}
}
死信隊列
產生的背景
俗稱備胎隊列,用於存放消息多次消費失敗、消費超時、超過隊列最大長度被拒絕接收的消息。
消息中間件因爲某種原因拒收該消息後,可以轉移到死信隊列中存放,死信隊列也可以有交換機和路由key等。
SpringBoot整合死信隊列
maven依賴
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
</parent>
<dependencies>
<!-- springboot-web組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 添加springboot對amqp的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
</dependencies>
application.yml
spring:
rabbitmq:
####連接地址
host: 127.0.0.1
####端口號
port: 5672
####賬號
username: yanxiaohui
####密碼
password: yanxiaohui
### 地址
virtual-host: /yxh
server:
port: 8080
###模擬演示死信隊列
yxh:
dlx:
exchange: yxh_dlx_exchange
queue: yxh_order_dlx_queue
routingKey: dlx
###備胎交換機
order:
exchange: yxh_order_exchange
queue: yxh_order_queue
routingKey: yxh.order
死信隊列配置
@Component
public class DeadLetterMQConfig {
/**
* 訂單交換機
*/
@Value("${yxh.order.exchange}")
private String orderExchange;
/**
* 訂單隊列
*/
@Value("${yxh.order.queue}")
private String orderQueue;
/**
* 訂單路由key
*/
@Value("${yxh.order.routingKey}")
private String orderRoutingKey;
/**
* 死信交換機
*/
@Value("${yxh.dlx.exchange}")
private String dlxExchange;
/**
* 死信隊列
*/
@Value("${yxh.dlx.queue}")
private String dlxQueue;
/**
* 死信路由
*/
@Value("${yxh.dlx.routingKey}")
private String dlxRoutingKey;
/**
* 聲明死信交換機
*
* @return DirectExchange
*/
@Bean
public DirectExchange dlxExchange() {
return new DirectExchange(dlxExchange);
}
/**
* 聲明死信隊列
*
* @return Queue
*/
@Bean
public Queue dlxQueue() {
return new Queue(dlxQueue);
}
/**
* 聲明訂單業務交換機
*
* @return DirectExchange
*/
@Bean
public DirectExchange orderExchange() {
return new DirectExchange(orderExchange);
}
/**
* 綁定死信隊列到死信交換機
*
* @return Binding
*/
@Bean
public Binding binding() {
return BindingBuilder.bind(dlxQueue())
.to(dlxExchange())
.with(dlxRoutingKey);
}
/**
* 聲明訂單隊列
*
* @return Queue
*/
@Bean
public Queue orderQueue() {
// 訂單隊列綁定我們的死信交換機
Map<String, Object> arguments = new HashMap<>(2);
arguments.put("x-dead-letter-exchange", dlxExchange);
arguments.put("x-dead-letter-routing-key", dlxRoutingKey);
return new Queue(orderQueue, true, false, false, arguments);
}
/**
* 綁定訂單隊列到訂單交換機
*
* @return Binding
*/
@Bean
public Binding orderBinding() {
return BindingBuilder.bind(orderQueue())
.to(orderExchange())
.with(orderRoutingKey);
}
死信消費者
@Component
public class OrderDlxConsumer {
/**
* 死信隊列監聽隊列回調的方法
*
* @param msg
*/
@RabbitListener(queues = "yxh_order_dlx_queue")
public void orderConsumer(String msg) {
System.out.println("死信隊列消費訂單消息" + msg);
}
}
訂單消費者
@Component
public class OrderConsumer {
/**
* 監聽隊列回調的方法
*
* @param msg
*/
@RabbitListener(queues = "yxh_order_queue")
public void orderConsumer(String msg) {
System.out.println("正常訂單消費者消息msg:" + msg);
}
}
生產者投遞消息
@RestController
public class DeadLetterProducer {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 訂單交換機
*/
@Value("${yxh.order.exchange}")
private String orderExchange;
/**
* 訂單路由key
*/
@Value("${yxh.order.routingKey}")
private String orderRoutingKey;
@RequestMapping("/sendOrder")
public String sendOrder() {
String msg = "死信隊列的demo";
rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, msg, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
message.getMessageProperties().setExpiration("10000");
return message;
}
});
return "succcess";
}
}
消息中間件如何獲取消費結果
由於中間件的操作是異步的,所以想要獲取其操作結果,只有通過主動查詢的方式,才能知道消息是否已消費。
即通過業務生成全局的消息ID,使用消息ID去數據庫查找對應的業務是否發生了預期的變化,進而得出消息是否成功消費。
此方法也是其解決消息冪等性的思路
RabbitMQ消息冪等問題
消息自動重試機制
在消費者消費消息的代碼中出現異常後,默認是重複執行的,(默認無數次)。
如果代碼本身就有問題,而非外部因素(網絡抖動等)影響,那麼這種重試本質無意義。
所以應該對重試機制設置重試次數和時間間隔,超過重試機制還是拋出異常的,我們可以將消息放入死信或者數據庫將其記錄,方便補償。
SpringBoot開啓重試策略
spring:
rabbitmq:
####連接地址
host: 127.0.0.1
####端口號
port: 5672
####賬號
username: yanxiaohui
####密碼
password: yanxiaohui
### 地址
virtual-host: /yxh
listener:
simple:
retry:
####開啓消費者(程序出現異常的情況下會)進行重試
enabled: true
####最大重試次數
max-attempts: 5
####重試間隔次數
initial-interval: 3000
消費者開啓重試策略
System.out.println("消費者消息msg:" + msg);
JSONObject msgJson = JSONObject.parseObject(msg);
String email = msgJson.getString("email");
String emailUrl = "http://127.0.0.1:8081/sendEmail?email=" + email;
JSONObject jsonObject = null;
try {
jsonObject = HttpClientUtils.httpGet(emailUrl);
} catch (Exception e) {
String errorMsg = email + ",調用第三方郵件接口失敗:" + ",錯誤原因:" + e.getMessage();
throw new Exception(errorMsg);
}
System.out.println("郵件消費者調用第三方接口結果:" + jsonObject);
rabbitMQ如何解決消息冪等問題
採用消息全局id根據業務來定
生產者
@RequestMapping("/sendOrderMsg")
public String sendOrderMsg() {
// 1.生產訂單id
String orderId = System.currentTimeMillis() + "";
String orderName = "生成消息冪等的訂單";
OrderEntity orderEntity = new OrderEntity(orderName, orderId);
String msg = JSONObject.toJSONString(orderEntity);
sendMsg(msg, orderId);
return orderId;
// 後期客戶端主動使用orderId調用服務器接口 查詢該訂單id是否在數據庫中存在數據 消費成功 消費失敗
}
@Async
public void sendMsg(String msg, String orderId) {
rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, msg,
new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
// message.getMessageProperties().setExpiration("10000");
message.getMessageProperties().setMessageId(orderId);
return message;
}
});
// 消息投遞失敗
}
消費者
String msg = new String(message.getBody());
System.out.println("訂單隊列獲取消息:" + msg);
OrderEntity orderEntity = JSONObject.parseObject(msg, OrderEntity.class);
if (orderEntity == null) {
return;
}
// messageId根據具體業務來定,如果已經在數據表中插入過數據,則不會插入
String orderId = message.getMessageProperties().getMessageId();
if (StringUtils.isEmpty(orderId)) {
// 開啓消息確認機制
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
return;
}
OrderEntity dbOrderEntity = orderMapper.getOrder(orderId);
if (dbOrderEntity != null) {
// 說明已經處理過請求
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
return;
}
int result = orderMapper.addOrder(orderEntity);
if (result >= 0) {
// 開啓消息確認機制
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
RabbitMQ如何解決分佈式事務
什麼是分佈式事務
官方說法:在分佈式系統中,因爲跨服務調用接口,存在多個不同的事務,每個事務都互不影響。就存在分佈式事務的問題。
說白了就是:在一個事務中,出現rpc遠程調用,其中對數據的變更脫離當前事務的管理,導致當前事務回滾時,無法將遠程事務一併回滾。
解決分佈式事務核心思想
最終一致性。分佈式領域不存在強一致性,對於短暫期間的不一致,可以允許通過補償或延時使其最終數據保持一致。
RabbitMQ解決分佈式事務的思路
1.通過消息確認機制confirm確保消息一定投遞至消息中間中
2.消費者手動ack確定消息的消費成功
3.對於消費成功但事務回滾的操作,需要進行補充,即將整個業務操作(除去消息投遞)都記錄至補償隊列,然後補償業務的數據缺失。
Maven依賴
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<!-- mysql 依賴 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 阿里巴巴數據源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.14</version>
</dependency>
<!-- SpringBoot整合Web組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 添加springboot對amqp的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
application.yml
spring:
rabbitmq:
####連接地址
host: 127.0.0.1
####端口號
port: 5672
####賬號
username: yanxiaohui
####密碼
password: yanxiaohui
### 地址
virtual-host: /yxh
###開啓消息確認機制 confirms
publisher-confirms: true
publisher-returns: true
listener:
simple:
retry:
####開啓消費者(程序出現異常的情況下會)進行重試
enabled: true
####最大重試次數
max-attempts: 5
####重試間隔次數
initial-interval: 3000
###開啓ack模式
acknowledge-mode: manual
datasource:
url: jdbc:mysql://localhost:3306/order?useUnicode=true&characterEncoding=UTF-8
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
server:
port: 8080
mq配置
@Component
public class OrderRabbitMQConfig {
/**
* 派單隊列
*/
public static final String ORDER_DIC_QUEUE = "order_dic_queue";
/**
* 補單對接
*/
public static final String ORDER_CREATE_QUEUE = "order_create_queue";
/**
* 派單交換機
*/
private static final String ORDER_EXCHANGE_NAME = "order_exchange_name";
/**
* 定義派單隊列
*
* @return
*/
@Bean
public Queue directOrderDicQueue() {
return new Queue(ORDER_DIC_QUEUE);
}
/**
* 定義補派單隊列
*
* @return
*/
@Bean
public Queue directCreateOrderQueue() {
return new Queue(ORDER_CREATE_QUEUE);
}
/**
* 定義訂單交換機
*
* @return
*/
@Bean
DirectExchange directOrderExchange() {
return new DirectExchange(ORDER_EXCHANGE_NAME);
}
/**
* 派單隊列與交換機綁定
*
* @return
*/
@Bean
Binding bindingExchangeOrderDicQueue() {
return BindingBuilder.bind(directOrderDicQueue()).to(directOrderExchange()).with("orderRoutingKey");
}
/**
* 補單隊列與交換機綁定
*
* @return
*/
@Bean
Binding bindingExchangeCreateOrder() {
return BindingBuilder.bind(directCreateOrderQueue()).to(directOrderExchange()).with("orderRoutingKey");
}
}
生產者
@Component
@Slf4j
public class OrderProducer implements RabbitTemplate.ConfirmCallback {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
@Transactional
public String send() {
// 1.創建訂單
String orderId = System.currentTimeMillis() + "";
OrderEntity orderEntity = createOrder(orderId);
//2.將訂單添加到數據庫中(步驟一 先往數據庫中添加一條數據)
int result = orderMapper.addOrder(orderEntity);
if (result <= 0) {
return orderId;
}
//3.使用消息中間件異步 ,分配訂單
String sendMsgJson = JSONObject.toJSONString(orderEntity);
send(sendMsgJson);
int i = 1 / 0;
return orderId;
}
public OrderEntity createOrder(String orderId) {
OrderEntity orderEntity = new OrderEntity();
orderEntity.setName("分佈式事務");
orderEntity.setOrderCreatetime(new Date());
// 價格是300元
orderEntity.setOrderMoney(300d);
// 狀態爲 未支付
orderEntity.setOrderState(0);
Long commodityId = 30L;
// 商品id
orderEntity.setCommodityId(commodityId);
orderEntity.setOrderId(orderId);
return orderEntity;
}
private void send(String sendMsg) {
log.info(">>>生產者發送訂單數據:" + sendMsg);
// 設置生產者消息確認機制
this.rabbitTemplate.setMandatory(true);
this.rabbitTemplate.setConfirmCallback(this);
// 構建回調返回參數
CorrelationData correlationData = new CorrelationData(sendMsg);
String orderExchange = "order_exchange_name";
String orderRoutingKey = "orderRoutingKey";
rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, sendMsg, correlationData);
}
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
String sendMsg = correlationData.getId();
System.out.println("生產者開始消息確認orderId:" + sendMsg);
if (!ack) {
// 遞歸調用發送
send(sendMsg);
return;
}
System.out.println("生產者消息確認orderId:" + sendMsg);
}
}
消費者
@Component
public class DistriLeafleConsumer {
@Autowired
private DispatchMapper dispatchMapper;
@RabbitListener(queues = "order_dic_queue")
public void distriLeafleConsumer(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
System.out.println("派代服務平臺msg:" + msg);
JSONObject jsonObject = JSONObject.parseObject(msg);
// 訂單id
String orderId = jsonObject.getString("orderId");
// 假設派單userID 1234
Long userId = 1234L;
DispatchEntity dispatchEntity = new DispatchEntity(orderId, userId);
int result = dispatchMapper.insertDistribute(dispatchEntity);
if (result >= 0) {
// 手動ack 刪除該消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
}
補單消費者(補償隊列的消費)
@Component
public class CreateOrderConsumer {
@Autowired
private OrderMapper orderMapper;
@RabbitListener(queues = "order_create_queue")
public void createOrderConsumer(Message message, Channel channel) throws IOException {
String msg = new String(message.getBody());
OrderEntity orderEntity = JSONObject.parseObject(msg, OrderEntity.class);
String orderId = orderEntity.getOrderId();
// 根據訂單號碼查詢該筆訂單是否創建
OrderEntity dbOrderEntity = orderMapper.findOrderId(orderId);
if (dbOrderEntity != null) {
// 手動ack 刪除該消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
return;
}
int result = orderMapper.addOrder(orderEntity);
if (result >= 0) {
// 手動ack 刪除該消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
}