【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【23】【订单服务】


持续学习&持续更新中...

守破离


【雷丰阳-谷粒商城 】【分布式高级篇-微服务架构篇】【23】【订单服务】

订单中心

电商系统涉及到 3 流,分别是信息流,资金流,物流,而订单系统作为中枢将三者有机的集合起来。

订单模块是电商系统的枢纽,在订单这个环节上需求获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息流通。

订单信息

用户信息

用户信息包括用户账号、用户等级、用户的收货地址、收货人、收货人电话等组成

用户账户需要绑定手机号码,但是用户绑定的手机号码不一定是收货信息上的电话。

用户可以添加多个收货信息,用户等级信息可以用来和促销系统进行匹配,获取商品折扣,同时用户等级还可以获取积分的奖励等

订单基础信息

订单基础信息是订单流转的核心,其包括订单类型、父/子订单、订单编号、订单状态、订单流转的时间等。

  • 订单类型包括实体商品订单和虚拟订单商品等,这个根据商城商品和服务类型进行区分。
  • 同时订单都需要做父子订单处理,之前在初创公司一直只有一个订单,没有做父子订 单处理后期需要进行拆单的时候就比较麻烦,尤其是多商户商场,和不同仓库商品的时候, 父子订单就是为后期做拆单准备的。
  • 订单编号不多说了,需要强调的一点是父子订单都需要有订单编号,需要完善的时候 可以对订单编号的每个字段进行统一定义和诠释。
  • 订单状态记录订单每次流转过程,后面会对订单状态进行单独的说明。
  • 订单流转时间需要记录下单时间,支付时间,发货时间,结束时间/关闭时间等等

商品信息

商品信息从商品库中获取商品的 SKU 信息、图片、名称、属性规格、商品单价、商户信息等,从用户下单行为记录的用户下单数量,商品合计价格等。

优惠信息

优惠信息记录用户参与的优惠活动,包括优惠促销活动,比如满减、满赠、秒杀等,用户使用的优惠券信息,优惠券满足条件的优惠券需要默认展示出来,具体方式已在之前的优惠券 篇章做过详细介绍,另外还虚拟币抵扣信息等进行记录。

为什么把优惠信息单独拿出来而不放在支付信息里面呢?

因为优惠信息只是记录用户使用的条目,而支付信息需要加入数据进行计算,所以做为区分。

支付信息

  • 支付流水单号,这个流水单号是在唤起网关支付后支付通道返回给电商业务平台的支 付流水号,财务通过订单号和流水单号与支付通道进行对账使用。
  • 支付方式用户使用的支付方式,比如微信支付、支付宝支付、钱包支付、快捷支付等。 支付方式有时候可能有两个------余额支付+第三方支付。
  • 商品总金额,每个商品加总后的金额;运费,物流产生的费用;优惠总金额,包括促 销活动的优惠金额,优惠券优惠金额,虚拟积分或者虚拟币抵扣的金额,会员折扣的金额等 之和;实付金额,用户实际需要付款的金额。
  • 用户实付金额=商品总金额+运费-优惠总金额

物流信息

物流信息包括配送方式,物流公司,物流单号,物流状态

物流状态可以通过第三方接口来获取和向用户展示物流每个状态节点。

订单状态

  1. 待付款

    用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支付,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超 时后将自动取消订单,订单变更关闭状态。

  2. 已付款/待发货

    用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到 WMS 系统,仓库进行调拨,配货,分拣,出库等操作。

  3. 待收货/已发货

    仓储将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时知悉物 品物流状态

  4. 已完成

    用户确认收货后,订单交易完成。后续支付侧进行结算,如果订单存在问题进入售后状态 5. 售后中

    用户在付款后申请退款,或商家发货后用户申请退换货。

    售后也同样存在各种状态,当发起售后申请后生成售后订单,售后订单状态为待审核,等待 商家审核,商家审核通过后订单状态变更为待退货,等待用户将商品寄回,商家收货后 订单 状态更新为待退款状态,退款到用户原账户后订单状态更新为售后成功。

  5. 已取消

    付款之前取消订单。包括超时未付款或用户商户取消订单都会产生这种订单状态。

