秒杀P10-异步化扣减库存

569 阅读8分钟

目录

事务性消息来异步扣减库存

image.png

RocketMQ基本概念

  • 生产者
  • 消费者
  • 路由/名字服务器
  • 代理服务器
  • 主题等

image.png

引入RocketMQ

  • 服务器上下载rocketmq,安装。此时没有两个文件夹,后续启动后自动生成。
  • 修改配置
  • 调用安装好的包内的测试命令。启动命令:sh 程序名

image.png

  • 启动生产者发送,启动消费者接受,ok

代码引入依赖

  • pom.xml导入依赖

image.png

  • 配置resources/application-dev.properties
    • 配置路由ip和端口
    • 配置生产者组名
    • 先不配置消费者

image.png

采用异步消息扣减库存

最终项目采用事务方式,比较复杂。单纯异步比较简单,保证事务性复杂些

rocket官方文档有示例代码

  • 一个消费者可以同步消费多个队列
  • 不同队列的消息,消费顺序不固定
  • 同一队列中,消息按顺序消费

重点:

  • 消息发送:同步发送;异步发送
  • 消费消息:统一
  • 顺序消息
  • 延时消息:xx分钟以后再处理消息
    • 场景1:抢票15分钟未支付取消订单
    • 场景2:线上笔试强制交卷
    • 少卖问题
  • 事务消息

RocketMQ测试代码

  • RocketMQTest测试类
  • main方法里,调用两个测试方法。
  • 两测试方法,格式固定

1、testDefaultMQProducer()方法,测试生产者怎么发消息

  • DefaultMQProducer producer = new DefaultMQProducer("seckill_producer");
    • 通过DefaultMQProducer实例化生产者,要指定生产者的组
  • 设置路由NamerServer的ip和端口
  • 启动
  • 定义主题
  • 定义标签
    • 一个业务一个topic主题。
    • 子业务可以再把主题细分tag标签
  • 循环十次,发十个消息
  • 定义body格式,随便一字符串+数字。用来组成消息
  • new一个message,传入参数。组成消息的参数:主题,标签,字符串body转换成字节(二进制)
  • 发送消息,用producer.send()方法。有两参数
  • 一个参数是messgae消息内容
  • 另一个参数是new的回调方法SendCallback()——异步消息
  • 发消息后不会立刻得到回应,等返回数据后,通过Callback()处理。类似Ajax
  • 成功了调onSuccess(SendResult sendResult)方法,处理成功返回的数据
  • 失败调onException(Throwable e)处理异常

1h16min image.png

image.png

2、testDefaultMQConsumer();测试消费者接收消息

  • 同上,通过DefaultMQPushConsumernew消费者对象,指定消费者组
  • 声明NameServer路由地址+ip
  • 声明订阅哪个主题,哪个或哪些标签。订阅哪个就能接受哪个的消息
  • 启用?消息的监听器,有消息可以消费,消费者得到,就调用监听器的consumeMessage()方法处理。有两参数:message消息、context消息的上下文环境。
  • 一次处理一组?消息
  • 返回给producer生产者,成功/失败的标志
  • 启动消费者

image.png

以上是异步消息用法底层原理

spring中不用new生产者消费者,直接注入rocketMQTemplate的bean(内部集成了生产者消费者)

spring中MQ使用

test方法底层是多线程模式,test主线程启动后,会开启子线程,但是主线程test方法运行完以后直接挂,不会等待子线程。在main里不会,mian里启动子线程,main方法会等待子线程完毕再结束。

  • 启动运行环境
    • 想在spring环境下运行main,测试代码需要实现CommandLineRunner接口
    • 在main方法中,调用run方法运行测试代码
  • 执行代码逻辑
    • 在run()方法中写测试代码具体的执行逻辑

image.png

逻辑类似底层。

过一遍代码

image.png

⭐事务性消息

  • 保证本地应用服务器的事务与消息消费的最终一致性
  • 本地事务“提交成功”/“失败回滚”,消息“消费成功”/“不消费”
  • 两阶段提交

⭐两阶段提交

  1. 生产者发送half半成品消息。放入MQ服务器临时队列
  2. MQ服务器收到半成品消息,给生产者返回ok。等待后续通知。
  3. 生产者访问MySQL,执行本地事务
  4. 生产者根据本地事务执行结果,commit/rollback,通知MQ服务器要不要发送消息。第一次提交
  5. 如果因为网络等原因,MQServer持有半成品消息,但未收到后续通知,启用回查机制。通知producer回查。
  6. producer去check回查数据库commit是否成功。
  7. producer把回查结果,通知给MQserver。第二次提交。是重复的。如果还没有,继续三四五....次回查。始终没有,加入死信队列。。。。。。
  8. 当MQserver接收到后续通知以后,半成品消息转化成正式消息,commit提交消息发送给消费者

image.png

代码体现

  • 主要写1发消息3本地事务6回查,三步的代码。
  • test/RocketMQTestInSpring.java中

