【大型电商项目开发】订单功能实现(拦截器、feign丢失请求头、接口幂等性)-55

导读:本篇文章讲解 【大型电商项目开发】订单功能实现(拦截器、feign丢失请求头、接口幂等性)-55,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

一:订单概念

1.1 订单中心

  电商系统涉及到 3 流,分别时信息流,资金流,物流,而订单系统作为中枢将三者有机的集合起来。订单模块是电商系统的枢纽,在订单这个环节上需求获取多个模块的数据和信息,同时对这些信息进行加工处理后流向下个环节,这一系列就构成了订单的信息流通。

1.2 订单构成

在这里插入图片描述

1.2.1 用户信息

用户信息包括用户账号、用户等级、用户的收货地址、收货人、收货人电话等组成,用户账户需要绑定手机号码,但是用户绑定的手机号码不一定是收货信息上的电话。用户可以添加多个收货信息,用户等级信息可以用来和促销系统进行匹配,获取商品折扣,同时用户等级还可以获取积分的奖励等

1.2.2 订单基础信息

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

1.2.3 商品信息

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

1.2.4 优惠信息

优惠信息记录用户参与的优惠活动,包括优惠促销活动,比如满减、满赠、秒杀等,用户使用的优惠券信息,优惠券满足条件的优惠券需要默认展示出来,具体方式已在之前的优惠券篇章做过详细介绍,另外还虚拟币抵扣信息等进行记录。
为什么把优惠信息单独拿出来而不放在支付信息里面呢?
因为优惠信息只是记录用户使用的条目,而支付信息需要加入数据进行计算,所以做为区分。

1.2.5 支付信息

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

1.2.6 物流信息

物流信息包括配送方式,物流公司,物流单号,物流状态,物流状态可以通过第三方接口来获取和向用户展示物流每个状态节点。

1.3 订单状态

  1. 待付款
    用户提交订单后,订单进行预下单,目前主流电商网站都会唤起支付,便于用户快速完成支付,需要注意的是待付款状态下可以对库存进行锁定,锁定库存需要配置支付超时时间,超时后将自动取消订单,订单变更关闭状态。
  2. 已付款/待发货
    用户完成订单支付,订单系统需要记录支付时间,支付流水单号便于对账,订单下放到 WMS系统,仓库进行调拨,配货,分拣,出库等操作。
  3. 待收货/已发货
    仓储将商品出库后,订单进入物流环节,订单系统需要同步物流信息,便于用户实时知悉物品物流状态
  4. 已完成
    用户确认收货后,订单交易完成。后续支付侧进行结算,如果订单存在问题进入售后状态
  5. 已取消
    付款之前取消订单。包括超时未付款或用户商户取消订单都会产生这种订单状态。
  6. 售后中
    用户在付款后申请退款,或商家发货后用户申请退换货。售后也同样存在各种状态,当发起售后申请后生成售后订单,售后订单状态为待审核,等待商家审核,商家审核通过后订单状态变更为待退货,等待用户将商品寄回,商家收货后订单状态更新为待退款状态,退款到用户原账户后订单状态更新为售后成功。

1.4 订单流程

订单流程是指从订单产生到完成整个流转的过程,从而行程了一套标准流程规则。而不同的产品类型或业务类型在系统中的流程会千差万别,比如上面提到的线上实物订单和虚拟订单的流程,线上实物订单与 O2O 订单等,所以需要根据不同的类型进行构建订单流程。不管类型如何订单都包括正向流程和逆向流程,对应的场景就是购买商品和退换货流程,正向流程就是一个正常的网购步骤:订单生成–>支付订单–>卖家发货–>确认收货–>交易成功。而每个步骤的背后,订单是如何在多系统之间交互流转的,可概括如下图
在这里插入图片描述

1.4.1 订单创建与支付

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

1.4.2 逆向流程

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

二:订单登录拦截

因为订单登录的controller必须确保处于登录状态,所以需要添加拦截器进行校验

2.1 新建LoginUserInterceptor拦截器

