乐优商城第十六天(rabbitmq实现商品详情页的更新和索引库的更新)

之前,在做商品搜索和圣品详情页静态化的时候,我就产生了一个问题。怎么更新数据?

现在问题来了,之前我们做了搜索系统和商品详情也的静态化,可是,当我们如果更新商品怎么办,如果我们直接在新增商品的时候去调用其他接口,那么又会造成各个微服务耦合在一起,而且会造成服务的效率降低。

这个时候,出来了一种解决方案,mq,消息队列技术

消息队列有两种模式,JMS和AMQP,JMS定义了统一的接口,对消息操作进行统一,而amqp是通过规定通信协议的方式

JMS必须使用java语言,而amqp是一种协议

JMS规定了两种消息模型,而amqp的消息模型更为丰富

我们使用的是rabbitmq,我们主要使用他的五中模型

1.基本消息模型

一个生产者对应一个消费者

生产者

private final static String QUEUE_NAME = "simple_queue";

public static void main(String[] argv) throws Exception {
    // 获取到连接
    Connection connection = ConnectionUtil.getConnection();
    // 从连接中创建通道,使用通道才能完成消息相关的操作
    Channel channel = connection.createChannel();
    // 声明(创建)队列
    channel.queueDeclare(QUEUE_NAME, false, false, false, null);
    // 消息内容
    String message = "Hello World!";
    // 向指定的队列中发送消息
    channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
    
    System.out.println(" [x] Sent '" + message + "'");

    //关闭通道和连接
    channel.close();
    connection.close();
}

我们的消息回执ACk,当消息不是很重要的时候,我们可以接到消息直接给回执,不管消息有没有被执行

当消息很重要的时候,我们就需要手动ack,防止出异常,如果我们ackfalse的时候,

2.work模型,多个消费者绑定一个队列,都可以消费一个队列中的消息

send

private final static String QUEUE_NAME = "test_work_queue";

public static void main(String[] argv) throws Exception {
    // 获取到连接
    Connection connection = ConnectionUtil.getConnection();
    // 获取通道
    Channel channel = connection.createChannel();
    // 声明队列
    channel.queueDeclare(QUEUE_NAME, false, false, false, null);
    // 循环发布任务
    for (int i = 0; i < 50; i++) {
        // 消息内容
        String message = "task .. " + i;
        channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
        System.out.println(" [x] Sent '" + message + "'");

        Thread.sleep(i * 2);
    }
    // 关闭通道和连接
    channel.close();
    connection.close();
}

recv

  private final static String QUEUE_NAME = "simple_queue";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 创建通道
        Channel channel = connection.createChannel();
        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 定义队列的消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                    byte[] body) throws IOException {
                // body 即消息体
                String msg = new String(body);
                System.out.println(" [x] received : " + msg + "!");
            }
        };
        // 监听队列,第二个参数:是否自动进行消息确认。
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
 
 

逐一,我们这里还有一个能者多劳的配置,能够让有能力的人做更多的事情,那就是每次只让一个人接受同一条消息,这样,处理的快的就可以处理的多。

订阅者模型

订阅者模型相对前面的两种,多了一个交换机机制,消息的生产者会先把消息交给交换机

订阅模式一般有3种

  • Fanout:广播,将消息交给所有绑定到交换机的队列

  • Direct:定向,把消息交给符合指定routing key 的队列

  • Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列

注意,exchange只有消息的转发能力,没有消息的存贮能力,如果没有任何队列与之绑定,消息便会丢失。

1.广播模式

交换机把所有的消息都交给绑定的队列,也就是说一个消息可能被多个人消费

private final static String EXCHANGE_NAME = "fanout_exchange_test";

public static void main(String[] argv) throws Exception {
    // 获取到连接
    Connection connection = ConnectionUtil.getConnection();
    // 获取通道
    Channel channel = connection.createChannel();
    
    // 声明exchange,指定类型为fanout
    channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
    
    // 消息内容
    String message = "Hello everyone";
       // 发布消息到Exchange
    channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
    System.out.println(" [生产者] Sent '" + message + "'");

    channel.close();
    connection.close();
}

消费者

