RabbitMQ 系列之分布式事务处理-最终一致性-SpringBoot实现

本文介绍如何使用RabbitMQ实现分布式事务的最终一致性,通过具体案例——用户下单服务与扣减用户积分服务的交互,展示如何确保消息的可靠投递和消费。涉及Maven配置、Spring Boot整合、消息确认机制及消息补偿策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

目录

 

分布式事务应用场景:

如何使用RabbitMQ 解决分布式事务-最终一致性:

实现场景:用户下单service - 扣减用户积分 service

结合Springboot 处理分布式事务-为减少篇幅,只列出主要几块代码,详见文末源码链接:

maven-parent . pom.xml

以下是Maven-module.orderProducer 生产者模块内容

maven-module.orderProducer.application.yml

maven-module.orderProducer-com.study.mqconfig.OrderMQMsgConfig.java

maven-module.orderProducer-com.study.service.OrderService.java

maven-module.orderProducer-com.study.listen.ReCreateorderConsumer.java

以下是Maven-module.scoreConsumer 积分消费者模块内容

maven-module.scoreConsumer.application.yml

maven-module.scoreConsumer-com.study.listen.ScoreReceiverConsumer.java

测试结果


分布式事务应用场景:

  1. 【分库分表时】- 服务内存在跨数据库场景;
  2. 【应用SOA化】- 不同服务容器之间服务调用场景;
    1. 用户注册service -> 调用【积分服务-赠送积分service】;
    2. 用户下单service -> 调用【扣减用户积分service】or【扣减用户消费券service】or 【派单service】;
    3. 用户支付service -> 调用【更新订单service】+【账户记账service】+【增加用户积分service】;
    4. 。。。

如何使用RabbitMQ 解决分布式事务-最终一致性:

  1. 确认Producer 一定将消息投递到MQ服务中;
  2. 消息持久化;
  3. 确认Consumer  一定将消息消费了,ACK手动应答模式(消息补偿-考虑消息处理幂等);

实现场景:用户下单service - 扣减用户积分 service

结合Springboot 处理分布式事务-为减少篇幅,只列出主要几块代码,详见文末源码链接:

maven-parent . pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.mq.study</groupId>
    <artifactId>MqParent</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>orderProducer</module>
        <module>scoreConsumer</module>
    </modules>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.report.outputEncoding>UTF-8</project.report.outputEncoding>
        <java.version>1.8</java.version>
        <spring.boot.mybatis>1.3.0</spring.boot.mybatis>
    </properties>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.1.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${spring.boot.mybatis}</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-test</artifactId>
            <version>2.1.1.RELEASE</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.1.3.RELEASE</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.54</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

以下是Maven-module.orderProducer 生产者模块内容

maven-module.orderProducer.application.yml

server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=round&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver

  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    publisher-confirms: true #消息发送到交换机确认机制,是否确认回调
    publisher-returns: true  #消息发送到交换机确认机制,是否返回回调
    #consumer
    listener:
      simple:
        prefetch: 1
        retry:
          #开启消费者重试
          enabled: true
          #重试间隔时间ms
          initial-interval: 3000
          #最大重试次数
          max-attempts: 3
        #采用手动回答
        acknowledge-mode: manual
mybatis:
  type-aliases-package: com.study.model
  mapper-locations: classpath:mapper/*Mapper.xml

#log_level config
LOG_LEVEL: INFO

maven-module.orderProducer-com.study.mqconfig.OrderMQMsgConfig.java

package com.study.mqconfig;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Description: 订单消息MQconfig
 * @Author mengfanzhu
 * @Date 2019/6/4 22:28
 * @Version 1.0
 */
@Configuration
@Slf4j
public class OrderMQMsgConfig {

    /**
     * 积分处理队列
     */
    public static final String QUEUE_NAME_TRANSACTION = "api.score.queue";
    /**
     * 补单队列
     */
    public static final String CREATE_QUEUE_NAME_TRANSACTION = "api.order.reCreate.queue";
    /**
     * 积分处理路由KEY
     */
    public static final String ROUTE_NAME_TRANSACTION = "api.score.route";
    /**
     * 积分处理交换机
     */
    public static final String EXCHANGE_NAME_TRANSACTION = "api.score.exchange";