@Component
public class LoginUserInterceptor implements HandlerInterceptor {
    public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取登录的用户信息
        MemberResponseVo attribute = (MemberResponseVo) request.getSession().getAttribute(LOGIN_USER);
        if (attribute != null) {
            //把登录后用户的信息放在ThreadLocal里面进行保存
            loginUser.set(attribute);
            return true;
        } else {
            //未登录,返回登录页面
            session.setAttribute("msg", "请先进行登录");
            response.sendRedirect("http://auth.gulimall.com/login.html");
            return false;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

2.2 新建OrderWebConfig配置类,用于处理拦截器

@Configuration
public class OrderWebConfig implements WebMvcConfigurer {

    @Autowired
    private LoginUserInterceptor loginUserInterceptor;

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

三:查询订单信息

3.1 创建实体类

3.1.1 建立订单确认页信息实体类OrderConfirmVo

/**
 * 订单确认页信息实体类
 * @author
 */
public class OrderConfirmVo {
    /**
     * 会员收货地址列表
     */
    @Getter @Setter
    private List<MemberAddressVo> address;

    /**
     * 所有选中的购物项
     */
    @Getter @Setter
    private List<OrderItemVo> items;

    /**
     * 优惠券信息
     */
    @Getter @Setter
    private Integer integration;

    /**
     * 订单的防重令牌
     */
    @Getter @Setter
    private String orderToken;

    /**
     *订单总额
     */
    public BigDecimal getTotal() {
        BigDecimal sumTotal = new BigDecimal("0");
        if(!CollectionUtils.isEmpty(items)){
            for (OrderItemVo item : items) {
                //价格*数量=总价
                BigDecimal multiply = item.getPrice().multiply(BigDecimal.valueOf(item.getCount()));
                sumTotal = sumTotal.add(multiply);
            }

        }
        return sumTotal;
    }

    /**
     * 应付价格
     */
    public BigDecimal getPayPrice() {
        return getTotal();
    }

3.1.2 建立会员收货地址实体类

/**
 * 会员收货地址
 */
@Data
public class MemberAddressVo {
    private Long id;
    /**
     * member_id
     */
    private Long memberId;
    /**
     * 收货人姓名
     */
    private String name;
    /**
     * 电话
     */
    private String phone;
    /**
     * 邮政编码
     */
    private String postCode;
    /**
     * 省份/直辖市
     */
    private String province;
    /**
     * 城市
     */
    private String city;
    /**
     * 区
     */
    private String region;
    /**
     * 详细地址(街道)
     */
    private String detailAddress;
    /**
     * 省市区代码
     */
    private String areacode;
    /**
     * 是否默认
     */
    private Integer defaultStatus;
}

3.1.3 新建购物项实体类OrderItemVo

/**
 * 购物项
 */
@Data
public class OrderItemVo {
    /**
     * 商品id
     */
    private Long skuId;
    /**
     * 商品标题
     */
    private String title;
    /**
     * 商品图片
     */
    private String image;
    /**
     * 商品属性
     */
    private List<String> skuAttr;
    /**
     * 商品价格
     */
    private BigDecimal price;
    /**
     * 商品总数
     */
    private Integer count;
    /**
     * 商品小计
     */
    private BigDecimal totalPrice;
}

3.2 编写service方法

3.2.1 编写OrderServiceImpl的方法

@Override
    public OrderConfirmVo confirmOrder() {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        //通过threadLocal获取用户的基本信息
        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.其他数据自动计算
        //5.订单的防重令牌
        return confirmVo;
    }

3.2.2 编写getAddress方法

feign:远程调用

@FeignClient("gulimail-member")
public interface MemberFeignService {

    /**
     * 获取会员地址信息
     * @param memberId
     * @return
     */
    @GetMapping("/member/memberreceiveaddress/{memberId}/addresses")
    List<MemberAddressVo> getAddress(@PathVariable("memberId") Long memberId);
}
    @Override
    public List<MemberReceiveAddressEntity> getAddress(Long memberId) {
        return this.list(new QueryWrapper<MemberReceiveAddressEntity>().eq("member_id",memberId));
    }

3.2.3 编写getCurrentUserCartItems方法

@FeignClient("gulimail-cart")
public interface CartFeignService {
    /**
     * 获取当前登录用户的购物项
     * @return
     */
    @GetMapping("/currentUserCartItems")
    List<OrderItemVo> getCurrentUserCartItems();
}
@Override
    public List<CartItem> getUserCartItems() {
        UserInfoTo userInfoTo = CartInterceptor.threadLocal.get();
        List<CartItem> cartItems = null;
        //判断是否登录
        if (userInfoTo != null) {
            String cartKey = CART_PREFIX + userInfoTo.getUserId();
            cartItems = getCartItems(cartKey);
            //获取所有被选中的购物项
            if (!CollectionUtils.isEmpty(cartItems)) {
                return cartItems.stream()
                        .filter(CartItem::getCheck)
                        .map(item -> {
                            BigDecimal price = productFeignService.getPrice(item.getSkuId());
                            //更新为最新价格
                            item.setPrice(price);
                            return item;
                        }).collect(Collectors.toList());
            }
        }
        return null;
    }

四:feign远程调用丢失请求头问题

在这里插入图片描述
处理方案:在新建request的时候,添加feign远程调用的拦截器

4.1 新建feign的配置类,用来处理远程调用时cookie丢失问题

@Configuration
public class GuliFeignConfig {
    /**
     * 给容器中添加拦截器
     * @return
     */
    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor(){
        return new RequestInterceptor(){
            @Override
            public void apply(RequestTemplate template) {
                //1.使用RequestContextHolder获取刚进来的请求
                ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
                //老请求
                assert attributes != null;
                HttpServletRequest request = attributes.getRequest();
                //同步请求数据cookie
                String cookie = request.getHeader("Cookie");
                //给新请求同步老请求的cookie
                template.header("Cookie",cookie);
            }
        };
    }
}

注:feign远程调用时,请求头必须从老请求中获取过来

五:使用多线程异步获取订单确认信息

5.1 配置多线程

5.1.1 添加多线程配置类

@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
@Configuration
public class MyThreadConfig {
    @Bean
    public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool) {
        return new ThreadPoolExecutor(
                pool.getCoreSize(),
                pool.getMaxSize(),
                pool.getKeepAliveTime(),
                TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(100000),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()
        );
    }
}

5.1.2 配置核心线程数

@ConfigurationProperties(prefix = "gulimail.thread")
@Component
@Data
public class ThreadPoolConfigProperties {

    private Integer coreSize;

    private Integer maxSize;

    private Integer keepAliveTime;


}

5.1.3 配置文件中配置核心参数

gulimail.thread.core-size=20
gulimail.thread.max-size=200
gulimail.thread.keep-alive-time=10

5.2 使用异步进行远程调用

    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        //通过threadLocal获取用户的基本信息
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            //1.远程查询所有的收获列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            confirmVo.setAddress(address);
        }, executor);
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //2.远程查询购物车所选中的购物项
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
        }, executor);
        //feign在远程调用之前会构造请求,构造请求会调用很多拦截器
        //3.查询优惠券信息、用户积分
        Integer integration = memberRespVo.getIntegration();
        confirmVo.setIntegration(integration);
        //4.其他数据自动计算
        //5.订单的防重令牌
        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        return confirmVo;
    }

