使用RabbitMQ实现分布式事务
基于SOA理念的微服务越来越流行,甚至一些局域网部署的项目也采用微服务架构。微服务的好处很多,但同时也带来了很多新的问题,分布式事务便是其中一个,出现问题后自然也会出现解决办法,比如两段提交、三段提交等。
用RabbitMQ实现分布式事务主要是利用消息确认机制,以及后期补偿措施。消息确认有3个部分:消息发送到交换机的确认、交换机路由消息到队列的确认、消费者消费完消息的确认。消息确认机制可以保证消息没有丢失,针对于事务的一致性可以通过补偿的措施完成。
一、安装RabbitMQ并启动
访问http://127.0.0.1:15672,对mq进行各种配置或操作
二、搭建消息发布端
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
rabbitmq: host: 127.0.0.1 port: 5672 username: guest password: guest
以话题模式创建mq,创建topic.ab topic.abc两个话题,创建topic.exchange交换机,路由键支持固定的某个话题,或者*匹配一个单词,或者#匹配多个单词,这里展示了第一种、第三种两种方式。通过设置mq的PublisherConfirms与PublisherReturns属性监听消息,可以在回调事件里实现自己的处理逻辑,比如根据数据标识把消息重发或者异常处理。
@Configuration public class TopicRabbitConfig { //话题 public final static String TOPIC_AB = "topic.ab"; public final static String TOPIC_ABC = "topic.abc"; public final static String TOPIC_EXCHANGE = "topic.exchange"; /** * 创建队列 */ @Bean public Queue firstQueue() { return new Queue(TOPIC_AB); } @Bean public Queue secondQueue() { return new Queue(TOPIC_ABC); } /** * 创建交换机 */ @Bean TopicExchange exchange() { return new TopicExchange(TOPIC_EXCHANGE); } /** * 将firstQueue和topicExchange绑定,路由键值为topic.ab * 路由键是topic.ab的消息才会分发到该队列 */ @Bean Binding bindingFirstQueue() { return BindingBuilder.bind(firstQueue()).to(exchange()).with(TOPIC_AB); } /** * 将secondQueue和topicExchange绑定,通配路由键规则topic.# * 路由键是topic.开头的消息都会分发到该队列 */ @Bean Binding bindingSecondQueue() { return BindingBuilder.bind(secondQueue()).to(exchange()).with("topic.#"); } @Bean public RabbitTemplate createRabbitTemplate(CachingConnectionFactory connectionFactory) { RabbitTemplate rabbitTemplate = new RabbitTemplate(); connectionFactory.setPublisherConfirms(true); connectionFactory.setPublisherReturns(true); //增加connection数 https://www.jianshu.com/p/6579e48d18ae connectionFactory.setChannelCacheSize(100); rabbitTemplate.setConnectionFactory(connectionFactory); rabbitTemplate.setMandatory(true); //消息是否能投递到交换机反馈 rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { if (!ack) { System.out.println(String.format("ConfirmCallback 数据标识:%s,是否成功:%s,失败原因:%s", correlationData.getId(), ack, cause)); } } }); //消息是否能投递到队列反馈 rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() { @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { System.out.println(String.format("ReturnCallback 回应码:%s,回应信息:%s,交换机:%s,路由键:%s,数据标识:%s", replyCode, replyText, exchange, routingKey, message.getMessageProperties().getMessageId())); } }); return rabbitTemplate; } }
实现一个发送消息的接口,分别模拟发送异常消息,测试消息反馈
发送到交换机异常:ConfirmCallback 数据标识:8f276e00-0a0f-4302-87c2-818fe827712e,是否成功:false,失败原因:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'eee' in vhost '/', class-id=60, method-id=40)
发送到队列异常:ReturnCallback 回应码:312,回应信息:NO_ROUTE,交换机:topic.exchange,路由键:ccc,数据标识:dd022151-1029-418e-a75d-4db642b98aaa
@RestController public class RabbitmqController { @Resource private RabbitTemplate rabbitTemplate; @RequestMapping("/sendTopicMessage") public String sendTopicMessage1() { try { Employee employee = new Employee(1, "join", 12); String msgId = UUID.randomUUID().toString(); ObjectMapper mapper = new ObjectMapper(); String messaged = mapper.writeValueAsString(employee); Message message = MessageBuilder.withBody(messaged.getBytes()).setMessageId(msgId).build(); CorrelationData correlationData = new CorrelationData(msgId);
//模拟发送到交换机异常 // rabbitTemplate.send("eee", TopicRabbitConfig.TOPIC_AB, message, correlationData);
//模拟发送到队列异常 // rabbitTemplate.send(TopicRabbitConfig.TOPIC_EXCHANGE, "ccc", message, correlationData); rabbitTemplate.convertAndSend(TopicRabbitConfig.TOPIC_EXCHANGE, TopicRabbitConfig.TOPIC_AB, message, correlationData); } catch (Exception e) { e.printStackTrace(); } return "ok"; } }
@Data @AllArgsConstructor public class Employee implements Serializable { private Integer id; private String name; private Integer age; }
三、搭建消息消费端
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
设置监听容器
@Configuration public class TopicRabbitConfig { @Autowired private CachingConnectionFactory connectionFactory; @Autowired private MyAckReceiver myAckReceiver; //话题 public final static String TOPIC_AB = "topic.ab"; public final static String TOPIC_ABC = "topic.abc"; public final static String TOPIC_EXCHANGE = "topic.exchange"; /** * 创建队列 */ @Bean public Queue firstQueue() { return new Queue(TOPIC_AB); } @Bean public Queue secondQueue() { return new Queue(TOPIC_ABC); } /** * 创建交换机 */ @Bean TopicExchange exchange() { return new TopicExchange(TOPIC_EXCHANGE); } /** * 将firstQueue和topicExchange绑定,路由键值为topic.ab * 路由键是topic.ab的消息才会分发到该队列 */ @Bean Binding bindingFirstQueue() { return BindingBuilder.bind(firstQueue()).to(exchange()).with(TOPIC_AB); } /** * 将secondQueue和topicExchange绑定,通配路由键规则topic.# * 路由键是topic.开头的消息都会分发到该队列 */ @Bean Binding bindingSecondQueue() { return BindingBuilder.bind(secondQueue()).to(exchange()).with("topic.#"); } @Bean public SimpleMessageListenerContainer simpleMessageListenerContainer() { SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); // RabbitMQ默认是自动确认,这里改为手动确认消息 container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
//需要监听的队列 container.setQueueNames(TOPIC_AB, TOPIC_ABC); container.setMessageListener(myAckReceiver); return container; } }
创建监听事件
basicAck:对消费的消息进行确认,确认后消息消费成功,如果不确认的话会一直阻塞住,直到杀死程序消息恢复到发送队列。
basicReject:对于正在处理的消息,但是消费异常或流程异常不能继续执行,可以设置true重新把消息添加到队列或设置false把消息丢掉。
对于这两种模式可以灵活运用,结合业务对数据进行补偿或回滚。
@Component public class MyAckReceiver implements ChannelAwareMessageListener { @Override public void onMessage(Message message, Channel channel) throws Exception { long deliveryTag = message.getMessageProperties().getDeliveryTag(); try { if (message.getMessageProperties().getConsumerQueue().equals(TopicRabbitConfig.TOPIC_AB)) { ObjectMapper mapper = new ObjectMapper(); String messaged = new String(message.getBody()); Employee student = mapper.readValue(messaged.getBytes("utf-8"), Employee.class); System.out.println("MyAckReceiver: TOPIC_AB " + JSON.toJSONString(student)); } if (message.getMessageProperties().getConsumerQueue().equals(TopicRabbitConfig.TOPIC_ABC)) { ObjectMapper mapper = new ObjectMapper(); String messaged = new String(message.getBody()); Employee student = mapper.readValue(messaged.getBytes("utf-8"), Employee.class); System.out.println("MyAckReceiver: TOPIC_ABC " + JSON.toJSONString(student)); } //没有确认时候 关闭程序后 消息自动恢复到队列 channel.basicAck(deliveryTag, true); } catch (Exception e) { e.printStackTrace(); channel.basicReject(deliveryTag, false);//收到消息了 没处理 但不放回队列 } } }

浙公网安备 33010602011771号