订单流程

订单流程是指从订单产生到完成整个流转的过程,从而行程了一套标准流程规则。

不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程,线上实物订单与 O2O 订单等,所以需要根据不同的类型进行构建订单流程。

不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程

正向流程就是一个正常的网购步骤:订单生成-->支付订单-->卖家发货-->确认收货-->交易成功。 而每个步骤的背后,订单是如何在多系统之间交互流转的,可概括如下图:

订单创建与支付

  1. 订单创建前需要预览订单,选择收货信息等
  2. 订单创建需要锁定库存,库存有才可创建,否则不能创建
  3. 订单创建后超时未支付需要解锁库存
  4. 支付成功后,需要进行拆单,根据商品打包方式,所在仓库,物流等进行拆单
  5. 支付的每笔流水都需要记录,以待查账
  6. 订单创建,支付成功等状态都需要给 MQ 发送消息,方便其他系统感知订阅

逆向流程

  1. 修改订单,用户没有提交订单,可以对订单一些信息进行修改,比如配送信息, 优惠信息,及其他一些订单可修改范围的内容,此时只需对数据进行变更即可。
  2. 订单取消,用户主动取消订单和用户超时未支付,两种情况下订单都会取消订 单,而超时情况是系统自动关闭订单,所以在订单支付的响应机制上面要做支付的限时处理,尤其是在前面说的下单减库存的情形下面,可以保证快速的释放库存。 另外需要需要处理的是促销优惠中使用的优惠券,权益等视平台规则,进行相应补回给用户。
  3. 退款,在待发货订单状态下取消订单时,分为缺货退款和用户申请退款。如果是 全部退款则订单更新为关闭状态,若只是做部分退款则订单仍需进行进行,同时生 成一条退款的售后订单,走退款流程。退款金额需原路返回用户的账户。
  4. 发货后的退款,发生在仓储货物配送,在配送过程中商品遗失,用户拒收,用户 收货后对商品不满意,这样情况下用户发起退款的售后诉求后,需要商户进行退款 的审核,双方达成一致后,系统更新退款状态,对订单进行退款操作,金额原路返 回用户的账户,同时关闭原订单数据。仅退款情况下暂不考虑仓库系统变化。如果 发生双方协调不一致情况下,可以申请平台客服介入。在退款订单商户不处理的情 况下,系统需要做限期判断,比如 5 天商户不处理,退款单自动变更同意退款。

订单确认页

Feign远程调用丢失请求头问题

用户访问订单确认页面会来到OrderWebController的toTrade方法,在这之前,我们通过LoginUserInterceptor对用户请求进行拦截,判断用户是否登录,如果用户登陆了会把登录的用户信息放到ThreadLocal中,方便之后的service等使用:

java 复制代码
@Component
public class LoginUserInterceptor implements HandlerInterceptor {

    public static ThreadLocal<MemberRespVo> loginUser = new ThreadLocal<>();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //登录了就放行
        MemberRespVo attribute = (MemberRespVo) request.getSession().getAttribute(AuthServerConstant.LOGIN_USER);
        if (attribute != null) {
            loginUser.set(attribute);
            return true;
        }

        //没登录就去登录
        request.getSession().setAttribute("msg", "请先进行登录");
        response.sendRedirect("http://auth.gulimall.com/login.html");
        return false;
    }

}
java 复制代码
@Configuration
public class OrderWebConfiguration implements WebMvcConfigurer {

    @Autowired
    LoginUserInterceptor interceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor).addPathPatterns("/**");
    }

}

通过 LoginUserInterceptor 拦截器后,来到OrderWebController的toTrade方法,我们会通过orderService.confirmOrder();去获取用户的确认订单信息,我们还得通过Feign的远程调用去获取一些信息,代码如下:

java 复制代码
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        final MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();

        //1、远程查询所有的收货地址列表
        List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
        confirmVo.setAddress(address);

        //2、远程查询购物车所有选中的购物项
        List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
        confirmVo.setItems(items);

        //3、查询用户积分
        Integer integration = memberRespVo.getIntegration();
        confirmVo.setIntegration(integration);

        //4、其他数据自动计算

        CompletableFuture.allOf(getAddressFuture, cartFuture).get();

        return confirmVo;
    }