5.3 使用异步远程调用丢失上下文的问题

在这里插入图片描述
问题:使用异步调用address和cart的过程中调用了另外一个线程,导致threadLocal不能共享数据了

解决方案

使用RequestContextHolder在每个线程中将信息重新赋值

 @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        //通过threadLocal获取用户的基本信息
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        //获取之前的请求
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            //每个线程都获取之前的请求
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //1.远程查询所有的收获列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            confirmVo.setAddress(address);
        }, executor);
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //每个线程都获取之前的请求
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //2.远程查询购物车所选中的购物项
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
        }, executor).thenRunAsync(()->{
            //3.查询库存信息
            List<OrderItemVo> items = confirmVo.getItems();
            if(!CollectionUtils.isEmpty(items)){
                List<Long> skuIds = items.stream().map(OrderItemVo::getSkuId).collect(Collectors.toList());
                R skusHasStock = wmsFeignService.getSkusHasStock(skuIds);
                List<SkuStockVo> data = skusHasStock.getData(new TypeReference<List<SkuStockVo>>() {
                });
                if(!CollectionUtils.isEmpty(data)){
                    Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                    confirmVo.setStocks(map);
                }
            }
        }, executor);
        //feign在远程调用之前会构造请求,构造请求会调用很多拦截器
        //3.查询优惠券信息、用户积分
        Integer integration = memberRespVo.getIntegration();
        confirmVo.setIntegration(integration);
        //4.其他数据自动计算
        //5.订单的防重令牌
        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        return confirmVo;
    }

