真实系统优化:解决生产环境中的重复订单问题

真实系统优化:解决生产环境中的重复订单问题

重复订单问题场景

扫码零售流程:

  • 在扫码零售流程中,导购使用零售通APP扫码,先调用条码平台的接口,接口返回车辆信息与是否已被扫(未出售或已出售)。当车辆是未出售时,系统就可以进行我们零售通系统的创建订单的业务逻辑。然而,在一些情况下可能会出现重复订单的情况,导致同一辆车被多次操作

订单创建的逻辑:

@GlobalTransactional(timeoutMills = 300000, name = "dubbo-gts-seata-retail-order-create")
public BaseResponse<Long> createOrder(RetailOrderReq request) {
    CurrentUserInfo currentUserInfo = WebUtil.getCurrentUserInfo();
    log.info("创建扫码零售订单入参:{}", JSON.toJSONString(request));

    // 校验限价检查标识是否为空
    Boolean isCheckPrice = request.getIsCheckPrice();
    Assert.isTrue(Objects.nonNull(isCheckPrice), "是否限价检查标识不能为空");

    // 计算结算价
    RetailOrderReq retailOrderReq = calculateSettlementPrice(request);
    RetailOrderReqDTO orderReq = retailOrderReq.getRetailOrderReq();

    // 获取并设置客户信息
    // ...

    // 校验零售金额是否符合范围
    // ...

    // 设置订单默认状态
    orderReq.setIsReturn(WhetherEum.FALSE_VALUE.getValue());
    orderReq.setIsClaim(WhetherEum.FALSE_VALUE.getValue());
    orderReq.setCouponType(CouponTypeEnum.TYPE_1.getValue());
    orderReq.setPickUpStatus(WhetherEum.TRUE_VALUE.getValue());
    orderReq.setPickUpTime(LocalDateTime.now());

    // 设置商品信息
    // ...

    // 获取门店信息并设置
    // ...

    // 校验零售价格是否符合政策
    // ...

    // 校验仓库状态
    checkWareHouseStatus(orderReq, itemReqDTO.getWarehouseCode());

    // 校验是否超出提报数量限制
    log.info("创建扫码零售订单限制begin");
    BaseResponse<Boolean> checkLimitResp = orderFacade.checkIsOverLimit(orderReq.getPurchaserMobile());
    log.info("创建扫码零售订单限制end:{}", checkLimitResp);
    if (!checkLimitResp.isSuccess()) {
        throw new ApplicationException(checkLimitResp.getChnDesc());
    }

    // 锁机制和订单创建逻辑
    return RedisLockUtil.executeSynchOperate(result -> {
        if (result) {
            log.info("获取退货订单信息begin");
            // 退货订单处理
            if (Objects.nonNull(orderReq.getReturnOrderId())) {
                // ... 省略退货订单的处理逻辑
            }

            // 赔付新车逻辑
            if (Objects.nonNull(orderReq.getInsuranceClaimsId())) {
                // ... 省略保险理赔处理逻辑
            }

            // 增加双重检测锁机制 防止一个码同时卖几次
            log.info("车辆信息校验begin:{}");
            Boolean check = checkBikeInfo();
            log.info("车辆信息校验end:{}", check);
            if (check) {
                checkBarcodeIsExist(currentUserInfo.getSetsOfBooksId(), orderReq.getQryCode(), orderReq.getBarCodeType());
            }

            // 门店派单的订单需要校验 成车数量和扫码数量
            if (OrderTypeEnum.TYPE_4.getValue().equals(orderReq.getOrderType()) && StringUtil.isNotBlank(orderReq.getSalesOrderNo())) {
                checkShopDeliveryOrder(request);
            }

            // 创建订单主表信息
            log.info("创建订单主表信息begin:{}", orderReq);
            BaseResponse<RetailOrderResponseDTO> orderRes = orderFacade.create(orderReq);
            Assert.isTrue(orderRes.isSuccess(), String.format("订单创建失败:%s", orderRes.getChnDesc()));
            RetailOrderResponseDTO retailOrderResp = orderRes.getContent();
            log.info("创建订单主表信息end:{}", retailOrderResp);
            Long retailOrderId = retailOrderResp.getRetailOrderId();

            // 创建订单商品
            itemReqList.forEach(itemReq -> {
                itemReq.setRetailOrderId(retailOrderId);
                BaseResponse<RetailOrderItemResponseDTO> itemRes = itemFacade.create(itemReq);
                Assert.isTrue(itemRes.isSuccess(), String.format("创建订单商品失败:%s", itemRes.getChnDesc()));
            });

            // 优惠券处理
            if (Objects.nonNull(orderReq.getCouponReq()) && Objects.nonNull(orderReq.getCouponReq().getCouponCode())) {
                // ... 省略优惠券相关的处理
            }

            // 更新订单和发送短信
            if (Objects.nonNull(retailOrderId)) {
                // ... 省略保险推送、门店派单和短信发送的逻辑
            }

            BaseResponse<Long> res = new BaseResponse<>();
            res.setContent(retailOrderId);
            return res;
        } else {
            log.error("======扫码零售,竞争锁失败,请稍后刷新数据后重试======");
            throw new ApplicationException(CommonErrorCode.LOCK_FAIL);
        }
    }, buildLockCode(orderReq.getBarCodeType(), orderReq.getQryCode(), currentUserInfo.getSetsOfBooksId()), 300000);
}

