跳到正文
Kale的数字花园
目录 文章
/3 分钟/7 阅读/0 评论

seata分布式事务的脏读问题

用户将两个调拨单(A001,A002)进行拼单生产拼单后的调拨单A003,并将A003提交到仓库 问题: 什么时候能够查询到调拨单A003? 答: 在T1时刻后 T2时刻前 问题: 在T1时刻后T2时刻前提交,该调拨单能否提交成功? 因为s

问题描述

用户将两个调拨单(A001,A002)进行拼单生产拼单后的调拨单A003,并将A003提交到仓库

问题: 什么时候能够查询到调拨单A003?

答: 在T1时刻后 T2时刻前

allocation调拨单拼单代码


@ApiOperation(value = "调拨单拼单")
@PostMapping
@LockOrder(fieldOrderNo = "#cmd.orderNoList", orderType = OrderTypeEnum.ALLOCATION_ORDER)
public void mergeAllocationOrder(@RequestBody MergeOrderCmd cmd) {
  allocationOrderMergeBusinessService.mergeAllocationOrder(cmd);
}

@GlobalTransactional(rollbackFor = Exception.class, timeoutMills = 600000)
@Override
public void mergeAllocationOrder(MergeOrderCmd mergeOrderCmd) {

 // 时刻T1开始
 // 拼单的前置逻辑处理,这部分可忽略
  // ......

  // 合并调拨单信息
  AllocationOrder mergedOrder = this.mergeOrder(needMergeOrderList, operatorId);
  this.mergeOrderFileCollection(mergedOrder.getOrderNo(), orderNoList);
  allocationOrderService.saveMergedOrder(mergedOrder, sellerIdList);
  // 时刻T1结束
  //
  // 拼单后的后置逻辑处理,忽略
  // .....

// 时刻T2开始
  // 根据mergeOrder 和 stockList 转换成创建出库的对象 OutboundOrderBo
  List<OutboundOrderStockBo> outboundOrderStockList =
      allocationOrderBusiness.convertOutboundOrderStockBo(stockList);
  final OutboundOrderBo outboundOrderBo =
      allocationOrderBusiness.buildOutboundOrderBo(
          mergedOrder, outboundOrderStockList, user.getRealName());
  // 这里暂时先任取一个销售,用于出库单
  outboundOrderBo.setSellerId(sellerIdList.get(0));
  outboundOrderBo.setSellerName(localCacheHelper.getUser(sellerIdList.get(0)).getRealName());

  // 创建调拨单合并出库单
  final MergeOutboundOrderCmd mergeOutboundOrderCmd = new MergeOutboundOrderCmd();
  mergeOutboundOrderCmd.setMergedOrderBo(outboundOrderBo);
  mergeOutboundOrderCmd.setOriginOutboundOrderNoList(originalOrderNoList);
  mergeOutboundOrderCmd.setNeedCancelOutboundOrderNoList(orderNoList);
  mergeOutboundOrderCmd.setOperatorId(user.getId());
  mergeOutboundOrderCmd.setOperator(user.getRealName());
  outboundRemoteService.createAllocationMergeOutboundOrder(mergeOutboundOrderCmd);
  log.info("合并调拨单成功!");
}

allocation提交到仓库代码


public List<OutboundOrderVo> submitToWarehouse(SubmitWarehouseBo submitWarehouseBo) {
  if (Objects.isNull(submitWarehouseBo)) {
    throw AllocationErrors.allocationParameterError().withArgs("提交信息为空,请检查!");
  }
  String key = RedisPath.ALLOCATION_ORDER + submitWarehouseBo.getAllocationOrderNo();
  try {
    return lockService.lockAndGetResult(
        key,
        -1,
        TimeUnit.SECONDS,
        () ->
            SpringContextUtils.getBean(this.getClass())
                .handleSubmitToWarehouse(submitWarehouseBo));
  } catch (LockFailedException e) {
    throw new BusinessException(500, "调拨单已提交仓库,请勿重复提交!");
  } catch (BusinessException e) {
    throw new BusinessException(500, e.getMessage());
  } catch (Exception e) {
    throw new BusinessException(500, "提交给仓库失败," + e.getMessage());
  }
}