但是我们会发现cartFeignService.getCurrentUserCartItems()这句代码会返回空结果。这就是因为出现了Feign远程调用丢失请求头问题:

Feign在远程调用之前会构造新请求,在构造请求过程中,会调用很多类型为RequestInterceptor的拦截器

那么我们就可以自定义拦截器,让在Feign构造新请求的时候,通过拦截器让它带上之前请求的请求头信息,就可以解决此问题:

java 复制代码
@Configuration
public class GuliFeignConfig {

    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor() {
        return template -> {
            // 通过RequestContextHolder拿到刚进来的这个请求
            // 通过RequestContextHolder获取到的RequestAttributes是Spring自动设置的
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes != null) {
                HttpServletRequest request = attributes.getRequest(); //老请求
                //同步请求头数据,Cookie
                String cookie = request.getHeader("Cookie");
                template.header("Cookie", cookie); //给新请求同步老请求的header头信息,比如Cookie信息
            }
        };
    }

}

Feign异步情况丢失上下文问题

我们发现通过orderService.confirmOrder();去获取用户的确认订单信息,会调用两个Feign的远程请求,这种情况下,为了提高该接口的响应速度,执行效率,提升性能等,我们应该使用异步编排的方式,让两个Feign远程请求同时执行,加快速度。但如果直接开启异步任务又会有新的问题出现:

我们之前会通过RequestContextHolder拿到刚进来的请求 :ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); ,然后将其设置给每个Feign创建的新请求,这样通过Feign发出的远程调用请求就可以带上用户通过浏览器发送过来的请求头数据,解决了Feign远程调用丢失请求头这个问题

但是RequestContextHolder 内部是通过 ThreadLocal 共享数据的

以前同步调用这两个Feign的远程请求是这样工作的:

发送Feign请求,Feign在构建新请求时会先来到 RequestInterceptor 拦截器,我们在拦截器中会获取并使用 RequestAttributes,由于是同步调用也就是说大家都是同一个线程(Tomcat进来使用同一条线程执行我们的Controller/Service等),使用RequestContextHolder.getRequestAttributes()获取数据时当然可以获取到。

然而直接开启异步任务发送Feign请求,Feign来到 RequestInterceptor 拦截器获取 RequestAttributes 时,由于是不同的线程,当然获取不到之前线程的RequestAttributes对象,也就无法使用了。

所以,开启异步调用Feign时,为了可以获取到之前的请求信息,我们可以这样写:

java 复制代码
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        final MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();

//        List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
//        confirmVo.setAddress(address);
//
        //feign在远程调用之前要构造请求,调用很多的拦截器
        //RequestInterceptor interceptor : requestInterceptors
//        List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
//        confirmVo.setItems(items);


        System.out.println("主线程...." + Thread.currentThread().getId());


        //获取之前的请求
        final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //1、远程查询所有的收货地址列表
            System.out.println("member线程...." + Thread.currentThread().getId());
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            confirmVo.setAddress(address);
        }, executor);

        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //2、远程查询购物车所有选中的购物项
            System.out.println("cart线程...." + Thread.currentThread().getId());
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
        }, executor);

        //3、查询用户积分
        Integer integration = memberRespVo.getIntegration();
        confirmVo.setIntegration(integration);

        //4、其他数据自动计算

        CompletableFuture.allOf(getAddressFuture, cartFuture).get();

        return confirmVo;
    }

通过RequestContextHolder.setRequestAttributes(requestAttributes);给每个线程的ThreadLocal都设置上requestAttributes,这样,当我们开启异步任务调用Feign发送请求,Feign在构建请求的时候,来到拦截器,由于我们已经给当前线程的ThreadLocal设置过requestAttributes了,那么我们就可以正常获取到RequestAttributes并使用了。

下订单

java 复制代码
    //本地事务,在分布式系统下,只能控制住自己的回滚,控制不了其他服务的回滚
    //应该使用分布式事务,但是分布式事务比较复杂,比较复杂的最大原因:网络问题+分布式机器。
