使用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);//收到消息了 没处理 但不放回队列
        }
    }
}

 

posted @ 2020-08-06 20:08  你好。世界!  阅读(1314)  评论(0)    收藏  举报