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

news2024/12/23 9:58:20

持续学习&持续更新中…

守破离


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

  • 订单中心
  • 订单信息
    • 用户信息
    • 订单基础信息
    • 商品信息
    • 优惠信息
    • 支付信息
    • 物流信息
  • 订单状态
  • 订单流程
    • 订单创建与支付
    • 逆向流程
  • 订单确认页
  • Feign远程调用丢失请求头问题
  • Feign异步情况丢失上下文问题
  • 下订单
  • 关订单
  • 解锁库存
  • 收单
  • 加密-对称加密—不安全
  • 加密-非对称加密
  • 参考

订单中心

电商系统涉及到 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等使用:

@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;
    }

}
@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的远程调用去获取一些信息,代码如下:

    @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构造新请求的时候,通过拦截器让它带上之前请求的请求头信息,就可以解决此问题:

在这里插入图片描述

@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时,为了可以获取到之前的请求信息,我们可以这样写:

    @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并使用了。

下订单

在这里插入图片描述

    //本地事务,在分布式系统下,只能控制住自己的回滚,控制不了其他服务的回滚
    //应该使用分布式事务,但是分布式事务比较复杂,比较复杂的最大原因:网络问题+分布式机器。
//    @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;
            }
        }
    }

关订单

在这里插入图片描述

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);
    }
}

    @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
            }
        }
    }
@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);
        }
    }
}
    @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,让解锁库存服务去解锁库存

在这里插入图片描述

    /**
     * 订单释放直接和库存释放进行绑定
     */
    @Bean
    public Binding orderReleaseOtherBinding() {
        return new Binding("stock.release.stock.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.other.#",
                null);
    }
    @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的延时队列,使用延时队列,为了方便追溯,可以保存库存工作单的详情。

在这里插入图片描述

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

在这里插入图片描述

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);
    }
}