可能产生问题原因:

  1. 在条码平台车辆没有正在扫码的车辆库存占用态,导致能进入到下一步

  2. 业务处理时间可能会太长,锁时间设置五分钟,可能会造成锁失效,导致其他进程能获取锁

  3. 锁粒度太大,用车辆码+导购的事业信息 作为锁的key,假设有别的事业部的导购同事扫 车辆码+导购的事业信息 不见得能锁住,尽管里面使用了双重检查的设定,但是两个事务未提交前是不可见的,所以也可能会导致同一辆车创建两个订单

具体问题分析

  1. 条码平台没有正在扫码的车辆库存占用态:
    • 如果条码平台没有一个"正在扫码"的状态标记,那么就没有办法明确判断当前车辆是否正在被处理。当不同的导购扫描相同的车辆码时,系统就无法有效地避免重复扫码。
    • 潜在风险:如果条码平台仅有“未出售”和“已出售”状态,那么系统在处理过程中可能会允许多个订单同时创建,导致重复订单。
  2. 业务处理时间太长导致锁超时:
    • 锁时间设置为5分钟(300秒),如果订单创建的业务逻辑过于复杂或者其他因素导致处理时间过长,可能会在锁的有效期内未完成处理,导致锁失效。
    • 潜在风险:在锁失效后,其他请求能够获取到锁并开始处理订单,导致重复订单的创建。
  3. 锁粒度太大:
    • 锁的粒度设置为“车辆码 + 导购的事业信息”,这意味着只有相同事业部的导购才能够成功获取锁。但在业务过程中,存在以下问题:
      • 同一辆车可能被不同事业部的导购扫描,这样就无法通过锁来避免重复订单的创建。
      • 双重检查机制虽然尝试解决这个问题,但在事务提交前,两个操作是不可见的,因此可能会导致重复订单。

问题解决方案

  1. 改进条码平台状态(但是不太可能,那边没有太成型的开发团队,难以依赖,开发时间无法评估),所以只能在我们自己的系统增加Redis缓存来保存车辆码的占用状态
  2. 锁的优化(光延长锁的时长确实可以让重复订单量减少,不过最好还是用Redisson )
    • 延长锁时间: 如果业务逻辑本身的处理时间较长,考虑将锁的超时时间适当延长(例如设置为10分钟)。不过,延长锁时间并不是长久之计,因为它可能会导致更高的锁竞争问题。
    • 锁定机制优化: 可以在锁定前进行更精细的粒度划分,例如将锁粒度细化到每辆车每次扫码操作,而不仅仅是“车辆码 + 导购事业部信息”。
  3. 改进锁粒度
    • 统一使用车辆码作为锁粒度:使用车辆码(vehicleCode)作为锁的唯一标识,这样无论是哪一个事业部的导购扫码,都能通过同一个锁粒度来确保同一车辆不会被同时处理。
  4. 订单重复检查:
    • 在订单创建的业务逻辑中,增加一个校验逻辑,确保同一辆车在没有创建相同的订单。可以通过以下方式进行:
      • 增加唯一性校验: 在创建订单时,查询该车辆是否已经存在未完成的订单,若存在则直接返回错误。
      • 标记订单状态: 订单在创建时标记为“处理中”状态,确保其他用户无法重复操作。

