springboot + rabbitmq Send Mail (guarantees 100% success and consumption of messages)

Keywords: Programming Spring RabbitMQ SpringBoot github

1. Throw a picture first

Explain:
This article covers many aspects of RabbitMQ, such as:

Message Sending Confirmation Mechanism Consumer Confirmation Mechanism Message Re-Delivery Consumer Idempotency, etc.

These are all around the overall flowchart above, so it's necessary to post them first, see the diagram

 

2. Ideas for implementation

  1. Briefly introduce the acquisition of 163 mailbox authorization number
  2. Write Send Mail Tool Class
  3. Write RabbitMQ profile
  4. Producer Initiate Call
  5. Consumers Send Mail
  6. Timing Task Pulls Delivery Failed Messages Timely and Redelivers
  7. Test Validation for Various Exceptions
  8. Expansion: Use dynamic proxy for consumer idempotency verification and message acknowledgment

 

3. Project introduction

  1. springboot version 2.1.5.RELEASE, older versions may have configuration properties that are not available and need to be configured in code
  2. RabbitMQ Version 3.7.15
  3. MailUtil: Send Mail Tool Class
  4. RabbitConfig: rabbitmq related configuration
  5. TestServiceImpl: Producer, send message
  6. MailConsumer: Consumer, Consumer Message, Send Mail
  7. ResendMsg: Timed Task, Repost Failed Messages

Note: The above is the core code, MsgLogService mapper xml and so on are not posted. The complete code can refer to my GitHub, welcome fork, https://github.com/wangzaiplus/springboot/tree/wxw

 

4. Code implementation

  1. 163 Obtain mailbox authorization numbers as shown in Fig.

The authorization number is the password required for the configuration file spring.mail.password

  1. pom
<!--mq--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <!--mail--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> 
  1. rabbitmq, mailbox configuration
# rabbitmq spring.rabbitmq.host=localhost spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest # open confirms Callback P -> Exchange spring.rabbitmq.publisher-confirms=true # open returnedMessage Callback Exchange -> Queue spring.rabbitmq.publisher-returns=true # Set up manual confirmation(ack) Queue -> C spring.rabbitmq.listener.simple.acknowledge-mode=manual spring.rabbitmq.listener.simple.prefetch=100 # mail spring.mail.host=smtp.163.com spring.mail.username=18621142249@163.com spring.mail.password=123456wangzai spring.mail.from=18621142249@163.com spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true 

Note: password is the authorization number, username and from should be the same

  1. Table structure