public static void main(String[] argv) throws Exception {
    // 获取到连接
    Connection connection = ConnectionUtil.getConnection();
    // 获取通道
    Channel channel = connection.createChannel();
    // 声明队列
    channel.queueDeclare(QUEUE_NAME, false, false, false, null);

    // 绑定队列到交换机
    channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

    // 定义队列的消费者
    DefaultConsumer consumer = new DefaultConsumer(channel) {
        // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                byte[] body) throws IOException {
            // body 即消息体
            String msg = new String(body);
            System.out.println(" [消费者1] received : " + msg + "!");
        }
    };
    // 监听队列,自动返回完成
    channel.basicConsume(QUEUE_NAME, true, consumer);
}

2. 定向模式

生产者指定交换机的名称和消息的类型

  private final static String EXCHANGE_NAME = "direct_exchange_test";

    public static void main(String[] argv) throws Exception {
        // 获取到连接
        Connection connection = ConnectionUtil.getConnection();
        // 获取通道
        Channel channel = connection.createChannel();
        // 声明exchange,指定类型为direct
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        // 消息内容
        String message = "商品删除了, id = 1001";
        // 发送消息,并且指定routing key 为:insert ,代表新增商品
        channel.basicPublish(EXCHANGE_NAME, "delete", null, message.getBytes());
        System.out.println(" [商品服务:] Sent '" + message + "'");

        channel.close();
        connection.close();
    }
}

消费者指定交换机,队列,还有队列可以接收的消息的类型

private final static String QUEUE_NAME = "direct_exchange_queue_1";
private final static String EXCHANGE_NAME = "direct_exchange_test";

public static void main(String[] argv) throws Exception {
    // 获取到连接
    Connection connection = ConnectionUtil.getConnection();
    // 获取通道
    Channel channel = connection.createChannel();
    // 声明队列
    channel.queueDeclare(QUEUE_NAME, false, false, false, null);
    
    // 绑定队列到交换机,同时指定需要订阅的routing key。假设此处需要updatedelete消息
    channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "update");
    channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "delete");

    // 定义队列的消费者
    DefaultConsumer consumer = new DefaultConsumer(channel) {
        // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                byte[] body) throws IOException {
            // body 即消息体
            String msg = new String(body);
            System.out.println(" [消费者1] received : " + msg + "!");
        }
    };
    // 监听队列,自动ACK
    channel.basicConsume(QUEUE_NAME, true, consumer);
}

3.通配符模式

TOPic模式,就是在direct模式的基础上,使用通配符,简化配置

private final static String EXCHANGE_NAME = "topic_exchange_test";

public static void main(String[] argv) throws Exception {
    // 获取到连接
    Connection connection = ConnectionUtil.getConnection();
    // 获取通道
    Channel channel = connection.createChannel();
    // 声明exchange,指定类型为topic
    channel.exchangeDeclare(EXCHANGE_NAME, "topic");
    // 消息内容
    String message = "新增商品 : id = 1001";
    // 发送消息,并且指定routing key 为:insert ,代表新增商品
    channel.basicPublish(EXCHANGE_NAME, "item.insert", null, message.getBytes());
    System.out.println(" [商品服务:] Sent '" + message + "'");

    channel.close();
    connection.close();
}

接受者

shiy9ong通配符的方式接收

private final static String QUEUE_NAME = "topic_exchange_queue_2";
private final static String EXCHANGE_NAME = "topic_exchange_test";

public static void main(String[] argv) throws Exception {
    // 获取到连接
    Connection connection = ConnectionUtil.getConnection();
    // 获取通道
    Channel channel = connection.createChannel();
    // 声明队列
    channel.queueDeclare(QUEUE_NAME, false, false, false, null);
    
    // 绑定队列到交换机,同时指定需要订阅的routing key。订阅 insertupdatedelete
    channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "item.*");

    // 定义队列的消费者
    DefaultConsumer consumer = new DefaultConsumer(channel) {
        // 获取消息,并且处理,这个方法类似事件监听,如果有消息的时候,会被自动调用
        @Override
        public void handleDelivery(String consumerTag, Envelope envelope, BasicProperties properties,
                byte[] body) throws IOException {
            // body 即消息体
            String msg = new String(body);
            System.out.println(" [消费者2] received : " + msg + "!");
        }
    };
    // 监听队列,自动ACK
    channel.basicConsume(QUEUE_NAME, true, consumer);
}