1发送消息

  • testProduceInTransaction()方法,以事务方式消费。
  • 声明消息发到哪个主题-标签
  • 发十次消息
  • 构建消息体
  • 调用rocketMQTemplate.sendMessageInTransaction(destination,message,arg:nul1) 发消息。参数:传入哪个主题,消息内容,空参数
  • 返回result,可以观察发送结果

image.png

3本地事务和6回查check

  • TransactionListenerImpl,组件实现类,实现RocketMQLocalTransactionListener监听器接口
  • @RocketMQTransactionListener注解
  • 实现监听器接口的executeLocalTransaction()方法,执行事务。checkLocalTransaction()方法,回查。都返回执行的状态,commit/rollback/unknow,对应MQserver发送消息/撤消消息/不发送

注:

  • 监听器全局(正式代码和测试代码中)只能有一个,只能打一个@RocketMQTransactionListener注解。
  • test代码反馈状态是写死的以测试
  • 重点理解执行流程,和为什么写这三块代码?
  • 发消息依赖1步,两次提交分别依赖3、6步。

image.png

image.png

private void testProduceInTransaction() throws Exception {
    String destination = "seckillTest:tagT";
    for (int i = 0; i < 10; i++) {
        Message message = MessageBuilder.withPayload("message " + i).build();
        TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(destination, message, null);
        System.out.println(sendResult);
    }
}

@RocketMQTransactionListener
private class TransactionListenerImpl implements RocketMQLocalTransactionListener {

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        System.out.println("executeLocalTransaction: " + msg + ", " + arg);
        return RocketMQLocalTransactionState.COMMIT;
    }

    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        System.out.println("checkLocalTransaction: " + msg);
        return RocketMQLocalTransactionState.COMMIT;
    }
}

“事务性消息”在秒杀项目业务中的逻辑

  • 创建订单时扣减库存,之前直接在数据库中扣,并发量大情况下吞吐量很低。
  • 所以把库存存到缓存中,先扣缓存里面的库存,发消息稍后异步扣MySQL的正式库存
  • 最终缓存库存与MySQL库存要一致

OrderServiceImpl.Java

image.png

⭐解决方案

  • 先生成流水,再发送消息
  • 预减库存和创建订单后,再更新流水
  • 回查机制是检查库存流水
  • MySQL维护库存流水表。是一个单独的表,流水是递增
  • 为什么不直接更新库存,而是选择更新流水表?
    • 库存是所有用户共享的,更新时会锁整个库存表。
    • 流水是每个当订单独有,更新流水每次只锁一行(一条流水),锁的力度不同。

image.png

======================================================

⭐⭐⭐项目重点/难点/亮点:⭐逻辑过程?⭐什么是两阶段提交?⭐该模式下把什么做成异步?本地事务要做什么?消费是干什么?检查是检查什么?⭐为什么引入流水?

如果不引入流水,当第四步失败时,回查机制检查订单,订单可能没都没创建成功,没法检查。

消息: 广义,就是各种数据

⭐⭐整体逻辑:

  • 消费者要扣减库存,本地事务要创建订单之前还要先预减库存。
  • 生产者发半成品消息,server接收到后,本地事务预减库存创建订单,事务结果通知server。
  • server发送消息,消费者扣减库存。
  • 如果订单创建失败,通知server要rollback,网络中断server没收到通知,回查,check订单,但是订单都没创建成功,没法检查。故回查订单的话不合理【回查有问题,所以引入流水解决
    • 订单创建失败,可能数据库性能慢或者前面的问题,阻塞在这,等下可能就成功了。不能因为订单没创建成功,就判定事务失败。
  • 引入流水,让回查有一个合适的目标。先生成流水再发消息,流水生成失败直接结束不发消息。
  • 发消息后执行本地事务,本地事务订单创建成功后更新流水的状态。更新后流水状态就是合理的状态。
  • 即使4步通知server断网发送失败,回查时检查流水状态,那么下一次commit/rollback通知就成功了。
  • 通知server发送成功后,生产者(controller)就直接给客户端返回响应成功。即使消费者此时还没开始扣减库存,但后续延迟一点,一定会消费。因为两阶段提交保证了最终一致性。

⭐预减库存:

  • 把mysql库存挪到redis里,叫缓存预热。
  • 库存影响下单的一个条件(库存0不能再下单),如果不扣,所有人能秒杀到。【库存影响下单,所以要预扣再建单
  • 预减库存性能好,正式扣在MySQL里,是异步的

⭐流水

  • 回查时就可以知道事务运行的状态。已完成or未完成
  • 同样是直接操作MySQL,为什么不直接更新库存,而是选择更新库存流水表?
    • 库存是所有用户共享的,多个用户同时改,更新时会锁整个库存表,性能差。
    • 库存流水在MySQL中也进行了持久化?
    • 流水是每个订单创建之前新建,每个订单独有、很小、顺序创建,性能很好。
    • 更新流水每次只锁一行(一条流水),锁的力度不同。

⭐⭐⭐项目重点/难点/亮点:⭐逻辑过程?⭐什么是两阶段提交?⭐该模式下把什么做成异步?本地事务要做什么?消费是干什么?检查是检查什么?⭐为什么引入流水?

消息丢失问题 重复消费问题 最终一致性解决方案