RocketMQ(2)---实现分布式事务原理
之前讲过有关分布式事务2PC、3PC、TCC的理论知识,博客地址:
这篇讲有关RocketMQ实现分布式事务的理论知识,下篇也会示例 通过SpringCloud来实例RocketMQ实现分布式事务的项目。
一、举个分布式事务场景
列子
:假设 A 给 B 转 100块钱,同时它们不是同一个服务上。
目标
:就是 A 减100块钱,B 加100块钱。
实际情况可能有四种:
1)就是A账户减100 (成功),B账户加100 (成功)
2)就是A账户减100(失败),B账户加100 (失败)
3)就是A账户减100(成功),B账户加100 (失败)
4)就是A账户减100 (失败),B账户加100 (成功)
这里 第1和第2 种情况是能够保证事务的一致性的,但是 第3和第4 是无法保证事务的一致性的。
那我们来看下RocketMQ是如何来保证事务的一致性的。
二、RocketMQ实现分布式事务原理
RocketMQ虽然之前也支持分布式事务,但并没有开源,等到RocketMQ 4.3才正式开源。
1、基础概念
最终一致性
RocketMQ是一种最终一致性的分布式事务,就是说它保证的是消息最终一致性,而不是像2PC、3PC、TCC那样强一致分布式事务,至于为什么说它是最终一致性事务下面会详细说明。
Half Message(半消息)
是指暂不能被Consumer消费的消息。Producer 已经把消息成功发送到了 Broker 端,但此消息被标记为暂不能投递
状态,处于该种状态下的消息称为半消息。需要 Producer
对消息的二次确认
后,Consumer才能去消费它。
消息回查
由于网络闪段,生产者应用重启等原因。导致 Producer 端一直没有对 Half Message(半消息) 进行 二次确认。这是Brock服务器会定时扫描长期处于半消息的消息
,会
主动询问 Producer端 该消息的最终状态(Commit或者Rollback),该消息即为 消息回查。
2、分布式事务交互流程
理解这张阿里官方的图,就能理解RocketMQ分布式事务的原理了。
我们来说明下上面这张图
1、A服务先发送个Half Message给Brock端,消息中携带 B服务 即将要+100元的信息。
2、当A服务知道Half Message发送成功后,那么开始第3步执行本地事务。
3、执行本地事务(会有三种情况1、执行成功。2、执行失败。3、网络等原因导致没有响应)
4.1)、如果本地事务成功,那么Product像Brock服务器发送Commit,这样B服务就可以消费该message。
4.2)、如果本地事务失败,那么Product像Brock服务器发送Rollback,那么就会直接删除上面这条半消息。
4.3)、如果因为网络等原因迟迟没有返回失败还是成功,那么会执行RocketMQ的回调接口,来进行事务的回查。
从上面流程可以得知 只有A服务本地事务执行成功 ,B服务才能消费该message
。
然后我们再来思考几个问题?
为什么要先发送Half Message(半消息)
我觉得主要有两点
1)可以先确认 Brock服务器是否正常 ,如果半消息都发送失败了 那说明Brock挂了。
2)可以通过半消息来回查事务,如果半消息发送成功后一直没有被二次确认,那么就会回查事务状态。
什么情况会回查
也会有两种情况
1)执行本地事务的时候,由于突然网络等原因一直没有返回执行事务的结果(commit或者rollback)导致最终返回UNKNOW,那么就会回查。
2) 本地事务执行成功后,返回Commit进行消息二次确认的时候的服务挂了,在重启服务那么这个时候在brock端
它还是个Half Message(半消息),这也会回查。
特别注意: 如果回查,那么一定要先查看当前事务的执行情况,再看是否需要重新执行本地事务。
想象下如果出现第二种情况而引起的回查,如果不先查看当前事务的执行情况,而是直接执行事务,那么就相当于成功执行了两个本地事务。
为什么说MQ是最终一致性事务
通过上面这幅图,我们可以看出,在上面举例事务不一致的两种情况中,永远不会发生
A账户减100 (失败),B账户加100 (成功)
因为:如果A服务本地事务都失败了,那B服务永远不会执行任何操作,因为消息压根就不会传到B服务。
那么 A账户减100 (成功),B账户加100 (失败) 会不会可能存在的。
答案是会的
因为A服务只负责当我消息执行成功了,保证消息能够送达到B,至于B服务接到消息后最终执行结果A并不管。
那B服务失败怎么办?
如果B最终执行失败,几乎可以断定就是代码有问题所以才引起的异常,因为消费端RocketMQ有重试机制,如果不是代码问题一般重试几次就能成功。
如果是代码的原因引起多次重试失败后,也没有关系,将该异常记录下来,由人工处理
,人工兜底处理后,就可以让事务达到最终的一致性。
有关RocketMQ实现分布式事务前面写了一篇博客
下面就这个项目做个整体简单介绍,并在文字最下方附上项目Github地址。
一、项目概述
1、技术架构
项目总体技术选型
SpringCloud(Finchley.RELEASE) + SpringBoot2.0.4 + Maven3.5.4 + RocketMQ4.3 +MySQL + lombok(插件)
有关SpringCloud主要用到以下四个组建
Eureka Server +config-server(配置中心)+ Eureka Client + Feign(服务间调用)
配置中心是用MySQL存储数据。
2、项目整体结构
config-service # 配置中心
eureka # 注册中心
service-order #订单微服务
service-produce #商品微服务
各服务的启动顺序就安装上面的顺序启动。
大致流程
启动后,配置中心、订单微服务、商品微服务都会将信息注册到注册中心。
如果访问:localhost:7001
(注册中心地址),以上服务都出现说明启动成功。
3、分布式服务流程
用户在订单微服务下单后,会去回调商品微服务去减库存。这个过程需要事务的一致性。
4、测试流程
页面输入:
http://localhost:9001/api/v1/order/save?userId=1&productId=1&total=4
订单微服务执行情况(订单服务事务执行成功)
商品微服务执行情况(商品服务事务执行成功)
当然你也可以通过修改参数来模拟分布式事务出现的各种情况。
二、MQ中生产者核心代码
这里展示下,生产者发送消息核心代码。
@Slf4j
@Component
public class TransactionProducer {
/**
* 需要自定义事务监听器 用于 事务的二次确认 和 事务回查
*/
private TransactionListener transactionListener ;
/**
* 这里的生产者和之前的不一样
*/
private TransactionMQProducer producer = null;
/**
* 官方建议自定义线程 给线程取自定义名称 发现问题更好排查
*/
private ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("client-transaction-msg-check-thread");
return thread;
}
});
public TransactionProducer(@Autowired Jms jms, @Autowired ProduceOrderService produceOrderService) {
transactionListener = new TransactionListenerImpl(produceOrderService);
// 初始化 事务生产者
producer = new TransactionMQProducer(jms.getOrderTopic());
// 添加服务器地址
producer.setNamesrvAddr(jms.getNameServer());
// 添加事务监听器
producer.setTransactionListener(transactionListener);
// 添加自定义线程池
producer.setExecutorService(executorService);
start();
}
public TransactionMQProducer getProducer() {
return this.producer;
}
/**
* 对象在使用之前必须要调用一次,只能初始化一次
*/
public void start() {
try {
this.producer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}
/**
* 一般在应用上下文,使用上下文监听器,进行关闭
*/
public void shutdown() {
this.producer.shutdown();
}
}
/**
* @author xub
* @Description: 自定义事务监听器
* @date 2019/7/15 下午12:20
*/
@Slf4j
class TransactionListenerImpl implements TransactionListener {
@Autowired
private ProduceOrderService produceOrderService ;
public TransactionListenerImpl( ProduceOrderService produceOrderService) {
this.produceOrderService = produceOrderService;
}
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
log.info("=========本地事务开始执行=============");
String message = new String(msg.getBody());
JSONObject jsonObject = JSONObject.parseObject(message);
Integer productId = jsonObject.getInteger("productId");
Integer total = jsonObject.getInteger("total");
int userId = Integer.parseInt(arg.toString());
//模拟执行本地事务begin=======
/**
* 本地事务执行会有三种可能
* 1、commit 成功
* 2、Rollback 失败
* 3、网络等原因服务宕机收不到返回结果
*/
log.info("本地事务执行参数,用户id={},商品ID={},销售库存={}",userId,productId,total);
int result = produceOrderService.save(userId, productId, total);
//模拟执行本地事务end========
//TODO 实际开发下面不需要我们手动返回,而是根据本地事务执行结果自动返回
//1、二次确认消息,然后消费者可以消费
if (result == 0) {
return LocalTransactionState.COMMIT_MESSAGE;
}
//2、回滚消息,Broker端会删除半消息
if (result == 1) {
return LocalTransactionState.ROLLBACK_MESSAGE;
}
//3、Broker端会进行回查消息
if (result == 2) {
return LocalTransactionState.UNKNOW;
}
return LocalTransactionState.COMMIT_MESSAGE;
}
/**
* 只有上面接口返回 LocalTransactionState.UNKNOW 才会调用查接口被调用
*
* @param msg 消息
* @return
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
log.info("==========回查接口=========");
String key = msg.getKeys();
//TODO 1、必须根据key先去检查本地事务消息是否完成。
/**
* 因为有种情况就是:上面本地事务执行成功了,但是return LocalTransactionState.COMMIT_MESSAG的时候
* 服务挂了,那么最终 Brock还未收到消息的二次确定,还是个半消息 ,所以当重新启动的时候还是回调这个回调接口。
* 如果不先查询上面本地事务的执行情况 直接在执行本地事务,那么就相当于成功执行了两次本地事务了。
*/
// TODO 2、这里返回要么commit 要么rollback。没有必要在返回 UNKNOW
return LocalTransactionState.COMMIT_MESSAGE;
}
}
三、MQ消费端核心代码
这里展示下,消费端消费消息核心代码。消费端和普通消费一样。
@Slf4j
@Component
public class OrderConsumer {
private DefaultMQPushConsumer consumer;
private String consumerGroup = "produce_consumer_group";
public OrderConsumer(@Autowired Jms jms,@Autowired ProduceService produceService) throws MQClientException {
//设置消费组
consumer = new DefaultMQPushConsumer(consumerGroup);
// 添加服务器地址
consumer.setNamesrvAddr(jms.getNameServer());
// 添加订阅号
consumer.subscribe(jms.getOrderTopic(), "*");
// 监听消息
consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
MessageExt msg = msgs.get(0);
String message = new String(msgs.get(0).getBody());
JSONObject jsonObject = JSONObject.parseObject(message);
Integer productId = jsonObject.getInteger("productId");
Integer total = jsonObject.getInteger("total");
String key = msg.getKeys();
log.info("消费端消费消息,商品ID={},销售数量={}",productId,total);
try {
produceService.updateStore(productId, total, key);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
} catch (Exception e) {
log.info("消费失败,进行重试,重试到一定次数 那么将该条记录记录到数据库中,进行如果处理");
e.printStackTrace();
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
});
consumer.start();
System.out.println("consumer start ...");
}
}
至于完整的项目地址见GitHub。
RocketMQ 发送普通消息有三种实现方式:可靠同步发送
、可靠异步发送
、单向(Oneway)发送
。
注意
:顺序消息只支持可靠同步发送。
GitHub地址: https://github.com/yudiandemingzi/SpringBootBlog
一、概念
1、可靠同步发送
原理
:同步发送是指消息发送方发出数据后,会在收到接收方发回响应之后才发下一个数据包的通讯方式。
应用场景:此种方式应用场景非常广泛,例如重要通知邮件、报名短信通知、营销短信系统等。
2、可靠异步发送
原理
:异步发送是指发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。 消息队列 RocketMQ 的异步发送,需要用户实现异步发送回调接口(SendCallback)。
应用场景:异步发送一般用于链路耗时较长,对 RT 响应时间较为敏感的业务场景,例如批量发货等操作。
3、单向(Oneway)发送
原理
:单向(Oneway)发送特点为发送方只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答。 此方式发送消息的过程耗时非常短,一般在微秒级别。
应用场景:适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。
4、三种对比
下表概括了三者的特点和主要区别。
发送方式 | 发送 TPS | 发送结果反馈 | 可靠性 |
---|---|---|---|
同步发送 | 快 | 有 | 不丢失 |
异步发送 | 快 | 有 | 不丢失 |
单向发送 | 最快 | 无 | 可能丢失 |
二、代码示例
1、三种方式代码示例
@Slf4j
@RestController
public class Controller {
/**
* 生产者组
*/
private static String PRODUCE_RGROUP = "test_producer";
/**
* 创建生产者对象
*/
private static DefaultMQProducer producer = null;
static {
producer = new DefaultMQProducer(PRODUCE_RGROUP);
//不开启vip通道 开通口端口会减2
producer.setVipChannelEnabled(false);
//绑定name server
producer.setNamesrvAddr("47.99.03.25:9876");
try {
producer.start();
} catch (MQClientException e) {
e.printStackTrace();
}
}
@GetMapping("/message")
public void message() throws Exception {
//1、同步
sync();
//2、异步
async();
//3、单项发送
oneWay();
}
/**
* 1、同步发送消息
*/
private void sync() throws Exception {
//创建消息
Message message = new Message("topic_family", (" 同步发送 ").getBytes());
//同步发送消息
SendResult sendResult = producer.send(message);
log.info("Product-同步发送-Product信息={}", sendResult);
}
/**
* 2、异步发送消息
*/
private void async() throws Exception {
//创建消息
Message message = new Message("topic_family", (" 异步发送 ").getBytes());
//异步发送消息
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("Product-异步发送-输出信息={}", sendResult);
}
@Override
public void onException(Throwable e) {
e.printStackTrace();
//补偿机制,根据业务情况进行使用,看是否进行重试
}
});
}
/**
* 3、单项发送消息
*/
private void oneWay() throws Exception {
//创建消息
Message message = new Message("topic_family", (" 单项发送 ").getBytes());
//同步发送消息
producer.sendOneway(message);
}
}
2、测试结果
这里消费者代码就不贴出来了。
通过这个很明显可以看出三种方式都被 Consumer 消费了。只不过对于 Product 同步和异步发送是有返回信息的,单项发送是没有返回信息的。
三、SendStatus状态
当Product发送消息的时候,会返回SendResult对象,该对象又包含了一个SendStatus对象。
package org.apache.rocketmq.client.producer;
public enum SendStatus {
SEND_OK,
FLUSH_DISK_TIMEOUT,
FLUSH_SLAVE_TIMEOUT,
SLAVE_NOT_AVAILABLE,
}
下面对这几种状态进行说明
SEND_OK
代表发送成功!但并不保证它是可靠的。要确保不会丢失任何消息,还应启用SYNC_MASTER或SYNC_FLUSH。
SLAVE_NOT_AVAILABLE
如果Broker的角色是SYNC_MASTER(同步复制)(默认为异步),但没有配置Slave Broker,将获得此状态。
FLUSH_DISK_TIMEOUT
如果Broker设置为 SYNC_FLUSH(同步刷盘)(默认为ASYNC_FLUSH),并且Broker的syncFlushTimeout(默认为5秒)内完成刷新磁盘,将获得此状态。
FLUSH_SLAVE_TIMEOUT
如果Broker的角色是SYNC_MASTER(同步复制)(默认为ASYNC_MASTER),并且从属Broker的syncFlushTimeout(默认为5秒)内完成与主服务器的同步,将获得此状态。
如果要保证顺序消费,那么他的核心点就是:生产者有序存储
、消费者有序消费
。
一、概念
1、什么是无序消息
无序消息
无序消息也指普通的消息,Producer 只管发送消息,Consumer 只管接收消息,至于消息和消息之间的顺序并没有保证。
举例
Producer 依次发送 orderId 为 1、2、3 的消息,Consumer 接到的消息顺序有可能是 1、2、3,也有可能是 2、1、3 等情况,这就是普通消息。
2、什么是全局顺序
对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。
举例
比如 Producer 发送orderId 1,3,2 的消息, 那么 Consumer 也必须要按照 1,3,2 的顺序进行消费。
3、局部顺序
在实际开发有些场景中,我并不需要消息完全按照完全按的先进先出,而是某些消息保证先进先出就可以了。
就好比一个订单涉及 订单生成
,订单支付
、订单完成
。我不用管其它的订单,只保证同样订单ID能保证这个顺序
就可以了。
二、实现原理
我们知道 生产的message最终会存放在Queue中,如果一个Topic关联了16个Queue,如果我们不指定消息往哪个队列里放,那么默认是平均分配消息到16个queue,
好比有100条消息,那么这100条消息会平均分配在这16个Queue上,那么每个Queue大概放5~6个左右。这里有一点很重的是:
同一个queue,存储在里面的message 是按照先进先出的原则
这个时候思路就来了,好比有orderId=1的3条消息,分别是 订单生产、订单付款、订单完成。只要保证它们放到同一个Queue那就保证消费者先进先出了。
这就保证局部顺序了,即同一订单按照先后顺序放到同一Queue,那么取消息的时候就可以保证先进先取出。
那么全局消息呢?
这个就简单啦,你把所有消息都放在一个Queue里,这样不就保证全局消息了。
就这么简单
当然不是,这里还有很关键的一点,好比在一个消费者集群的情况下,消费者1先去Queue拿消息,它拿到了 订单生成,它拿完后,消费者2去queue拿到的是 订单支付。
拿的顺序是没毛病了,但关键是先拿到不代表先消费完它。会存在虽然你消费者1先拿到订单生成,但由于网络等原因,消费者2比你真正的先消费消息。这是不是很尴尬了。
订单付款还是可能会比订单生成更早消费的情况。那怎么办。
分布式锁来了
Rocker采用的是分段锁,它不是锁整个Broker而是锁里面的单个Queue,因为只要锁单个Queue就可以保证局部顺序消费了。
所以最终的消费者这边的逻辑就是
消费者1去Queue拿 订单生成,它就锁住了整个Queue,只有它消费完成并返回成功后,这个锁才会释放。
然后下一个消费者去拿到 订单支付 同样锁住当前Queue,这样的一个过程来真正保证对同一个Queue能够真正意义上的顺序消费,而不仅仅是顺序取出。
全局顺序与分区顺序对比
消息类型对比
发送方式对比
其它的注意事项
1、顺序消息暂不支持广播模式。
2、顺序消息不支持异步发送方式,否则将无法严格保证顺序。
3、建议同一个 Group ID 只对应一种类型的 Topic,即不同时用于顺序消息和无序消息的收发。
4、对于全局顺序消息,建议创建实例个数 >=2。
一、RocketMq有3中消息类型
1.普通消费
2. 顺序消费
3.事务消费
- 顺序消费场景
在网购的时候,我们需要下单,那么下单需要假如有三个顺序,第一、创建订单 ,第二:订单付款,第三:订单完成。也就是这个三个环节要有顺序,这个订单才有意义。RocketMQ可以保证顺序消费。
- rocketMq实现顺序消费的原理
produce在发送消息的时候,把消息发到同一个队列(queue)中,消费者注册消息监听器为MessageListenerOrderly,这样就可以保证消费端只有一个线程去消费消息
注意:是把把消息发到同一个队列(queue),不是同一个topic,默认情况下一个topic包括4个queue
单个节点(Producer端1个、Consumer端1个)
三、代码示例
这里保证两点
1、生产端 同一orderID的订单放到同一个queue。
2、消费端 同一个queue取出消息的时候锁住整个queue,直到消费后再解锁。
1、ProductOrder实体
@AllArgsConstructor
@Data
@ToString
public class ProductOrder {
/**
* 订单编号
*/
private String orderId;
/**
* 订单类型(订单创建、订单付款、订单完成)
*/
private String type;
}
2、Product(生产者)
生产者和之前发送普通消息最大的区别,就是针对每一个message都手动通过MessageQueueSelector
选择好queue。
@RestController
public class Product {
private static List<ProductOrder> orderList = null;
private static String producerGroup = "test_producer";
/**
* 模拟数据
*/
static {
orderList = new ArrayList<>();
orderList.add(new ProductOrder("XXX001", "订单创建"));
orderList.add(new ProductOrder("XXX001", "订单付款"));
orderList.add(new ProductOrder("XXX001", "订单完成"));
orderList.add(new ProductOrder("XXX002", "订单创建"));
orderList.add(new ProductOrder("XXX002", "订单付款"));
orderList.add(new ProductOrder("XXX002", "订单完成"));
orderList.add(new ProductOrder("XXX003", "订单创建"));
orderList.add(new ProductOrder("XXX003", "订单付款"));
orderList.add(new ProductOrder("XXX003", "订单完成"));
}
@GetMapping("message")
public void sendMessage() throws Exception {
//示例生产者
DefaultMQProducer producer = new DefaultMQProducer(producerGroup);
//不开启vip通道 开通口端口会减2
producer.setVipChannelEnabled(false);
//绑定name server
producer.setNamesrvAddr("IP:9876");
producer.start();
for (ProductOrder order : orderList) {
//1、生成消息
Message message = new Message(JmsConfig.TOPIC, "", order.getOrderId(), order.toString().getBytes());
//2、发送消息是 针对每条消息选择对应的队列
SendResult sendResult = producer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
//3、arg的值其实就是下面传入 orderId
String orderid = (String) arg;
//4、因为订单是String类型,所以通过hashCode转成int类型
int hashCode = orderid.hashCode();
//5、因为hashCode可能为负数 所以取绝对值
hashCode = Math.abs(hashCode);
//6、保证同一个订单号 一定分配在同一个queue上
long index = hashCode % mqs.size();
return mqs.get((int) index);
}
}, order.getOrderId(),50000);
System.out.printf("Product:发送状态=%s, 存储queue=%s ,orderid=%s, type=%s\n", sendResult.getSendStatus(),
sendResult.getMessageQueue().getQueueId(), order.getOrderId(), order.getType());
}
producer.shutdown();
}
}
看看生产者有没有把相同订单指定到同一个queue
通过测试结果可以看出:相同订单已经存到同一queue中了
。
3、Consumer(消费者)
上面说过,消费者真正要达到消费顺序,需要分布式锁,所以这里需要将MessageListenerOrderly
替换之前的MessageListenerConcurrently,因为它里面实现了分布式锁。
@Slf4j
@Component
public class Consumer {
/**
* 消费者实体对象
*/
private DefaultMQPushConsumer consumer;
/**
* 消费者组
*/
public static final String CONSUMER_GROUP = "consumer_group";
/**
* 通过构造函数 实例化对象
*/
public Consumer() throws MQClientException {
consumer = new DefaultMQPushConsumer(CONSUMER_GROUP);
consumer.setNamesrvAddr("IP:9876");
//TODO 这里真的是个坑,我product设置VipChannelEnabled(false),但消费者并没有设置这个参数,之前发送普通消息的时候也没有问题。能正常消费。
//TODO 但在顺序消息时,consumer一直不消费消息了,找了好久都没有找到原因,直到我这里也设置为VipChannelEnabled(false),竟然才可以消费消息。
consumer.setVipChannelEnabled(false);
//订阅主题和 标签( * 代表所有标签)下信息
consumer.subscribe(JmsConfig.TOPIC, "*");
//注册消费的监听 这里注意顺序消费为MessageListenerOrderly 之前并发为ConsumeConcurrentlyContext
consumer.registerMessageListener((MessageListenerOrderly) (msgs, context) -> {
//获取消息
MessageExt msg = msgs.get(0);
//消费者获取消息 这里只输出 不做后面逻辑处理
log.info("Consumer-线程名称={},消息={}", Thread.currentThread().getName(), new String(msg.getBody()));
return ConsumeOrderlyStatus.SUCCESS;
});
consumer.start();
}
}
看看消费结果是不是我们需要的结果
通过测试结果我们看出
1、消费消息的顺序并没有完全按照之前的先进先出,即没有满足全局顺序。
2、同一订单来讲,订单的 订单生成、订单支付、订单完成 消费顺序是保证的。
这是局部保证顺序消费就已经满足我们当前实际开发中的需求了。