优化后的代码实现

@GlobalTransactional(timeoutMills = 300000, name = "dubbo-gts-seata-retail-order-create")
public BaseResponse<Long> createOrder(RetailOrderReq request) {
    CurrentUserInfo currentUserInfo = WebUtil.getCurrentUserInfo();
    log.info("创建扫码零售订单入参:{}", JSON.toJSONString(request));

    // 校验限价检查标识是否为空
    Boolean isCheckPrice = request.getIsCheckPrice();
    Assert.isTrue(Objects.nonNull(isCheckPrice), "是否限价检查标识不能为空");

    // 计算结算价
    RetailOrderReq retailOrderReq = calculateSettlementPrice(request);
    RetailOrderReqDTO orderReq = retailOrderReq.getRetailOrderReq();

    // 获取并设置客户信息
    // ... (省略)

    // 校验零售金额是否符合范围
    // ... (省略)

    // 设置订单默认状态
    orderReq.setIsReturn(WhetherEum.FALSE_VALUE.getValue());
    orderReq.setIsClaim(WhetherEum.FALSE_VALUE.getValue());
    orderReq.setCouponType(CouponTypeEnum.TYPE_1.getValue());
    orderReq.setPickUpStatus(WhetherEum.TRUE_VALUE.getValue());
    orderReq.setPickUpTime(LocalDateTime.now());

    // 设置商品信息
    // ... (省略)

    // 获取门店信息并设置
    // ... (省略)

    // 校验零售价格是否符合政策
    // ... (省略)

    // 校验仓库状态
    checkWareHouseStatus(orderReq, itemReqDTO.getWarehouseCode());

    // 校验是否超出提报数量限制
    log.info("创建扫码零售订单限制begin");
    BaseResponse<Boolean> checkLimitResp = orderFacade.checkIsOverLimit(orderReq.getPurchaserMobile());
    log.info("创建扫码零售订单限制end:{}", checkLimitResp);
    if (!checkLimitResp.isSuccess()) {
        throw new ApplicationException(checkLimitResp.getChnDesc());
    }

    // 订单重复检查:查询是否有未完成订单
    if (checkIfOrderExists(orderReq.getQryCode())) {
        throw new ApplicationException("该车辆已存在未完成的订单,请稍后再试");
    }

    // 锁机制和订单创建逻辑
    return RedisLockUtil.executeSynchOperate(result -> {
        if (result) {
            log.info("获取退货订单信息begin");
            // 退货订单处理
            if (Objects.nonNull(orderReq.getReturnOrderId())) {
                // ... 省略退货订单的处理逻辑
            }

            // 赔付新车逻辑
            if (Objects.nonNull(orderReq.getInsuranceClaimsId())) {
                // ... 省略保险理赔处理逻辑
            }

            // 增加双重检测锁机制 防止一个码同时卖几次
            log.info("车辆信息校验begin:{}");
            Boolean check = checkBikeInfo();
            log.info("车辆信息校验end:{}", check);
            if (check) {
                // 使用车辆码作为锁粒度,确保同一辆车只有一个导购在处理
                String lockKey = "vehicle_lock_" + orderReq.getQryCode();  // 使用车辆码作为唯一标识
                RLock lock = redissonClient.getLock(lockKey);

                // 尝试获取锁,超时时间设为5分钟
                boolean lockAcquired = lock.tryLock(0, 5, TimeUnit.MINUTES);
                if (!lockAcquired) {
                    log.error("======扫码零售,竞争锁失败,请稍后刷新数据后重试======");
                    throw new ApplicationException(CommonErrorCode.LOCK_FAIL);
                }

                try {
                    checkBarcodeIsExist(currentUserInfo.getSetsOfBooksId(), orderReq.getQryCode(), orderReq.getBarCodeType());

                    // 门店派单的订单需要校验 成车数量和扫码数量
                    if (OrderTypeEnum.TYPE_4.getValue().equals(orderReq.getOrderType()) && StringUtil.isNotBlank(orderReq.getSalesOrderNo())) {
                        checkShopDeliveryOrder(request);
                    }

                    // 创建订单主表信息
                    log.info("创建订单主表信息begin:{}", orderReq);
                    BaseResponse<RetailOrderResponseDTO> orderRes = orderFacade.create(orderReq);
                    Assert.isTrue(orderRes.isSuccess(), String.format("订单创建失败:%s", orderRes.getChnDesc()));
                    RetailOrderResponseDTO retailOrderResp = orderRes.getContent();
                    log.info("创建订单主表信息end:{}", retailOrderResp);
                    Long retailOrderId = retailOrderResp.getRetailOrderId();

                    // 创建订单商品
                    itemReqList.forEach(itemReq -> {
                        itemReq.setRetailOrderId(retailOrderId);
                        BaseResponse<RetailOrderItemResponseDTO> itemRes = itemFacade.create(itemReq);
                        Assert.isTrue(itemRes.isSuccess(), String.format("创建订单商品失败:%s", itemRes.getChnDesc()));
                    });

                    // 优惠券处理
                    if (Objects.nonNull(orderReq.getCouponReq()) && Objects.nonNull(orderReq.getCouponReq().getCouponCode())) {
                        // ... 省略优惠券相关的处理
                    }

                    // 更新订单和发送短信
                    if (Objects.nonNull(retailOrderId)) {
                        // ... 省略保险推送、门店派单和短信发送的逻辑
                    }

                    BaseResponse<Long> res = new BaseResponse<>();
                    res.setContent(retailOrderId);
                    return res;

                } finally {
                    // 释放锁
                    lock.unlock();
                }
            } else {
                log.error("车辆信息校验失败");
                throw new ApplicationException("车辆信息校验失败");
            }
        } else {
            log.error("扫码零售,获取锁失败");
            throw new ApplicationException(CommonErrorCode.LOCK_FAIL);
        }
    }, buildLockCode(orderReq.getBarCodeType(), orderReq.getQryCode(), currentUserInfo.getSetsOfBooksId()), 300000);
}