//    @GlobalTransactional //    高并发场景,Seata的AT模式不适合
    @Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
        confirmVoThreadLocal.set(vo);
        SubmitOrderResponseVo response = new SubmitOrderResponseVo();
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        response.setCode(0);
        //验证令牌【令牌的获取对比和删除必须保证原子性】
        //0令牌失败 - 1删除成功
//        String redisToken = redisTemplate.opsForValue().get(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId());
//        if(orderToken!=null && orderToken.equals(redisToken)){
//            //令牌验证通过
//            redisTemplate.delete(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId());
//        }else{
//            //不通过
//        }
        //原子验证令牌和删除令牌【处理接口幂等性】
        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(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
        if (result == 0L) {
            //令牌验证失败
            response.setCode(1);
            return response;
        } else {
            //令牌验证成功  //下单:去创建订单,验令牌,验价格,锁库存...

            //1、创建订单,订单项等信息
            OrderCreateTo order = createOrder();
            //2、验价
            if (Math.abs(order.getOrder().getPayAmount().subtract(vo.getPayPrice()).doubleValue()) < 0.01) { //金额对比
                // 3、保存订单
                saveOrder(order);
                //4、库存锁定。只要有异常回滚订单数据。
                // 库存锁定需要的数据:订单号,所有订单项(skuId,skuName,num)

                //4、远程锁库存
                //TODO 问题1:库存调用成功了,但是网络原因,或者其他原因,导致Feign调用超时了,出现异常,此时:订单回滚,库存不会回滚。
                R r = wareFeignService.orderLockStock(getLockVo(order));

                // TODO 为了保证高并发 不使用seata,使用消息队列
                // 方式1、在这儿可以发消息给库存服务让库存服务回滚
                // 方式2、库存服务本身也可以使用自动解锁模式(使用延时队列实现定时任务)

                if (r.getCode() == 0) {
                    //锁成功了
                    response.setOrder(order.getOrder());

                    //TODO 问题2:假如还有个远程扣减积分服务
                    // 该服务出异常 :订单会回滚;由于库存服务已经成功的远程执行,不会回滚。
                    int i = 10/0; //模拟扣减积分出异常

                    //TODO 清除购物车已经下单的商品

                    return response;
                } else {
                    //锁定失败
                    response.setCode(3);
                    String msg = (String) r.get("msg");
                    throw new NoStockException(msg);
                }
            } else {
                response.setCode(2);
                return response;
            }
        }
    }

关订单

java 复制代码
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class MyMQConfig {

    //@Bean Binding,Queue,Exchange

    /**
     * 容器中的 Binding,Queue,Exchange 都会自动创建(RabbitMQ没有的情况)
     * RabbitMQ中已有的话 @Bean中声明属性发生了变化也不会覆盖
     */
    @Bean
    public Queue orderDelayQueue() {
        Map<String,Object> arguments = new HashMap<>();
        /**
         * x-dead-letter-exchange: order-event-exchange
         * x-dead-letter-routing-key: order.release.order
         * x-message-ttl: 60000
         */
        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分钟
        //String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        return new Queue("order.delay.queue", true, false, false,arguments);
    }

    @Bean
    public Queue orderReleaseOrderQueue() {
        return new Queue("order.release.order.queue", true, false, false);
    }

    @Bean
    public Exchange orderEventExchange() {
        //String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
       return new TopicExchange("order-event-exchange",true,false);
    }

    @Bean
    public Binding orderCreateOrderBinding() {
        //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);
    }
}
java 复制代码
    @Transactional
    @Override
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
        //验证令牌【令牌的获取对比和删除必须保证原子性】
        if (result == 0L) {
            //令牌验证失败
            xxx
        } else {
            //令牌验证成功  //下单:去创建订单,验令牌,验价格,锁库存...

            //1、创建订单,订单项等信息
            OrderCreateTo order = createOrder();
            //2、验价
            if (Math.abs(order.getOrder().getPayAmount().subtract(vo.getPayPrice()).doubleValue()) < 0.01) { //金额对比
                // 3、保存订单
                saveOrder(order);
                // 4、锁定库存
                R r = wareFeignService.orderLockStock(getLockVo(order));
                if (r.getCode() == 0) {
                    // 锁成功了
                    // 订单创建成功发送消息给MQ
                    rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder());
					xxx
                } else {
                    //锁定失败xxx
                }
            } else {
            	xxx
            }
        }
    }
