086:RabbitMQ解决分布式事务问题
1 RPC分布式事务产生的背景
课程内容:
- 30分钟未支付,系统自动超时关闭订单实现方案
- 消息中间件自动补偿触发原理
- 消息中间件如何合理选择重试机制
- 消息中间件如何解决消费者幂等性
- SpringBoot整合消息确认机制
业务场景:使用公众号下单邮寄包裹,下单后会调用派单接口,快递员上门取件
分布式事务产生的背景:
- 在rpc通讯中,每个服务都有自己独立的数据源,每个数据源都互不影响;
- 在单个项目存在多个不同jdbc连接(多数据源)
每个jdbc连接都有自己独立的事务数据管理。
2 如何理解分布式事务最终一致性
基于RabbitMQ解决分布式事务的思路–采用最终一致性的方案
强一致性:线程B读取userName一定为mayikt;
解决方案:数据库A非常迅速的将数据同步给数据库B,或者数据库A没有同步完成之前数据库B不能够读取userName。
弱一致性:允许读取数据为老的数据,允许读取的结果不一致性。
在分布式系统中,数据的同步走网络通讯,多多少少可能会因为网络的延迟、数据抖动产生延迟,这是正常现象,因此不能保证强一致性。但是最终结果一定要同步,这就是最终一致性,涉及补偿和人工补偿。
3 基于RabbitMQ解决分布式事务思路
如何基于MQ解决分布式事务的问题 (最终一致性概念)
- 确保生产者往MQ投递消息一定要成功(生产者消息确认机制confirm),失败重试;
- 确保消费者能够消费成功(手动的ack机制)如果消费失败的情况下,MQ自动帮消费者重试,如果多次重试失败,表记录人工补偿;
- 确保生产者第一事务先执行成功,如果执行失败采用补单队列;(补单队列和派单队列订阅同一个路由键,补单队列查询订单创建是否成功,如果没有创建成功,补偿创建;订阅的消息内容包含下单的完整信息和派单信息。)
4 代码实现基于RabbitMQ解决分布式事务1
订单系统mayikt_order_producer
maven依赖
<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>1.1.1</version>
</dependency>
<!-- mysql 依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 阿里巴巴数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.14</version>
</dependency>
<!-- SpringBoot整合Web组件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 添加springboot对amqp的支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
数据库表
CREATE TABLE `order_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`orderCreatetime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`orderMoney` decimal(10,0) DEFAULT NULL,
`orderState` int(60) DEFAULT NULL,
`commodityId` int(60) DEFAULT NULL,
`orderId` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
application.yml
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /mayikt_rabbitmq
###开启消息确认机制 confirms
publisher-confirms: true
publisher-returns: true
listener:
simple:
retry:
####开启消费者(程序出现异常的情况下会)进行重试
enabled: true
####最大重试次数
max-attempts: 5
####重试间隔次数
initial-interval: 3000
###开启ack模式
acknowledge-mode: manual
datasource:
url: jdbc:mysql://localhost:3306/order?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
server:
port: 8080
核心代码
@Component
public class OrderRabbitMQConfig {
/**
* 派单队列
*/
public static final String ORDER_DIC_QUEUE = "order_dic_queue";
/**
* 补单对接
*/
public static final String ORDER_CREATE_QUEUE = "order_create_queue";
/**
* 派单交换机
*/
private static final String ORDER_EXCHANGE_NAME = "order_exchange_name";
/**
* 定义派单队列
*
* @return
*/
@Bean
public Queue directOrderDicQueue() {
return new Queue(ORDER_DIC_QUEUE);
}
/**
* 定义补派单队列
*
* @return
*/
@Bean
public Queue directCreateOrderQueue() {
return new Queue(ORDER_CREATE_QUEUE);
}
/**
* 定义订单交换机
*
* @return
*/
@Bean
DirectExchange directOrderExchange() {
return new DirectExchange(ORDER_EXCHANGE_NAME);
}
/**
* 派单队列与交换机绑定
*
* @return
*/
@Bean
Binding bindingExchangeOrderDicQueue() {
return BindingBuilder.bind(directOrderDicQueue()).to(directOrderExchange()).with("orderRoutingKey");
}
/**
* 补单队列与交换机绑定
*
* @return
*/
@Bean
Binding bindingExchangeCreateOrder() {
return BindingBuilder.bind(directCreateOrderQueue()).to(directOrderExchange()).with("orderRoutingKey");
}
}
@Data
public class OrderEntity {
private Long id;
// 订单名称
private String name;
// 订单时间
private Date orderCreatetime;
// 下单金额
private Double orderMoney;
// 订单状态
private int orderState;
// 商品id
private Long commodityId;
// 订单id
private String orderId;
public OrderEntity(Long id, String name, Date orderCreatetime, Double orderMoney, int orderState, Long commodityId, String orderId) {
this.id = id;
this.name = name;
this.orderCreatetime = orderCreatetime;
this.orderMoney = orderMoney;
this.orderState = orderState;
this.commodityId = commodityId;
this.orderId = orderId;
}
public OrderEntity() {
}
}
public interface OrderMapper {
@Insert(value = "INSERT INTO `order_info` VALUES (#{id}, " +
"#{name}, #{orderCreatetime}, #{orderMoney}, #{orderState}, #{commodityId},#{orderId})")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
public int addOrder(OrderEntity orderEntity);
@Select("SELECT * from order_info where orderId=#{orderId};")
public OrderEntity findOrderId(@Param("orderId") String orderId);
}
@Component
@Slf4j
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
public class OrderProducer implements RabbitTemplate.ConfirmCallback {
@Resource
private OrderMapper orderMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 基于MQ发送投递订单消息
*/
public String sendOrder() {
// 1.创建订单信息
String orderId = System.currentTimeMillis() + "";
OrderEntity orderEntity = createOrder(orderId);
// 2.添加到数据库
int result = orderMapper.addOrder(orderEntity);
if (result <= 0) {
return null;
}
// 3.订单数据库插入成功,使用MQ异步发送派单信息
String jsonString = JSONObject.toJSONString(orderEntity);
sendMsg(jsonString);
return orderId;
}
@Async
public void sendMsg(String msgJson) {
// 设置生产者消息确认机制
this.rabbitTemplate.setMandatory(true);
this.rabbitTemplate.setConfirmCallback(this);
CorrelationData correlationData = new CorrelationData();
correlationData.setId(msgJson);
String orderExchange = "order_exchange_name";
String orderRoutingKey = "orderRoutingKey";
rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, msgJson, correlationData);
}
public OrderEntity createOrder(String orderId) {
OrderEntity orderEntity = new OrderEntity();
orderEntity.setName("每特教育第六期平均就业薪资破10万");
orderEntity.setOrderCreatetime(new Date());
// 价格是300元
orderEntity.setOrderMoney(300d);
// 状态为 未支付
orderEntity.setOrderState(0);
Long commodityId = 30L;
// 商品id
orderEntity.setCommodityId(commodityId);
orderEntity.setOrderId(orderId);
return orderEntity;
}
/**
* @param correlationData 投递失败回调的消息
* @param ack true,投递成功,否则false
* @param s
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String s) {
String msg = correlationData.getId();
if (!ack) {
log.info("往mq中投递消息失败>>" + msg);
// 采用递归算法重试 可以加上限制重试次数,使用threadLocal
sendMsg(msg);
return;
}
log.info("往mq中投递消息成功>>" + msg);
// 生产者投递多次还是失败,记录到表中后期人工补偿
}
}
@RestController
public class OrderController {
@Autowired
private OrderProducer orderProducer;
@RequestMapping("/sendOrder")
public String sendOrder() {
return orderProducer.sendOrder();
}
}
@SpringBootApplication
@MapperScan("com.mayikt.mapper")
@EnableAsync
public class AppProducer {
public static void main(String[] args) {
SpringApplication.run(AppProducer.class);
}
}
5 代码实现基于RabbitMQ解决分布式事务2
派单系统mayikt_dl_consumer
数据库表
CREATE TABLE `platoon` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`orderId` varchar(255) DEFAULT NULL,
`userId` int(60) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
application.yml
spring:
rabbitmq:
####连接地址
host: 127.0.0.1
####端口号
port: 5672
####账号
username: guest
####密码
password: guest
### 地址
virtual-host: /mayikt_rabbitmq
###开启消息确认机制 confirms
publisher-confirms: true
publisher-returns: true
datasource:
url: jdbc:mysql://localhost:3306/order?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
server:
port: 8081
核心代码
@Data
public class DispatchEntity {
private Long id;
// 订单号
private String orderId;
// 派单id
private Long userId;
public DispatchEntity(String orderId, Long userId) {
this.orderId = orderId;
this.userId = userId;
}
}
public interface DispatchMapper {
/**
* 新增派单任务
*/
@Insert("INSERT into platoon values (null,#{orderId},#{userId});")
int insertDistribute(DispatchEntity distributeEntity);
}
@Component
public class DispatchConsumer {
@Resource
private DispatchMapper dispatchMapper;
@RabbitListener(queues = "order_dic_queue")
public void dispatchConsumer(Message message, Channel channel) throws IOException {
// 1.获取消息
String msg = new String(message.getBody());
// 2.转换json
JSONObject jsonObject = JSONObject.parseObject(msg);
String orderId = jsonObject.getString("orderId");
// 幂等性 主动根据orderId查询是否派单过,如果已经派单情况下,直接返回
// 计算分配的快递员id
DispatchEntity dispatchEntity = new DispatchEntity(orderId, 123L);
// 插入数据库
int result = dispatchMapper.insertDistribute(dispatchEntity);
if (result > 0) {
// 手动ack 删除消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
}
@SpringBootApplication
@MapperScan("com.mayikt.mapper")
public class AppConsumer {
public static void main(String[] args) {
SpringApplication.run(AppConsumer.class);
}
}
运行结果:
6 分布式事务补单队列的实现
补单消费者代码
@Component
public class CreateOrderConsumer {
@Resource
private OrderMapper orderMapper;
/**
* 订阅补单队列
*
* @param message
* @param channel
* @throws IOException
*/
@RabbitListener(queues = "order_create_queue")
public void createOrderConsumer(Message message, Channel channel) throws IOException {
// 1.获取消息
String msg = new String(message.getBody());
// 2.获取order对象
OrderEntity orderEntity = JSONObject.parseObject(msg, OrderEntity.class);
// 3.获取订单号码
String orderId = orderEntity.getOrderId();
// 4.根据订单号码查询是否存在
OrderEntity dbOrderEntity = orderMapper.findOrderId(orderId);
if (dbOrderEntity != null) {
// 手动ack 删除该消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
return;
}
// 说明该订单不存在,创建该订单
// 2.添加到我们的数据库中
int result = orderMapper.addOrder(orderEntity);
if (result > 0) {
// 手动ack 删除该消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
}
测试结果:
注意:
补单的消费不应该和订单生产者放到一个服务器节点;
补单消费者如果不存在的情况下 队列缓存补单消息;
补偿分布式事务解决框架 思想最终一致性;