订单服务

您所在的位置:网站首页 订单服务保障中是指什么服务内容 订单服务

订单服务

2023-10-09 19:58| 来源: 网络整理| 查看: 265

环境搭建: 搭建SpringSession 环境: 1.导入SpringSession 的依赖 在这里插入图片描述 2. 在配置文件中配置session 是用redis 进行存储的 在这里插入图片描述

3.编写Session 的配置 在这里插入图片描述

线程池配置: 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

引入redis: 在这里插入图片描述

服务开启SpringSession: 在这里插入图片描述

在这里插入图片描述

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

一个订单的核心调度过程: 在这里插入图片描述 我们的项目没有售后服务。

完成功能:购物车中点击“去结算” 时,去到订单页面。注意:订单页面是只有登录以后才能进入的,所以order 服务要配置一个拦截器 在这里插入图片描述 配置拦截器 在这里插入图片描述 配置拦截器路径: 在这里插入图片描述

以上就能保证在用户点击购物车的“去结算” 功能时,用户是处于登录状态的。

构建订单模型vo: 订单页面展示: 在这里插入图片描述 在这里插入图片描述 尤其要注意的是商品的价格一定是最新商品的价格,而不是当时下订单时的价格。

在这里插入图片描述

在这里插入图片描述 在这里插入图片描述

在这里插入图片描述 在这里插入图片描述

整体请求代码: Controller: 在这里插入图片描述

ServiceImpl:

@Service("orderService") public class OrderServiceImpl extends ServiceImpl implements OrderService { @Autowired MemberFeignService memberFeignService; @Autowired CartFeignService cartFeignService; @Autowired ThreadPoolExecutor executor; @Autowired WmsFeignService wmsFeignService; @Override public PageUtils queryPage(Map params) { IPage page = this.page( new Query().getPage(params), new QueryWrapper() ); return new PageUtils(page); } @Override public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException { OrderConfirmVo confirmVo = new OrderConfirmVo(); //从拦截器中获取用户信息 MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get(); //从主线程中获取到request 的原信息 RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); CompletableFuture getAddressFuture = CompletableFuture.runAsync(() -> { //给新开的每一个线程都共享之前的数据 RequestContextHolder.setRequestAttributes(requestAttributes); //1.远程查询所有的收货地址列表 List address = memberFeignService.getAddress(memberRespVo.getId()); confirmVo.setAddress(address); }, executor).thenRunAsync(()->{ List items = confirmVo.getItems(); List collect = items.stream().map(item -> item.getSkuId()).collect(Collectors.toList()); R hasStock = wmsFeignService.getSkuHasStock(collect); List data = hasStock.getData(new TypeReference() { }); if (data != null){ Map map = data.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHashStock)); confirmVo.setStocks(map); } },executor); CompletableFuture cartFuture = CompletableFuture.runAsync(() -> { RequestContextHolder.setRequestAttributes(requestAttributes); //2.远程查询购物车所有选中的购物项(价格是最新状态) List currentUserCartItems = cartFeignService.getCurrentUserCartItems(); confirmVo.setItems(currentUserCartItems); }, executor); //3.查询用户积分 Integer integration = memberRespVo.getIntegration(); confirmVo.setIntegration(integration); //4.total(订单总额),payPrice(应付金额) 可以来vo 中自动计算 //TODO 5.防重令牌 CompletableFuture.allOf(getAddressFuture,cartFuture).get(); return confirmVo; } }

细化地址栏的功能: 在这里插入图片描述 当选中哪个地址时,哪个地址就会变成高亮红色 效果: 在这里插入图片描述 对应代码: 在这里插入图片描述 在这里插入图片描述

运费计算(距离 + 货物重量): 根据收货地址的id 在数据库中查询应该要收多少钱运费。

给收货地址的p 标签添加两个属性def 是选中的地址属性(选中为1,不选中为 0),addrId 是地址的id。 在这里插入图片描述 在这里插入图片描述 要根据gateway 的规则来发送后端请求: 在这里插入图片描述

对应发送的后端请求: 在这里插入图片描述 在这里插入图片描述

最终实现效果: 当选中地址时,就能把运费给算出 在这里插入图片描述

完成功能:在计算运费的时候同时返回收货人 在这里插入图片描述

为返回的这个数据构成一个Vo 对象:包含地址信息,运费。 在这里插入图片描述

在这里插入图片描述

前端用ajax 进行获取: 在这里插入图片描述 在这里插入图片描述

接口幂等性: 当一个人网速很慢,在点击提交订单的时候点击了很多次,那么数据库中就会生成很章订单对象。所以订单的防重复提交是很重要的。专业术语叫:要保证提交订单的幂等性。也就是订单提交一次和提交100 次的结果都是一样的,数据库只有一份订单。 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 所以数据库中,可以利用添加唯一索引来具有数据幂等性: 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

后删除token(令牌)危害:当我连续两次点击提交订单时,服务器处理第一次订单时,token 没有删除,然后此时第二次请求又进来了,所以第二个请求还是能被处理,所以此时就有两个订单同时创建。

在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

在这里插入图片描述