@GlobalTransactional(rollbackFor = Exception.class, timeoutMills = 600000)
public List<OutboundOrderVo> handleSubmitToWarehouse(SubmitWarehouseBo submitWarehouseBo) {
  AllocationOrder queryAllocationOrder = verifyOrderNo(submitWarehouseBo.getAllocationOrderNo());
  if (!WAITING_DISTRIBUTE.getCode().equals(queryAllocationOrder.getStatus())) {
    throw new BusinessException(500, "调拨单不处于待配货状态,无法提交给仓库!");
  }
  List<Long> prohibitUserIdList = getProhibitSubmitWarehouseUsers();
  if (CollectionUtil.isNotEmpty(prohibitUserIdList)
      && prohibitUserIdList.contains(queryAllocationOrder.getCreatorId())) {
    throw new BusinessException(500, queryAllocationOrder.getCreatedBy() + "创建的调拨单不允许提交给仓库!");
  }
  // 查询所有的出库单,提交仓库
  List<OutboundOrderVo> outboundOrderVoList =
      listOutboundOderByAllocationOrderNo(queryAllocationOrder.getOrderNo());

  // 查询调拨单对应的销售关系
  final List<OrderSellerRelation> orderSellerRelationList =
      orderSellerRelationService.queryOrderSellerRelationByOrderNo(
          queryAllocationOrder.getOrderNo());

  final List<Long> sellerIdList =
      orderSellerRelationList.stream()
          .map(OrderSellerRelation::getSellerId)
          .distinct()
          .collect(Collectors.toList());
  final List<User> sellerList = localCacheHelper.getUser(sellerIdList);
  final List<String> sellerNameList =
      sellerList.stream().map(User::getRealName).collect(Collectors.toList());
  String sellerName = StrUtil.join(",", sellerNameList);

  for (OutboundOrderVo outboundOrderVo : outboundOrderVoList) {
    try {
      submitOutboundOrderToRemote(
          outboundOrderVo.getOutboundOrderNo(),
          StrUtil.isNotBlank(sellerName) ? sellerName : queryAllocationOrder.getCreatedBy());
    } catch (Exception e) {
      log.warn("推送出库单:{} 到仓库失败!", outboundOrderVo.getOutboundOrderNo(), e);
      throw new BusinessException(500, "推送出库单到仓库失败!");
    }
  }

// 调拨单信息相关的持久化代码,下面的代码可以忽略不看

  // 更新调拨单状态和记录操作的物流人员
  UpdateWrapper<AllocationOrder> updateWrapper = Wrappers.update();
  updateWrapper
      .lambda()
      .set(AllocationOrder::getStatus, WAREHOUSE_PROCESS.getCode())
      .set(AllocationOrder::getSubmitWarehouseAt, new Date())
      .set(AllocationOrder::getLogisticsStaffId, submitWarehouseBo.getOperatorId())
      .set(AllocationOrder::getLogisticsStaffName, submitWarehouseBo.getOperator())
      .eq(AllocationOrder::getId, queryAllocationOrder.getId());
  allocationOrderService.update(updateWrapper);

  // 记录上传的外箱标签
  List<FileCollection> fileCollectionList =
      fileCollectionService.listBySourceCodeAndType(
          queryAllocationOrder.getOrderNo(),
          FileConstants.OutboundFileType.PACKAGE_LABEL.getValue());
  if (CollectionUtil.isNotEmpty(fileCollectionList)) {
    String url = fileCollectionList.get(0).getUrl();
    OrderLog orderLog =
        new OrderLog(
            queryAllocationOrder.getOrderNo(),
            submitWarehouseBo.getOperatorId(),
            submitWarehouseBo.getOperator(),
            "推送外箱标签地址:" + url);
    orderLogService.save(orderLog);
  }

  // 记录日志信息
  OrderLog orderLog =
      new OrderLog(
          queryAllocationOrder.getOrderNo(),
          submitWarehouseBo.getOperatorId(),
          submitWarehouseBo.getOperator(),
          "提交仓库成功!");
  orderLogService.save(orderLog);
  return outboundOrderVoList;
}

问题: 在T1时刻后T2时刻前提交,该调拨单能否提交成功?

问题分析

分布式事务问题

因为seata的分布式事务是会将本地事务先提交,在提交本地事务后,如果整个全局事务中出现了异常,则seata会根据事务提交前记录的undo_log进行回滚

之前遇到的问题实例: Inventory回推状态给OMS改造

creeks目前的锁问题

  1. 因为调拨单拼单会生成新的调拨单A003和出库单A003,而拼单过程中是不会对A003进行加锁的,所以即使拼单事务未完全提交,也能将A003提交到仓库
  2. 目前的creeks的@LockOrder锁存在问题,只能对数据库已经入库的单据 或者直接对Id进行加锁

解决方案: 应该优化@LockOrder逻辑, 允许直接根据OrderNo进行加锁,即使这个单据在数据库中不存在

脏读问题

方案1: 在查询调拨单的方法上添加 @GlobalLock 注解 + select for update 语句

该方案不可行,这可能会导致由于获取不到锁,导致用户无法查询到所有的调拨单

方案2: 调整拼单方法的顺序,应该先创建出库单后, 再执行调拨单A003的持久化,来解决分布式事务下的脏读问题

相关阅读

2 分钟

在项目中集成 Seata(AT模式)

首先,更新 到 版本: 引入 Seata 相关的依赖定义: 修改 加入 Seata 相关的配置: 只需要用普通的 标签来声明: 业务端(通常是微服务)的数据库中需要创建一个 表: 需要用 Seata 提供的 标签声明一次分布式事务的开始:

面试#分布式事务6 阅读 · 0 评论

评论需要登录。生产环境使用 GitHub 登录,本地可前往 登录页 使用开发登录。

还没有评论,来抢沙发。