【项目学习】谷粒商城学习记录9 - 消息队列,实现订单服务

【项目学习】谷粒商城学习记录9 - 消息队列,实现订单服务


一、介绍

  • 作用:异步处理,应用解耦合,
  • docker安装rabbitmq
    • 结果:

二、springboot整合

1、整体步骤

2、整合与测试

  • 引入spring-boot-starter-amqp依赖

    <!--amqp-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    
  • 为了能连接上rabbitmq,进行配置

    spring.rabbitmq.host=服务器ip
    spring.rabbitmq.port=5672
    spring.rabbitmq.virtual-host=/
    
  • 创建新交换机

      @Autowired
      AmqpAdmin amqpAdmin;
    
      @Test
      public void createExchange() {
          DirectExchange directExchange = new DirectExchange("hello-java-exchange", true, false);
          amqpAdmin.declareExchange(directExchange);
          log.info("Exchange[{}]创建成功", "hello-java-exchange");
      }
    

  • 创建新队列

    @Test
    public void createQueue() {
        Queue queue = new Queue("hello-java-queue", true, false, false);
        amqpAdmin.declareQueue(queue);
        log.info("Queue[{}]创建成功", "hello-java-queue");
    }
    

  • 创建新绑定关系

    /**
     * 创建新绑定关系
     */
    @Test
    public void createBinding() {
        // String destination【目的地】
        // DestinationType destinationType【目的地类型】,
        // String exchange【交换机】
        // String routingKey 【路由键】
        // Map<String, Object> arguments【自定义参数】
        //将`exchange`指定的交换机和destination对象绑定,绑定方式可以是和队列也可以是和交换机,
        Binding binding = new Binding("hello-java-queue",
                Binding.DestinationType.QUEUE,
                "hello-java-exchange",
                "hello.java",
                null);
        amqpAdmin.declareBinding(binding);
        log.info("Binding[{}]创建成功", "hello-java-binding");
    }
    

  • 发送消息(string)

    @Autowired
    RabbitTemplate rabbitTemplate;
    
    @Test
    public void sendMessageTest() {
        //1、发送消息
        String msg = "Hello World!";
        rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", msg);
        log.info("消息发送完成:{}", msg);
    }
    

  • 发送消息(类对象)

    • 为了能显示,实现配置类
      @Configuration
      public class MyRabbitConfig {
      
          public MessageConverter messageConverter() {
              return new Jackson2JsonMessageConverter();
          }
      }
      
    • 测试代码
      @Test
      public void sendMessageTest() {
          //1、发送消息
          OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();
          reasonEntity.setId(1L);
          reasonEntity.setCreateTime(new Date());
          reasonEntity.setName("哈哈");
          String msg = "Hello World!";
          rabbitTemplate.convertAndSend("hello-java-exchange", "hello.java", reasonEntity);
          log.info("消息发送完成:{}", reasonEntity);
      }
      
    • 结果
  • 监听消息

使用@RabbitListener; 必须有注解@EnableRabbit

/**
 * queues:声明需要监听的所有队列
 * @param message
 */
@RabbitListener(queues = {"hello-java-queue"})
public void recieveMessage(Message message, OrderReturnReasonEntity content, Channel channel) {

    byte[] body = message.getBody();
    MessageProperties properties = message.getMessageProperties();

    System.out.println("接收到消息...内容:" + message + "===>内容:" + content);
}

  • 可靠抵达

三、实现订单服务

1、准备工作:

  • 整合静态资源:等待付款,订单页,结算页,收银页 分别对应detail, list, confirm, pay
  • 设置域名order.gulimall.com, 重启nginx
  • 配置网关
    # 订单
    - id: gulimall_order_route
      uri: lb://gulimall-order
      predicates:
        - Host=order.gulimall.com
    
  • 实现web.HelloController
    @Controller
    public class HelloController {
    
        @GetMapping("/{page}.html")
        public String listPage(@PathVariable("page") String page) {
            return page;
        }
    }
    
  • 整合SpringSession和redis, 将之前的session配置类和线程池配置类复制到order服务下
  • 打通页面跳转

2、订单页面介绍:

  • VO抽取:
    • OrderConfirmVo
    @Data
    public class OrderConfirmVo {
    
        //收货地址
        List<MemberAddressVo> address;
    
        //所有选中的购物项
        List<OrderItemVo> items;
    
        //发票记录
    
        //优惠券信息
        Integer integertion;
    
        //订单总额
        BigDecimal total;
    
        //应付价格
        BigDecimal payPrice;
    
    }
    
    • 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;
    }
    
  • 实现订单数据确认
    @GetMapping("/toTrade")
    public String toTrade(Model model) throws ExecutionException, InterruptedException {
    
        OrderConfirmVo confirmVo = orderService.confirmOrder();
        System.out.println("confirmVo=" + confirmVo);
        model.addAttribute("orderConfirmData", confirmVo);
        //展示订单确认的数据
        return "confirm";
    }
    
    • 具体实现
      @Override
      public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
          OrderConfirmVo confirmVo = new OrderConfirmVo();
          MemberRespVo memberRespVo = LoginUserInterceptor.loginUser.get();
          //获取之前的请求
          RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    
          CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
              //1、远程查询用户地址信息
              //每个线程同步主线程的请求
              RequestContextHolder.setRequestAttributes(requestAttributes);
              List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
              confirmVo.setAddress(address);
          }, executor);
    
    
          CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
              //2、远程查询购物车所有选中的购物项
              //每个线程同步主线程的请求
              RequestContextHolder.setRequestAttributes(requestAttributes);
              List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
              confirmVo.setItems(items);
          }, executor);
    
    
          //3、查询用户积分
          Integer integration = memberRespVo.getIntegration();
          confirmVo.setIntegertion(integration);
    
          //4、其他数据自动计算
    
          // TODO 5、防重令牌
    
    
          CompletableFuture.allOf(getAddressFuture, cartFuture).get();
    
          return confirmVo;
      }
    

  • 实现订单提交数据确认

