消息队列流程图
监听库存解锁
下单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
配置队列和交换机
@Configuration
public class MyRabbitConfig {
/**
* 使用json序列化机制,将消息转为json格式
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
/**
* 模拟消费者监听队列
*/
// @RabbitListener(queues = "stock.release.stock.queue")
// public void listener(Channel channel, Message message){
//
// }
//声明交换机
@Bean
public Exchange stockEventExchange(){
return new TopicExchange("stock-event-exchange",true,false,null);
}
//声明队列
@Bean
public Queue stockDelayQueue(){
Map<String,Object> arguments = new HashMap<>();
arguments.put("x-dead-letter-exchange","stock-event-exchange");
arguments.put("x-dead-letter-routing-key","stock.release");
arguments.put("x-message-ttl",120000);
Queue queue = new Queue("stock.delay.queue",true,false,false,arguments);
return queue;
}
@Bean
public Queue stockReleaseStockQueue(){
Queue queue = new Queue("stock.release.stock.queue",true,false,false,null);
return queue;
}
//声明绑定
@Bean
public Binding stockDelayQueueBinding(){
return new Binding(
"stock.delay.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.locked",
null);
}
@Bean
public Binding stockReleaseStockQueueBinding(){
return new Binding(
"stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"stock-event-exchange",
"stock.release.#",
null);
}
}
第一步:发送库存锁定成功消息到mq
告诉mq库存锁定成功,并在库存工作单里记录了哪个skuId商品,在哪个仓库wareId,扣除了多少数量num的物品
/**
* 为某个订单锁定库存
* 一个商品在多个仓库有库存,返回锁定成功还是失败的结果
*
* @Transactional 保证事务可以失败后(抛异常)回滚。默认只要是运行时异常,都会回滚
* <p>
* 库存解锁的场景
* 1、下订单成功,订单过期没有支付、被系统自动或手动取消,都要解锁库存
* 2、下订单成功,库存锁定成功,接下来的业务失败,导致订单回滚,之前锁定的库存就要自动解锁
*/
@Transactional
@Override
public Boolean orderLockStock(WareSkuLockVo vo) {
//获取订单号
String orderSn = vo.getOrderSn();
//保存库存工作单的详情,声明哪个具体的订单要锁库存,订单号是唯一可识别
WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
taskEntity.setOrderSn(orderSn);
wareOrderTaskService.save(taskEntity);
//TODO 1、找到每个商品在哪些仓库有库存
//获取所有要锁库存的商品,订单号下所有的订单
List<OrderItemVo> locks = vo.getLocks();
//返回 列表(skuId、num、wareId) list(指定商品在不同库存的加购数量)
List<SkuWareHasStock> skuWareHasStocks = locks.stream().map((item) -> {
SkuWareHasStock stock = new SkuWareHasStock();
Long skuId = item.getSkuId();
stock.setSkuId(skuId);
Integer num = item.getCount();
stock.setNum(num);
//查询skuId对应的wareId 表wms_ware_sku
List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
stock.setWareId(wareIds);
return stock;
}).collect(Collectors.toList());
//TODO 2、锁定库存
//默认所有的库存都锁定
Boolean allLock = true;
for (SkuWareHasStock skuWareHasStock : skuWareHasStocks) {
//默认skuId商品没锁定库存
Boolean skuStocked = false;
//获取要锁定库存的商品
Long skuId = skuWareHasStock.getSkuId();
//查询到此商品所在哪些仓库里
List<Long> wareIds = skuWareHasStock.getWareId();
if (wareIds == null || wareIds.size() == 0) {
//如果指定skuId的商品没有仓库里有库存,就抛出异常
throw new NoStockException(skuId);
}
//TODO 3、如果每一个商品都锁定成功,将当前商品锁定的工作单记录表发送给mq
//如果有库存,遍历每一个有此skuId商品的仓库Id
for (Long wareId : wareIds) {
//锁住库存,传入 商品id、仓库id、锁库存数量,指明哪个skuId的商品,要在哪个仓库,更新多少库存
//返回0行受影响就是没成功,1行受影响就是成功
Integer num = skuWareHasStock.getNum();
Long count = wareSkuDao.lockSkuStock(skuId, wareId, num);
if (count == 1) {
//如果当前库存锁定成功
skuStocked = true;
//TODO 4、告诉mq库存锁定成功,并在库存工作单里记录了哪个skuId商品,在哪个仓库wareId,扣除了多少数量num的物品
//封装库存订单详情表,并添加到数据库中,保存库存工作详情单
WareOrderTaskDetailEntity detailEntity = new WareOrderTaskDetailEntity(null, skuId, "", num, taskEntity.getId(), wareId, 1);
wareOrderTaskDetailService.save(detailEntity);
// StockLockedTo 是 mq专用传输类
//设置 【id】 封装工作单的实体类
StockLockedTo lockedTo = new StockLockedTo();
lockedTo.setId(taskEntity.getId());
//设置【detailTo】 封装工作详情单的实体类
StockDetailTo stockDetailTo = new StockDetailTo();
//因为detailEntity和stockDetailTo 属性相同,可以对拷
BeanUtils.copyProperties(detailEntity, stockDetailTo);
// lockedTo 封装了 库存订单工作表id和详情表实体类
lockedTo.setDetailTo(stockDetailTo);
//mq发送,只要有一个库存锁定,就发送给mq
rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", lockedTo);
//订单和库存都锁定成功了,就调出for循环
break;
} else {
//如果当前仓库锁失败了,说明此仓库库存不足,就尝试下一个仓库
}
}
//如果当前商品的所有仓库都没有锁住,说明所有仓库都没有此skuId商品的库存了
if (skuStocked == false) {
//如果指定skuId的商品没有仓库里有库存,就抛出异常
throw new NoStockException(skuId);
}
}
//如果上面整个for循环都没有抛出异常,说明是全部skuId的商品都锁定库存了
return true;
}
//内部类
@Getter
@Setter
class SkuWareHasStock {
private Long skuId; //商品
private Integer num; //购买数量
private List<Long> wareId; //仓库id
}
第二步:mq监听队列消息
/**
* 监听mq的消息,释放锁库存
* 生产者发送消息--交换机--队列(延迟2分钟无人消费)--根据loutingKey和交换机到达--交换机--队列(被监听)
*
*/
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
/**
* 解锁库存
* 当下订单后,订单表锁定库存成功、库存表锁定库存成功,但是30分钟内未支付,消费者监听队列,自动解锁库存
* @param to 库存锁定成功后,通知mq库存锁定成功,并在库存工作单里记录了哪个skuId商品,在哪个仓库wareId,扣除了多少数量num的物品
* @param message
* @param channel
* @throws IOException
*/
@RabbitHandler
public void handleStockLockRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
System.out.println("2分钟后收到解锁库存的消息,调用解锁方法");
try {
wareSkuService.unlockStock(to);
/**
* 手动应答,消费者读取完mq消息后,要给生产者一个答复,执行成功了就回复成功,不是批量回复
* 1、消息的标记 tag
* 2、是否批量应答 multiple false代表不批量应答信道中的消息 true代表批量
*/
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
//有异常就拒绝消费,将消息继续扔到队列里,让别人消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
第三步:库存解锁业务实现
根据mq监听队列得到的信息,用来解锁库存操作
/**
* 库存自动解锁:
* 下单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁
*/
@Override
public void unlockStock(StockLockedTo to) {
//获取到库存详情表vo vo=实体类
StockDetailTo detailTo = to.getDetailTo();
//获取库存工作详情表的id
Long detailId = detailTo.getId();
//解锁
//1.查询数据库关于这个订单的锁定库存信息
// 有:证明锁定成功了
// 解锁订单情况
// 1.没有这个订单,不必解锁库存
// 2.有这个订单。
/// 订单状态:已取消,解锁库存状态
// 没取消,不能解锁
// 没有:库存锁定失败了,库存回滚了,这种情况无需解锁。
//根据id查询数据库 库存工作详情表的实体类
WareOrderTaskDetailEntity byId = wareOrderTaskDetailService.getById(detailId);
//如果从数据库中可以查询出数据,说明库存已经被扣减,就需要解锁库存,释放和恢复仓库的商品数量
if (byId != null) {
//如果工作详情表有数据,就解锁
//库存工作单的id wms_ware_order_task
Long id = to.getId();
//根据工作单id查询到工作表实体类
WareOrderTaskEntity taskEntity = wareOrderTaskService.getById(id);
//从工作表中查询到订单号
String orderSn = taskEntity.getOrderSn();
//远程调用订单服务,根据订单号查询订单实体类,再查询状态,如果是取消状态,才可以解锁
R r = orderFeignService.getOrderStatus(orderSn);
if (r.getCode() == 0) {
//订单数据返回成功
OrderVo data = r.getData(new TypeReference<OrderVo>() {});
//订单已经被取消或被取消状态或没有订单信息,就解锁库存
if (data == null || data.getStatus() == 4) {
//如果当前订单是库存已锁定,才可以解锁
if (byId.getLockStatus() == 1) {
Long skuId = detailTo.getSkuId();
Long wareId = detailTo.getWareId();
Integer skuNum = detailTo.getSkuNum();
unLockStock(skuId, wareId, skuNum, detailId);
}
}
} else {
throw new RuntimeException("远程服务异常");
}
}
}
/**
* 解锁库存
* 哪个商品、哪个仓库、数量、库存工作详情表id
*/
public void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) {
//库存解锁
wareSkuDao.unLockStock(skuId, wareId, num, taskDetailId);
//更新库存工作详情表的状态为已解锁库存
WareOrderTaskDetailEntity detailEntity = new WareOrderTaskDetailEntity();
detailEntity.setId(taskDetailId);
detailEntity.setLockStatus(2); //变为已解锁
//根据id更新 lock_status 锁状态为 已解锁
wareOrderTaskDetailService.updateById(detailEntity);
}
定时关单
流程图分析
1、订单创建成功消息——loutingKey(order-create-order)——交换机(order-event-exchange)
/**
* TODO 订单创建成功给mq发送消息
* 默认订单锁定成功,库存锁定也成功,但后面的业务方法错误,因为有@Transactional本地事务,造成订单未下单回滚了,但是库存已经扣除了,没有回滚需要解锁库存(根据工作表和工作详情表),在库存服务里添加监听器监听队列,自动解锁库存
*/
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
2、交换机(order-event-exchange)—— 延时队列(order-delay-queue)——交换机(order-event-exchange)
交换机将订单创建成功的消息发送到延时队列里,30分钟后自动又将订单创建成功的消息发送到交换机里
3、交换机(order-event-exchange)—— loutingKey(order-release-order)——普通队列(order-release-order-queue)
/**
* 监听mq消息,自动关单
*/
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(OrderEntity entity, Channel channel, Message message) throws Exception{
System.out.println("收到过期的订单信息,准备关闭订单"+entity.getOrderSn());
try {
orderService.closeOrder(entity);
/**
* 手动应答,消费者读取完mq消息后,要给生产者一个答复
* 1、消息的标记 tag
* 2、是否批量应答 multiple false代表不批量应答信道中的消息 true代表批量
*/
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
//如果失败,信道就拒绝消费消息,将此消息扔到队列中,让其他人消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
//进入orderService.closeOrder(entity);
@Override
public void closeOrder(OrderEntity entity) {
//查询当前数据库中订单的最新状态
OrderEntity orderEntity = this.getById(entity.getId());
//待付款状态需要关闭订单,状态0,待付款
if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()){
//关单:将指定订单号的订单表的状态更改,因为数据库里的订单表和传过来的订单表状态可能不一样,所以要新建订单表
OrderEntity update = new OrderEntity();
update.setId(entity.getId());
//设置订单为已取消订单状态,状态4,已关闭
update.setStatus(OrderStatusEnum.CANCELED.getCode());
//将订单状态更新到数据库
this.updateById(update);
4、释放订单服务消息———— loutingKey(order-release-other)——交换机(order-event-exchange)
@Override
public void closeOrder(OrderEntity entity) {
//查询当前数据库中订单的最新状态
OrderEntity orderEntity = this.getById(entity.getId());
//待付款状态需要关闭订单,状态0,待付款
if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()){
//关单:将指定订单号的订单表的状态更改,因为数据库里的订单表和传过来的订单表状态可能不一样,所以要新建订单表
OrderEntity update = new OrderEntity();
update.setId(entity.getId());
//设置订单为已取消订单状态,状态4,已关闭
update.setStatus(OrderStatusEnum.CANCELED.getCode());
//将订单状态更新到数据库
this.updateById(update);
/**
* 创建 mq 专用传输类 ,等于 OrderEntity
* 订单关闭后,也应该主动发一个消息,告诉服务有一个订单释放了
* 绑定订单交换机和库存队列,订单释放和库存释放进行绑定
* 队列发送给交换机信息,需要手动人工用 rabbitTemplate 工具
* 交换机给队列发信息,自动无须人工手动即可发送
*/
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderEntity,orderTo);
//发送给mq一个order
rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
}
}
5、交换机(order-event-exchange)—— loutingKey(order-release-other.#)——库存释放队列(stock-release-stock-queue)
/**
* 监听mq的消息,释放锁库存
* 生产者发送消息--交换机--队列(延迟2分钟无人消费)--根据loutingKey和交换机到达--交换机--队列(被监听)
*
*/
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
/**
* 解锁库存
* 当下订单后,订单表锁定库存成功、库存表锁定库存成功,但是30分钟内未支付,消费者监听队列,自动解锁库存
* @param to 库存锁定成功后,通知mq库存锁定成功,并在库存工作单里记录了哪个skuId商品,在哪个仓库wareId,扣除了多少数量num的物品
* @param message
* @param channel
* @throws IOException
*/
@RabbitHandler
public void handleStockLockRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
System.out.println("2分钟后收到解锁库存的消息,调用解锁方法");
try {
wareSkuService.unlockStock(to);
/**
* 手动应答,消费者读取完mq消息后,要给生产者一个答复,执行成功了就回复成功,不是批量回复
* 1、消息的标记 tag
* 2、是否批量应答 multiple false代表不批量应答信道中的消息 true代表批量
*/
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
//有异常就拒绝消费,将消息继续扔到队列里,让别人消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
/**
* 库存服务监听 订单服务的关单信息--------------------关单---------------
* @param orderTo orderEntity
* @param message
* @param channel
* @throws IOException
*/
@RabbitHandler
public void handleOrderCloseRelease(OrderTo orderTo,Message message,Channel channel) throws IOException {
try {
//监听到订单服务的消息后,库存服务开始解锁库存
wareSkuService.unlockStock(orderTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
当下订单后,订单表锁定库存成功、库存表锁定库存成功,但是30分钟内未支付,消费者监听队列,自动解锁库存
配置队列和交换机
/**
* mq的配置类
* 1、只要rabbitmq控制台已经有了我们要创建的队列、要想修改只能删除重新生成队列
*/
@Configuration
public class MyMqConfig {
/**
* 消费者监听消费队列 order.release.order.queue
*/
// @RabbitListener(queues = "order.release.order.queue")
// public void listener(OrderEntity entity, Channel channel,Message message) throws Exception{
// System.out.println("收到过期的订单信息,准备关闭订单"+entity.getOrderSn());
// /**
// * 手动应答,消费者读取完mq消息后,要给生产者一个答复
// * 1、消息的标记 tag
// * 2、是否批量应答 multiple false代表不批量应答信道中的消息 true代表批量
// */
// channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
// }
//声明延迟队列,指定时间内未消费变为死信
@Bean
public Queue orderDelayQueue (){
//此队列携带参数(交换机、loutingKey、过期时间)
Map<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);
//Queue(String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
Queue queue = new Queue("order.delay.queue", true, false, false,arguments);
return queue;
}
//普通队列,没有参数
@Bean
public Queue orderReleaseOrderQueue (){
Queue queue = new Queue("order.release.order.queue", true, false, false, null);
return queue;
}
//声明普通交换机(没有参数)
@Bean
public Exchange orderEventExchange(){
//TopicExchange(String name, boolean durable, boolean autoDelete, Map<String, Object> arguments)
return new TopicExchange("order-event-exchange",true,false,null);
}
//声明延迟队列和交换机绑定
@Bean
public Binding orderCreateOrderBinding(){
//Binding(String destination, DestinationType destinationType, String exchange, String routingKey,Map<String, Object> arguments)
return new Binding(
"order.delay.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.create.order",
null);
}
//声明普通队列和交换机绑定
@Bean
public Binding orderReleaseOrderBinding(){
return new Binding(
"order.release.order.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.order",
null);
}
//声明库存队列和订单交换机绑定 订单释放和库存释放绑定
@Bean
public Binding OrderReleaseOtherBinding(){
return new Binding("stock.release.stock.queue",
Binding.DestinationType.QUEUE,
"order-event-exchange",
"order.release.other.#",
null);
}
}
第一步:发送订单成功消息到mq
到了第5步,订单创建成功,发送消息到mq
@Transactional
@Override
public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
//将订单提交页面的数据放入此 threadLocal 中,用于共享数据
orderSubmitVoThreadLocal.set(vo);
//调用 ThreadLocal,获取同一线程的数据,得到当前登录的用户
MemberRespVo memberRespVo = LoginUserIntereptor.loginUser.get();
//生成提交订单后返回的封装类
SubmitOrderResponseVo response = new SubmitOrderResponseVo();
//没有抛出异常就设置状态码为0,代表成功
response.setCode(0);
//1、验证令牌【令牌的对比和删除必须保证原子性】
//对比防重删令牌,返回0和1,0代表令牌失败、1代表令牌删除成功
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();
//redis执行lua脚本,将页面输入令牌和redis中保存的令牌做对比,验证成功返回1,失败返回0 order:token+用户id
Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class), Arrays.asList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
if (result == 0l){
//验证失败
response.setCode(1);
return response;
}else {
//1、令牌验证成功,解决重复提交订单成功问题,下单、创建订单、验令牌、验价格、锁库存
OrderCreateTo order = createOrder();
//2、验价,获取到应付金额
BigDecimal payAmount = order.getOrder().getPayAmount();
//获取页面提交的应付金额
BigDecimal payPrice = vo.getPayPrice();
//如果数据库订单里的应付金额和页面提交的金额差值小于0.01,就验证价格成功,因为是省略到小数点后两位,如果算相等的话,可能会出现 0.12和0.123456...
double value = Math.abs(payAmount.subtract(payPrice).doubleValue());
//如果验价成功
if (value < 0.01){
//TODO 三、保存订单
saveOrder(order);
// 四、库存锁定,stock_locked 锁定库存数 <= stock 库存数
//库存锁定,只要有异常回滚订单数据 (订单号,所有订单项)
WareSkuLockVo wareSkuLockVo = new WareSkuLockVo();
//封装订单号
wareSkuLockVo.setOrderSn(order.getOrder().getOrderSn());
List<OrderItemVo> locks = 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());
//封装订单项
wareSkuLockVo.setLocks(locks);
//TODO 4、远程锁库存
//远程调用库存服务调用锁定库存方法
R r = wareFeignService.orderLockStock(wareSkuLockVo);
if (r.getCode() == 0){
//如果锁定成功
response.setOrder(order.getOrder());
//TODO 5、远程扣减积分
//模拟异常,订单数据库回滚,但是库存数据库不回滚,本地事务只能对自己的服务有效
//int i = 1/0;
/**
* TODO 订单创建成功给mq发送消息
* 默认订单锁定成功,库存锁定也成功,但后面的业务方法错误,因为有@Transactional本地事务,造成订单未下单回滚了,但是库存已经扣除了,没有回滚
* 需要解锁库存(根据工作表和工作详情表),在库存服务里添加监听器监听队列,自动解锁库存
*/
rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
return response;
}else {
response.setCode(3);
return response;
}
}else {
//如果验价失败
response.setCode(2);
return response;
}
}
}
第二步:监听队列
/**
* 监听mq消息,自动关单
*/
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
@Autowired
OrderService orderService;
@RabbitHandler
public void listener(OrderEntity entity, Channel channel, Message message) throws Exception{
System.out.println("收到过期的订单信息,准备关闭订单"+entity.getOrderSn());
try {
orderService.closeOrder(entity);
/**
* 手动应答,消费者读取完mq消息后,要给生产者一个答复
* 1、消息的标记 tag
* 2、是否批量应答 multiple false代表不批量应答信道中的消息 true代表批量
*/
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
//如果失败,信道就拒绝消费消息,将此消息扔到队列中,让其他人消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
第三步:关单业务实现
关单:就是到 oms_order 表里将指定订单的状态改变即可
//OrderServiceImpl类
@Override
public void closeOrder(OrderEntity entity) {
//查询当前订单的最新状态
OrderEntity orderEntity = this.getById(entity.getId());
//待付款状态需要关闭订单,状态0,待付款
if (orderEntity.getStatus() == OrderStatusEnum.CREATE_NEW.getCode()){
//关单:将指定订单号的订单表的状态更改,因为数据库里的订单表和传过来的订单表状态可能不一样,所以要新建订单表
OrderEntity update = new OrderEntity();
update.setId(entity.getId());
//设置订单已取消订单状态 状态4,已取消
update.setStatus(OrderStatusEnum.CANCELED.getCode());
//将订单状态更新到数据库
this.updateById(update);
/**
* 创建 mq 专用传输类 ,等于 OrderEntity
* 订单关闭后,也应该主动发一个消息,告诉服务有一个订单释放了
* 绑定订单交换机和库存队列,订单释放和库存释放进行绑定
* 队列发送给交换机信息,需要手动人工用 rabbitTemplate 工具
* 交换机给队列发信息,自动无须人工手动即可发送
*/
OrderTo orderTo = new OrderTo();
BeanUtils.copyProperties(orderEntity,orderTo);
//发送给mq一个order
rabbitTemplate.convertAndSend("order-event-exchange","order.release.other",orderTo);
}
}
第四步:库存服务监听队列
/**
* 监听mq的消息,释放锁库存
* 生产者发送消息--交换机--队列(延迟2分钟无人消费)--根据loutingKey和交换机到达--交换机--队列(被监听)
*
*/
@RabbitListener(queues = "stock.release.stock.queue")
@Service
public class StockReleaseListener {
@Autowired
WareSkuService wareSkuService;
/**
* 解锁库存
* 当下订单后,订单表锁定库存成功、库存表锁定库存成功,但是30分钟内未支付,消费者监听队列,自动解锁库存
* @param to 库存锁定成功后,通知mq库存锁定成功,并在库存工作单里记录了哪个skuId商品,在哪个仓库wareId,扣除了多少数量num的物品
* @param message
* @param channel
* @throws IOException
*/
@RabbitHandler
public void handleStockLockRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
System.out.println("2分钟后收到解锁库存的消息,调用解锁方法");
try {
wareSkuService.unlockStock(to);
/**
* 手动应答,消费者读取完mq消息后,要给生产者一个答复,执行成功了就回复成功,不是批量回复
* 1、消息的标记 tag
* 2、是否批量应答 multiple false代表不批量应答信道中的消息 true代表批量
*/
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
//有异常就拒绝消费,将消息继续扔到队列里,让别人消费
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
/**
* 库存服务监听 订单服务的关单信息,订单服务关单后,又发送了一条信息给交换机,交换机发送到了库存的释放队列上
* @param orderTo orderEntity
* @param message
* @param channel
* @throws IOException
*/
@RabbitHandler
public void handleOrderCloseRelease(OrderTo orderTo,Message message,Channel channel) throws IOException {
try {
wareSkuService.unlockStock(orderTo);
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
} catch (Exception e) {
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
}
}
}
第五步:解锁业务实现
/**
* 订单解锁
* 防止订单服务卡顿,导致订单状态消息一直改不了,库存消息优先到期,
* @param orderTo
*/
@Transactional
@Override
public void unlockStock(OrderTo orderTo) {
//获取订单号
String orderSn = orderTo.getOrderSn();
//获取工作单,防止重复解锁库存
WareOrderTaskEntity taskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn);
//查询工作单id
Long id = taskEntity.getId();
//从库存详情工作表里,根据工作单id找到所有没有解锁的库存,然后进行解锁,工作单id可能会重复
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());
}
}
/**
* 解锁库存
* 哪个商品、哪个仓库、数量、库存工作详情表id
*/
public void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) {
//库存解锁
wareSkuDao.unLockStock(skuId, wareId, num, taskDetailId);
//更新库存工作详情表的状态为已解锁库存
WareOrderTaskDetailEntity detailEntity = new WareOrderTaskDetailEntity();
detailEntity.setId(taskDetailId);
detailEntity.setLockStatus(2); //变为已解锁
//根据id更新 lock_status 锁状态为 已解锁
wareOrderTaskDetailService.updateById(detailEntity);
}
<update id="unLockStock">
update wms_ware_sku
set stock = stock + #{num},stock_locked = stock_locked - #{num}
where skuId =#{skuId}
and wareId =#{wareId}
</update>