(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);
}
}