订单服务 |
您所在的位置:网站首页 › 订单服务保障中是指什么服务内容 › 订单服务 |
环境搭建: 搭建SpringSession 环境: 1.导入SpringSession 的依赖 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。 对应发送的后端请求: 最终实现效果: 当选中地址时,就能把运费给算出 完成功能:在计算运费的时候同时返回收货人 为返回的这个数据构成一个Vo 对象:包含地址信息,运费。 前端用ajax 进行获取: 接口幂等性: 当一个人网速很慢,在点击提交订单的时候点击了很多次,那么数据库中就会生成很章订单对象。所以订单的防重复提交是很重要的。专业术语叫:要保证提交订单的幂等性。也就是订单提交一次和提交100 次的结果都是一样的,数据库只有一份订单。 后删除token(令牌)危害:当我连续两次点击提交订单时,服务器处理第一次订单时,token 没有删除,然后此时第二次请求又进来了,所以第二个请求还是能被处理,所以此时就有两个订单同时创建。
项目中我们使用令牌机制: 整体流程 因为是在分布式情况下,所以我们要在Redis 中保存这个令牌。 当点击“提交订单” 的时候,我们要有一个数据模型来接收它,所以构建提交订单的vo 对象。 点击“提交订单”所要提交的数据。 提交订单以后无非两种结果:生成订单成功,生成订单失败。所以也要为这两种可能构建一个vo 对象以便提交订单以后页面的显示与走向。 提交订单流程: 锁定库存的逻辑: 远程调用出现的问题: 1.出现网络抖动问题,或者在远程调用方法时执行了,但之后网络断了,此时没有接受到返回值,然后一个事务的操作中,我们会对这些不健康的返回值进行抛出异常,然后回滚订单。所以此时的效果就是:订单下了,同时库存也给扣除了,但是因为出现异常,订单回滚了,此时数据又没来到订单中,那么库存就会被锁没了。 2. 当远程调用没有问题时,但是它下面的操作出现了问题,按逻辑是整体回滚的,但是远程调用的是已经执行了的,没法回滚的。所以:已经执行了的远程服务没办法回滚 @Transactional : 本地事务,只能回滚本地方法。在分布式场景中,有要调用其他的服务,这不属于本地方法。 分布式事务最大问题:网络问题+ 分布式机器; 本地事务:
隔离级别由上至下增大,并且由上至下并发能力越低。 事务隔离级别可用注释的值来进行设置.
a 事务设置超时时间:30s, b 事务设置超时时间:2s。如果b 事务的传播行为是request,那么b 事务的超时时间也是30s 。总结: 只要传播行为是request,那么a 事务的设置就传播到了和它公用的一个事务的方法中
2.@EnableAspectJAutoProxy(exposeProxy = true): 开启aspectj 动态代理功能。以后所有的动态代理都是由aspectJ 创建的(即使没有接口也可以创建动态代理),对外暴露代理对象 3.通过aspectJ 创建的代理对象,由代理对象去调用同类方法,就能实现内部也有事务的方法了。 分布式事务: 定理:不能打破的理论。 所以分布式系统中,要么是CP (一致性+分区容错性)系统,要么是AP (可用性+分区容错性)系统。 只有本地系统才会出现acp 系统。 一致性算法的两个代表:raft,paxos 下面的链接是raft 算法的动画显示效果。或者看p285 分布式事务常见的解决方案:
我们使用Spring-cloud alibaba 提供的seata(它是2pc 的变形) 我们使用的是Seata 的AT 模式,根据官方文档进行操作: 当其中一个服务出现异常需要回滚时,由TC 来命令回滚所有的服务。 那么需要回滚的服务就要在数据库中增添一张回滚表,这张表记录着在回滚操作前的操作,以便TC 执行回滚操作。
点击网站进行下载: 项目整合Seata: 导入SeaTa 依赖: 下载完成后,进入conf 文件夹,修改registry.conf 的配置 在需要的地方写上该注解就算使用了seata 事务了。(注意:在使用@GlobalTransactional 注释时,该方法还得标注上@Transactional 来证明这是一个事务)。主事务才需要加这个注释,分支事务不需要写这个注释,依然保持@Transactional 即可。 但是seata 的AT 模式不适用于高并发场景。因为它只是2pc 的一个变形。所以要用“柔性事务-最大努力通知型方案”,“柔性事务-可靠消息+最终一致方案"这些适用于高并发场景的分布式事务。我们项目使用的是"柔性事务-可靠消息+最终一致方案”。 锁库存的增强版逻辑:
没用RabbitMQ 的延时队列情况下,当定时任务开始进行计时的时候,下一分钟刚好有一个订单生成,然后当定时任务经过30min 进行扫描失效订单时,此时这个订单只是生成了29 分钟,还没到30 min 的失效时间,所以只能等到下一次定时任务的生效时间才能被扫描到。 TTL:只要这段消息在设置的时间内没有被消费者取走,那么就被判定为死信。然后被服务器抛弃
推荐使用给队列设置过期时间。 为什么不用给消息设置过期时间来实现延时队列: 比如现在我连续发送3 条消息。第一条消息设置5 min过期,第二条是1 min过期,第三条是1 s以后过期。按正常逻辑,应该是1 s过期的这条消息优先弹出这条队列。但服务器不是这么检查的,服务器先从队列中那第一条消息,这条消息是5 min后才过期,所以服务器会等5 min后才来拿这条消息,那么服务器会在5 min之后再把队列中的消息拿出来,那么此时第一条消息才算过期。所以第二条消息,得等第一条消息过期以后,即过了5 分钟以后,才能轮到自己过期,然后才来到第三条消息。所以后两条消息都在5 min后才扔的。所以就应该使用给整个队列设置过期时间,这样队列中的所有消息都是这个过期时间。 库存服务消息队列模型: 我们项目的延时队列模型: 此时order 和ware服务要用到RabbitMQ,就要引入它的依赖: 分别在他们的主类中开启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 |