java 复制代码
@RabbitListener(queues = "order.release.order.queue")
@Service
public class OrderCloseListener {
    @Autowired
    OrderService orderService;
    @RabbitHandler
    public void listener(OrderEntity entity, Channel channel, Message message) throws IOException {
        System.out.println("收到过期的订单信息:准备关闭订单"+entity.getOrderSn()+"==>"+entity.getId());
        try{
            orderService.closeOrder(entity);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        }catch (Exception e){
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }
}
java 复制代码
    @Override
    public void closeOrder(OrderEntity entity) {
        //查询当前这个订单的最新状态
        OrderEntity orderEntity = this.getById(entity.getId());
        if (Objects.equals(orderEntity.getStatus(), OrderStatusEnum.CREATE_NEW.getCode())) {
            //关单
            OrderEntity update = new OrderEntity();
            update.setId(entity.getId());
            update.setStatus(OrderStatusEnum.CANCLED.getCode());
            this.updateById(update);
        }
    }

我们害怕出现如下图所示问题,所以我们关闭订单后,应该主动发一个消息order.release.other,让解锁库存服务去解锁库存

java 复制代码
    /**
     * 订单释放直接和库存释放进行绑定
     */
    @Bean
    public Binding orderReleaseOtherBinding() {
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.other.#",
                null);
    }
java 复制代码
    @Override
    public void closeOrder(OrderEntity entity) {
        //查询当前这个订单的最新状态
        OrderEntity orderEntity = this.getById(entity.getId());
        if (Objects.equals(orderEntity.getStatus(), OrderStatusEnum.CREATE_NEW.getCode())) {
            //关单
            OrderEntity update = new OrderEntity();
            update.setId(entity.getId());
            update.setStatus(OrderStatusEnum.CANCLED.getCode());
            this.updateById(update);
            OrderTo orderTo = new OrderTo();
            BeanUtils.copyProperties(orderEntity, orderTo);
            // 主动发一个消息`order.release.other`,让解锁库存服务去解锁库存
            rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo);
        }
    }

解锁库存

用户下单成功后先锁定库存,锁库存图示逻辑:

库存锁定成功,如果订单回滚,为了保证最终一致性,需要库存自动解锁

  • 库存调用成功了,但是网络原因,或者其他原因,导致Feign调用超时了,此时:订单回滚,库存不会回滚。

  • 假如还有个远程扣减积分服务是在订单服务调用成功后调用的,该服务出异常 :订单会回滚;由于库存服务已经成功的远程执行,不会回滚。

为了保证高并发不使用seata,使用定时任务让库存服务本身自动解锁

但由于定时任务(比如Spring的 schedule 定时任务轮询数据库):消耗系统内存、增加了数据库的压力、存在较大的时间误差;

所以使用RabbitMQ的延时队列,使用延时队列,为了方便追溯,可以保存库存工作单的详情。

创建业务队列/路由器等:

java 复制代码
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class GulimallWareMQConfig {
//    @RabbitListener(queues = "stock.release.stock.queue")
//    public void  handle(Message message){
//    }

    @Bean
    public Exchange stockEventExchange() {
        return new TopicExchange("stock-event-exchange", true, false);
    }

    @Bean
    public Queue stockReleaseStockQueue() {
        return new Queue("stock.release.stock.queue", true, false, false);
    }

    @Bean
    public Queue stockDelayQueue() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-dead-letter-exchange", "stock-event-exchange");
        args.put("x-dead-letter-routing-key", "stock.release");
        args.put("x-message-ttl", 120000); //测试期间设置2分钟
        return new Queue("stock.delay.queue", true, false, false, args);
    }

    @Bean
    public Binding stockReleaseBinding() {
        return new Binding("stock.release.stock.queue", Binding.DestinationType.QUEUE, "stock-event-exchange", "stock.release.#", null);
    }

    @Bean
    public Binding stockLockedBinding() {
        return new Binding("stock.delay.queue", Binding.DestinationType.QUEUE, "stock-event-exchange", "stock.locked", null);
    }
}

