文章目录

  • 前言
  • 一、前期介绍
  • 二、项目准备
  • 三、可靠生产者
  • 重发策略,我这里是task,也可以采取其他的处理方式,根据个人业务情况
  • 到这,消息可靠生产者形成闭环
  • 四、可靠生消费者
  • 可靠消费 监听可靠生产者队列
  • 可靠队列消费失败,消息转移到死信队列,监听死信队列
  • 这一套操作搞完,基于MQ的分布式事务形成闭环
  • 总结



前言


分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。以上是百度百科的解释,简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。


直接上代码基于MQ实现的分布式事务形成完整闭环

一、前期介绍

订单表
CREATE TABLE `order_test` (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `user_id` int(20) NOT NULL COMMENT '用户id',
  `goods` varchar(255) NOT NULL COMMENT '商品名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

订单镜像表
CREATE TABLE `order_mq_backup` (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `order_id` int(20) NOT NULL COMMENT '订单id',
  `user_id` int(20) NOT NULL COMMENT '用户id',
  `goods` varchar(255) NOT NULL COMMENT '商品名称',
  `type` int(20) NOT NULL DEFAULT '0' COMMENT '状态[0:未确认 1:已确认]',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 COMMENT='订单镜像表';

这里订单表和镜像表是在一个数据库的 配送中心表在其他的库,模拟分布式嘛!!!

-----------------------------------------------------------------------------

配送中心表
CREATE TABLE `delivery` (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `order_id` int(20) NOT NULL COMMENT '订单id',
  `goods` varchar(255) NOT NULL COMMENT '商品名称',
  `shipper_id` int(20) NOT NULL COMMENT '配送人_id',
  `type` int(20) NOT NULL DEFAULT '0' COMMENT '状态 [0:禁用 1:正常]',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COMMENT='配送中心表';

二、项目准备

这里是创建队列和交换机以及绑定关系,可以写在消费者模块,我这里两边都写了

package com.example.distributedproducer.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;

@Configuration
public class ReliableConfig {


    /**
     * 创建死信队列交换机
     * @return
     */
    @Bean
    public FanoutExchange deadExchange(){
        return new FanoutExchange("dead_order_exchange",true,false);
    }


    /**
     * 创建死信队列
     * @return
     */
    @Bean
    public Queue deadQueue(){
        return new Queue("dead_order_queue",true,false,false);
    }

    /**
     * 死信队列绑定关系
     * @param deadQueue
     * @return
     */
    @Bean
    public Binding deadBinding(@Qualifier(value = "deadQueue")Queue deadQueue){
        return BindingBuilder.bind(deadQueue).to(deadExchange());
    }


    /**
     * 可靠交换机
     *
     * @return
     */
    @Bean
    public DirectExchange reliableExchange() {
        return new DirectExchange("reliable_order_exchange", true, false);
    }

    /**
     * 可靠消息队列
     * @return
     */
    @Bean
    public Queue reliableQueue() {
        //绑定死信队列
        HashMap<String, Object> map = new HashMap<>();
        map.put("x-dead-letter-exchange","dead_order_exchange");
        return new Queue("reliable_order_queue", true, false, false,map);
    }
    
    /**
     * 绑定关系
     * @param reliableQueue
     * @return
     */
    @Bean
    public Binding reliableBinding(@Qualifier(value = "reliableQueue")Queue reliableQueue){
        return BindingBuilder.bind(reliableQueue).to(reliableExchange()).with("");
    }

}

springboot.rabbitmq.publisher-confirm 新版本已被弃用,现在使用 spring.rabbitmq.publisher-confirm-type = correlated 实现相同效果

server:
  port: 8074
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://ip:3306/distributed-demo?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
  rabbitmq:
    username: admin
    password: admin
    virtual-host: /
    host: ip
    port: 5672
   # 开启发布确认机制,默认NONE是禁用发布确认模式 
   # correlated 发布消息成功到交换器后会触发回调方法
    publisher-confirm-type: correlated

三、可靠生产者

package com.example.distributedproducer.service;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.distributedproducer.entity.OrderMqBackup;
import com.example.distributedproducer.entity.OrderTest;
import com.example.distributedproducer.mapper.OrderMapper;
import com.example.distributedproducer.mapper.OrderMqBackupMapper;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

@Service
public class ProducerService {


    @Resource
    private OrderMapper orderMapper;

    @Resource
    private OrderMqBackupMapper orderMqBackupMapper;

    @Resource
    private RabbitTemplate rabbitTemplate;


    /**
     * 可靠消息回调
     * PostConstruct 该注解 用来修饰 一个非静态的 void 方法,修饰的方法在加载 servlet的时候运行
     * 并且只会被服务器执行一次,在构造函数之后执行,init()方法之前执行
     */
    @PostConstruct
    public void reliableCallback(){
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {

            @Override
            public void confirm(CorrelationData correlationData, boolean b, String s) {
                //回执成功 收到回调改镜像表状态
                if (b){
                    Integer orderId = Integer.valueOf(correlationData.getId());
                    OrderMqBackup one = orderMqBackupMapper.selectOne(new LambdaQueryWrapper<OrderMqBackup>()
                            .eq(OrderMqBackup::getOrderId, orderId));
                   if (one != null) orderMqBackupMapper.updateById(one.setType(1));
                    return;
                }
                //TODO 回执失败,执行重发策略
            }
        });

    }


    /**
     * 模拟创建订单
     * 使用的是镜像模式,order表是订单表,order_mq_backup 就相当于备份表,
     * 但是这张表里面比订单表多一个 type状态(0:未确认 1:已确认) 用来保证消息的可靠性
     */
    public void save(){
        OrderTest orderTest = new OrderTest().setUserId(107).setGoods("泡面");
        orderMapper.insert(orderTest);

        OrderMqBackup backup = new OrderMqBackup();
        BeanUtils.copyProperties(orderTest,backup);
        backup.setOrderId(orderTest.getId());
        orderMqBackupMapper.insert(backup);

        String s = JSONObject.toJSONString(orderTest);

        //给MQ发送消息
        rabbitTemplate.convertAndSend("reliable_order_exchange","",
                s,new CorrelationData(orderTest.getId().toString()));
    }

}

重发策略,我这里是task,也可以采取其他的处理方式,根据个人业务情况

package com.example.distributedproducer.task;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.distributedproducer.entity.OrderMqBackup;
import com.example.distributedproducer.mapper.OrderMqBackupMapper;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;

/**
 * @author ChuYao
 * @version 1.0
 * @date 2021/8/10 10:07
 * @description: TODO MQ订单重发,这里将镜像表没有确认状态的订单重新发给队列
 */
@Component
public class ReliableTask {

    @Resource
    private RabbitTemplate rabbitTemplate;
    @Resource
    private OrderMqBackupMapper orderMqBackupMapper;

	//生产环境不建议开每分钟发一次哦 
    @Scheduled(cron = "0 */1 * * * ?")
    private void resendMessage(){
        try{
            List<OrderMqBackup> backupList = orderMqBackupMapper.selectList(new LambdaQueryWrapper<OrderMqBackup>().eq(OrderMqBackup::getType, 0));
            backupList.forEach(item ->{
                String s = JSONObject.toJSONString(item);
                rabbitTemplate.convertAndSend("reliable_order_exchange","",s,new CorrelationData(item.getOrderId().toString()));
            });
        }catch (Exception e){

        }
    }
}

到这,消息可靠生产者形成闭环


四、可靠生消费者

消费者配置文件,消费者的创建交换机,队列,绑定关系都一致,贴过来就行这里就不写了。

server:
  port: 8074
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://ip:3306/distributed-demo?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
  rabbitmq:
    username: admin
    password: admin
    virtual-host: /
    host: ip
    port: 5672
    listener:
      simple:
      #开启手动 ack手动管理 MQ消息删除,转移,默认 none 自动应答消息删除
        acknowledge-mode: manual

可靠消费 监听可靠生产者队列

package com.example.distributedconsumer.service;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.distributedconsumer.entity.Delivery;
import com.example.distributedconsumer.entity.OrderTest;
import com.example.distributedconsumer.mapper.DeliveryMapper;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.BeanUtils;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.IOException;


@Component
@RabbitListener(queues = "reliable_order_queue")
public class ReliableConsumer {

    @Resource
    private DeliveryMapper deliveryMapper;
    
    /**
     * 解决消息重试方案
     * 1.控制重发次数 + 死信队列
     * 2.try + catch + 手动 ack
     * 3.try + catch + 手动 ack + 死信队列 + 人工预警 [推荐!]
     */
    @RabbitHandler
    public void reliableMessage(String msg, Channel channel, CorrelationData correlationData,
                                @Header(AmqpHeaders.DELIVERY_TAG) long tag){
        try{
            //获取队列中的消息
            OrderTest orderTest = JSONObject.parseObject(msg, OrderTest.class);
            System.out.println("消费信息------->"+msg);
            Delivery one = deliveryMapper.selectOne(new LambdaQueryWrapper<Delivery>().eq(Delivery::getOrderId, orderTest.getId()));

			//TDOD 幂等性 我这里orderId 使用数据库主键解决重复消费问题,也可以上个锁来处理
            if (one != null){
                deliveryMapper.updateById(one.setType(1));
            }else {
                Delivery delivery = new Delivery();
                BeanUtils.copyProperties(orderTest,delivery);
                delivery.setOrderId(orderTest.getId()).setShipperId(999).setType(1);
                deliveryMapper.insert(delivery);
            }

			 //制造异常模拟消费失败
            System.out.println(1/0);
            //消息正常消费
            channel.basicAck(tag,false);
        }catch (Exception e){
            try {
                /**
                 * param1 消息的 tag
                 * param2 多条处理
                 * param3 是否重发
                 * TODO try + catch 模式 param3 不建议开启重发,会造成死循环
                 * 正常情况 Nack 后消息会被移除, 这里使用死信队列来转移消息
                 */
                System.out.println("消费失败");
                channel.basicNack(tag,false,false);
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }

        }
    }
}

可靠队列消费失败,消息转移到死信队列,监听死信队列

** 死信的监听基本和可靠队列消费一致,同时也要处理幂等性问题,唯一的区别就是死信队列也消费失败的话采取的处理机制不同(这里就要预警加其他备份手段)**

package com.example.distributedconsumer.service;

import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.distributedconsumer.entity.Delivery;
import com.example.distributedconsumer.entity.OrderTest;
import com.example.distributedconsumer.mapper.DeliveryMapper;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.beans.BeanUtils;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.IOException;

/**
 * @author ChuYao
 * @version 1.0
 * @date 2021/8/10 9:25
 * @description: TODO
 */
@Component
@RabbitListener(queues = "dead_order_queue")
public class DeadConsumer {

    @Resource
    private DeliveryMapper deliveryMapper;

    @RabbitHandler
    public void readMessage(String msg, Channel channel, CorrelationData correlationData,
                            @Header(AmqpHeaders.DELIVERY_TAG) long tag){

        try {
            //获取消息
            OrderTest orderTest = JSONObject.parseObject(msg, OrderTest.class);
            System.out.println("消费信息------->"+msg);
            Delivery one = deliveryMapper.selectOne(new LambdaQueryWrapper<Delivery>().eq(Delivery::getOrderId, orderTest.getId()));
            //TDOD 幂等性 我这里orderId 使用数据库主键解决重复消费问题,也可以上个锁来处理
            if (one != null){
                deliveryMapper.updateById(one.setType(1));
            }else {
                Delivery delivery = new Delivery();
                BeanUtils.copyProperties(orderTest,delivery);
                delivery.setOrderId(orderTest.getId()).setShipperId(999).setType(1);
                deliveryMapper.insert(delivery);
            }
            System.out.println(1/0);

            //消息正常消费
            channel.basicAck(tag,false);
        }catch (Exception e){
            try {
                /**
                 * 死信队列都无法正常消费
                 * 开启预警 加本地备份
                 */
                System.out.println("发短信给开发人员预警");
                channel.basicNack(tag,false,false);
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }
    }
}

这一套操作搞完,基于MQ的分布式事务形成闭环


总结

提示:基于MQ的分布式事务比较繁琐,需要注意的是如果MQ一旦挂掉那就全崩了!实战都是采用镜像集群模式。大家有兴趣的话可以去搭个集群玩玩,配置文件改成集群配置