目录
事务性消息来异步扣减库存
RocketMQ基本概念
- 生产者
- 消费者
- 路由/名字服务器
- 代理服务器
- 主题等
引入RocketMQ
- 服务器上下载rocketmq,安装。此时没有两个文件夹,后续启动后自动生成。
- 修改配置
- 调用安装好的包内的测试命令。启动命令:sh 程序名
- 启动生产者发送,启动消费者接受,ok
代码引入依赖
- pom.xml导入依赖
- 配置resources/application-dev.properties
- 配置路由ip和端口
- 配置生产者组名
- 先不配置消费者
采用异步消息扣减库存
最终项目采用事务方式,比较复杂。单纯异步比较简单,保证事务性复杂些
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
2、testDefaultMQConsumer();测试消费者接收消息
- 同上,通过
DefaultMQPushConsumernew消费者对象,指定消费者组 - 声明NameServer路由地址+ip
- 声明订阅哪个主题,哪个或哪些标签。订阅哪个就能接受哪个的消息
启用?消息的监听器,有消息可以消费,消费者得到,就调用监听器的consumeMessage()方法处理。有两参数:message消息、context消息的上下文环境。- 一次处理一组?消息
- 返回给producer生产者,成功/失败的标志
- 启动消费者
以上是异步消息用法底层原理
spring中不用new生产者消费者,直接注入rocketMQTemplate的bean(内部集成了生产者消费者)
spring中MQ使用
test方法底层是多线程模式,test主线程启动后,会开启子线程,但是主线程test方法运行完以后直接挂,不会等待子线程。在main里不会,mian里启动子线程,main方法会等待子线程完毕再结束。
- 启动运行环境
- 想在spring环境下运行main,测试代码需要实现
CommandLineRunner接口 - 在main方法中,调用run方法运行测试代码
- 想在spring环境下运行main,测试代码需要实现
- 执行代码逻辑
- 在run()方法中写测试代码具体的执行逻辑
逻辑类似底层。
过一遍代码
⭐事务性消息
- 保证本地
应用服务器的事务与消息消费的最终一致性。 - 本地事务“提交成功”/“失败回滚”,消息“消费成功”/“不消费”
- 两阶段提交
⭐两阶段提交
- 生产者发送half半成品消息。放入MQ服务器临时队列
- MQ服务器收到半成品消息,给生产者返回ok。等待后续通知。
- 生产者访问MySQL,执行本地事务。
- 生产者根据本地事务执行结果,commit/rollback,通知MQ服务器要不要发送消息。第一次提交
- 如果因为网络等原因,MQServer持有半成品消息,但未收到后续通知,启用回查机制。通知producer回查。
- producer去check回查数据库commit是否成功。
- producer把回查结果,通知给MQserver。第二次提交。是重复的。如果还没有,继续三四五....次回查。始终没有,加入死信队列。。。。。。
- 当MQserver接收到后续通知以后,半成品消息转化成正式消息,commit提交消息发送给消费者。
代码体现
- 主要写1发消息3本地事务6回查,三步的代码。
- test/RocketMQTestInSpring.java中
1发送消息
- testProduceInTransaction()方法,以事务方式消费。
- 声明消息发到哪个主题-标签
- 发十次消息
- 构建消息体
- 调用
rocketMQTemplate.sendMessageInTransaction(destination,message,arg:nul1)发消息。参数:传入哪个主题,消息内容,空参数 - 返回result,可以观察发送结果
3本地事务和6回查check
- TransactionListenerImpl,组件实现类,实现RocketMQLocalTransactionListener监听器接口
- 加
@RocketMQTransactionListener注解 - 实现监听器接口的executeLocalTransaction()方法,执行事务。checkLocalTransaction()方法,回查。都返回执行的状态,commit/rollback/unknow,对应MQserver发送消息/撤消消息/不发送
注:
- 监听器全局(正式代码和测试代码中)只能有一个,只能打一个
@RocketMQTransactionListener注解。 - test代码反馈状态是写死的以测试
- 重点理解执行流程,和为什么写这三块代码?
- 发消息依赖1步,两次提交分别依赖3、6步。
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
⭐解决方案
- 先生成流水,再发送消息
- 预减库存和创建订单后,再更新流水
- 回查机制是检查库存流水
- MySQL维护库存流水表。是一个单独的表,流水是递增
- 为什么不直接更新库存,而是选择更新流水表?
- 库存是所有用户共享的,更新时会锁整个库存表。
- 流水是每个当订单独有,更新流水每次只锁一行(一条流水),锁的力度不同。
======================================================
⭐⭐⭐项目重点/难点/亮点:⭐逻辑过程?⭐什么是两阶段提交?⭐该模式下把什么做成异步?本地事务要做什么?消费是干什么?检查是检查什么?⭐为什么引入流水?
如果不引入流水,当第四步失败时,回查机制检查订单,订单可能没都没创建成功,没法检查。
消息: 广义,就是各种数据
⭐⭐整体逻辑:
- 消费者要扣减库存,本地事务要创建订单之前还要先预减库存。
- 生产者发半成品消息,server接收到后,本地事务预减库存创建订单,事务结果通知server。
- server发送消息,消费者扣减库存。
- 如果订单创建失败,通知server要rollback,网络中断server没收到通知,回查,check订单,但是订单都没创建成功,没法检查。故回查订单的话不合理【回查有问题,所以引入流水解决】
- 订单创建失败,可能数据库性能慢或者前面的问题,阻塞在这,等下可能就成功了。不能因为订单没创建成功,就判定事务失败。
- 引入流水,让回查有一个合适的目标。先生成流水再发消息,流水生成失败直接结束不发消息。
- 发消息后执行本地事务,本地事务订单创建成功后更新流水的状态。更新后流水状态就是合理的状态。
- 即使4步通知server断网发送失败,回查时检查流水状态,那么下一次commit/rollback通知就成功了。
- 通知server发送成功后,生产者(controller)就直接给客户端返回响应成功。即使消费者此时还没开始扣减库存,但后续延迟一点,一定会消费。因为两阶段提交保证了最终一致性。
⭐预减库存:
- 把mysql库存挪到redis里,叫缓存预热。
- 库存影响下单的一个条件(库存0不能再下单),如果不扣,所有人能秒杀到。【库存影响下单,所以要预扣再建单】
- 预减库存性能好,正式扣在MySQL里,是异步的
⭐流水
- 回查时就可以知道事务运行的状态。已完成or未完成
- 同样是直接操作MySQL,为什么不直接更新库存,而是选择更新库存流水表?
- 库存是所有用户共享的,多个用户同时改,更新时会锁整个库存表,性能差。
- 库存流水在MySQL中也进行了持久化?
- 流水是每个订单创建之前新建,每个订单独有、很小、顺序创建,性能很好。
- 更新流水每次只锁一行(一条流水),锁的力度不同。
⭐⭐⭐项目重点/难点/亮点:⭐逻辑过程?⭐什么是两阶段提交?⭐该模式下把什么做成异步?本地事务要做什么?消费是干什么?检查是检查什么?⭐为什么引入流水?
消息丢失问题 重复消费问题 最终一致性解决方案