我们的通配符也是有规则的

通配符规则:

#:匹配一个或多个词

*:匹配不多不少恰好1个词

以上就是消息队列的集中方式

那么,我们的spring是怎么操作消息队列的呢?

Spring-AMQP

springAmqp是对amqp协议抽象的实现,底层使用的是rabbitmq

第一步,导入依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

第二步,导入配置

spring:
  rabbitmq:
    host: 192.168.56.101
    username: admin
    password: admin
    virtual-host: /leyou

第三步,生产者指定交换机

@Autowired
private AmqpTemplate amqpTemplate;

@Test
public void testSend() throws InterruptedException {
    String msg = "hello, Spring boot amqp";
    this.amqpTemplate.convertAndSend("spring.test.exchange","a.b", msg);
    // 等待10秒后再结束
    Thread.sleep(10000);
}

第四步,消费者用注解的方式指定路由key,队列,交换机

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "spring.test.queue", durable = "true"),
            exchange = @Exchange(
                    value = "spring.test.exchange",
                    ignoreDeclarationExceptions = "true",
                    type = ExchangeTypes.TOPIC
            ),
            key = {"#.#"}))
    public void listen(String msg){
        System.out.println("接收到消息:" + msg);
    }
}

好了,基础的知识完成了,我们接下来就是改造我们的项目了,我们对商品的微服务进行改造,让它发送消息

第一步,引入依赖

第二步,配置文件

rabbitmq:
    host: 192.168.56.101
    username: admin
    password: admin
    virtual-host: /leyou
    template:
      retry:
        enabled: true
        initial-interval: 10000ms
        max-interval: 300000ms
        multiplier: 2
      exchange: ly.item.exchange
    publisher-confirms: true

第三步,定义一个发送消息的方法,当然,我们要自动注入amqpTemplate

/**
 * 发送消息的方法
 * @param id
 * @param type
 */
public void sendMessage(Long id,String type){

    try {
        this.amqpTemplate.convertAndSend("item."+type,id);
    } catch (AmqpException e) {
        e.printStackTrace();
        logger.error("发送消息失败");
    }


}

第四步,在改,删,增的结尾都加一个发送消息的方法

//发送消息
this.sendMessage(id,"delete");

这样,生产者的消息就完成了。

接下来是消费者

第一个消费者,商品详情微服务

第一步,引入依赖,配置

rabbitmq:
  host: 192.168.56.101
  username: admin
  password: admin
  virtual-host: /leyou

第二步,编写一个监听的类,来处理消息,我们要注意的是,这里所有的异常都要抛出,让springamqp回执失败,从而保证消息的安全性

@Component
public class Goodslistener {

    private static final org.slf4j.Logger logger = LoggerFactory.getLogger(Goodslistener.class);

    @Autowired
    private FileService fileService;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "ly.create.page.queue", durable = "true"),
            exchange = @Exchange(
                    value = "ly.item.exchange",
                    ignoreDeclarationExceptions = "true",
                    type = ExchangeTypes.TOPIC),
            key = {"item.insert", "item.update"}))
    public void  listenCreateHtml(Long id){
        if (id==null){
            logger.error("id不存在");
            return;
        }
        this.fileService.createHtml(id);
    }


    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "ly.delete.page.queue", durable = "true"),
            exchange = @Exchange(
                    value = "ly.item.exchange",
                    ignoreDeclarationExceptions = "true",
                    type = ExchangeTypes.TOPIC),
            key = "item.delete"))
    public void  listenDeleteHtml(Long id){
        if (id==null){
            logger.error("id不存在");
            return;
        }
        this.fileService.deleteHtml(id);
    }

}

然后,我们调用service层的方法,来处理消息

我们创建页面的方法还是之前的那个方法,就是新增商品详情也的方法,因为,只要我们有新的,我们就会覆盖。

删除的方法

public void deleteHtml(Long id) {
    File file = new File(destPath + id + ".html");
    file.deleteOnExit();
}

接下来是搜索微服务,与商品详情微服务差不多,这里我只将消费者作为展示

/**
 * 添加或者修改索引的方法
 * @param id
 */