六:接口幂等性

6.1 什么是幂等性

  接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用;比如说支付场景,用户购买了商品支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条,这就没有保证接口的幂等性。

6.2 哪些情况需要防止

  • 用户多次点击按钮
  • 用户页面回退再次提交
  • 微服务互相调用,由于网络问题,导致请求失败。feign 触发重试机制
  • 其他业务情况

6.3 什么情况下需要幂等

以 SQL 为例,有些操作是天然幂等的。

  • SELECT * FROM table WHER id=?,无论执行多少次都不会改变状态,是天然的幂等。
  • UPDATE tab1 SET col1=1 WHERE col2=2,无论执行成功多少次状态都是一致的,也是幂等操作。
  • delete from user where userid=1,多次操作,结果一样,具备幂等性
  • insert into user(userid,name) values(1,‘a’) 如 userid 为唯一主键,即重复操作上面的业务,只
    会插入一条用户数据,具备幂等性。
  • UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次执行的结果都会发生变化,不是幂等的。
  • insert into user(userid,name) values(1,‘a’) 如 userid 不是主键,可以重复,那上面业务多次操
    作,数据都会新增多条,不具备幂等性。

6.4 幂等解决方案

6.4.1 token 机制

  • 1、服务端提供了发送 token 的接口。我们在分析业务的时候,哪些业务是存在幂等问题的,就必须在执行业务前,先去获取 token,服务器会把 token 保存到 redis 中。
  • 2、然后调用业务接口请求时,把 token 携带过去,一般放在请求头部。
  • 3、服务器判断 token 是否存在 redis 中,存在表示第一次请求,然后删除 token,继续执行业
    务。
  • 4、如果判断 token 不存在 redis 中,就表示是重复操作,直接返回重复标记给 client,这样就保证了业务代码,不被重复执行。
    危险性:
  • 1、先删除 token 还是后删除 token;
    (1) 先删除可能导致,业务确实没有执行,重试还带上之前 token,由于防重设计导致,请求还是不能执行。
    (2) 后删除可能导致,业务处理成功,但是服务闪断,出现超时,没有删除 token,别人继续重试,导致业务被执行两边
    (3) 我们最好设计为先删除 token,如果业务调用失败,就重新获取 token 再次请求。
  • 2、Token 获取、比较和删除必须是原子性
    (1) redis.get(token) 、token.equals、redis.del(token)如果这两个操作不是原子,可能导致,高并发下,都 get 到同样的数据,判断都成功,继续业务并发执行
    (2) 可以在 redis 使用 lua 脚本完成这个操作
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

6.4.2 各种锁机制

  • 1.数据库悲观锁
    select * from xxxx where id = 1 for update;
    悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。
    另外要注意的是,id 字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会
    非常麻烦。
  • 2.数据库乐观锁
    这种方法适合在更新的场景中,
    update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1
    根据 version 版本,也就是在操作库存前先获取当前商品的 version 版本号,然后操作的时候
    带上此 version 号。我们梳理下,我们第一次操作库存时,得到 version 为 1,调用库存服务
    version 变成了 2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订
    单服务传如的 version 还是 1,再执行上面的 sql 语句时,就不会执行;因为 version 已经变
    为 2 了,where 条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。
    乐观锁主要使用于处理读多写少的问题
  • 3.业务层分布式锁
    如果多个机器可能在同一时间同时处理相同的数据,比如多台机器定时任务都拿到了相同数
    据处理,我们就可以加分布式锁,锁定此数据,处理完成后释放锁。获取到锁的必须先判断
    这个数据是否被处理过。