库存解锁逻辑:

java 复制代码
    /**
     * 为某个订单锁定库存
     *
     * @Transactional(rollbackFor = NoStockException.class)
     * 默认只要是运行时异常都会回滚
     *
     * 库存解锁的场景
     * 1)、下订单成功,订单过期没有支付被系统自动取消、被用户手动取消。都要解锁库存
     * 2)、下订单成功,库存锁定成功,接下来的其他业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁。
     */
    @Transactional
    @Override
    public Boolean orderLockStock(WareSkuLockVo vo) {
        /**
         * 保存库存工作单的详情。
         * 方便追溯。
         */
        WareOrderTaskEntity taskEntity = new WareOrderTaskEntity();
        taskEntity.setOrderSn(vo.getOrderSn());
        orderTaskService.save(taskEntity);

        //1、按照下单的收货地址,找到一个就近仓库,锁定库存。
        //1、找到每个商品在哪个仓库都有库存
        List<OrderItemVo> locks = vo.getLocks();

        List<SkuWareHasStock> collect = locks.stream().map(item -> {
            SkuWareHasStock stock = new SkuWareHasStock();
            Long skuId = item.getSkuId();
            stock.setSkuId(skuId);
            stock.setNum(item.getCount());
            //查询这个商品在哪里有库存
            List<Long> wareIds = wareSkuDao.listWareIdHasSkuStock(skuId);
            stock.setWareId(wareIds);
            return stock;
        }).collect(Collectors.toList());

        //2、锁定库存
        for (SkuWareHasStock hasStock : collect) {
            boolean skuStocked = false;
            Long skuId = hasStock.getSkuId();
            List<Long> wareIds = hasStock.getWareId();
            if (wareIds == null || wareIds.size() == 0) {
                //没有任何仓库有这个商品的库存
                throw new NoStockException(skuId);
            }
            //1、如果每一个商品都锁定成功,那么就已经将当前商品锁定了几件的工作单详情记录发给了MQ
            //2、有一个商品锁定失败,库存就会回滚。发送出去的消息,即使要解锁记录,去数据库查不到id,就不用解锁库存
            for (Long wareId : wareIds) {
                //成功就返回1,否则就是0
                Long count = wareSkuDao.lockSkuStock(skuId, wareId, hasStock.getNum());
                if (count == 1) {
                    skuStocked = true;
                    //告诉MQ库存锁定成功
                    WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity(null, skuId, null, hasStock.getNum(), taskEntity.getId(), wareId, 1);
                    orderTaskDetailService.save(entity);
                    StockLockedTo lockedTo = new StockLockedTo();
                    lockedTo.setId(taskEntity.getId());
                    StockDetailTo stockDetailTo = new StockDetailTo();
                    BeanUtils.copyProperties(entity, stockDetailTo);
                    //只发id不行,防止回滚以后找不到数据
                    lockedTo.setDetail(stockDetailTo);
//                    rabbitTemplate
                    rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", lockedTo);
                    break;
                } else {
                    //当前仓库锁失败,重试下一个仓库
                }
            }
            if (skuStocked == false) {
                //当前商品所有仓库都没有锁住
                throw new NoStockException(skuId);
            }
        }

        //3、肯定全部都是锁定成功的
        return true;
    }
java 复制代码
@Service
@RabbitListener(queues = "stock.release.stock.queue")
public class StockReleaseListener {

    @Autowired
    WareSkuService wareSkuService;