后面就一直跟着视频调前端

四、幂等性

  • 介绍:就是指一个操作执行1次和多次要结果一致。如生成订单时,一个订单肯定只会要求存一次,那么如果用户多次点击提交订单按钮,就要避免多次存一个订单。
  • 保持幂等性的做法:
    • token令牌机制
    • 各种锁机制
    • 各种唯一约束
    • 防重表
    • 全局请求唯一id
  • P283结果:

五、分布式事务

5.1、之前方法存在的问题:

  • 主要就是当出现异常时,之前方法没有办法很好地控制好回滚。同时由于分布式事务涉及很多远程调用,没法通过事务机制来限制。
  • 本地事务失效问题:
    • 同一个对象内的事务互相调用默认失效,原因是这样做绕过了代理对象,事务使用代理对象来控制。
    • 解决办法:使用代理对象来调用事务方法
      • 1)、引入aop-starter;spring-boot-starter-aop; 引入aspectj
      • 2)、@EnableAspectJAutoProxy(exposeProxy=true);开启 aspectj 动态代理功能。以后所有的动态代理都是aspectj创建的(即使没有接口也可以创建动态代理) 并对外暴露代理对象
      • 3)、本类互相调用对象
          OrderServiceImpl orderService = (OrderServiceImpl) AopContext.currentProxy();
          orderService.b();
          orderService.c();
        

5.2、seata实现分布式事务

  • seata官网-快速开始
  • 5.2.1、为各数据库创建日志表
    CREATE TABLE `undo_log` (
      `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
      `branch_id` BIGINT(20) NOT NULL,
      `xid` VARCHAR(100) NOT NULL,
      `context` VARCHAR(128) NOT NULL,
      `rollback_info` LONGBLOB NOT NULL,
      `log_status` INT(11) NOT NULL,
      `log_created` DATETIME NOT NULL,
      `log_modified` DATETIME NOT NULL,
      `ext` VARCHAR(100) DEFAULT NULL,
      PRIMARY KEY (`id`),
      UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
    ) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
    
  • 5.2.2、安装事务协调器seata-server
  • 5.2.3、整合
    • 导入依赖:spring-cloud-starter-alibaba-seataseata-all
    • 解压并启动seata-server:
      registry.conf: 注册中心配置;修改registry type=nacos
      file.conf
    • 启动测试分布式事务
    • 给分布式大事务的入口标注@GlobalTransactional
    • 给远程小事务标注@Transactional

seata适合那种有分布式事务的场景,不过由于内部实现时会设置许多锁,会使并行事务变为串行处理,所以不适合高并发的业务场景

5.3、消息队列实现

  • 5.3.1、延迟队列介绍:
  • 5.3.2、延迟队列应用:
    • 库存服务ware中导入amqp依赖
      <!--amqp-->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-amqp</artifactId>
      </dependency>
      
    • 配置相关依赖
      spring.rabbitmq.host=rabbitMQ服务地址
      spring.rabbitmq.port=5672
      spring.rabbitmq.virtual-host=/
      
    • 开启功能注解@EnableRabbit
    • 添加配置类,使用JSON序列化机制
      @Configuration
      public class MyRabbitConfig {
          /**
           * 使用JSON序列化机制,进行消息转换
           * @return
           */
          @Bean
          public MessageConverter messageConverter() {
              return new Jackson2JsonMessageConverter();
          }
      }
      
    • 配置交换机,队列,使用Bean注解注入 (这些代码写在MyRabbitConfig中)
      @RabbitListener(queues = "stock.release.stock.queue")
      public void handle(Message message) {
      
      }
      
      @Bean
      public Exchange stockEventExchange() {
          //String name, boolean durable, boolean autoDelete, Map<String, Object> arguments
          return new TopicExchange("stock-event-exchange", true, false);
      }
      
      @Bean
      public Queue stockReleaseStockQueue() {
          // String name, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
          return new Queue("stock.release.stock.queue", true, false, false);
      }
      
      @Bean
      public Queue stockDelayQueue() {
          HashMap args = new HashMap<>();
          args.put("x-dead-letter-exchange", "stock-event-exchange");
          args.put("x-dead-routing-key", "stock.release");                //死信路由
          args.put("x-message-ttl", 120000);                              //消息过期时间
          return new Queue("stock.delay.queue", true, false, false);
      }
      
      @Bean
      public Binding stockLockedBinding() {
          /**
           * String destination, Binding.DestinationType destinationType, String exchange,
           * String routingKey, Map<String, Object> arguments
           */
          return new Binding("stock.delay.queue",
                  Binding.DestinationType.QUEUE,
                  "stock-event-exchange",
                  "stock.locked",
                  null);
      }
      
      @Bean
      public Binding stockReleaseBinding() {
          return new Binding("stock.release.stock.queue",
                  Binding.DestinationType.QUEUE,
                  "stock-event-exchange",
                  "stock.release.#",
                  null);
      }
      
posted @ 2023-12-20 16:32  A_sc  阅读(49)  评论(0编辑  收藏  举报