6.4.3 各种唯一约束

  • 1、数据库唯一约束
    插入数据,应该按照唯一索引进行插入,比如订单号,相同的订单就不可能有两条记录插入。我们在数据库层面防止重复。这个机制是利用了数据库的主键唯一约束的特性,解决了在 insert 场景时幂等问题。但主键的要求不是自增的主键,这样就需要业务生成全局唯一的主键。如果是分库分表场景下,路由规则要保证相同请求下,落地在同一个数据库和同一表中,要不然数据库主键约束就不起效果了,因为是不同的数据库和表主键不相关。
  • 2、redis set 防重
    很多数据需要处理,只能被处理一次,比如我们可以计算数据的 MD5 将其放入 redis 的 set,
    每次处理数据,先看这个 MD5 是否已经存在,存在就不处理。
  • 3、防重表
    使用订单号 orderNo 做为去重表的唯一索引,把唯一索引插入去重表,再进行业务操作,且他们在同一个事务中。这个保证了重复请求时,因为去重表有唯一约束,导致请求失败,避免了幂等问题。这里要注意的是,去重表和业务表应该在同一库中,这样就保证了在同一个事务,即使业务操作失败了,也会把去重表的数据回滚。这个很好的保证了数据一致性。之前说的 redis 防重也算
  • 4、全局请求唯一 id
    调用接口时,生成一个唯一 id,redis 将数据保存到集合中(去重),存在即处理过。可以使用 nginx 设置每一个请求的唯一 id;
    proxy_set_header X-Request-Id $request_id;

6.5 幂等性项目实战

在这里插入图片描述

6.5.1 查询订单信息时添加token令牌,分别返回给客户端和保存在服务端

@Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo confirmVo = new OrderConfirmVo();
        //通过threadLocal获取用户的基本信息
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        //获取之前的请求
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            //每个线程都获取之前的请求
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //1.远程查询所有的收获列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
            confirmVo.setAddress(address);
        }, executor);
        CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
            //每个线程都获取之前的请求
            RequestContextHolder.setRequestAttributes(requestAttributes);
            //2.远程查询购物车所选中的购物项
            List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
            confirmVo.setItems(items);
        }, executor).thenRunAsync(()->{
            //3.查询库存信息
            List<OrderItemVo> items = confirmVo.getItems();
            if(!CollectionUtils.isEmpty(items)){
                List<Long> skuIds = items.stream().map(OrderItemVo::getSkuId).collect(Collectors.toList());
                R skusHasStock = wmsFeignService.getSkusHasStock(skuIds);
                List<SkuStockVo> data = skusHasStock.getData(new TypeReference<List<SkuStockVo>>() {
                });
                if(!CollectionUtils.isEmpty(data)){
                    Map<Long, Boolean> map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                    confirmVo.setStocks(map);
                }
            }
        }, executor);
        //feign在远程调用之前会构造请求,构造请求会调用很多拦截器
        //3.查询优惠券信息、用户积分
        Integer integration = memberRespVo.getIntegration();
        confirmVo.setIntegration(integration);
        //4.其他数据自动计算
        //5.订单的防重令牌
        String token = UUID.randomUUID().toString().replace("-", "");
        //返回给客户端
        confirmVo.setOrderToken(token);
        //保存在服务端
        stringRedisTemplate.opsForValue().set(OrderConstant.USER_ORDER_TOKEN_PREFIX+memberRespVo.getId(),token,30, TimeUnit.MINUTES);
        CompletableFuture.allOf(getAddressFuture,cartFuture).get();
        return confirmVo;
    }

6.5.2 提交订单携带token

 @Override
    @Transactional(rollbackFor = Exception.class)
    public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) {
        SubmitOrderResponseVo response = new SubmitOrderResponseVo();
        confirmVoThreadLocal.set(vo);
        //通过threadLocal获取用户的基本信息
        MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
        //下单功能——下单后去创建订单,校验令牌,验价格,锁库存
        //1.验证令牌-保证对比和删除是原则性的
        //lua脚本返回的是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();
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script,Long.class);
        //原则删除和校验
        Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(OrderConstant.USER_ORDER_TOKEN_PREFIX + memberRespVo.getId()), orderToken);
        if(result == 0L){
            //令牌验证失败
            response.setCode(1);
            return response;
        } else {
            //令牌验证成功
        }
    }

七:验证价格、保存订单、库存锁定

7.1 验证价格

//2、验证价格
BigDecimal payAmount = order.getOrder().getPayAmount();
BigDecimal payPrice = vo.getPayPrice();
if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {

}

7.2 保存订单