    /**
     * 积分队列
     *
     * @return
     */
    @Bean
    public Queue scoreQueue() {
        return new Queue(QUEUE_NAME_TRANSACTION, true);
    }

    /**
     * 补单队列
     *
     * @return
     */
    @Bean
    public Queue createOrderReceiver() {
        return new Queue(CREATE_QUEUE_NAME_TRANSACTION, true);
    }

    @Bean
    public DirectExchange transExchange() {
        return new DirectExchange(EXCHANGE_NAME_TRANSACTION);
    }

    /**
     * 交换机绑定到积分队列
     * @return
     */
    @Bean
    public Binding bindingExchangeOrderReceiverQueue() {
        return BindingBuilder.bind(scoreQueue()).to(transExchange()).with(ROUTE_NAME_TRANSACTION);
    }

    /**
     * 交换机绑定到补单队列
     * @return
     */
    @Bean
    public Binding bindingExchangeCreateOrderQueue() {
        return BindingBuilder.bind(createOrderReceiver()).to(transExchange()).with(ROUTE_NAME_TRANSACTION);
    }
}

maven-module.orderProducer-com.study.service.OrderService.java

package com.study.service;

import com.alibaba.fastjson.JSONObject;
import com.study.mapper.OrderMapper;
import com.study.model.OrderInfoModel;
import com.study.mqconfig.OrderMQMsgConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageBuilder;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

/**
 * @Description: 订单服务
 * @Author mengfanzhu
 * @Date 2019/6/4 14:19
 * @Version 1.0
 */
@Service
@Slf4j
public class OrderService implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private OrderMapper orderMapper;


    /**
     * 创建订单
     */
    public void saveOrder() {
        OrderInfoModel orderInfoModel = new OrderInfoModel();
        orderInfoModel.initOrder();
        orderInfoModel.setAmount(new BigDecimal(100));
        orderInfoModel.setCustomerName("张三");
        Long result = orderMapper.insert(orderInfoModel);
        //send mq 消费积分
        if (result == 1L) {
            sendMsg(orderInfoModel);
        }
    }

    /**
     * 发送消息
     *
     * @param orderInfoModel
     */
    public void sendMsg(OrderInfoModel orderInfoModel) {
        rabbitTemplate.setMandatory(true);
        rabbitTemplate.setConfirmCallback(this);

        //消息封装
        Message message = MessageBuilder.withBody(JSONObject.toJSONString(orderInfoModel).getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON)
                .setContentEncoding("utf-8").setMessageId(orderInfoModel.getOrderNo()).build();

        CorrelationData correlationData = new CorrelationData(orderInfoModel.getOrderNo());
        rabbitTemplate.convertAndSend(OrderMQMsgConfig.EXCHANGE_NAME_TRANSACTION,
                OrderMQMsgConfig.ROUTE_NAME_TRANSACTION, message, correlationData);
        log.info("Transactional Message success.");
    }

    /**
     * 用来确认消息是否有送达消息队列
     *
     * @param correlationData
     * @param ack
     * @param cause
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        log.info(JSONObject.toJSONString(correlationData));
        if (ack) {
            System.out.println("success");
        } else {
            // retry
            System.out.println("failed");
        }
    }

    /**
     * 消息找不到对应的Exchange会先触发此方法
     *
     * @param message
     * @param replyCode
     * @param replyText
     * @param tempExchange
     * @param tmpRoutingKey
     */
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String tempExchange
            , String tmpRoutingKey) {
        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Send message failed:" + replyCode + " " + replyText);
        rabbitTemplate.send(message);
    }
}

maven-module.orderProducer-com.study.listen.ReCreateorderConsumer.java

package com.study.listen;

import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;
import com.study.mapper.OrderMapper;
import com.study.model.OrderInfoModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * @Description: 补单消费者
 * @Author mengfanzhu
 * @Date 2019/6/4 15:06
 * @Version 1.0
 */
@Component
@Slf4j
public class ReCreateOrderConsumer {

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 补单队列
     */
    public static final String CREATE_QUEUE_NAME_TRANSACTION = "api.order.reCreate.queue";

