之前,在做商品搜索和圣品详情页静态化的时候,我就产生了一个问题。怎么更新数据?
现在问题来了,之前我们做了搜索系统和商品详情也的静态化,可是,当我们如果更新商品怎么办,如果我们直接在新增商品的时候去调用其他接口,那么又会造成各个微服务耦合在一起,而且会造成服务的效率降低。
这个时候,出来了一种解决方案,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种
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。假设此处需要update和delete消息 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。订阅 insert、update、delete 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的是我们索引库需要的信息