谷粒商城-个人笔记(高级篇四)
p247—p249是概念,不建议回看视频,宁愿你温习一下以往的笔记 RabbitMQ—基础部分 和 RabbitMQ—高级部分
p252—p254是在192.168.56.106:15672官网上演示,也就是手动演示,没有涉及到代码
讲了SpringBoot整合RabbitMQ的具体过程
p256是用AmqpAdmin
绑定队列和交换机。
之前讲了用AmqpAdmin
绑定队列和交换机,p257就是用RabbitTemplate
发送消息,
发消息就一行代码rabbitTemplate.convertAndSend(xxx,xxxx,xxx);
之前讲了用AmqpAdmin
绑定队列和交换机,用RabbitTemplate
发送消息,那么现在就是用RabbitListener
&RabbitHandler
来接收消息。
监听消息:使用@RabbitListener
注解; 主启动类必须有@EnableRabbit
@RabbitListener: 标在类 或 方法上(监听哪些队列) @RabbitHandler: 标在方法上(可以区分队列里的不同的消息)
@RabbitListener(queues = {“queue1”,“queue2”}),可以监听多个队列
1.编写发送消息
@Slf4j
@SpringBootTest
class GulimallOrderApplicationTests {
@Autowired
AmqpAdmin amqpAdmin;
@Autowired
RabbitTemplate rabbitTemplate;
/**
* 让某个类发送消息
*/
@Test
public void sendMessageTest(){
OrderReturnApplyEntity orderReturnApplyEntity = new OrderReturnApplyEntity();
orderReturnApplyEntity.setId(1L);
orderReturnApplyEntity.setCreateTime(new Date());
orderReturnApplyEntity.setReturnName("哈哈哈");
rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",orderReturnApplyEntity);
log.info("消息发送完成{}",orderReturnApplyEntity);
}
}
2.编写接收消息
3.测试结果
/**
* 接收消息的receiverMessage()方法的参数可以写以下内容
* 1、Message message:这个message包括消息头+消息体
* 2、OrderReturnApplyEntity content:发送的消息类型 (可选参数)
* 3、Channel channel 当前传输数据的通道(可选参数)
*/
@RabbitListener(queues = {
"hello-java-queue"})
public void receiverMessage(Message message,OrderReturnApplyEntity content,
Channel channel) throws InterruptedException {
byte[] body = message.getBody();//从message中获取消息体
MessageProperties properties = message.getMessageProperties(); //从message中消息头属性信息
System.out.println("接收到消息...内容:" + content);
System.out.println("消息处理完成=》"+content.getReturnName());
}
1.编写发送消息
如果是偶数就发送OrderReturnApplyEntity类型的数据,如果是基数就发送OrderEntity类型的数据
@RestController
public class RabbitController {
@Autowired
RabbitTemplate rabbitTemplate;
@GetMapping("/sendMq")
public String sendMq(@RequestParam(value = "num",defaultValue = "10") Integer num){
for (int i = 0; i < num; i++){
if (i%2==0){
OrderReturnApplyEntity orderReturnApplyEntity = new OrderReturnApplyEntity();
orderReturnApplyEntity.setId(1L);
orderReturnApplyEntity.setCreateTime(new Date());
orderReturnApplyEntity.setReturnName("哈哈哈");
rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",orderReturnApplyEntity, new CorrelationData(UUID.randomUUID().toString()));
}else {
OrderEntity entity = new OrderEntity();
entity.setOrderSn(UUID.randomUUID().toString());
rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",entity, new CorrelationData(UUID.randomUUID().toString()));
}
}
return "OK";
}
}
2.编写接收消息
3.测试
本节内容一开始,老师启动的服务端口被占用
1.在yml中配置
#开启发送端确认
spring.rabbitmq.publisher-confirms=true
2.添加配置类“com.atguigu.gulimall.order.config.MyRabbitConfig”,代码如下:
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
@PostConstruct
public void initRabbitTemplate(){
//设置消息抵达服务器的确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* 只要消息抵达Broker就b = true
* @param correlationData 当前消息的唯一关联数据(这个消息的唯一id)
* @param b 消息是否成功收到
* @param s 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
System.out.println("confirm...correlationData["+correlationData+"]==>b["+b+"]s==>["+s+"]");
}
});
}
}
1.修改application.properties
#开启发送端确认
spring.rabbitmq.publisher-confirms=true
#开启发送端抵达队列确认
spring.rabbitmq.publisher-returns=true
#只要抵达队列,以异步发送优先回调我们这个returnConfirm
spring.rabbitmq.template.mandatory=true
2.修改配置类“com.atguigu.gulimall.order.config.MyRabbitConfig”,代码如下:
@Configuration
public class MyRabbitConfig {
@Autowired
RabbitTemplate rabbitTemplate;
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
@PostConstruct
public void initRabbitTemplate(){
//设置消息抵达服务器的确认回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* 只要消息抵达Broker就b = true
* @param correlationData 当前消息的唯一关联数据(这个消息的唯一id)
* @param b 消息是否成功收到
* @param s 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
System.out.println("confirm...correlationData["+correlationData+"]==>b["+b+"]s==>["+s+"]");
}
});
//设置消息抵达队列的确认回调
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
/**
* 只要消息没有投递给指定的队列,就触发这个失败回调
* @param message 投递失败的消息详细信息
* @param i 回复的状态码
* @param s 回复的文本内容
* @param s1 当时这个消息发给哪个交换机
* @param s2 当时这个消息用哪个路由键
*/
@Override
public void returnedMessage(Message message, int i, String s, String s1, String s2) {
System.out.println("Fail Message["+message+"]==>i["+i+"]==>s["+s+"]==>s1["+s1+"]==>s2["+s2+"]");
}
});
}
}
1、服务器收到消息就回调
1、yml配置文件中:
spring.rabbitmq.publisher-confirms=true
2、配置类中:
设置确认回调ConfirmCallback
2、消息抵达队列就回调
1、yml配置文件中:
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.template.mandatory=true
2、配置类中:
设置确认回调ReturnCallback
3、消费端确认(保证每个消息被正确消费,此时才可以保证broker删除这个消息)
1、默认是自动确认的,只要消息接收到,客户端会自动确认,服务端就会移除这个消息
问题:
我们收到很多消息,自动回复给服务器ack,发送了5条消息即使服务端只处理了一条,此时宕机了,就会发生消息丢失。
2、手动确认模式。只要我们没有明确告诉MQ,货物被签收,没有ACK,消息就一直unacked状态,
即使Consumer宕机。消息不会丢失,会重新变为Ready,下一次有新的Consumer连接进来就发给他
2、消费端的手动确认
1)、yml配置:
spring.rabbitmq.listener.simple.acknowledge-mode=manual
2)、手动确认:
channel.basicAck(deliveryTag,false);签收;业务成功完成就应该签收
channel.basicNack(deliveryTag,false,true);拒签:业务失败,拒签
1.添加application.properties
#手动确认收货(ack)
spring.rabbitmq.listener.simple.acknowledge-mode=manual
2.编写接收消息
@RabbitListener(queues = {
"hello-java-queue"})
public class ListenerTest {
@RabbitHandler
public void receiverMessage(Message message,OrderReturnApplyEntity content,
Channel channel) throws InterruptedException {
//channel内按顺序自增的
long deliveryTag = message.getMessageProperties().getDeliveryTag();
//签收货物,非批量模式
try{
if (deliveryTag % 2 == 0){
//收货
channel.basicAck(deliveryTag,false);
System.out.println("签收了货物。。。"+deliveryTag);
}else {
//退货requeue=false 丢弃 requeue=true发挥服务器,服务器重新入队。
channel.basicNack(deliveryTag,false,true);
System.out.println("没有签收货物..."+deliveryTag);
}
}catch (Exception e){
//网络中断
}
}
要支付,先登录,登录要用SpringSession
注意我们的登陆拦截器还有返回值的,返回了用户信息的,下面要用到。
总说:
前端发来请求访问http://order.gulimall.com/toTrade
,后台要带着必要的信息重定向到订单确认页。需要哪些信息呢?
List address
,需要使用OpenFeign远程查询gulimall-member,gulimall-member查询数据库ums_member_receive_address表即可List items
,需要使用OpenFeign远程查询gulimall-cart,在gulimall-cart里面当初我们把登录信息存到session中、把购物车信息存到redis中,所以根据登录拦截器返回的用户信息,只需要拿着用户信息查询redis中该用户的购物车中选中去结账的
商品,然后给这些商品结账时还要查询这些商品最新的价格(你添加商品到购物车一个月现在才结账,谁知道商品价格变了没),查询商品价格需要使用OpenFeign远程查询gulimall-product的pms_sku_info表即可。Integer integration;
,我们登录拦截器返回来的用户信息里面就包含用户积分信息BigDecimal total;
,(商品数量x价格)=订单总额BigDecimal payPrice;
,先不说String orderToken;
,你提交订单页面如果刷新多次就会造成提交多次订单,所以需要防重令牌。问题
p265的代码和思路没有问题,但是测试的时候发现gulimall-order使用OpenFeign远程查询gulimall-cart的购物车信息时,经过gulimall-cart的登录拦截器时居然是没有登陆状态。你明明已经在gulimall-order中登录了,为什么会被远程调用的gulimall-cart的登录拦截器拦截下来?
解释
因为浏览器先发送http://order.gulimall.com/toTrade
给gulimall-order时携带了请求头(有请求头就有cookie,有cookie就能查到session),但是使用OpenFeign远程查询gulimall-cart的购物车信息时会新创建一个请求头,丢弃原来的请求头。
解决办法
使用OpenFeign远程查询gulimall-cart的购物车信息时会新创建一个请求头,我们写一个feign的拦截器,在拦截器里面为请求加上cookie。
1.原来的代码
@Override
public OrderConfirmVo confirmOrder() {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
//1、远程查询所有的收货地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
confirmVo.setAddress(address);
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
//3、查询用户积分
Integer integration = memberResponseVO.getIntegration();
confirmVo.setIntegration(integration);
//4、其他数据自动计算
return confirmVo;
}
2.把原来的代码改为异步的方式
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
//异步任务编排
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//1、远程查询所有的收货地址列表
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
confirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//2、远程查询购物车所有选中的购物项
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor);
//3、查询用户积分
Integer integration = memberResponseVO.getIntegration();
confirmVo.setIntegration(integration);
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return confirmVo;
}
3.存在的问题
4.解决
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
MemberResponseVO memberResponseVO = LoginUserInterceptor.loginUser.get();
//获取之前的请求
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);//每一个线程都来共享之前的请求数据
List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVO.getId());
confirmVo.setAddress(address);
}, executor);
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
RequestContextHolder.setRequestAttributes(requestAttributes);//每一个线程都来共享之前的请求数据
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor);
Integer integration = memberResponseVO.getIntegration();
confirmVo.setIntegration(integration);
CompletableFuture.allOf(getAddressFuture,cartFuture).get();
return confirmVo;
}
这一节仅仅4分钟,解决了上节课留下的报错(远程调用要用R对象封装),笔记上没有
总说:
前台给gulimall-ware发过来/fare
请求(http://gulimall.com/api/ware/wareinfo/fare
),携带的参数是addrId(收货地址id),后台使用OpenFeign远程调用gulimall-member根据addrId(收货地址id)查询到addrInfo(收货地址信息)封装到MemberAddressVo里面返回,然后我们根据MemberAddressVo里面的电话的最后一位模拟计算运费
学习视频:接口幂等性讨论
何为幂等性?同一份订单提交一次和提交100次结果是一样的,用户不能因为网络不好提交了多词就下单多次。
1.什么是幂等性
接口幂等性就是用户对同一操作发起的一次请求和多次请求结果是一致的,不会因为多次点击而产生了副作用。比如支付场景,用户购买了商品,支付扣款成功,但是返回结果的时候出现了网络异常,此时钱已经扣了,用户再次点击按钮,此时就会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。。。这就没有保证接口幂等性
2.哪些情况需要考虑幂等性?
3.什么情况不用考虑幂等性?
4.幂等性的解决方法
每访问http://order.gulimall.com/toTrade
后台就会随机生成一个uuid(模拟token),然后存到redis中,也存到OrderConfirmVo中交给前端页面,在这个订单确认页面用户填好收货地址、支付方式以后,点击提交订单就把token带过去,后台比较前端带来的token和redis中的是否一致。假如用户点击多次“提交订单”,他每次带来的token都一样(因为页面并没有刷新),只有第一次能和redis匹配成功,其他的几次就是失败的,就能保证幂等性。
令牌机制的危险性:
前端发来“提交订单”的请求,我们要先根据用户id从redis中拿到token,然后比对前端的token和redis中的token,然后删掉redis中的令牌。
用户手速很快连点几次“提交订单”,第一次请求来了,后台根据用户id从redis中拿到token,和前台传进来的token进行比对,比对完了,还没有来得及删掉令牌,结果第二次带着token进来了又和redis匹配成功了,此时即使你删掉令牌也已经有两个业务进来了。
所以:从redis中拿token、比对token、删除token,一定要是原子性操做。
给订单确认页添加防重令牌token,然后建立提交订单页的初步逻辑
原子验证令牌
讲了builderOrder()方法
讲了builderOrderItem()方法
讲了createOrder() 和 computePrice()方法
saveOrder()方法、锁库存
锁库存
这一节内容你看IDEA中的代码可以;看笔记也可以,一定要注意人家笔记里面的序号,我觉得非常地条理
在购物车页选择要结算的商品,然后来到了订单确认页;
在订单确认页选择收货人地址信息、支付方式、显示商品、显示运费等等,然后点击“提交订单”后,就携带着数据来到了提交订单页;
提交订单页就是要锁定库存进行提交订单了,确认提交订单就会来到支付页;
支付页进行支付。
submitOrder()
的解说最复杂的莫过于OrderServiceImpl里的submitOrder()
方法,因为它调用了八大方法
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo)
干了什么:
第一步,比较前台传过来的令牌和redis中的令牌是否一致,如果一致就继续往下执行
第二步,创建订单(创建订单时会再次从购物车里面查询用户下单的商品再次计算总价),和前端传来的价格进行比对,如果一致就继续往下执行,用到了下面的①②③④⑤个方法
第三步,保存订单,用到了下面的方法⑥
第四步,锁定库存,用到了下面的方法⑦⑧
submitOrder()方法中调用的八大方法
①private OrderEntity builderOrder(String orderSn)
给我orderSn,返回OrderEntity。而OrderEntity里面有相当多的属性:
订单号
(参数里有)会员id
和会员名
(从LoginUserInterceptor中获取到)运费
还有 收货人姓名、电话、省份、地区、街道等等
,这些属性FareVo里面就有,如何得到FareVo?订单状态、确认时长、确认状态
(这些属性无需查询,现场设置)builderOrder()方法很明显,就你一个订单号,你要把用户信息、地址信息、订单状态信息全部生成到OrderEntity里返回
②private OrderItemEntity builderOrderItem(OrderItemVo items)
传来OrderItemVo返回OrderItemEntity,参数OrderItemVo就是购物车里面的购物项,OrderItemEntity里面有超多属性:
builderOrderItem()这个方法很明显就是给你一个具体商品,你后台查询它的信息还要计算它的价格,封装到OrderItemEntity里返回
③public List
,传来订单号,返回List
cartFeignService.getCurrentCartItems()
远程获取当前用户的购物车中选中要结账的那些商品;builderOrderItem(OrderItemVo item)
处理(builderOrderItem()这个方法就是给你一个具体商品,你后台查询它的信息还要计算它的价格,封装到OrderItemEntity里返回)builderOrderItems()这个方法很明显,就是给你订单号,然后你压根不用这个订单号,你会另辟蹊径拿到当前用户购物车里面选中要结账的商品以及它们的价格信息,封装到
List< OrderItemEntity>返回,为什么这么做?因为你想要比对价格,你要再从购物车里面拿一次商品和价格来比对
④private void computePrice(OrderEntity orderEntity, List
它会遍历List
里面的每一个OrderItemEntity,把每一个商品涉及到的优惠价(PromotionAmount、CouponAmount、IntegrationAmount)叠加,把每一个商品涉及到的(积分、成长值)叠加,叠加后的值就是这个订单的总优惠价、总积分、总成长值
然后把总优惠价、总积分、总成长值、总价格、应付价格设置到OrderEntity
里面
⑤private OrderCreateTo createOrder()
,没有参数,返回OrderCreateTo,具体流程 :
会先调用builderOrder(orderSn)方法生成订单信息OrderEntity
,
然后调用builderOrderItems(orderSn)拿到所有结账商品的信息List
,
然后计算价格computePrice(OrderEntity,List
,
然后把OrderEntity
与List
封装到OrderCreateTo里面返回
⑥private void saveOrder(OrderCreateTo orderCreateTo)
传进来OrderCreateTo,无返回值:
OrderCreateTo里面有OrderEntity
与List
OrderEntity
里面的信息存到oms_order表中
OrderItemEntity
里面的信息存到oms_order_item表中
⑦WareSkuLockVo是锁定库存的vo,里面的属性可以从哪里获取呢?
OrderCreateTo里面有OrderEntity
与List
,
WareSkuLockVo里的订单号orderSn可以从OrderEntity
里面获取
WareSkuLockVo里的OrderItemVo中的(skuid、title、count)可以从OrderItemEntity
里面获取
⑧public boolean orderLockStock(WareSkuLockVo vo)
传过来WareSkuLockVo,返回boolean表示锁定库存成功了没有:
第一步,根据WareSkuLockVo里封装的OrderItemVo中的skuid,执行listWareIdHasSkuStock(skuId)
,查询仓库里有这件商品而且这件商品库存数量大于0的那些仓库,把这些仓库id封装到List里面
第二步,把(List
)还有(skuid
)还有(用户要购买的数量num
)封装到SkuWareHasStock实体类里。(SkuWareHasStock实体类里面只有三个值:skuId、num、wareId,表示在某个仓库里面要锁定某个商品多少个)
第三步,遍历List
,执行lockSkuStock(skuId,wareId,num)
,之前得到了仓库里有这件商品而且库存大于0的那些仓库,但是库存大于0假如库存是1,而用户买10件,你还是不行嘛,lockSkuStock(skuId,wareId,num)
就是找到有库存而且商品数量足够支撑用户购买的数量;一旦找到一个符合条件的仓库就置为true,没有就继续遍历其它仓库。
注意,锁库存里面库存没锁住就抛异常,因为我们加了@Transactional事务注解,所以一旦抛了异常就立刻回滚,整个代码跟没执行一样。
p283—p299不看别人的笔记,只看自己的
p275—p282只是“提交订单页”的初步代码,因为该服务调用了很多其他微服务,其中一个服务失败会自己回滚事务但如何保证其它服务也回滚事务呢?
p283—p289是使用分布式事务解决这个问题,一开始引入了seata,但发现seata可以解决事务但解决不了高并发的难题,
所以p290—p299就是使用“可靠消息+最终一致性方案”来解决这个问题(RabbitMQ+事务来解决)。
①public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo)
干了什么:
第一步,比较前台传过来的令牌和redis中的令牌是否一致,如果一致就继续往下执行
第二步,创建订单(创建订单时会再次从购物车里面查询用户下单的商品再次计算总价),和前端传来的价格进行比对,如果一致就继续往下执行
第三步,保存订单
第四步,锁定库存,远程调用gulimall-ware锁库存,锁库存是在gulimall-ware里面执行的,但它会返回状态码,根据状态码就可以找到远程锁库存成功了没有
第五步,远程扣减积分(这个功能还没有实现,但我们假装它已经实现了)
②存在的分布式事务问题:
③我们用到的@Transactional都是本地事务,本地事务只能控制住自己的回滚,控制不了其它服务的回滚,在分布式事务下不使用。
概念的讲解,讲了事务的基本性质
、事务的隔离级别
、事务的传播行为
、在springboot中本地事务时效问题
原子性:下订单、减库存、扣积分这三个要么同时成功要么同时失败,这就是原子性。原子性就是一系列操做不可拆分,同成同败。
一致性:A给B转200,转完帐必须保证A和B的总额没有变,不能一方扣减另一方没有增加
隔离性:事物之间互相隔离,100个人同时下单,一个人下单失败,不能影响其他人。
持久性:一旦事务提交,那么它对数据库中的对应数据的状态的变更就会永久保存到数据库中。–即使发生系统崩溃或机器宕机等故障,只要数据库能够重新启动,那么一定能够将其恢复到事务成功结束的状态
事务的四大特征:
1. 原子性(atomicity):一个事务要么全部提交成功,要么全部失败回滚,不能只执行其中的一部分操作,这就是事务的原子性。
2. 一致性(consistency):事务操作前后,数据总量不变。如果数据库系统在运行过程中发生故障,有些事务尚未完成就被迫中断,这些未完成的事务对数据库所作的修改有一部分已写入物理数据库,这是数据库就处于一种不正确的状态,也就是不一致的状态
3. 隔离性(isolation):多个事务之间相互独立。对应着事务的四种隔离级别
4. 持久性(durability):一旦事务提交,那么它对数据库中的对应数据的状态的变更就会永久保存到数据库中。
* 概念:多个事务之间隔离的,相互独立的。但是如果多个事务操作同一批数据,则会引发一些问题,设置不同的隔离级别就可以解决这些问题。
* 存在问题:
1. 脏读:一个事务,读取到另一个事务中没有提交的数据
2. 不可重复读(虚读):在同一个事务中,两次读取到的数据不一样。
3. 幻读:一个事务操作(DML)数据表中所有记录,另一个事务添加了一条数据,则第一个事务查询不到自己的修改。
* 隔离级别:
1. read uncommitted:读未提交
* 产生的问题:脏读、不可重复读、幻读
2. read committed:读已提交 (Oracle)
* 产生的问题:不可重复读、幻读
3. repeatable read:可重复读 (MySQL默认)
* 产生的问题:幻读
4. serializable:串行化
* 可以解决所有的问题
* 注意:隔离级别从小到大安全性越来越高,但是效率越来越低
① a、b、c三个方法都标注了@Transactional说明它们三个都是事务,b事务隔离级别是REQUIRED那么它共用a的事务,c事务隔离级别是REQUIRES_NEW那么它自己用自己的事务,现在a方法调用b、c方法且执行时出现除0异常,那么a和b会回滚,c不会回滚。
@Transactional
public void a(){
b();
c();
int i = 10/0;
}
@Transactional(propagation=Propagation.REQUIRED)
public void b(){
}
@Transactional(propagation=Propagation.REQUIRES_NEW)
public void c(){
}
② a事务设置过期时间是30s,b事务设置过期时间是3s,由于b共用a的事务,所以b的实际过期时间也是30s
springboot中使用事务存在的坑:
查看 视频 的10:42以后
讲了CAP的概念
、Raft的领导选举、日志复制
概念这种东西,看视频不比看文字舒服?不必看文字理解的更深入?,所以回看视频即可
强一致
、弱一致
、最终一致
的概念
2PC模式
TCC事务补偿方案
最大努力通知型方案(重点)
可靠消息+最终一致性方案(重点)
p283—p299不看别人的笔记,只看自己的
1.相关概念的解释
2.环境准备
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
添加“com.atguigu.gulimall.order.config.MySeataConfig”类,代码如下:
@Configuration
public class MySeataConfig {
@Autowired
DataSourceProperties dataSourceProperties;
@Bean
public DataSource dataSource(DataSourceProperties dataSourceProperties){
//得到数据源
HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())){
dataSource.setPoolName(dataSourceProperties.getName());
}
return new DataSourceProxy(dataSource);
}
}
修改“com.atguigu.gulimall.ware.config.WareMybatisConfig”类,代码如下:
@Bean
public DataSource dataSource(DataSourceProperties dataSourceProperties){
//得到数据源
HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
if (StringUtils.hasText(dataSourceProperties.getName())){
dataSource.setPoolName(dataSourceProperties.getName());
}
return new DataSourceProxy(dataSource);
}
分别给gulimall-order和gulimall-ware加上file.conf和registry.conf这两个配置,并修改file.conf
给分布式大事务的路口标注@GlobalTransactional; 每一个远程的小事务用 @Transactional
OK了,这样下面的这两个分布式事务问题就解决了。
但是,我们这个场景还是不适合使用seata,我们下单是典型的高并发模式,seata中后台使用了很多锁,导致高并发串行化(一个一个执行),高并发不使用2PC
和TCC模式
,我们使用的就是最大努力通知型方案
或可靠消息+最终一致性方案
,接下来我们使用了可靠消息+最终一致性方案
p275—p282只是“提交订单页”的初步代码,因为该服务调用了很多其他微服务,其中一个服务失败会自己回滚事务但如何保证其它服务也回滚事务呢?
p283—p289是使用分布式事务解决这个问题,一开始引入了seata,但发现seata可以解决事务但解决不了高并发的难题,
所以p290—p299就是使用“可靠消息+最终一致性方案”来解决这个问题(RabbitMQ+事务来解决)。
p290就是比对了2PC
和TCC模式
,还有最大努力通知型方案
或可靠消息+最终一致性方案
,最后得出我们这个“订单确认页”要使用可靠消息+最终一致性方案
,所以就开启了下一节RabbitMQ的讲解
这一节是TTL、死信队列、延时队列的概念讲解,建议回看RabbitMQ—高级部分
p283—p299不看别人的笔记,只看自己的
本节课给gulimall-order服务添加RabbitMQ
根据上幅图写出来下面的代码
@Configuration
public class MyRabbitmqConfig {
@Bean
public Exchange orderEventExchange() {
return new TopicExchange("order-event-exchange", // name
true,// durable
false); // autoDelete
// 还有一个参数是Map arguments
}
/**
* 延迟队列
*/
@Bean
public Queue orderDelayQueue() {
HashMap<String, Object> arguments = new HashMap<>();
//死信交换机
arguments.put("x-dead-letter-exchange", "order-event-exchange");
//死信路由键
arguments.put("x-dead-letter-routing-key", "order.release.order");
arguments.put("x-message-ttl", 60000); // 消息过期时间 1分钟
return new Queue("order.delay.queue", // 队列名字
true, //是否持久化
false, // 是否排他
false, // 是否自动删除
arguments); // 属性map
}
/**
* 普通队列
*/
@Bean
public Queue orderReleaseQueue() {
Queue queue = new Queue("order.release.order.queue", true, false, false);
return queue;
}
/**
* 创建订单的binding
*/
@Bean
public Binding orderCreateBinding() {
return new Binding("order.delay.queue", // 目的地(队列名或者交换机名字)
Binding.DestinationType.QUEUE, // 目的地类型(Queue、Exhcange)
"order-event-exchange", // 交换器
"order.create.order", // 路由key
null); // 参数map
}
@Bean
public Binding orderReleaseBinding() {
return new Binding("order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
@Bean
public Binding orderReleaseOrderBinding() {
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.other.#",
null);
}
}
导入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
yml配置:
# RabbitMQ配置
spring.rabbitmq.host=192.168.56.106
spring.rabbitmq.port=5672
# 虚拟主机配置
spring.rabbitmq.virtual-host=/
# 手动ack消息,不使用默认的消费端确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual
主启动类添加注解
@EnableRabbit
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallWareApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallWareApplication.class, args);
}
}
写一个监听方:
@RabbitListener(queues="order.release.order.queue")
public void Listener(OrderEnitity entity,Channel channel,Message message){
System.out.println("收到过期的订单信息,准备关闭订单:"+entity.getOrderSn());
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);//手动确认
}
写一个Controller:
@ResponseBody
@GetMapping("/test/createOrder")
public String createOrderTest(){
//订单下单成功
OrderEntity orderEntity = new OrderEntity();
orderEntity.setOrderSn(UUID.randomUUID().toString());
orderEntity.setModifyTime(new Date());
//给MQ发送消息
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",orderEntity);
return "ok";
}
结果:
本节课给gulimall-ware服务添加RabbitMQ
根据上幅图写出了下面的代码:
@Configuration
public class MyRabbitMQConfig {
//使用JSON序列化机制,进行消息转换
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
//库存服务默认的交换机
@Bean
public Exchange stockEventExchange() {
//参数:String name, boolean durable, boolean autoDelete, Map arguments
TopicExchange topicExchange = new TopicExchange("stock-event-exchange", true, false);
return topicExchange;
}
//普通队列
@Bean
public Queue stockReleaseStockQueue() {
//参数:String name, boolean durable, boolean exclusive, boolean autoDelete, Map arguments
Queue queue = new Queue("stock.release.stock.queue", true, false, false);
return queue;
}
//延迟队列
@Bean
public Queue stockDelay() {
HashMap<String, Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange", "stock-event-exchange");
arguments.put("x-dead-letter-routing-key", "stock.release");
// 消息过期时间 2分钟
arguments.put("x-message-ttl", 120000);
Queue queue = new Queue("stock.delay.queue", true, false, false,arguments);
return queue;
}
//交换机与普通队列绑定
@Bean
public Binding stockLocked() {
//参数:String destination, DestinationType destinationType, String exchange, String routingKey,Map arguments
Binding binding = new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
null);
return binding;
}
//交换机与延迟队列绑定
@Bean
public Binding stockLockedBinding() {
return new Binding("stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
null);
}
}
导入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
添加yml配置
# RabbitMQ配置
spring.rabbitmq.host=192.168.56.106
spring.rabbitmq.port=5672
# 虚拟主机配置
spring.rabbitmq.virtual-host=/
# 手动ack消息,不使用默认的消费端确认
spring.rabbitmq.listener.simple.acknowledge-mode=manual
主启动类添加注解
@EnableRabbit
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallWareApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallWareApplication.class, args);
}
}
1.创建三个实体类
"wms_ware_order_task"这张表就是用来记录哪一天给哪个订单执行了锁库存操做。
"wms_ware_order_task_detail"表就是记录哪个订单中的哪个商品在哪个仓库中锁定了多少库存。
这两张表是干什么的?我们要根据订单号查询到"wms_ware_order_task"表的task_id
,然后拿着task_id
去查"wms_ware_order_task_detail"表就可以得到这个订单下所有商品的库存锁定详情
。
①添加“com.xunqi.gulimall.ware.entity.WareOrderTaskEntity”,这个实体类和数据库的"wms_ware_order_task"表绑定,
这个表字段很多,但是只需要关注两个字段——“orderSn”和“createTime”,哪一天给哪个订单执行了锁库存操做.
这张表就是用来记录哪一天给哪个订单执行了锁库存操做。
@Data
@TableName("wms_ware_order_task")
public class WareOrderTaskEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* id
*/
@TableId
private Long id;
/**
* order_id
*/
private Long orderId;
/**
* order_sn
*/
private String orderSn;
/**
* 收货人
*/
private String consignee;
/**
* 收货人电话
*/
private String consigneeTel;
/**
* 配送地址
*/
private String deliveryAddress;
/**
* 订单备注
*/
private String orderComment;
/**
* 付款方式【 1:在线付款 2:货到付款】
*/
private Integer paymentWay;
/**
* 任务状态
*/
private Integer taskStatus;
/**
* 订单描述
*/
private String orderBody;
/**
* 物流单号
*/
private String trackingNo;
/**
* create_time
*/
private Date createTime;
/**
* 仓库id
*/
private Long wareId;
/**
* 工作单备注
*/
private String taskComment;
}
②添加“com.atguigu.gulimall.ware.entity.WareOrderTaskDetailEntity”类,这个类和数据库的"wms_ware_order_task_detail"表绑定。
这个表就是记录哪个订单中的哪个商品在哪个仓库中锁定了多少库存
@AllArgsConstructor
@NoArgsConstructor
@Data
@TableName("wms_ware_order_task_detail")
public class WareOrderTaskDetailEntity implements Serializable {
/**
* id
*/
@TableId
private Long id;
/**
* sku_id
*/
private Long skuId;
/**
* sku_name
*/
private String skuName;
/**
* 购买个数
*/
private Integer skuNum;
/**
* 工作单id
*/
private Long taskId;
/**
* 仓库id
*/
private long wareId;
/**
* 锁定状态
*/
private Integer lockStatus;//(1表示锁定了,2表示解锁了,3表示锁定的库存已经扣减了)
}
添加StockLockedTo实体类,这个实体类相当于上面"wms_ware_order_task"表和"wms_ware_order_task_detail"表的结合
2.锁定库存的方法
修改“com.atguigu.gulimall.ware.service.impl.WareSkuServiceImpl”类,
代码的逻辑是:
先说明“什么是锁定库存"?——假如顾客购买的商品之一是10件马甲,如果遍历了所有拥有马甲的仓库都没有找到一个剩余马甲数量大于10的仓库,那么这就是锁定库存失败;如果找到一个剩余马甲数量大于10的仓库,那么这就是锁定库存成功。
①submitOrder()方法里面锁定库存代码执行成功后结果下面的代码执行失败,submitOrder()方法就会回滚,锁定库存已经执行了没办法回滚,所以我们需要库存工作单表"wms_ware_order_task"
记录哪一天给哪个订单执行了锁库存操做,从而做到让库存回滚。
②由于一个订单下有多个商品,我们给当前订单下的所有商品一个一个遍历进行锁定库存操做,当前商品锁定成功,就立刻做两件事:①给表"wms_ware_order_task_detail"
保存工作单详情(记录哪个订单中的哪个商品在哪个仓库中锁定了多少库存)②将当前商品锁定了几件的工作单记录发送给MQ;
③某一件商品的锁定库存失败,那么抛出库存不足的异常,有异常代码就回滚,前面所有商品在数据库的"wms_ware_order_task"和"wms_ware_order_task_detail"这两张表中插入的数据就都被回滚了,但问题是前面商品的MQ消息发出去了,没关系,让MQ到数据库中用id查就可以,由于回滚了肯定查不到相关记录,只要查不到id就不用解锁
代码如下:
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo wareSkuLockVo) {
//***********************************************************************************
//记录哪一天给哪个订单执行了锁库存操做
WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
taskEntity.setOrderSn(wareSkuLockVo.getOrderSn());
taskEntity.setCreateTime(new Date());
wareOrderTaskService.save(taskEntity);
//***********************************************************************************
List<OrderItemVo> itemVos = wareSkuLockVo.getLocks();
List<SkuLockVo> lockVos = itemVos.stream().map((item) -> {
SkuLockVo skuLockVo = new SkuLockVo();
skuLockVo.setSkuId(item.getSkuId());
skuLockVo.setNum(item.getCount());
List<Long> wareIds = baseMapper.listWareIdsHasStock(item.getSkuId(), item.getCount());
skuLockVo.setWareIds(wareIds);
return skuLockVo;
}).collect(Collectors.toList());
for (SkuLockVo lockVo : lockVos) {
boolean lock = true;
Long skuId = lockVo.getSkuId();
List<Long> wareIds = lockVo.getWareIds();
if (wareIds == null || wareIds.size() == 0) {
throw new NoStockException(skuId);
}else {
for (Long wareId : wareIds) {
Long count=baseMapper.lockWareSku(skuId, lockVo.getNum(), wareId);
if (count==0){
lock=false;//库存锁定失败
}else {
//***********************************************************************************
//1.锁定成功,保存工作单详情(记录哪个订单中的哪个商品在哪个仓库中锁定了多少库存)
WareOrderTaskDetailEntity detailEntity = WareOrderTaskDetailEntity.builder()
.skuId(skuId)
.skuName("")
.skuNum(lockVo.getNum())
.taskId(taskEntity.getId())
.wareId(wareId)
.lockStatus(1).build();
wareOrderTaskDetailService.save(detailEntity);
//2.一旦锁库存成功,就发送库存锁定消息至延迟队列
StockLockedTo lockedTo = new StockLockedTo();
lockedTo.setId(taskEntity.getId());
StockDetailTo detailTo = new StockDetailTo();
BeanUtils.copyProperties(detailEntity,detailTo);
lockedTo.setDetailTo(detailTo);
rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);//发送消息给gulimall-ware的RabbitMQ
lock = true;
break;
//***********************************************************************************
}
}
}
if (!lock) throw new NoStockException(skuId);
}
return true;
}
3.测试一下
p283—p299不看别人的笔记,只看自己的
gulimall-ware的Lintener包下写一个监听器
@Component
@RabbitListener(queues = {
"stock.release.stock.queue"})
public class StockReleaseListener {
@Autowired
private WareSkuService wareSkuService;
@RabbitHandler
public void handleStockLockedRelease(StockLockedTo stockLockedTo, Message message, Channel channel) throws IOException {
log.info("************************收到库存解锁的消息********************************");
try {
wareSkuService.unlock(stockLockedTo); //调用解锁库存方法,后面会讲
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); //签收消息
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true); //解锁库存失败,拒收消息,拒收的消息重新放到队列里面
}
}
}
1.库存解锁的场景:
1)、下订单成功,库存锁定成功,submitOrder()整个方法都执行无误,但由于订单过期没有支付被系统自动取消、或被用户手动取消。需要解锁库存。
2)、库存锁定成功,但submitOrder()方法中锁库存后面的代码执行失败,导致submitOrder()方法回滚。之前锁定的库存就要解锁。我们可以根据"wms_ware_order_task"和"wms_ware_order_task_detail"这两张表中记录进行解锁。
3)、库存锁定失败,导致ubmitOrder()方法回滚。库存锁定失败,那么锁定库存的代码会回滚,那么"wms_ware_order_task"和"wms_ware_order_task_detail"这两张表中插入的数据就都被回滚了,但问题是前面商品的MQ消息发出去了,没关系,让MQ到数据库中用id查就可以,由于回滚了肯定查不到相关记录,只要查不到id就不用解锁。
2.编写unlock()代码
unlock()代码的逻辑:
为保证幂等性,我们分别对订单的状态和工作单的状态都进行了判断,只有当订单过期且工作单显示当前库存处于锁定的状态时,才进行库存的解锁
com.atguigu.gulimall.ware.service.impl
@Override
public void unlock(StockLockedTo stockLockedTo) {
StockDetailTo detailTo = stockLockedTo.getDetailTo();
WareOrderTaskDetailEntity detailEntity = wareOrderTaskDetailService.getById(detailTo.getId());
//如果工作单详情不为空,说明该库存锁定成功
if (detailEntity != null) {
WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(stockLockedTo.getId());
R r = orderFeignService.getOrderStatus(taskEntity.getOrderSn());//根据订单号远程查询订单状态(后面会将)
if (r.getCode() == 0) {
//R状态码为0说明远程查询成功
OrderTo order = r.getData("order", new TypeReference<OrderTo>() {
});
//没有这个订单||订单状态已经取消 解锁库存
if (order == null||order.getStatus()== OrderStatusEnum.CANCLED.getCode()) {
//工作单详情有三种状态(1是已锁定,2是已解锁,3是已扣减),只有工作单详情状态是1才需要给它解锁
if (detailEntity.getLockStatus()== WareTaskStatusEnum.Locked.getCode()){
//调用"解锁库存方法"(后面会讲)
unlockStock(detailTo.getSkuId(), detailTo.getSkuNum(), detailTo.getWareId(), detailEntity.getId());
}
}
}else {
throw new RuntimeException("远程调用订单服务失败");
}
}else {
//无需解锁
}
}
3.gulimall-ware远程调用gulimall-order查询订单的状态
添加“com.atguigu.gulimall.ware.feign.OrderFeignService”类,代码如下:
@FeignClient("gulimall-order")
public interface OrderFeignService {
@GetMapping("/order/order/status/{orderSn}")
R getOrderStatus(@PathVariable("orderSn") String orderSn);
}
添加“com.atguigu.gulimall.order.controller.OrderController”类,代码如下:
@GetMapping("/status/{orderSn}")
public R getOrderStatus(@PathVariable("orderSn") String orderSn){
OrderEntity orderEntity = orderService.getOrderByOrderSn(orderSn);
return R.ok().setData(orderEntity);
}
修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:
@Override
public OrderEntity getOrderByOrderSn(String orderSn) {
OrderEntity order_sn = this.getOne(new QueryWrapper<OrderEntity>().eq("order_sn", orderSn));
return order_sn;
}
4.解锁库存unLockStock()方法
//根据skuid、num、wareid、task_id来解锁库存
public void unLockStock(Long skuId,Long wareId,Integer num,Long taskDetailId) {
//库存解锁
wareSkuDao.unLockStock(skuId,wareId,num);
//更新工作单的状态
WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity();
taskDetailEntity.setId(taskDetailId);
taskDetailEntity.setLockStatus(2);//变为已解锁
wareOrderTaskDetailService.updateById(taskDetailEntity);
}
修改“com.atguigu.gulimall.ware.dao.WareSkuDao”类,代码如下:
void unlockStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("num") Integer num, @Param("taskDetailId") Long taskDetailId);
<update id="unlockStock">
update wms_ware_sku set stock_locked = stock_locked - #{
num}
where sku_id = #{
skuId} and ware_id = #{
wareId}
</update>
5.调试
测试时出现异常,先调试
由于gulimall-order添加了拦截器,只要使用该服务必须登录才行。因为gulimall-ware需要远程调用订单,但不需要登录,所以给这个路径放行
修改gulimall-order的interceptor的LoginUserInterceptor.java
@Component
public class LoginUserInterceptor implements HandlerInterceptor {
public static ThreadLocal<MemberResponseVO> loginUser = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//放行远程调用订单的url
String uri = request.getRequestURI();
boolean match = new AntPathMatcher().match("/order/order/status/**", uri);
if (match){
return true;
}
MemberResponseVO attribute = (MemberResponseVO) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
if (attribute != null){
loginUser.set(attribute);
return true;
}else {
//没登录就去登录
request.getSession().setAttribute("msg","请先进行登录");
response.sendRedirect("http://auth.gulimall.com/login.html");
return false;
}
}
}
添加“com.atguigu.gulimall.order.service.impl”类,代码如下:
@Transactional(rollbackFor = Exception.class)
// @GlobalTransactional(rollbackFor = Exception.class)
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
confirmVoThreadLocal.set(vo);//设置到ThreadLocal里面,让大家共享
SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo();
MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();
responseVo.setCode(0);
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
String orderToken = vo.getOrderToken();
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()),
orderToken);
if (result == 0L) {
//令牌验证失败
responseVo.setCode(1);
return responseVo;
} else {
//令牌验证成功
//1、创建订单、订单项等信息
OrderCreateTo order = createOrder();
//2、验证价格
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
//金额对比
//TODO 3、保存订单
saveOrder(order);
//4、库存锁定,只要有异常,回滚订单数据
//订单号、所有订单项信息(skuId,skuNum,skuName)
WareSkuLockVo lockVo = new WareSkuLockVo();
lockVo.setOrderSn(order.getOrder().getOrderSn());
//获取出要锁定的商品数据信息
List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> {
OrderItemVo orderItemVo = new OrderItemVo();
orderItemVo.setSkuId(item.getSkuId());
orderItemVo.setCount(item.getSkuQuantity());
orderItemVo.setTitle(item.getSkuName());
return orderItemVo;
}).collect(Collectors.toList());
lockVo.setLocks(orderItemVos);
R r = wmsFeignService.orderLockStock(lockVo);
if (r.getCode() == 0) {
//锁定成功
responseVo.setOrder(order.getOrder());
// int i = 10/0;
//****************************************最新代码**************************************************
//订单创建成功,发送消息给MQ
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
//****************************************最新代码**************************************************
redisTemplate.delete(CART_PREFIX+memberResponseVo.getId());//删除购物车里的数据
return responseVo;
} else {
//锁定失败
String msg = (String) r.get("msg");
throw new NoStockException(msg);
}
} else {
responseVo.setCode(2);
return responseVo;
}
}
}
创建订单的消息会进入延迟队列,最终发送至队列order.release.order.queue,因此我们对该队列进行监听,进行订单的关闭
@Service
@RabbitListener(queues = "order.release.order.queue")
public class OrderCloseListener {
@Autowired
private OrderService orderService;
@RabbitHandler
public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
try {
orderService.closeOrder(entity);//定时关单(后面会讲)
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
// 修改失败 拒绝消息 使消息重新入队
channel.basicReject(message.getMessageProperties().getDeliveryTag(), true);
}
}
}
添加“com.atguigu.gulimall.order.service.OrderService”类,代码如下:
void closeOrder(OrderEntity entity);
修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”,代码如下:
@Override
public void closeOrder(OrderEntity entity) {
//查询当前这个订单地最新状态
OrderEntity orderEntity = this.getById(entity.getId());
if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()){
//如果订单的状态是“新建”(说明用户没付款),那就执行关单
OrderEntity update = new OrderEntity();
update.setId(entity.getId());
update.setStatus(OrderStatusEnum.CANCLED.getCode());//修改订单为“已取消”
this.updateById(update);//跟新"oms_order"表
//把订单数据封装发给RabbitMQ,然后交给库存解锁服务,让解锁库存
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderEntity,orderTo);
rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
}
}
添加“com.atguigu.common.to.mq.OrderTo”类,OrderTo其实和OrderEntity是一模一样的,只是传输数据用To,OrderTo代码如下:
@Data
public class OrderTo {
private Long id;
/**
* member_id
*/
private Long memberId;
/**
* 订单号
*/
private String orderSn;
/**
* 使用的优惠券
*/
private Long couponId;
/**
* create_time
*/
private Date createTime;
/**
* 用户名
*/
private String memberUsername;
/**
* 订单总额
*/
private BigDecimal totalAmount;
/**
* 应付总额
*/
private BigDecimal payAmount;
/**
* 运费金额
*/
private BigDecimal freightAmount;
/**
* 促销优化金额(促销价、满减、阶梯价)
*/
private BigDecimal promotionAmount;
/**
* 积分抵扣金额
*/
private BigDecimal integrationAmount;
/**
* 优惠券抵扣金额
*/
private BigDecimal couponAmount;
/**
* 后台调整订单使用的折扣金额
*/
private BigDecimal discountAmount;
/**
* 支付方式【1->支付宝;2->微信;3->银联; 4->货到付款;】
*/
private Integer payType;
/**
* 订单来源[0->PC订单;1->app订单]
*/
private Integer sourceType;
/**
* 订单状态【0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单】
*/
private Integer status;
/**
* 物流公司(配送方式)
*/
private String deliveryCompany;
/**
* 物流单号
*/
private String deliverySn;
/**
* 自动确认时间(天)
*/
private Integer autoConfirmDay;
/**
* 可以获得的积分
*/
private Integer integration;
/**
* 可以获得的成长值
*/
private Integer growth;
/**
* 发票类型[0->不开发票;1->电子发票;2->纸质发票]
*/
private Integer billType;
/**
* 发票抬头
*/
private String billHeader;
/**
* 发票内容
*/
private String billContent;
/**
* 收票人电话
*/
private String billReceiverPhone;
/**
* 收票人邮箱
*/
private String billReceiverEmail;
/**
* 收货人姓名
*/
private String receiverName;
/**
* 收货人电话
*/
private String receiverPhone;
/**
* 收货人邮编
*/
private String receiverPostCode;
/**
* 省份/直辖市
*/
private String receiverProvince;
/**
* 城市
*/
private String receiverCity;
/**
* 区
*/
private String receiverRegion;
/**
* 详细地址
*/
private String receiverDetailAddress;
/**
* 订单备注
*/
private String note;
/**
* 确认收货状态[0->未确认;1->已确认]
*/
private Integer confirmStatus;
/**
* 删除状态【0->未删除;1->已删除】
*/
private Integer deleteStatus;
/**
* 下单时使用的积分
*/
private Integer useIntegration;
/**
* 支付时间
*/
private Date paymentTime;
/**
* 发货时间
*/
private Date deliveryTime;
/**
* 确认收货时间
*/
private Date receiveTime;
/**
* 评价时间
*/
private Date commentTime;
/**
* 修改时间
*/
private Date modifyTime;
}
修改“com.atguigu.gulimall.ware.listener.StockReleaseListener”类,代码如下:
@Slf4j
@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
//这是老的代码,监听来自gulimall-ware那边的MQ发来的消息
@RabbitHandler
public void handleStockLockedRelease(StockLockedTO to, Message message, Channel channel) throws IOException {
log.info("************************收到库存解锁的消息********************************");
try {
wareSkuService.unLockStock(to);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
//这是最新的代码,监听来自gulimall-order那边的MQ发来的消息
@RabbitHandler
public void handleOrderCloseRelease(OrderTo to, Message message, Channel channel) throws IOException {
log.info("************************订单关闭准备解锁库存********************************");
try {
wareSkuService.unLockStockForOrder(to);//解锁库存
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
修改“com.atguigu.gulimall.ware.service.WareSkuService”类,代码如下:
void unLockStockForOrder(OrderTo to);
修改“com.atguigu.gulimall.order.service.impl.OrderServiceImpl”类,代码如下:
@Transactional
@Override
public void unLockStockForOrder(OrderTo to) {
String orderSn = to.getOrderSn();
/**
*
* 思考:我们之前解锁库存前先看一下订单的状态,这里就不用,因为订单状态肯定是"已取消"才会发消息到这里来解锁库存的。
* 那我们需要不需要查一下库存解锁状态?需要,在解锁之前先确认lock_status=1再进行解锁(只有库存锁定状态是"已锁定"的才需要解锁)
* 你看下面代码中有一个eq("lock_status", 1),也就是对lock_status=1的进行解锁
*
*/
//根据订单号从"wms_ware_order_task"表拿到task_id
WareOrderTaskEntity task = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
Long id = task.getId();
//根据task_id查找"wms_ware_order_task_detail"表中没有解锁的库存,进行解锁
List<WareOrderTaskDetailEntity> entities = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>().eq("task_id", id).eq("lock_status", 1));
for (WareOrderTaskDetailEntity entity : entities) {
unLockStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum(),entity.getId());
//unLockStock()方法就是调用以前写的解锁库存的方法
}
}
修改“com.atguigu.gulimall.ware.service.WareOrderTaskService”类,代码如下:
WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn);
修改“com.atguigu.gulimall.ware.service.impl.WareOrderTaskServiceImpl”类,代码如下:
@Override
public WareOrderTaskEntity getOrderTaskByOrderSn(String orderSn) {
WareOrderTaskEntity orderTaskEntity = this.getOne(new QueryWrapper<WareOrderTaskEntity>().eq("order_sn", orderSn));
return orderTaskEntity;
}
“可靠消息+最终一致性”,我们必须保证消息的可靠性。
1.使用try…catch语句
2.创建存放消息的数据库
3.使用ACK机制
一定要开启手动ACK模式。消息的发送者、接收者都要开启手动ack
有机会再学
有机会再学