    /**
     * @param message
     * @param headers
     * @param channel
     */
    @RabbitListener(queues = CREATE_QUEUE_NAME_TRANSACTION)
    public void orderReceiver(Message message, @Headers Map<String, Object> headers, Channel channel) {
        try {
            log.info("supplement-message;{}", JSONObject.toJSONString(message));
            String msgBody = new String(message.getBody(), "UTF-8");
            OrderInfoModel infoModel = JSONObject.parseObject(msgBody,OrderInfoModel.class);
            log.info("supplement-message body is {} ", infoModel);

            String orderNo = infoModel.getOrderNo();
            OrderInfoModel orderInfoModel = orderMapper.getByOrderNo(orderNo);
            if (null == orderInfoModel) {
               Long result = orderMapper.insert(infoModel);
               if(result != 1L){
                   throw new Exception("ReCreateOrderInfo Failed!");
               }
            }
            log.info("手动签收消息,通知MQ 删除此消息");
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            log.error("supplement error {}", e.toString(), e);
        }
    }
}

以下是Maven-module.scoreConsumer 积分消费者模块内容

maven-module.scoreConsumer.application.yml

server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql:/localhost:3306/demo?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=round&allowMultiQueries=true&autoReconnect=true&failOverReadOnly=false
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: root
    password: root
    #consumer
    listener:
      simple:
      #全局配置:公平派遣消息-消费者无应答并发消费数,默认为循环派遣
        prefetch: 2
        retry:
          #开启消费者重试-考虑幂等处理
          enabled: true
          #重试间隔时间ms
          initial-interval: 3000
          #最大重试次数
          max-attempts: 2
        #采用手动回答
        acknowledge-mode: manual

mybatis:
  type-aliases-package: com.study.model
  mapper-locations: classpath:mapper/*Mapper.xml
#log_level config
LOG_LEVEL: INFO

 

maven-module.scoreConsumer-com.study.listen.ScoreReceiverConsumer.java

package com.study.listen;

import com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;
import com.study.mapper.ScoreMapper;
import com.study.model.ScoreModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Map;

/**
 * @Description: 积分 消费者
 * @Author mengfanzhu
 * @Date 2019/6/4 15:06
 * @Version 1.0
 */
@Component
@Slf4j
public class ScoreReceiverConsumer {

    @Autowired
    private ScoreMapper scoreMapper;

    /**
     * 积分队列
     */
    public static final String QUEUE_NAME_TRANSACTION = "api.score.queue";

    @Bean
    public Queue scoreQueue() {
        return new Queue(QUEUE_NAME_TRANSACTION, true);
    }

    @RabbitListener(queues = QUEUE_NAME_TRANSACTION)
    public void orderReceiver(Message message, @Headers Map<String, Object> headers, Channel channel) {
        try {
            log.info("score-message:{}", JSONObject.toJSONString(message));
            String msgBody = new String(message.getBody(), "UTF-8");
            JSONObject jsonObject = JSONObject.parseObject(msgBody);
            String orderNo = jsonObject.getString("orderNo");
            log.info("score-message body is :{}" + jsonObject.toJSONString());
            //幂等处理
            ScoreModel scoreModel = scoreMapper.getByOrderNo(orderNo);
            if (scoreModel != null) {
                //丢弃消息
                log.info("repeat consumer ,discard the message.");
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
            }
            //处理积分逻辑,插入积分数据,成功则签收消息
            scoreModel = new ScoreModel();
            scoreModel.setOrderNo(orderNo);
            scoreModel.setScore(100);
            Boolean isInsertSuccess = scoreMapper.insert(scoreModel) == 1L;
            if (isInsertSuccess) {
                //手动签收消息,通知MQ 删除此消息
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
                log.info("scoreConsumer handler success.");
            }
        } catch (IOException e) {
            log.error("scoreConsumer handler Faild,{}", e.toString(), e);
        }
    }
}

 


测试结果

1.执行module.orderProducer - test.java  com.study.mq.hello.HelloProducerTest.java  - method saveServiceOrder()

发送消息到MQ,可见日志打印success, MQ 服务器QUEUE tab中也显示了对应两个队列的待消费消息数

 

2.启动积分消费者模块

 

3.启动补单消费者模块

本文源码:https://github.com/fzmeng/MqParent


评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值