    @RabbitHandler
    public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException {
        System.out.println("收到解锁库存的消息...");
        try{
            //当前消息是否被第二次及以后(重新)派发过来了。
//            Boolean redelivered = message.getMessageProperties().getRedelivered();
            wareSkuService.unlockStock(to);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        }catch (Exception e){
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }

    @RabbitHandler
    public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException {
        System.out.println("订单关闭准备解锁库存...");
        try{
            wareSkuService.unlockStock(orderTo);
            channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        }catch (Exception e){
            channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
        }
    }

}
java 复制代码
    @Override
    public void unlockStock(StockLockedTo to) {
        StockDetailTo detail = to.getDetail();
        Long detailId = detail.getId();
        //解锁
        //查询数据库关于这个订单的锁定库存信息。
        //  有:证明库存锁定成功了
        //    解锁:订单情况。
        //          1、没有这个订单。必须解锁
        //          2、有这个订单。不是解锁库存。
        //                订单状态: 已取消:解锁库存
        //                          没取消:不能解锁
        //  没有:库存锁定失败了,库存回滚了。这种情况无需解锁
        WareOrderTaskDetailEntity byId = orderTaskDetailService.getById(detailId);
        if (byId != null) {
            Long id = to.getId();
            WareOrderTaskEntity taskEntity = orderTaskService.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) { //当前库存工作单详情,状态1 已锁定但是未解锁才可以解锁
                        unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId);
                    }
                }
            } else {
                //消息拒绝以后重新放到队列里面,让别人继续消费解锁。
                throw new RuntimeException("远程服务失败");
            }
        } else {
            //无需解锁
        }
    }

    private void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) {
        //库存解锁
        wareSkuDao.unlockStock(skuId, wareId, num);
        //更新库存工作单的状态
        WareOrderTaskDetailEntity entity = new WareOrderTaskDetailEntity();
        entity.setId(taskDetailId);
        entity.setLockStatus(2);//变为已解锁
        orderTaskDetailService.updateById(entity);
    }
java 复制代码
    //防止订单服务卡顿,导致订单状态消息一直改不了,库存消息优先到期。查订单状态新建状态,什么都不做就走了。
    //导致卡顿的订单,永远不能解锁库存
    @Transactional
    @Override
    public void unlockStock(OrderTo orderTo) {
        String orderSn = orderTo.getOrderSn();
        //查一下最新库存的状态,防止重复解锁库存
        WareOrderTaskEntity task = orderTaskService.getOrderTaskByOrderSn(orderSn);
        Long id = task.getId();
        //按照工作单找到所有没有解锁的库存,进行解锁
        List<WareOrderTaskDetailEntity> entities = orderTaskDetailService.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());
        }
    }

收单

  • 订单在支付页,不支付,订单过期了才支付,订单状态改为已支付了,但是库存解锁了。
    • 使用支付宝自动收单功能解决。只要一段时间不支付,就不能支付了。
  • 由于时延等问题。订单解锁完成,正在解锁库存的时候,异步通知才到
    • 订单解锁,手动调用收单
  • 网络阻塞问题,订单支付成功的异步通知一直不到达
    • 查询订单列表时,ajax获取当前未支付的订单状态,查询订单状态时,再获取一下支付宝此订单的状态
  • 其他各种问题
    • 每天晚上闲时下载支付宝对账单,一一进行对账

加密-对称加密---不安全

加密解密使用同一把钥匙

加密-非对称加密

加密解密使用不同钥匙

除非你知道完整的4把钥匙,否则你就不能模拟完整的通信过程

参考

雷丰阳: Java项目《谷粒商城》Java架构师 | 微服务 | 大型电商项目.


本文完,感谢您的关注支持!


相关推荐
飞的肖26 分钟前
前端使用 Element Plus架构vue3.0实现图片拖拉拽,后等比压缩,上传到Spring Boot后端
前端·spring boot·架构
miss writer31 分钟前
Redis分布式锁释放锁是否必须用lua脚本?
redis·分布式·lua
m0_7482548838 分钟前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl
小屁不止是运维44 分钟前
麒麟操作系统服务架构保姆级教程(五)NGINX中间件详解
linux·运维·服务器·nginx·中间件·架构
程序猿进阶1 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
字节程序员2 小时前
Jmeter分布式压力测试
分布式·jmeter·压力测试
Hacker_Fuchen2 小时前
天融信网络架构安全实践
网络·安全·架构
ProtonBase2 小时前
如何从 0 到 1 ,打造全新一代分布式数据架构
java·网络·数据库·数据仓库·分布式·云原生·架构
时时刻刻看着自己的心2 小时前
clickhouse分布式表插入数据不用带ON CLUSTER
分布式·clickhouse