@RabbitListener(bindings = @QueueBinding(
        value = @Queue(value = "ly.create.index.queue", durable = "true"),
        exchange = @Exchange(
                value = "ly.item.exchange",
                ignoreDeclarationExceptions = "true",
                type = ExchangeTypes.TOPIC),
        key = {"item.insert", "item.update"}))
public void listenCreateAndUpdate(Long id){
    if (id==null){
        return;
    }
    this.searchService.createOrUpdateIndex(id);
    System.out.println("创建或者修改索引成功");
}

public void daleteIndex(Long id){
    if (id==null){
        return ;
    }
    this.searchService.deleteIndex(id);
    System.out.println("删除索引库成功");
}

 
 

创建或者更新索引的方法

ResponseEntity<Spu> spuResponseEntity = this.spuClient.querySpuBySpuId(id);
if (!spuResponseEntity.hasBody()) {
    logger.error("没有查到spu的信息");
    throw new RuntimeException("没有查到spu的信息");
}
Spu spu = spuResponseEntity.getBody();
//创建一个goods对象
Goods goods = new Goods();

Long cid1 = spu.getCid1();
Long cid2 = spu.getCid2();
Long cid3 = spu.getCid3();

ResponseEntity<SpuDetail> spuDetailResponseEntity = goodsClient.querySpuDetailById(id);
ResponseEntity<List<Sku>> listResponseEntity = goodsClient.querySkuList(spu.getId());
ResponseEntity<List<String>> categoryNames = categoryClient.queryCategoryNamesBycids(Arrays.asList(cid1, cid2, cid3));
if (!spuDetailResponseEntity.hasBody() || !listResponseEntity.hasBody() || !categoryNames.hasBody()) {
    logger.error("没有查到商品的详细信息的信息");
    throw new RuntimeException("没有查到商品的详细信息信息");
}

//查询spuDetail
SpuDetail spuDetail = spuDetailResponseEntity.getBody();
//将可搜索的属性导入
String specifications = spuDetail.getSpecifications();
//将字符串转为对象
List<Map<String, Object>> maps = JsonUtils.nativeRead(specifications, new TypeReference<List<Map<String, Object>>>() {
});
//map用来存储可搜索属性
Map<String, Object> specMap = new HashMap<>();
for (Map<String, Object> map : maps) {
    List<Map<String, Object>> paramsList = (List<Map<String, Object>>) map.get("params");
    for (Map<String, Object> paramsMap : paramsList) {
        Boolean searchable = (Boolean) paramsMap.get("searchable");
        if (searchable) {
            if (paramsMap.get("v") != null) {
                specMap.put((String) paramsMap.get("k"), paramsMap.get("v"));
            } else if (paramsMap.get("options") != null) {
                specMap.put((String) paramsMap.get("k"), paramsMap.get("options"));
            }
        }
    }
}

//获取sku的信息
List<Sku> skuList = listResponseEntity.getBody();
//sku的信息是一个json对象,里面有很多对象
List<Map<String, Object>> skuData = new ArrayList<>();
//准备价格的集合,价格不能重复
HashSet<Long> prices = new HashSet<>();
for (Sku sku : skuList) {
    Map<String, Object> map = new HashMap<>();
    map.put("id", sku.getId());
    map.put("title", sku.getTitle());
    map.put("image", StringUtils.isBlank(sku.getImages()) ? "" : sku.getImages().split(",")[0]);
    map.put("price", sku.getPrice());
    prices.add(sku.getPrice());
    skuData.add(map);
}
//sku的集合转为json
String skuDatas = JsonUtils.serialize(skuData);

//查询分类的集合
List<String> categoryNamesBody = categoryNames.getBody();
goods.setSubTitle(spu.getSubTitle());
goods.setSpecs(specMap);

goods.setSkus(skuDatas);

goods.setPrice(new ArrayList<>(prices));
goods.setAll(spu.getTitle() + StringUtils.join(categoryNamesBody, " "));//todo
goods.setBrandId(spu.getBrandId());

goods.setCreateTime(spu.getCreateTime());
goods.setId(spu.getId());

goods.setCid1(cid1);
goods.setCid2(cid2);
goods.setCid3(cid3);

goodsRepository.saveAll(Arrays.asList(goods));

索引库的信息是spu,spudetail,sku,goods.set的是我们索引库需要的信息


猜你喜欢

转载自blog.csdn.net/qpc672456416/article/details/80698849