CREATE TABLE `msg_log` ( `msg_id` varchar(255) NOT NULL DEFAULT '' COMMENT 'Message Unique Identification', `msg` text COMMENT 'Message Body, json Format', `exchange` varchar(255) NOT NULL DEFAULT '' COMMENT 'Switch', `routing_key` varchar(255) NOT NULL DEFAULT '' COMMENT 'Routing Key', `status` int(11) NOT NULL DEFAULT '0' COMMENT 'state: 0 Delivery 1 Delivery Success 2 Delivery Failure 3 Consumed', `try_count` int(11) NOT NULL DEFAULT '0' COMMENT 'retry count', `next_try_time` datetime DEFAULT NULL COMMENT 'Next retry time', `create_time` datetime DEFAULT NULL COMMENT 'Creation Time', `update_time` datetime DEFAULT NULL COMMENT 'Update Time', PRIMARY KEY (`msg_id`), UNIQUE KEY `unq_msg_id` (`msg_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Message Delivery Log'; 

Description: The exchange routing_key field is required for timed tasks to re-post messages

  1. MailUtil
@Component @Slf4j public class MailUtil { @Value("${spring.mail.from}") private String from; @Autowired private JavaMailSender mailSender; /** * Send Simple Mail* * @param mail */ public boolean send(Mail mail) { String to = mail.getTo();// Target mailbox String title = mail.getTitle(); //mail header String content = mail.getContent(); //mail body SimpleMailMessage message = new SimpleMailMessage (); message.setFrom (from); message.setTo (to); message.setSubject (title); message.setText (content); try {mailSender.send (message); (log.info) ("mail sent successfully"); return;} catch (Mail).Exception) {log.error ("Mail failed to send, to: {}, title: {}", to, title, e); return false;}}} 
  1. RabbitConfig
@Configuration @Slf4j public class RabbitConfig { @Autowired private CachingConnectionFactory connectionFactory; @Autowired private MsgLogService msgLogService; @Bean public RabbitTemplate rabbitTemplate() { RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); rabbitTemplate.setMessageConverter(converter()); // Whether the message was successfully sent to Exchange rabbitTemplate.setConfirmCallback ((correlation data, ack, cause) -> {if (ack) {log.info ("message successfully sent to Exchange"); String msgId = correlation onData.getId (); msgLogService.updateStatus (msgId, Constant.MsgLogStatus.DELIVER_SUCCESS);} else {log.info ("message sent to Exchange failed, {}, cause: {}"), correlation oNData, cause);};//Trigger setReturnCallback callback must set mandatory=true, otherwise Exchange will discard the message without finding Queue and will not trigger the callback rabbitTemplate.setMandatory(true); //Whether the message is routed from Exchange to Queue, note: This is a failed callback, which calls back the method rabbitTemplat only if the message is routed from Exchange to Queue failsE.setReturnCallback ((message, replyCode, replyText, exchange, routingKey) -> {log.info ("Message routing from Exchange to Queue failed: exchange: {}, route: {}, replyCode: {}, replyText: {}, message: {}", exchange, routingKey, replyCode, replyText, message);}); rabbitTemplate;} @Bean Jackson2JMessageConverter () {new Jason Converter {returnCkson2JsonMessageConverter ();} //Send mail public static final String MAIL_QUEUE_NAME = "mail.queue"; public static final String MAIL_EXCHANGE_NAME = "mail.exchange"; public static final String MAIL_ROUTING_KEY_NAME = "mail.routing.key"; @Bean public Queue mailQueue () {return new Queue (MAIL_QUEUE_NAME, true);} @public DirectBean ExchangemaIlExchange () {return new DirectExchange (MAIL_EXCHANGE_NAME, true, false);} @Bean public Binding mailBinding () {return BindingBuilder.bind (mailQueue (). to (mailExchange (). with (MAIL_ROUTING_KEY_NAME);}} 
  1. TestServiceImpl production message
@Service public class TestServiceImpl implements TestService { @Autowired private MsgLogMapper msgLogMapper; @Autowired private RabbitTemplate rabbitTemplate; @Override public ServerResponse send(Mail mail) { String msgId = RandomUtil.UUID32(); mail.setMsgId(msgId); MsgLog msgLog = new MsgLog(msgId, mail, RabbitConfig.MAIL_EXCHANGE_NAME, RabbitConfig.MAIL_ROUTING_KEY_NAME); msgLogMapper.insert(msgLog);// Message inbound CorrelationData correlationData = new CorrelationData(msgId); rabbitTemplate.convertAndSend(RabbitConfig.MAIL_EXCHANGE_NAME, RabbitConfig.MAIL_ROUTING_KEY_NAME, MessageHelper.objToMsg(mail), correlationData); return ServerResponse.success(ResponseCode.MAIL_SEND_SUCCESS.getMsg());}} 
  1. MailConsumer consumer message, send mail
@Component @Slf4j public class MailConsumer { @Autowired private MsgLogService msgLogService; @Autowired private MailUtil mailUtil; @RabbitListener(queues = RabbitConfig.MAIL_QUEUE_NAME) public void consume(Message message, Channel channel) throws IOException { Mail mail = MessageHelper.msgToObj(message, Mail.class); log.info("Received Message: {}", mail.toString()); String msgId = mail.getMsgId(); MsgLog msgLog = msgLogService.selectByMsgId(msgId); if (null == msgLog || msgLog.getStatus().equals(Constant.MsgLogStatus.CONSUMED_SUCCESS)) {// Consumption idempotency log.info (duplicate consumption, msgId: {}, msgId); return;} MessageProperties properties = message.getMessageProperties (); long tag = properties.getDeliveryTag (); Boolean success = mailUtil.send (mail); if (success) {msgLogService.updateStatus (msgId, Constant.MsgLogStatus.CONSUMED_SUCCESS); channel.basick (tag, false); //consumption confirmation}Else {channel.basicNack (tag, false, true);}} 

Description: In fact, you have completed three things: 1. Ensure the idempotency of consumption, 2. Send mail, 3. Update the status of the message, Manual ack

  1. ResendMsg Timer Task Repost Send Failed Message
@Component @Slf4j public class ResendMsg { @Autowired private MsgLogService msgLogService; @Autowired private RabbitTemplate rabbitTemplate; // Maximum private static final delivery number private static final int MAX_TRY_COUNT = 3; /** * pull every 30s to retrieve the message that failed delivery, and re-post */ @Scheduled (cron = "0/30 * * * ** * * * * * *?") public void resend () {log.info("Start executing timer task (re-post message)"); List<MsgLog> msgLogs = msgLogService.selectTimeoutMsg(); msgLogs.Each (msgLogs.forEach (msgLogs for msgLogs (msgLog -> {msgString -> {msgString {= msgLog.getMsGId (); if (msgLog.getTryCount () >== MAX_TRY_COUNT) {msgLogService.updateStatus (msgId, Constant.MsgLogStatus.Constant.MsgLogStatus.DELIVER_FAIL); log.info("Exceeding maximum retries, message delivery failed, msgId: {}", msgId);} else {msgLogService.updateTryCount (msgId, msgLog.getTryCount () >==== MAX_TRY_TRY_COUNT) {msgLogService.updateStatus (msgId, Constant.MsgLogStatus.DELIVER_FACorrelationData correlationData = new CorrelationData (msgId);RabbitTemplate.convertAndSend (msgLog.getExchange(), msgLog.getRoutingKey(), MessageHelper.objToMsg (msgLog.getMsg()), correlationData); //republish log.info("No"+ (msgLog.getTryCount()+ "Second Re Message");}; (log.info("End of Timed Task Execution (Re-post Message);}} 

Description: Each message is bound to exchange routingKey, and all messages can be retransmitted to share this timed task

 

V. Basic Testing

OK, so far, the code is ready to go through the normal process testing

1. Send request:

 

2. Background log:

3. Database message logging:

The status is 3, indicating consumption, and the number of message retries is 0, indicating that a delivery was successful

4. View your mailbox

Send Successfully

 

6. Testing of Various Exceptions

Step 1 lists a number of points of knowledge about RabbitMQ, which are important and core. This article also covers the implementation of these points, and then validates them through exception tests (these validations are all based on the flowchart thrown at the beginning of this article, which is important, so paste them again)

  1. A callback in case an authentication message fails to be sent to Exchange, corresponding to figure P -> X above

How to verify? You can arbitrarily specify a nonexistent switch name, request an interface, and see if a callback will be triggered

Send failed because: reply-code=404, reply-text=NOT_FOUND - no exchange'mail.exchangeabcd'in Vhost'/', this callback ensures that the message is sent to Exchange correctly and the test is complete

  1. Callback in case of failure to route validation messages from Exchange to Queue, corresponding to figure X -> Q above

Similarly, if you modify the routing key to be nonexistent, the routing will fail and trigger a callback

Send failed because: route: mail.routing.keyabcd, replyCode: 312, replyText: NO_ROUTE

  1. Verification In manual ack mode, the consumer must make a manual acknowledgment, otherwise the message will remain in the queue until consumed, corresponding to figure Q -> C above

Comment out the consumer code channel.basicAck(tag, false); //consumer confirmation to view console and rabbitmq console

You can see that although messages are indeed consumed, they are still saved by rabbitmq because they are manually acknowledged and not manually acknowledged in the end. Manual acks ensure that messages are consumed, but remember basicAck

  1. Verify consumer idempotency

Next step, remove the comment and restart the server. Because there is a message that has not been acked, you can listen to the message and consume it after restarting, but because the state of the message is not consumed before consuming, you will find status=3, which is already consumed, so direct return guarantees the idempotency of the consumer, even if the callback is not triggered due to successful delivery of the network and other reasons.Thus, multiple deliveries, no re-consumption and business anomalies will occur

  1. Verify consumer side exception messages are not lost

Obviously, exceptions may occur in consumer code. If not handled, the business does not execute correctly, the message disappears, giving us the feeling that the message is lost, because our consumer code captures exceptions, which trigger when the business exceptions occur: channel.basicNack(tag, false, true); this tells rabbitmq that the message failed to consume, needs to be requeued, and can be re-delivered toOther normal consumers consume so that messages are not lost

Test: send method returns false directly (meaning throwing an exception here)

You can see that because channel.basicNack(tag, false, true), unacked messages are requeued and consumed, which ensures that they are not lost

  1. Validate message replay for timed tasks

In a practical application scenario, the callback method ConfirmCallback for delivering acknowledgements is not executed, either because of network reasons or because the message is down without being persisted by MQ, which causes the message state in the database to remain in the delivering state, and message re-posting is required even if the message may have been consumed.

Timing tasks only guarantee 100% success of message delivery, while the consumer's idempotency of multiple deliveries requires self-assurance by the consumer

We can comment out the code that updates the status of messages after callbacks and consumer success, start a timed task, and check whether to re-enter

You can see that the message will be re-projected three times, more than three times abandoned, and the message state will be set to the delivery failure state. If this abnormal situation occurs, manual intervention is needed to investigate the cause.

 

7. Expansion: Use Dynamic Agent to Realize Consumer Identity Verification and Consumer Acknowledgment (ack)

I don't know if you find it. In MailConsumer, the real business logic is simply to send mail mailUtil.send(mail), but we have to check the consumption idempotency before calling the send method. After sending, we also need to update the message status to "consumed" state and manually ack. In the actual project, there may be many producer-consumer scenarios, such as logging,You need rabbitmq to send text messages and so on. If you write these duplicate public codes every time, it is unnecessary and difficult to maintain. Therefore, we can pull out the public code so that the core business logic only cares about its own implementation, and does not need to do other operations. In fact, it is AOP.

There are many ways to do this, using spring aop, interceptors, static proxies, or dynamic proxies, where I'm using dynamic proxies

The directory structure is as follows:

The core code is the implementation of the proxy. Instead of pasting all the code out here, we just provide a way to write it as concisely and elegantly as possible.

Teachers who support teachers can pay attention to collections and forwards, and teachers who want more information and interviews respond to "1".

Posted by BigToach on Tue, 19 Nov 2019 19:56:15 -0800