/**
     * 保存订单所有数据
     * @param orderCreateTo
     */
    private void saveOrder(OrderCreateTo orderCreateTo) {
        //获取订单信息
        OrderEntity order = orderCreateTo.getOrder();
        order.setModifyTime(new Date());
        order.setCreateTime(new Date());
        //保存订单
        this.baseMapper.insert(order);
        //获取订单项信息
        List<OrderItemEntity> orderItems = orderCreateTo.getOrderItems();
        //批量保存订单项数据
        orderItemService.saveBatch(orderItems);
    }

7.3 库存锁定

if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) {
                //金额对比
                //3、保存订单
                saveOrder(order);
                //4、库存锁定,只要有异常,回滚订单数据
                //订单号、所有订单项信息(skuId,skuNum,skuName)
                WareSkuLockVo lockVo = new WareSkuLockVo();
                lockVo.setOrderSn(order.getOrder().getOrderSn());
                //获取出要锁定的商品数据信息
                List<OrderItemVo> orderItemVos = 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());
                lockVo.setLocks(orderItemVos);
                //调用远程锁定库存的方法
                //出现的问题:扣减库存成功了,但是由于网络原因超时,出现异常,导致订单事务回滚,库存事务不回滚(解决方案:seata)
                //为了保证高并发,不推荐使用seata,因为是加锁,并行化,提升不了效率,可以发消息给库存服务
                R r = wmsFeignService.orderLockStock(lockVo);
                if (r.getCode() == 0) {
                    //锁定成功
                    response.setOrder(order.getOrder());
                    // int i = 10/0;
                    //订单创建成功,发送消息给MQ
                    rabbitTemplate.convertAndSend("order-event-exchange","order.create.order",order.getOrder());
                    //删除购物车里的数据
                    stringRedisTemplate.delete(CartConstant.CART_PREFIX + memberRespVo.getId());
                    return response;
                } else {
                    //锁定失败
                    String msg = (String) r.get("msg");
                    throw new NoStockException(msg);
                }
            } else {
                response.setCode(2);
                return response;
            }
/**
     * 为某个订单锁定库存
     * @param vo
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean orderLockStock(WareSkuLockVo vo) {
        /**
         * 保存库存工作单详情信息
         * 追溯
         */
        WareOrderTaskEntity wareOrderTaskEntity = new WareOrderTaskEntity();
        wareOrderTaskEntity.setOrderSn(vo.getOrderSn());
        wareOrderTaskEntity.setCreateTime(new Date());
        wareOrderTaskService.save(wareOrderTaskEntity);
        //1、按照下单的收货地址,找到一个就近仓库,锁定库存
        //2、找到每个商品在哪个仓库都有库存
        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> wareIdList = wareSkuDao.listWareIdHasSkuStock(skuId);
            stock.setWareId(wareIdList);
            return stock;
        }).collect(Collectors.toList());
        //2、锁定库存
        for (SkuWareHasStock hasStock : collect) {
            boolean skuStocked = false;
            Long skuId = hasStock.getSkuId();
            List<Long> wareIds = hasStock.getWareId();
            if (CollectionUtils.isEmpty(wareIds)) {
                //没有任何仓库有这个商品的库存
                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;
                    WareOrderTaskDetailEntity taskDetailEntity = WareOrderTaskDetailEntity.builder()
                            .skuId(skuId)
                            .skuName("")
                            .skuNum(hasStock.getNum())
                            .taskId(wareOrderTaskEntity.getId())
                            .wareId(wareId)
                            .lockStatus(1)
                            .build();
                    wareOrderTaskDetailService.save(taskDetailEntity);
                    //告诉MQ库存锁定成功
                    StockLockedTo lockedTo = new StockLockedTo();
                    lockedTo.setId(wareOrderTaskEntity.getId());
                    StockDetailTo detailTo = new StockDetailTo();
                    BeanUtils.copyProperties(taskDetailEntity,detailTo);
                    lockedTo.setDetailTo(detailTo);
                    rabbitTemplate.convertAndSend("stock-event-exchange","stock.locked",lockedTo);
                    break;
                } else {
                    //当前仓库锁失败,重试下一个仓库
                }
            }
            if (skuStocked == false) {
                //当前商品所有仓库都没有锁住
                throw new NoStockException(skuId);
            }
        }
        //3、肯定全部都是锁定成功的
        return true;
    }

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/84083.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!