實戰前言
RabbitMQ 作爲目前應用相當廣泛的消息中間件,在企業級應用、微服務應用中充當着重要的角色。特別是在一些典型的應用場景以及業務模塊中具有重要的作用,比如業務服務模塊解耦、異步通信、高併發限流、超時業務、數據延遲處理等。
RabbitMQ 官網拜讀
首先,讓我們先拜讀 RabbitMQ 官網的技術開發手冊以及相關的 Features,感興趣的朋友可以耐心的閱讀其中的相關介紹,相信會有一定的收穫,地址可見:http://www.rabbitmq.com/getstarted.html
在閱讀該手冊過程中,我們可以得知 RabbitMQ 其實核心就是圍繞 “消息模型” 來展開的,其中就包括了組成消息模型的相關組件:生產者,消費者,隊列,交換機,路由,消息等!而我們在實戰應用中,實際上也是緊緊圍繞着 “消息模型” 來展開擼碼的!
下面,我就介紹一下這一消息模型的演變歷程,當然,這一歷程在 RabbitMQ 官網也是可以窺覽得到的!
上面幾個圖就已經概述了幾個要點,而且,這幾個要點的含義可以說是字如其名!
- 生產者:發送消息的程序
- 消費者:監聽接收消費消息的程序
- 消息:一串二進制數據流
- 隊列:消息的暫存區/存儲區
- 交換機:消息的中轉站,用於接收分發消息。其中有 fanout、direct、topic、headers 四種
- 路由:相當於密鑰/第三者,與交換機綁定即可路由消息到指定的隊列!
- 正如上圖所展示的消息模型的演變,接下來我們將以代碼的形式實戰各種典型的業務場景!
SpringBoot 整合 RabbitMQ 實戰
工欲善其事,必先利其器。我們首先需要藉助 IDEA 的 Spring Initializr 用 Maven 構建一個 SpringBoot 的項目,並引入 RabbitMQ、Mybatis、Log4j 等第三方框架的依賴。搭建完成之後,可以簡單的寫個 RabbitMQController 測試一下項目是否搭建是否成功(可以暫時用單模塊方式構建),下圖是構建的項目以及創建好的規範目錄:
緊接着,我們進入實戰的核心階段,在項目或者服務中使用 RabbitMQ,其實無非是有幾個核心要點要牢牢把握住,這幾個核心要點在擼碼過程中需要“時刻的遊蕩在自己的腦海裏”,其中包括:
- 我要發送的消息是什麼
- 我應該需要創建什麼樣的消息模型:DirectExchange+RoutingKey?TopicExchange+RoutingKey?等
- 我要處理的消息是實時的還是需要延時/延遲的?
- 消息的生產者需要在哪裏寫,消息的監聽消費者需要在哪裏寫,各自的處理邏輯是啥
基於這樣的幾個要點,我們先小試牛刀一番,採用 RabbitMQ 實戰異步寫日誌與異步發郵件。當然啦,在進行實戰前,我們需要安裝好 RabbitMQ 及其後端控制檯應用,並在項目中配置一下 RabbitMQ 的相關參數以及相關 Bean 組件。
1.RabbitMQ 安裝完成後,打開後端控制檯應用:http://localhost:15672/ guest guest 登錄,看到下圖即表示安裝成功
2.然後是項目配置文件層面的配置 application.properties
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.listener.concurrency=10
spring.rabbitmq.listener.max-concurrency=20
spring.rabbitmq.listener.prefetch=5
其中,後面三個參數主要是用於“併發量的配置”,表示:併發消費者的初始化值,併發消費者的最大值,每個消費者每次監聽時可拉取處理的消息數量。
接下來,我們需要以 Configuration 的方式配置 RabbitMQ 並以 Bean 的方式顯示注入 RabbitMQ 在發送接收處理消息時相關 Bean 組件配置其中典型的配置是 RabbitTemplate 以及 SimpleRabbitListenerContainerFactory,前者是充當消息的發送組件,後者是用於管理 RabbitMQ監聽器 的容器工廠,其代碼如下:
@Configuration
public class RabbitmqConfig {
private static final Logger log= LoggerFactory.getLogger(RabbitmqConfig.class);
@Autowired
private Environment env;
@Autowired
private CachingConnectionFactory connectionFactory;
@Autowired
private SimpleRabbitListenerContainerFactoryConfigurer factoryConfigurer;
/**
* 單一消費者
* @return
*/
@Bean(name = "singleListenerContainer")
public SimpleRabbitListenerContainerFactory listenerContainer(){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setConcurrentConsumers(1);
factory.setMaxConcurrentConsumers(1);
factory.setPrefetchCount(1);
factory.setTxSize(1);
factory.setAcknowledgeMode(AcknowledgeMode.AUTO);
return factory;
}
/**
* 多個消費者
* @return
*/
@Bean(name = "multiListenerContainer")
public SimpleRabbitListenerContainerFactory multiListenerContainer(){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factoryConfigurer.configure(factory,connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setAcknowledgeMode(AcknowledgeMode.NONE);
factory.setConcurrentConsumers(env.getProperty("spring.rabbitmq.listener.concurrency",int.class));
factory.setMaxConcurrentConsumers(env.getProperty("spring.rabbitmq.listener.max-concurrency",int.class));
factory.setPrefetchCount(env.getProperty("spring.rabbitmq.listener.prefetch",int.class));
return factory;
}
@Bean
public RabbitTemplate rabbitTemplate(){
connectionFactory.setPublisherConfirms(true);
connectionFactory.setPublisherReturns(true);
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMandatory(true);
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
log.info("消息發送成功:correlationData({}),ack({}),cause({})",correlationData,ack,cause);
}
});
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.info("消息丟失:exchange({}),route({}),replyCode({}),replyText({}),message:{}",exchange,routingKey,replyCode,replyText,message);
}
});
return rabbitTemplate;
}}
RabbitMQ 實戰:業務模塊解耦以及異步通信
在一些企業級系統中,我們經常可以見到一個執行 function 通常是由許多子模塊組成的,這個 function 在執行過程中,需要 同步 的將其代碼從頭開始執行到尾,即執行流程是 module_A -> module_B -> module_C -> module_D,典型的案例可以參見彙編或者 C 語言等面向過程語言開發的應用,現在的一些 JavaWeb 應用也存在着這樣的寫法。
而我們知道,這個執行流程其實對於整個 function 來講是有一定的弊端的,主要有幾點:
- 整個 function 的執行響應時間將很久;
- 如果某個 module 發生異常而沒有處理得當,可能會影響其他 module 甚至整個 function 的執行流程與結果;
- 整個 function 中代碼可能會很冗長,模塊與模塊之間可能需要進行強通信以及數據的交互,出現問題時難以定位與維護,甚至會陷入 “改一處代碼而動全身”的尷尬境地!
故而,我們需要想辦法進行優化,我們需要將強關聯的業務模塊解耦以及某些模塊之間實行異步通信!下面就以兩個場景來實戰我們的優化措施!
場景一:異步記錄用戶操作日誌
對於企業級應用系統或者微服務應用中,我們經常需要追溯跟蹤記錄用戶的操作日誌,而這部分的業務在某種程度上是不應該跟主業務模塊耦合在一起的,故而我們需要將其單獨抽出並以異步的方式與主模塊進行異步通信交互數據。
下面我們就用 RabbitMQ 的 DirectExchange+RoutingKey 消息模型也實現“用戶登錄成功記錄日誌”的場景。如前面所言,我們需要在腦海裏迴盪着幾個要點:
- 消息模型:DirectExchange+RoutingKey 消息模型
- 消息:用戶登錄的實體信息,包括用戶名,登錄事件,來源的IP,所屬日誌模塊等信息
- 發送接收:在登錄的 Controller 中實現發送,在某個 listener 中實現接收並將監聽消費到的消息入數據表;實時發送接收
首先我們需要在上面的 RabbitmqConfig 類中創建消息模型:包括 Queue、Exchange、RoutingKey 等的建立,代碼如下:
上圖中 env 獲取的信息,我們需要在 application.properties 進行配置,其中 mq.env=local
此時,我們將整個項目/服務跑起來,並打開 RabbitMQ 後端控制檯應用,即可看到隊列以及交換機及其綁定已經建立好了,如下所示:
接下來,我們需要在 Controller 中執行用戶登錄邏輯,記錄用戶登錄日誌,查詢獲取用戶角色視野資源信息等,由於篇幅關係,在這裏我們重點要實現的是用MQ實現 “異步記錄用戶登錄日誌” 的邏輯,即在這裏 Controller 將充當“生產者”的角色,核心代碼如下:
@RestController
public class UserController {
private static final Logger log= LoggerFactory.getLogger(HelloWorldController.class);
private static final String Prefix="user";
@Autowired
private ObjectMapper objectMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private UserLogMapper userLogMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private Environment env;
@RequestMapping(value = Prefix+"/login",method = RequestMethod.POST,consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public BaseResponse login(@RequestParam("userName") String userName,@RequestParam("password") String password){
BaseResponse response=new BaseResponse(StatusCode.Success);
try {
//TODO:執行登錄邏輯
User user=userMapper.selectByUserNamePassword(userName,password);
if (user!=null){
//TODO:異步寫用戶日誌
try {
UserLog userLog=new UserLog(userName,"Login","login",objectMapper.writeValueAsString(user));
userLog.setCreateTime(new Date());
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
rabbitTemplate.setExchange(env.getProperty("log.user.exchange.name"));
rabbitTemplate.setRoutingKey(env.getProperty("log.user.routing.key.name"));
Message message=MessageBuilder.withBody(objectMapper.writeValueAsBytes(userLog)).setDeliveryMode(MessageDeliveryMode.PERSISTENT).build();
message.getMessageProperties().setHeader(AbstractJavaTypeMapper.DEFAULT_CONTENT_CLASSID_FIELD_NAME, MessageProperties.CONTENT_TYPE_JSON);
rabbitTemplate.convertAndSend(message);
}catch (Exception e){
e.printStackTrace();
}
//TODO:塞權限數據-資源數據-視野數據
}else{
response=new BaseResponse(StatusCode.Fail);
}
}catch (Exception e){
e.printStackTrace();
}
return response;
}}
在上面的“發送邏輯”代碼中,其實也體現了我們最開始介紹的演進中的幾種消息模型,比如我們是將消息發送到 Exchange 的而不是 Queue,消息是以二進制流的形式進行傳輸等等。當用 postman 請求到這個 controller 的方法時,我們可以在 RabbitMQ 的後端控制檯應用看到一條未確認的消息,通過 GetMessage 即可看到其中的詳情,如下:
最後,我們將開發消費端的業務代碼,如下:
@Component
public class CommonMqListener {
private static final Logger log= LoggerFactory.getLogger(CommonMqListener.class);
@Autowired
private ObjectMapper objectMapper;
@Autowired
private UserLogMapper userLogMapper;
@Autowired
private MailService mailService;
/**
* 監聽消費用戶日誌
* @param message
*/
@RabbitListener(queues = "${log.user.queue.name}",containerFactory = "singleListenerContainer")
public void consumeUserLogQueue(@Payload byte[] message){
try {
UserLog userLog=objectMapper.readValue(message, UserLog.class);
log.info("監聽消費用戶日誌 監聽到消息: {} ",userLog);
//TODO:記錄日誌入數據表
userLogMapper.insertSelective(userLog);
}catch (Exception e){
e.printStackTrace();
}
}
將服務跑起來之後,我們即可監聽消費到上面 Queue 中的消息,即當前用戶登錄的信息,而且,我們也可以看到“記錄用戶登錄日誌”的邏輯是由一條異於主業務線程的異步線程去執行的:
“異步記錄用戶操作日誌”的案例我想足以用於詮釋上面所講的相關理論知識點了,在後續篇章中,由於篇幅限制,我將重點介紹其核心的業務邏輯!
場景二:異步發送郵件
發送郵件的場景,其實也是比較常見的,比如用戶註冊需要郵箱驗證,用戶異地登錄發送郵件通知等等,在這裏我以 RabbitMQ 實現異步發送郵件。實現的步驟跟場景一幾乎一致!
- 消息模型的創建
- 配置信息的創建
- 生產端
- 消費端
彩蛋:本博文就先介紹RabbitMQ實戰的典型業務場景之業務服務模塊異步解耦與通信吧,下篇博文將繼續講解RabbitMQ實戰在高併發系統的場景的應用記憶消息確認機制跟併發量的配置實戰,相關源碼數據庫可以來這裏下載
https://pan.baidu.com/s/1KUuz_eeFXOKF3XRMY2Jcew
學習過程有任何問題均可以與我交流,QQ:1974544863!感興趣的童鞋可以關注一下我的微信公衆號!