库存解锁逻辑:


    /**
     * 为某个订单锁定库存
     *
     * @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;
    }
@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);
        }
    }

}
    @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);
    }
    //防止订单服务卡顿,导致订单状态消息一直改不了,库存消息优先到期。查订单状态新建状态,什么都不做就走了。
    //导致卡顿的订单,永远不能解锁库存
    @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架构师 | 微服务 | 大型电商项目.


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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1931320.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Qt第十一章 其他控件

其他控件 文章目录 其他控件按钮组项目小部件输入控件显示控件容器 按钮组 命令链接按钮 对话框按钮盒子 添加基础按钮 改变排列方向 项目小部件 列表控件List Widget 也可以通过代码添加 // 添加ui->listWidget->addItem("你好啊");ui->listWidge…

数据链路层重点协议

目录 一、以太网 二、MTU 1、MTU对IP协议的影响 2、MTU对UDP的影响 3、MTU对TCP协议的影响 三、ARP协议 1、作用&#xff1a;建立主机IP地址和MAC地址的映射关系 2、工作流程 一、以太网 以太网不是一种具体的网络&#xff0c;而是一种技术标准。既包含了数据链路层的…

数据库第9

安装redis&#xff0c;启动客户端、验证 C:\Windows\System32>redis-cli string类型数据的命令操作&#xff1a; 设置键值 set k1 12 读取键值 get k1 ​ 数值类型自增1 incr k1 数值类型自减1 decr k1 查看值的长度 STRLEN k1 list类型数据的命令操作&#xff1a; &#x…

[MySQL][内置函数][日期函数][字符串函数][数学函数]详细讲解

目录 1.日期函数1.基础语法2.示例13.示例2 2.字符串函数1.基础语法2.示例 3.数学函数1.基础语法2.示例 4.其他函数 1.日期函数 1.基础语法 日期时间在MYSQL中是区分开的 日期&#xff1a;年月日时间&#xff1a;时分秒 获得年月日select current_date();----------------| cur…

Open3D 最小二乘法拟合点云平面

目录 一、概述 1.1最小二乘法原理 1.2实现步骤 1.3应用场景 二、代码实现 2.1关键函数 2.2完整代码 三、实现效果 3.1原始点云 3.2matplotlib可视化 3.3平面拟合方程 前期试读&#xff0c;后续会将博客加入该专栏&#xff0c;欢迎订阅 Open3D点云算法与点云深度学习…

opencv学习:图像视频的读取截取部分图像数据颜色通道提取合并颜色通道边界填充数值计算图像融合

一、计算机眼中的图像 1.图像操作 构成像素点的数字在0~255之间 RGB叫做图像的颜色通道 h500&#xff0c;w500 2.灰度图像 3. 彩色图像 4.图像的读取 5.视频的读取 cv2.VideoCapture()--在OpenCV中&#xff0c;可以使用VideoCapture来读取视频文件&#xff0c;或是摄像头数…

前缀和算法——部分OJ题详解

&#xff08;文章的题目解释可能存在一些问题&#xff0c;欢迎各位小伙伴私信或评论指点&#xff08;双手合十&#xff09;&#xff09; 关于前缀和算法 前缀和算法解决的是“快速得出一个连续区间的和”&#xff0c;以前求区间和的时间复杂度是O(N)&#xff0c;使用前缀和可…

关于springboot的@DS(““)多数据源的注解无法生效的原因

对于com.baomidou.dynamic.datasource.annotation的DS注解&#xff0c;但凡有一个AOP的修改都会影响到多数据源无法生效的问题&#xff0c;本次我是添加了方法上添加了Transactional&#xff0c;例如下图&#xff1a; 在方法上写了这个注解&#xff0c;会影响到DS("db2&qu…

MODEL4高性价比工业级HMI芯片在喷码机解决方案中的应用

一、概述 随着工业自动化与智能化的发展&#xff0c;喷码机作为标识设备在各行各业中扮演着至关重要的角色。为满足市场对于高效、精准、灵活喷码的需求&#xff0c;我们推出了基于MODEL4工业级HMI芯片的喷码机解决方案。 该方案集成了高性能国产嵌入式64位RISC-V内核芯片组&…

<数据集>铁轨缺陷检测数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;844张 标注数量(xml文件个数)&#xff1a;844 标注数量(txt文件个数)&#xff1a;844 标注类别数&#xff1a;3 标注类别名称&#xff1a;[Spalling, Squat, Wheel Burn] 序号类别名称图片数框数1Spalling3315522…

集线器、交换机、路由器的区别,冲突域、广播域

冲突域 定义&#xff1a;同一时间内只能有一台设备发送信息的范围。 分层&#xff1a;基于OSI模型的第一层物理层。 广播域 定义&#xff1a;如果某个站点发出一个广播信号&#xff0c;所有能接受到这个信号的设备的范围称为一个广播域。 分层&#xff1a;基于OSI模型的第二…

绿色水利,智慧未来:数字孪生技术在智慧水库建设中的应用,助力实现水资源的可持续利用与环境保护的双赢

本文关键词&#xff1a;智慧水利、智慧水利工程、智慧水利发展前景、智慧水利技术、智慧水利信息化系统、智慧水利解决方案、数字水利和智慧水利、数字水利工程、数字水利建设、数字水利概念、人水和协、智慧水库、智慧水库管理平台、智慧水库建设方案、智慧水库解决方案、智慧…

【Python】open()函数的全面解析:如何读取和写入文件

文章目录 1. 基本用法&#xff1a;打开文件2. 不同模式的使用3. 文件读取方法3.1 readline()方法3.2 readlines()方法 4. 上下文管理器5. 错误处理6. 小结 在编程过程中&#xff0c;文件操作是一个非常常见的任务&#xff0c;而Python的open()函数是进行文件操作的基础。通过op…

Sparse4D-v3:稀疏感知的性能优化及端到端拓展

极致的感知性能与极简的感知pipeline一直是牵引我们持续向前的目标。为了实现该目标&#xff0c;打造一个性能优异的端到端感知模型是重中之重&#xff0c;充分发挥深度神经网络数据闭环的作用&#xff0c;才能打破当前感知系统的性能上限&#xff0c;解决更多的corner case&am…

分布式 I/O 系统Modbus TCP 耦合器BL200

BL200 耦合器是一个数据采集和控制系统&#xff0c;基于强大的 32 位微处理器设计&#xff0c;采用 Linux 操作系统&#xff0c;可以快速接入现场 PLC、SCADA 以及 ERP 系统&#xff0c; 内置逻辑控制、边缘计算应用&#xff0c;支持标准 Modbus TCP 服务器通讯&#xff0c;以太…

Ubuntu Desktop Docker 配置代理

Ubuntu Desktop Docker 配置代理 主要解决 docker pull 拉取不了镜像问题. Docker Desktop 配置代理 这个比较简单, 直接在 Docker Desktop 里设置 Proxies, 示例如下: http://127.0.0.1:7890 Docker Engine 配置代理 1.Docker Engine 使用下面配置文件即可, root 用户可…

Java面试八股之简述单例redis并发承载能力

简述单例redis并发承载能力 单例Redis实例的并发承载上限受到多种因素的影响&#xff0c;包括但不限于硬件性能、网络条件、数据集大小、操作类型以及Redis自身的配置。以下是几个关键因素的详细说明&#xff1a; 硬件性能&#xff1a; CPU&#xff1a;Redis主要依赖于CPU的…

服务器基础1

服务器基础复习01 1.环境部署 系统&#xff1a;华为欧拉系统 网络简单配置nmtui 因为华为欧拉系统密码需要复杂度 所以我们可以进入后更改密码 echo 123 | passwd --stdin root也可以 echo "root:123" | chpasswd2.关闭防火墙&#xff0c;禁用SElinux 首先先关…

BlueToothLE 拓展中writeBytesWithResponse与writeBytes有什么区别?

writeBytesWithResponse与writeBytes有什么区别&#xff1f; 根据文档&#xff0c;有WithRespon的&#xff0c;会触发一个 BytesWritten 事件&#xff0c;另一个不触发这个事件&#xff1a;App Inventor 2 低功耗蓝牙 BlueToothLE 拓展 App Inventor 2 中文网

SQl server 练习3

课后作业 在homework库下执行&#xff1a; CREATE TABLE user_profile_2 ( id int NOT NULL, device_id int NOT NULL, gender varchar(14) NOT NULL, age int , university varchar(32) NOT NULL, gpa float, active_days_within_30 float, question_cnt float, answer_cnt fl…