【RabbitMQ Learning Journey】分布式事务解决方案思想

(MQ解决分布式事务,内容来源于蚂蚁课堂

案例: 点外卖案例。 用户下单后,调用订单服务,然后订单服务调用派单服务通知外卖人员送单, 这时候订单系统与派单系统采用MQ异步通讯。

消费者端
RabbitMQ分布式事务解决方案,最终一致性思想。(可以暂时不一致,但是最终要一致)
需要保证以下三要素
1、确认生产者一定要将数据投递到MQ服务器中(采用MQ消息确认机制)
2、MQ消费者消息能够正确消费消息,采用手动ACK模式(注意重试幂等性问题)
3、如何保证第一个事务先执行,采用补偿机制,再创建一个补单消费者进行监听,如果订单没有创建成功,进行补单。

如果生产者投递消息到MQ服务成功
场景1:
    如果消费者消费消息失败了,生产者是否需要回滚事务?
    (派单服务消费失败,订单服务是否需要回滚事务)
    不需要!
    解决方法:
        消费者采用ack手动应答的方式,采用MQ进行补偿重试机制,注意MQ补偿幂等性问题
问题:
    如何确认(订单服务新增订单)生产者一定会将数据投递到MQ服务器中?
    confirm机制(确认应答机制)。

场景2:
    如果生产者发送消息到MQ服务器失败呢?
    解决方法:
        使用生产者重试机制进行发消息。implements RabbitTemplate.ConfirmCallback

场景3:
    如何保证第一个事务先执行?生产者投递消息到MQ服务器端成功,消费者消费消息成功了,但是订单事务回滚了。
    (订单服务投递消息成功后,报错。派单服务消费信息成功)
    // 2.使用消息中间件将参数存在派单队列中
    send(orderId);
    //模拟场景3 前面创建的订单会回滚,补单开始起作用
    //int i = 1 / 0;

 

工程结构目录:

一.订单服务代码

1.创建订单;投递消息;订单补偿。

配置文件

spring:
  #数据库连接信息
  datasource:
    name: test
    url: jdbc:mysql://127.0.0.1:3306/kzone?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=TRUE&useSSL=false
    username: root
    password: 1234
    # 使用druid数据源
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: root
    password: 1234
    virtual-host: /wk_virtual_hosts
    ###开启消息确认机制 confirms
    publisher-confirms: true
    publisher-returns: true
server:
  port: 6002
# 关闭eureka注册发现
eureka.client.register-with-eureka: false
eureka.client.fetch-registry: false

基本类:addOrder返回值

package com.google.base;

public interface ApiConstants {
	// 响应请求成功
	String HTTP_RES_CODE_200_VALUE = "success";
	// 系统错误
	String HTTP_RES_CODE_500_VALUE = "fail";
	// 响应请求成功code
	Integer HTTP_RES_CODE_200 = 200;
	// 系统错误
	Integer HTTP_RES_CODE_500 = 500;
	// 未关联QQ账号
	Integer HTTP_RES_CODE_201 = 201;

}
package com.google.base;

import org.springframework.stereotype.Component;

@Component
public class BaseApiService {

	public ResponseBase setResultError(Integer code, String msg) {
		return setResult(code, msg, null);
	}

	// 返回错误,可以传msg
	public ResponseBase setResultError(String msg) {
		return setResult(ApiConstants.HTTP_RES_CODE_500, msg, null);
	}

	// 返回成功,可以传data值
	public ResponseBase setResultSuccess(Object data) {
		return setResult(ApiConstants.HTTP_RES_CODE_200, ApiConstants.HTTP_RES_CODE_200_VALUE, data);
	}

	// 返回成功,沒有data值
	public ResponseBase setResultSuccess() {
		return setResult(ApiConstants.HTTP_RES_CODE_200, ApiConstants.HTTP_RES_CODE_200_VALUE, null);
	}

	// 返回成功,沒有data值
	public ResponseBase setResultSuccess(String msg) {
		return setResult(ApiConstants.HTTP_RES_CODE_200, msg, null);
	}

	// 通用封装
	public ResponseBase setResult(Integer code, String msg, Object data) {
		return new ResponseBase(code, msg, data);
	}

}
package com.google.base;

import lombok.Data;

@Data
public class ResponseBase {

	private Integer rtnCode;
	private String msg;
	private Object data;

	public ResponseBase() {

	}

	public ResponseBase(Integer rtnCode, String msg, Object data) {
		super();
		this.rtnCode = rtnCode;
		this.msg = msg;
		this.data = data;
	}

	@Override
	public String toString() {
		return "ResponseBase [rtnCode=" + rtnCode + ", msg=" + msg + ", data=" + data + "]";
	}

}

实体类、mapper接口:

/**
 * 功能说明:
 * 功能作者:
 * 创建日期:
 * 版权归属:每特教育|蚂蚁课堂所有 www.itmayiedu.com
 */
package com.google.entity;

import java.math.BigDecimal;
import java.util.Date;

import lombok.Data;

@Data
public class OrderEntity {

    private Long id;
    // 订单名称
    private String name;
    // 订单时间
    private Date orderCreatetime;
    // 下单金额
    private BigDecimal orderMoney;
    // 订单状态
    private int orderState;
    // 商品id
    private Long commodityId;

    // 订单id
    private String orderId;
}
package com.google.mapper;

import com.google.entity.OrderEntity;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;


public interface OrderMapper {

    @Insert(value = "INSERT INTO `order_info` (name,orderCreatetime,orderMoney,orderState,commodityId,orderId)" +
            "VALUE (#{name}, #{orderCreatetime}, #{orderMoney}, #{orderState}, #{commodityId},#{orderId})")
    int addOrder(OrderEntity orderEntity);

    @Select("SELECT id,name,orderCreatetime,"
            + "orderState,orderMoney, "
            + "commodityid,orderId from order_info where orderId=#{orderId};")
    OrderEntity findOrderId(@Param("orderId") String orderId);

}

订单生产者:

package com.google.service;

import java.util.Date;
import java.util.UUID;

import com.alibaba.fastjson.JSON;
import com.google.base.BaseApiService;
import com.google.base.ResponseBase;
import com.google.entity.OrderEntity;
import com.google.mapper.OrderMapper;
import com.google.rabbitmq.RabbitmqConfig;
import org.apache.commons.lang.StringUtils;
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 com.alibaba.fastjson.JSONObject;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService extends BaseApiService implements RabbitTemplate.ConfirmCallback {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Transactional
    public ResponseBase addOrderAndDispatch(String data) {

        //获取前台传来的数据
        if (StringUtils.isBlank(data)) {
            return setResultError("参数不能为空!");
        }
        JSONObject jsonObject = JSON.parseObject(data);
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setName(jsonObject.getString("name"));
        orderEntity.setOrderCreatetime(new Date());
        // 价格是300元
        orderEntity.setOrderMoney(jsonObject.getBigDecimal("money"));
        // 状态为 未支付
        orderEntity.setOrderState(0);
        // 商品id
        orderEntity.setCommodityId(jsonObject.getLong("CommodityId"));
        String orderId = UUID.randomUUID().toString();
        orderEntity.setOrderId(orderId);

        // 1.先下单,创建订单 (往订单数据库中插入一条数据)
        int orderResult = orderMapper.addOrder(orderEntity);
        System.out.println("创建订单orderResult:" + orderResult);
        if (orderResult <= 0) {
            return setResultError("下单失败!");
        }
        // 2.使用消息中间件将参数存在派单队列中
        send(orderId);

        //模拟场景3 前面创建的订单会回滚,补单开始起作用
        //int i = 1 / 0;

        return setResultSuccess();
    }

    private void send(String orderId) {
        JSONObject jsonObect = new JSONObject();
        jsonObect.put("orderId", orderId);
        String msg = jsonObect.toJSONString();
        System.out.println("生产者发送的msg:" + msg);
        // 封装消息
        Message message = MessageBuilder.withBody(msg.getBytes()).setContentType(MessageProperties.CONTENT_TYPE_JSON)
                .setContentEncoding("utf-8").setMessageId(orderId).build();
        // 构建回调返回的数据(消息Id)
        CorrelationData correlationData = new CorrelationData(orderId);

        // 发送消息 true:RabbitMQ会调用Basic.Return命令将消息返回给生产者
        //false:RabbitMQ会把消息直接丢弃
        this.rabbitTemplate.setMandatory(true);
        //Confirm机制,会调用该send方法
        this.rabbitTemplate.setConfirmCallback(this);

        //(交换机,路由Key,Message,CorrelationData)
        rabbitTemplate.convertAndSend(RabbitmqConfig.ORDER_EXCHANGE_NAME, RabbitmqConfig.ORDER_ROUTING_KEY, message, correlationData);

    }

    /**
     * 生产消息确认机制 (生产者往服务器端发送消息的时候,采用应答机制)
     *
     * @param correlationData
     * @param ack
     * @param cause
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        String orderId = correlationData.getId();
        System.out.println("消息id:" + correlationData.getId());
        if (ack) {
            System.out.println("消息发送确认成功");
        } else {
            //重试机制 (最大重试次数配置)
            System.out.println("生产者消息发送确认失败,开始重试:" + cause);
            send(orderId);
        }

    }

}

队列交换机绑定配置:

package com.google.rabbitmq;

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component
public class RabbitmqConfig {

    // 下单并且派单存队列
    public static final String ORDER_DIC_QUEUE = "order_dic_queue";
    // 补单队列,判断订单是否已经被创建
    public static final String ORDER_CREATE_QUEUE = "order_create_queue";
    // 下单并且派单交换机
    public static final String ORDER_EXCHANGE_NAME = "order_exchange_name";

    //orderRoutingKey
    public static final String ORDER_ROUTING_KEY = "orderRoutingKey";

    // 1.定义订单队列
    @Bean
    public Queue directOrderDicQueue() {
        return new Queue(ORDER_DIC_QUEUE);
    }

    // 2.定义补订单队列
    @Bean
    public Queue directCreateOrderQueue() {
        return new Queue(ORDER_CREATE_QUEUE);
    }

    // 2.定义交换机
    @Bean
    DirectExchange directOrderExchange() {
        return new DirectExchange(ORDER_EXCHANGE_NAME);
    }

    // 3.订单队列与交换机绑定
    @Bean
    Binding bindingExchangeOrderDicQueue() {
        return BindingBuilder.bind(directOrderDicQueue()).to(directOrderExchange()).with(ORDER_ROUTING_KEY);
    }

    // 3.补单队列与交换机绑定
    @Bean
    Binding bindingExchangeCreateOrder() {
        return BindingBuilder.bind(directCreateOrderQueue()).to(directOrderExchange()).with(ORDER_ROUTING_KEY);
    }

}

controller层:

package com.google.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.google.base.BaseApiService;
import com.google.base.ResponseBase;
import com.google.service.OrderService;

@RestController
public class OrderController extends BaseApiService {
    @Autowired
    private OrderService orderService;

    @RequestMapping("/addOrder")
    public ResponseBase addOrder(@RequestBody String data) {
        return orderService.addOrderAndDispatch(data);
    }

}

补单消费者(订单服务):

package com.google.consumer;

import java.math.BigDecimal;
import java.util.Date;
import java.util.Map;

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 com.alibaba.fastjson.JSONObject;
import com.google.entity.OrderEntity;
import com.google.mapper.OrderMapper;
import com.rabbitmq.client.Channel;

/**
 * 补单消费者
 */
@Component
public class CreateOrderConsumer {

    @Autowired
    private OrderMapper orderMapper;

    @RabbitListener(queues = "order_create_queue")
    public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {

        String messageId = message.getMessageProperties().getMessageId();
        String msg = new String(message.getBody(), "UTF-8");
        System.out.println("补单消费者" + msg + ",消息id:" + messageId);
        JSONObject jsonObject = JSONObject.parseObject(msg);
        String orderId = jsonObject.getString("orderId");

        // 判断订单是否存在,如果不存在 实现自动补单机制
        OrderEntity orderEntityResult = orderMapper.findOrderId(orderId);
        if (orderEntityResult != null) {
            System.out.println("订单已经存在 无需补单  orderId:" + orderId);
            // 手动签收消息,通知mq服务器端删除该消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            return;
        }

        System.out.println("订单号码:" + orderId + ", 不存在,开始进行补单!");
        // 订单不存在 ,则需要进行补单
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setName("蚂蚁课堂永久会员充值");
        orderEntity.setOrderCreatetime(new Date());
        // 价格是300元
        orderEntity.setOrderMoney(new BigDecimal("300"));
        // 状态为 未支付
        orderEntity.setOrderState(0);
        Long commodityId = 30l;
        // 商品id
        orderEntity.setCommodityId(commodityId);
        orderEntity.setOrderId(orderId);

        // ##################################################
        // 1.先下单,创建订单 (往订单数据库中插入一条数据)
        try {
            int orderResult = orderMapper.addOrder(orderEntity);
            System.out.println("orderResult:" + orderResult);
            if (orderResult >= 0) {
                // 手动签收消息,通知mq服务器端删除该消息
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            }
            //重试进行补偿 多次失败 使用日志记录采用定时job检查或者人工补偿
        } catch (Exception e) {
            //日志记录
            // 丢弃该消息
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }

    }
}

启动类:

package com.google;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("com.google.mapper")
@SpringBootApplication
public class AppOrder {

	public static void main(String[] args) {
		SpringApplication.run(AppOrder.class, args);
	}

}

测试运行:

二.派单服务代码

配置文件:

spring:
  #数据库连接信息
  datasource:
    name: test
    url: jdbc:mysql://127.0.0.1:3306/kzone?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=TRUE&useSSL=false
    username: root
    password: 1234
    # 使用druid数据源
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: root
    password: 1234
    virtual-host: /wk_virtual_hosts
    # 修改重试策略
    listener:
      simple:
        retry:
          ####开启消费者重试 (程序出现异常的情况下会)进行重试
          enabled: true
          ####最大重试次数
          max-attempts: 5
          ####重试间隔次数
          initial-interval: 10000
        #(应答模式)开启手动ack
        acknowledge-mode: manual
server:
  port: 6001
# 关闭eureka注册发现
eureka.client.register-with-eureka: false
eureka.client.fetch-registry: false

实体类、mappper接口:

package com.google.entity;

import lombok.Data;

@Data
public class DispatchEntity {

	private Long id;
	// 订单号
	private String orderId;
	// 派单路线
	private String dispatchRoute;
	// 外卖员id
	private Long takeoutUserId;
	
}
package com.google.mapper;

import com.google.entity.DispatchEntity;
import org.apache.ibatis.annotations.Insert;


public interface DispatchMapper {

    /**
     * 新增派单任务
     */
    @Insert("INSERT into platoon values (null,#{orderId},#{dispatchRoute},#{takeoutUserId});")
    public int insertDistribute(DispatchEntity distributeEntity);

}

派单消费者:

package com.google.rabbitmq.consumer;

import java.util.Map;

import com.google.entity.DispatchEntity;
import com.google.mapper.DispatchMapper;
import org.apache.commons.lang3.StringUtils;
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 com.alibaba.fastjson.JSONObject;
import com.rabbitmq.client.Channel;

/**
 * 派单服务
 *
 * @author
 */
@Component
public class DispatchConsumer {

    @Autowired
    private DispatchMapper dispatchMapper;

    @RabbitListener(queues = "order_dic_queue")
    public void process(Message message, @Headers Map<String, Object> headers, Channel channel) throws Exception {

        String messageId = message.getMessageProperties().getMessageId();
        String msg = new String(message.getBody(), "UTF-8");
        JSONObject jsonObject = JSONObject.parseObject(msg);
        String orderId = jsonObject.getString("orderId");

        if (StringUtils.isEmpty(orderId)) {
            // 日志记录
            return;
        }

        try {
            int insertDistribute = dispatchMapper.insertDistribute(generateDic(orderId));
            if (insertDistribute > 0) {
                // 手动签收消息,通知mq服务器端删除该消息
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
                System.out.println("派单服务平台" + msg + ",消息id:" + messageId);
            }
        } catch (Exception e) {
            e.printStackTrace();
            // // 丢弃该消息
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
        }
    }

    /**
     * 生成派单
     *
     * @return
     */
    private DispatchEntity generateDic(String orderId) {
        DispatchEntity dispatchEntity = new DispatchEntity();
        // 订单id
        dispatchEntity.setOrderId(orderId);
        // 外卖员id
        dispatchEntity.setTakeoutUserId(12l);
        // 外卖路线
        dispatchEntity.setDispatchRoute("40,40");
        return dispatchEntity;
    }

}

启动类:

package com.google;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("com.google.mapper")
@SpringBootApplication
public class AppDispatch {

    public static void main(String[] args) {
        SpringApplication.run(AppDispatch.class, args);
    }

}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值