 在这里插入图片描述

项目中我们使用令牌机制: 整体流程 在这里插入图片描述

因为是在分布式情况下,所以我们要在Redis 中保存这个令牌。 在这里插入图片描述 给页面也保存一个令牌号 在这里插入图片描述

当点击“提交订单” 的时候,我们要有一个数据模型来接收它,所以构建提交订单的vo 对象。 在这里插入图片描述 在这里插入图片描述

点击“提交订单”所要提交的数据。 在这里插入图片描述 为什么不提交商品Id? 因为商品是实时都去Redis 中进行查询的,包括它的价格,因为有可能在提交订单的时候,又回去购物车再勾上几个商品,又生成了一张新的订单,所以此时要以购物车中勾中的商品为准,所以这里也不直接获取商品id了。而是单独去redis 中自己获取。

提交订单以后无非两种结果:生成订单成功,生成订单失败。所以也要为这两种可能构建一个vo 对象以便提交订单以后页面的显示与走向。

提交订单流程: 在这里插入图片描述

锁定库存的逻辑: 在这里插入图片描述

在这里插入图片描述

远程调用出现的问题: 1.出现网络抖动问题,或者在远程调用方法时执行了,但之后网络断了,此时没有接受到返回值,然后一个事务的操作中,我们会对这些不健康的返回值进行抛出异常,然后回滚订单。所以此时的效果就是:订单下了,同时库存也给扣除了,但是因为出现异常,订单回滚了,此时数据又没来到订单中,那么库存就会被锁没了。 2. 当远程调用没有问题时,但是它下面的操作出现了问题,按逻辑是整体回滚的,但是远程调用的是已经执行了的,没法回滚的。所以:已经执行了的远程服务没办法回滚

@Transactional : 本地事务,只能回滚本地方法。在分布式场景中,有要调用其他的服务,这不属于本地方法。 分布式事务最大问题:网络问题+ 分布式机器;

本地事务: 在这里插入图片描述 在这里插入图片描述

在这里插入图片描述 可重复读:在整个事务期间内,只要事务没结束,比如我要读1号记录数据100,无论后面读多少次,1 号数据还是100,即使外面的人在我们事务操作期间把1 号数据都删了,或者把1 号数据修改成200 了,我们读到的1 号数据还是100. 在这里插入图片描述

隔离级别由上至下增大,并且由上至下并发能力越低。 事务隔离级别可用注释的值来进行设置. 在这里插入图片描述

在这里插入图片描述 b 事务就是和a 事务共用一个事务。 c 事务不和a 公用一个事务,自己单独一个事务。 MYSQL 默认的事务传播行为是:request 在这里插入图片描述 当a 事务实现异常时,a ,b事务都会回滚,但c 事务中没有出现异常的话它不会回滚。 在这里插入图片描述

a 事务设置超时时间:30s, b 事务设置超时时间:2s。如果b 事务的传播行为是request,那么b 事务的超时时间也是30s 。总结: 只要传播行为是request,那么a 事务的设置就传播到了和它公用的一个事务的方法中 在这里插入图片描述

在这里插入图片描述 在这里插入图片描述 事务的本质是使用代理对象来控制的。如果同一个类中,一个事务方法a调用另一个同类中的另一个事务方法b。a 方法是用代理对象调用的,但b 方法不是代理对象对用的,所以b 方法的事务会失效。 解决方法: 1.引入aop,aop 中会引入aspectJ,它具有动态代理功能。 在这里插入图片描述 在这里插入图片描述

2.@EnableAspectJAutoProxy(exposeProxy = true): 开启aspectj 动态代理功能。以后所有的动态代理都是由aspectJ 创建的(即使没有接口也可以创建动态代理),对外暴露代理对象 在这里插入图片描述

3.通过aspectJ 创建的代理对象,由代理对象去调用同类方法,就能实现内部也有事务的方法了。 在这里插入图片描述 以上都是本地事务的讨论。

分布式事务: 在这里插入图片描述

定理:不能打破的理论。 在这里插入图片描述 一致性:比如现在有3 台机器。往1 号数据中存储了一个1 号数据,那么当请求访问2号机器和3 号机器时都要有这个数据。所有数据的备份在同一时刻是否都有同样的值。 可用性: 有3 台机器,有一台机器完蛋了,其他的机器集群还能不能响应客户端的所有操作。如果可以,那说明是可用的。如果不能响应,那说明是不可用的。 分区容错性:3 台机器分布在不同的节点,他们之间的通信是通过网络进行通信的,如果通信期间出现网络问题,这就叫分区容错。 在分布式系统中,永远都要满足分区容错,因为网络肯定会出现问题。当分区出错了,一定要想办法解决。 所以一定要在“一致性”和“可用性”二者选一。如果满足可用性时,一台机器出现故障,但是还是能访问到,那么用户访问到的将是不一致的数据。但如果想满足一致性,我们一定让用户访问每个节点数据都是一样的话,就不能让整个集群可用。当一台机器出现问题时,数据必然也会出现问题,那么此时已经选择了一致性,所以为了保证数据的一致性,只能让这台故障的机器停掉,也就牺牲掉了可用性。 在这里插入图片描述

所以分布式系统中,要么是CP (一致性+分区容错性)系统,要么是AP (可用性+分区容错性)系统。 只有本地系统才会出现acp 系统。

一致性算法的两个代表:raft,paxos 下面的链接是raft 算法的动画显示效果。或者看p285 在这里插入图片描述 cp 定理:假如有6 台服务器,运行过程中出现分区错误,3台机器一个分区。那么此时就一定要选取一个领导出来,而选取领导的条件是获取到大部分的选票,即6 张票要占4 张,而这两个分区只有3 台机器,所以这两个分区永远都选不出一个领导出现。那么此时就算有数据传送过来,也不能响应数据给它,即使这6 台机器都是好的。 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

分布式事务常见的解决方案: 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

在这里插入图片描述 在这里插入图片描述

在这里插入图片描述 在这里插入图片描述

我们使用Spring-cloud alibaba 提供的seata(它是2pc 的变形) 在这里插入图片描述

我们使用的是Seata 的AT 模式,根据官方文档进行操作: 在这里插入图片描述 在这里插入图片描述

当其中一个服务出现异常需要回滚时,由TC 来命令回滚所有的服务。 那么需要回滚的服务就要在数据库中增添一张回滚表,这张表记录着在回滚操作前的操作,以便TC 执行回滚操作。 在这里插入图片描述 在我们的项目中,给每一个数据库都添上这张表

在这里插入图片描述 下载的这个服务器就是图中的TC 部分

点击网站进行下载: 在这里插入图片描述

项目整合Seata: 导入SeaTa 依赖: 在这里插入图片描述 注意:当导入seata 依赖时它还会帮你自动导入seata-all ,这个依赖会带有一个版本号,而这个版本号就得和seata-server 的版本号对应。 在这里插入图片描述 所以我们就得下载seata-server 0.7.1 的版本 在这里插入图片描述

下载完成后,进入conf 文件夹,修改registry.conf 的配置 在这里插入图片描述 然后先启动nacos, 再启动seata.bat 在这里插入图片描述 然后在nacos 中就能看到seata 在注册列表中了。 在这里插入图片描述

在需要的地方写上该注解就算使用了seata 事务了。(注意:在使用@GlobalTransactional 注释时,该方法还得标注上@Transactional 来证明这是一个事务)。主事务才需要加这个注释,分支事务不需要写这个注释,依然保持@Transactional 即可。 在这里插入图片描述 给需要用到seata 分布式事务的服务都加上以下配置: 在这里插入图片描述 给每一个用到seata 事务的服务都加上file.conf 和registry.conf 文件 在这里插入图片描述 给file.conf 中修改配置 在这里插入图片描述

但是seata 的AT 模式不适用于高并发场景。因为它只是2pc 的一个变形。所以要用“柔性事务-最大努力通知型方案”,“柔性事务-可靠消息+最终一致方案"这些适用于高并发场景的分布式事务。我们项目使用的是"柔性事务-可靠消息+最终一致方案”。 锁库存的增强版逻辑: 在这里插入图片描述

在这里插入图片描述 下订单到关闭订单,锁库存到解锁库存。这两个过程都能用RabbitMQ 的延时队列。 在这里插入图片描述

没用RabbitMQ 的延时队列情况下,当定时任务开始进行计时的时候,下一分钟刚好有一个订单生成,然后当定时任务经过30min 进行扫描失效订单时,此时这个订单只是生成了29 分钟,还没到30 min 的失效时间,所以只能等到下一次定时任务的生效时间才能被扫描到。 在这里插入图片描述 使用延时队列: 当在锁库存的时候,只要锁库存成功了,就发消息给MQ,MQ 先把消息保持上一段时间,到时间以后把消息发出去,然后此时检查订单是否没支付,是的话就直接解锁库存,而不像普通的定时任务那样出现大面积的时效性问题。 所以RabbitMQ 解决了事务最终一致性。

TTL:只要这段消息在设置的时间内没有被消费者取走,那么就被判定为死信。然后被服务器抛弃 在这里插入图片描述 DLX: 它也是一个普通的路由(Exchange),只不过专门用于接收死信。 那么结合TTL 就是:当消息一过期,让服务器别乱丢,让这些死信都去到这个DLX(死信路由)中,然后再由另一个队列来监听这个路由,然后让消费者监听这个队列。这个队列中的消息都是过了30 min(设置的过期时间)。 在这里插入图片描述

在这里插入图片描述 在这里插入图片描述

推荐使用给队列设置过期时间。 为什么不用给消息设置过期时间来实现延时队列: 比如现在我连续发送3 条消息。第一条消息设置5 min过期,第二条是1 min过期,第三条是1 s以后过期。按正常逻辑,应该是1 s过期的这条消息优先弹出这条队列。但服务器不是这么检查的,服务器先从队列中那第一条消息,这条消息是5 min后才过期,所以服务器会等5 min后才来拿这条消息,那么服务器会在5 min之后再把队列中的消息拿出来,那么此时第一条消息才算过期。所以第二条消息,得等第一条消息过期以后,即过了5 分钟以后,才能轮到自己过期,然后才来到第三条消息。所以后两条消息都在5 min后才扔的。所以就应该使用给整个队列设置过期时间,这样队列中的所有消息都是这个过期时间。

库存服务消息队列模型: 在这里插入图片描述 在这里插入图片描述 在这里插入图片描述

我们项目的延时队列模型: 在这里插入图片描述

此时order 和ware服务要用到RabbitMQ,就要引入它的依赖: 在这里插入图片描述 在这里插入图片描述 Order 服务配置RabbitMQ 配置: 在这里插入图片描述 ware 服务: 在这里插入图片描述

分别在他们的主类中开启RabbitMQ 在这里插入图片描述

让order 服务自动创建Queue,Exchange

@Configuration public class MyMQConfig { @RabbitListener(queues = "order.release.order.queue") public void listener(OrderEntity entity, Channel channel, Message message) throws IOException { System.out.println("收到过期的订单信息,准备关闭订单"+entity.getOrderSn()); channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); } /** * @Bean 的作用: 容器中的Binding, Queue, Exchange 都会自动创建(RabbitMQ 没有的情况) * @return */ @Bean public Queue orderDelayQueue(){ Map arguments = new HashMap(); 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); Queue queue = new Queue("order.delay.queue", true, false, false,arguments); return queue; } @Bean public Queue orderReleaseOrderQueue(){ Queue queue = new Queue("order.release.order.queue", true, false, false); return queue; } @Bean public Exchange orderEventExchange(){ return new TopicExchange("order-event-exchange", true, false); } @Bean public Binding orderCreateOrderBinding(){ 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); } }

注意:想第一次登陆就看到RabbitMQ 中有我们代码创建的Queue 和Exchange。一定要加上@RabbitListener 来监听一个队列,方法体可以为空,这样RabbitMQ 就会识别出当前Queue 和Exchange 中没有我们代码中的队列,然后就会自己去创建Queue 和Exchange 了。

库存锁定的场景: 1)下订单成功,订单过期没有支付被系统自动取消,被用户手动取消,都要解锁库存 2) 下订单成功,库存锁定成功,但是接下来的业务调用失败,导致订单回滚,之前锁定的库存就要自动解锁(用seata 来解决分布式事务,太慢了,所以不用seata 了。而改用了最终一致性的策略)

锁库存逻辑 在这里插入图片描述

在这里插入图片描述

在这里插入图片描述 在这里插入图片描述



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3