// 订单重复检查:查询是否已存在未完成订单
private boolean checkIfOrderExists(String vehicleCode) {
    // 查询该车辆是否已经有未完成的订单
    BaseResponse<List<RetailOrder>> existingOrders = orderFacade.findOrdersByVehicleCode(vehicleCode);
    return existingOrders != null && !existingOrders.getContent().isEmpty();
}

关键改动:

  1. 订单重复检查: 增加了 checkIfOrderExists() 方法,用来查询是否存在未完成的订单,避免重复扫码创建订单。
  2. Redisson 分布式锁: 使用车辆码作为锁粒度,在创建订单时,通过 Redisson 获取锁并设置超时时间为 5 分钟,确保同一辆车只能被一个导购处理。
  3. 订单状态标记: 增加了订单状态为“处理中”状态的标记,确保在处理订单时,其他扫码请求无法重复操作。

优化后的流程:

  1. 检查车辆码是否已占用:通过 Redis 缓存存储车辆码的占用状态,避免同一辆车在短时间内被重复扫码。
  2. 获取分布式锁:使用车辆码作为锁粒度,通过 Redisson 获取锁,确保同一辆车只能被一个导购处理。
  3. 校验是否已存在未完成订单:在订单创建时,检查该车辆是否已有未完成的订单,如果有则不允许重复创建订单。

通过这些优化,可以有效避免重复扫码和订单冲突问题,提升系统的可靠性和并发处理能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

清河大善人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值