Spring Boot 整合RabbitMQ 解决分布式事务

项目说明:

模拟外卖案例,用户下单之后,调用订单服务,然后订单服务间消息发给派送服务通知外卖人员送餐,订单系统与派单系统采用MQ异步通讯。

RabbitMQ解决分布式事务原理方案

  1. 确保生产者一定要将数据投递到MQ服务器中
    • 生产者采用confirm,确认应答机制
    • 如果失败,生产者进行重试。
  2. MQ消费者消息能够正常消费消息。
    • 采用手动ACK模式,使用补偿机制,注意幂等性问题。
  3. 采用补单机制。
    • 在创建一个补单消费者进行监听,如果订单创建后,又回滚了(数据不一致),此时需要将订单进行补偿。
    • 交换机采用路由键模式,补单队列和派但队列都绑定同一个路由键。

订单服务

配置交换机,可以配置多个

package com.gpdi.order.config;

import org.springframework.amqp.core.DirectExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author Lxq
 * @Date 2020/3/7 13:59
 * @Version 1.0
 * 消息交换机配置,可以配置多个
 */
@Configuration
public class ExchangeConfig {

    private final String EXCHANGE_ORDER_DISPATCH = "order-dispatch";

    @Bean
    public DirectExchange directExchange() {
        DirectExchange directExchange = new DirectExchange(EXCHANGE_ORDER_DISPATCH, true, false);
        return directExchange;
    }
}

配置队列信息

package com.gpdi.order.config;

import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author Lxq
 * @Date 2020/3/7 14:06
 * @Version 1.0
 * 配置队列,这里只使用第一个
 */
@Configuration
public class QueueConfig {

    /*对列名称*/
    public static final String QUEUE_NAME1 = "dispatch-queue";
    public static final String QUEUE_NAME2 = "second-queue";
    public static final String QUEUE_NAME3 = "third-queue";


    @Bean
    public Queue DispatchQueue() {
        /**
         durable="true" 持久化消息队列 , rabbitmq重启的时候不需要创建新的队列
         auto-delete 表示消息队列没有在使用时将被自动删除 默认是false
         exclusive  表示该消息队列是否只在当前connection生效,默认是false
         */
        return new Queue(QUEUE_NAME1,true,false,false);
    }

    @Bean
    public Queue SecondQueue() {
        return new Queue(QUEUE_NAME2,true,false,false);
    }

    @Bean
    public Queue ThirdQueue() {
        // 配置 自动删除
        Map<String, Object> arguments = new HashMap<>();
        arguments.put("x-message-ttl", 60000);//60秒自动删除
        return new Queue(QUEUE_NAME3,true,false,true,arguments);
    }

}

将配置的交换机和队列进行绑定

package com.gpdi.order.config;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author Lxq
 * @Date 2020/3/7 14:10
 * @Version 1.0
 * 将交换机和队列进行绑定
 */
@Configuration
public class RabbitMqConfig {

    /**
     * key: queue在该direct-exchange中的key值,当消息发送给direct-exchange中指定key为设置值时,
     * 消息将会转发给queue参数指定的消息队列
     */
    /** 队列key*/
    public static final String ROUTING_KEY_ORDER_DISPATCH = "order-dispatch-key";

    @Autowired
    private QueueConfig queueConfig;

    @Autowired
    private ExchangeConfig exchangeConfig;


    /**
     * 将消息队列和交换机进行绑定,指定队列ROUTING_KEY_ORDER_DISPATCH
     */
    @Bean
    public Binding bindingDispatch(){
        return BindingBuilder.bind(queueConfig.DispatchQueue())
                .to(exchangeConfig.directExchange())
                .with(ROUTING_KEY_ORDER_DISPATCH);
    }

}

新建两个表,一个订单表(存放下单信息),一个本地信息表(关联订单表,存放订单状态,是否发送给mq)

DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单编号',
  `user_id` int(11) DEFAULT NULL COMMENT '用户编号',
  `order_content` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '订单内容',
  `create_time` datetime(0) DEFAULT NULL COMMENT '订单时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 22 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '订单表' ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;
DROP TABLE IF EXISTS `tb_message`;
CREATE TABLE `tb_message`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '消息编号',
  `msg_content` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '消息内容',
  `msg_status` int(255) DEFAULT NULL COMMENT '消息状态',
  `create_time` datetime(0) DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 22 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '本地消息表' ROW_FORMAT = Dynamic;

 订单核心代码

package com.gpdi.order.service;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.gpdi.order.dao.MessageMapper;
import com.gpdi.order.dao.OrderMapper;
import com.gpdi.order.domain.Message;
import com.gpdi.order.domain.Order;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Author Lxq
 * @Date 2020/3/7 15:56
 * @Version 1.0
 */
@Service
@Transactional(rollbackFor = Exception.class)
public class OrderServiceImpl implements OrderService {


    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private MessageMapper messageMapper;

    @PostConstruct
    public void setUP() {
        // Mq的确认回执
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {

            @Override
            public void confirm(@Nullable CorrelationData correlationData, boolean ack, @Nullable String cause) {
                if (!ack) {
                    //mq没有接收到消息
                    //ToDo 忽略或者重新发送消息
                    return;
                }
                // 修改本地消息表
                int i = messageMapper.updateById(Long.valueOf(correlationData.getId()));
                if (i > 0){
                    System.out.println("消息发送成功");
                }
            }
        });
    }

    @Override
    public void createOrder(String order) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Order content = new Order();
        content.setUserId(1);
        content.setOrderContent(order);
        content.setCreateTime(sdf.format(new Date()));
        orderMapper.insert(content);
        System.out.println(content.getId());
        //存放到本地消息表
        Message message = new Message();
        message.setId(content.getId());
        message.setMsgContent(content.getOrderContent());
        message.setMsgStatus(0);
        message.setCreateTime(sdf.format(new Date()));
        messageMapper.insert(message);

        sendMessage(message);


    }

    @Override
    public void sendMessage(Message message) {
        rabbitTemplate.convertAndSend("order-dispatch", "order-dispatch-key", JSONObject.toJSONString(message),
                new CorrelationData(String.valueOf(message.getId())));
    }
}

 派送订单核心代码

package com.gpdi.dispatch.service;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * @Author Lxq
 * @Date 2020/3/7 17:43
 * @Version 1.0
 * 消费者
 */
@Component
public class OrderDispatchConsumer {

    @RabbitListener(queues = "dispatch-queue")
    public void messageConsumer(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
        try {
            // 收到MQ消息
            System.out.println(message);
            // 插入到派送单中
            //todo
            // 给MQ回执
            channel.basicAck(tag,false);

        } catch (Exception e) {
            // 重新发一次
            channel.basicNack(tag, false, true);
        }

    }
}

直接下载源码:

链接:https://pan.baidu.com/s/1tR94adXviEFLEmQZ6G4iDw 
提取码:lnbq 
复制这段内容后打开百度网盘手机App,操作更方便哦
发布了90 篇原创文章 · 获赞 29 · 访问量 7258

猜你喜欢

转载自blog.csdn.net/weixin_38982591/article/details/104723705