diff --git a/自学/力扣Hot 100题.md b/自学/力扣Hot 100题.md index b4b4329..f025dd2 100644 --- a/自学/力扣Hot 100题.md +++ b/自学/力扣Hot 100题.md @@ -2251,3 +2251,83 @@ public int multipleKnapsack(int V, int[] weight, int[] value, int[] count) { } ``` + + +## ACM风格输入输出 + +```text +** + * 题目描述 + * + * 给定一个整数数组 Array,请计算该数组在每个指定区间内元素的总和。 + * + * 输入描述 + * + * 第一行输入为整数数组 Array 的长度 n,接下来 n 行,每行一个整数,表示数组的元素。随后的输入为需要计算总和的区间下标:a,b (b > = a),直至文件结束。 + * 输入示例: + * 5 + * 1 + * 2 + * 3 + * 4 + * 5 + * 0 1 + * 1 3 + *输出: + * 3 + * 9 + * 输出每个指定区间内元素的总和。 + */ +``` + +```java +import java.util.*; +import java.io.*; + +public class Main { + public static void main(String[] args) throws IOException { + // 快速 IO + BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + PrintWriter out = new PrintWriter(System.out); + String line; + + // 1)读数组长度 + line = br.readLine(); + if (line == null) return; + int n = Integer.parseInt(line.trim()); + + // 2)读数组元素并构造前缀和 + long[] prefix = new long[n + 1]; + for (int i = 0; i < n; i++) { + line = br.readLine(); + if (line == null) throw new IOException("Unexpected EOF when reading array"); + prefix[i + 1] = prefix[i] + Long.parseLong(line.trim()); + } + + // 3)依次读查询直到 EOF + // 每行两个整数 a, b (0 ≤ a ≤ b < n) + while ((line = br.readLine()) != null) { + line = line.trim(); + if (line.isEmpty()) continue; + StringTokenizer st = new StringTokenizer(line); + int a = Integer.parseInt(st.nextToken()); + int b = Integer.parseInt(st.nextToken()); + + // 区间和 = prefix[b+1] - prefix[a] + long sum = prefix[b + 1] - prefix[a]; + out.println(sum); + } + + out.flush(); + } +} +``` + +`line.trim()` 是把原 `line` 首尾的所有空白字符(空格、制表符、换行符等)都去掉之后的结果。 + +`StringTokenizer st = new StringTokenizer(line);`把一整行字符串 `line` 按默认的分隔符(空格、制表符、换行符等)拆成一个一个的“词”(token) + +`StringTokenizer st = new StringTokenizer(line, ",;");` 第二个参数 `",;"` 中的**每个字符**都会被当作分隔符;如果想把空白也当分隔符,可以在里边加上空格 `" ,; "` + + + diff --git a/自学/微服务.md b/自学/微服务.md index f605d09..b7bb86c 100644 --- a/自学/微服务.md +++ b/自学/微服务.md @@ -162,6 +162,34 @@ mysql是服务名,不能写localhost(或 `127.0.0.1`),它永远只会指 +### Docker Compose问题 + +如果你把某个服务从 `docker-compose.yml` 里删掉,然后再执行: + +```shell +docker compose down +``` + +默认情况下 **并不会** 停止或删除那个已经“离开”了 Compose 配置的容器。 + +只能: + +```shell +docker compose down --remove-orphans #清理这些“孤儿”容器 +``` + +或者手动清理: + +```shell +docker ps #列出容器 +docker stop +docker rm +``` + + + + + ## 认识微服务 微服务架构,首先是服务化,就是将单体架构中的功能模块从单体应用中拆分出来,独立部署为多个服务。 diff --git a/自学/消息队列MQ.md b/自学/消息队列MQ.md index bc6a39f..0cd8fb4 100644 --- a/自学/消息队列MQ.md +++ b/自学/消息队列MQ.md @@ -37,6 +37,7 @@ mq: restart: unless-stopped hostname: mq environment: + - TZ=Asia/Shanghai RABBITMQ_DEFAULT_USER: admin RABBITMQ_DEFAULT_PASS: "admin" RABBITMQ_PLUGINS_DIR: "/plugins:/custom-plugins" @@ -61,4 +62,217 @@ http://localhost:15672/ 访问控制台 - **`consumer`**:消费者,消费消息的一方 - **`queue`**:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理 - **`exchange`**:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。**不存储** -- **`virtual host`**:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue \ No newline at end of file +- **`virtual host`**:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue(每个项目+环境有各自的vhost) + +一个队列最多指定给一个消费者! + + + +## Spring AMQP + +### 快速开始 + +**交换机和队列都是直接在控制台创建,消息的发送和接收在Java应用中实现!** + +简单案例:直接向队列发送消息,**不经过交换机** + +![image-20250528120304174](https://pic.bitday.top/i/2025/05/28/jwaiez-0.png) + +**引入依赖** + +```xml + + + org.springframework.boot + spring-boot-starter-amqp + +``` + +**配置MQ地址**,在`publisher`和`consumer`服务的`application.yml`中添加配置: + +```yaml +spring: + rabbitmq: + host: localhost # 你的虚拟机IP + port: 5672 # 端口 + virtual-host: /hmall # 虚拟主机 + username: hmall # 用户名 + password: 123 # 密码 +``` + + + +**消息发送:** + +然后在`publisher`服务中编写测试类`SpringAmqpTest`,并利用`RabbitTemplate`实现消息发送: + +```java +@SpringBootTest +public class SpringAmqpTest { + + @Autowired + private RabbitTemplate rabbitTemplate; + + @Test + public void testSimpleQueue() { + // 队列名称 + String queueName = "simple.queue"; + // 消息 + String message = "hello, spring amqp!"; + // 发送消息 + rabbitTemplate.convertAndSend(queueName, message); + } +} +``` + + + +**消息接收** + +```java +@Component +public class SpringRabbitListener { + // 利用RabbitListener来声明要监听的队列信息 + // 将来一旦监听的队列中有了消息,就会推送给当前服务,调用当前方法,处理消息。 + // 可以看到方法体中接收的就是消息体的内容 + @RabbitListener(queues = "simple.queue") + public void listenSimpleQueueMessage(String msg) throws InterruptedException { + System.out.println("spring 消费者接收到消息:【" + msg + "】"); + } +} +``` + +然后启动启动类,它能自动从队列中取出消息。取出后队列中就没消息了! + + + +### 交换机 + +**1)fanout:广播给每个绑定的队列** + +![image-20250528133703660](https://pic.bitday.top/i/2025/05/28/m40swr-0.png) + +![image-20250528132709273](https://pic.bitday.top/i/2025/05/28/ly3wne-0.png) + +发送消息: + +`convertAndSend`如果2个参数,第一个表示队列名,第二个表示消息;如果3个参数,第一个表示交换机,第二个表示`RoutingKey`,第三个表示消息。 + +```java +@Test +public void testFanoutExchange() { + // 交换机名称 + String exchangeName = "hmall.fanout"; + // 消息 + String message = "hello, everyone!"; + rabbitTemplate.convertAndSend(exchangeName, "", message); +} +``` + + + +**2)Direct交换机** + +- 队列与交换机的绑定,不能是任意绑定了,而是要**指定**一个`RoutingKey`(路由key) +- 消息的发送方在 向 Exchange发送消息时,也必须指定消息的 `RoutingKey`。 +- Exchange不再把消息交给每一个绑定的队列,而是根据消息的`Routing Key`进行判断,只有队列的`Routingkey`与消息的 `Routing key`完全一致,才会接收到消息 + +**注意,RoutingKey不等于队列名称** + +![image-20250528141029943](https://pic.bitday.top/i/2025/05/28/nbn2ku-0.png) + + + +**3)Topic交换机** + +`Topic`类型的`Exchange`与`Direct`相比,都是可以根据`RoutingKey`把消息路由到不同的队列。 + +只不过`Topic`类型`Exchange`可以让队列在绑定`BindingKey` 的时候使用**通配符**! + +BindingKey一般都是有一个或**多个单词**组成,多个单词之间以`.`分割 + +通配符规则: + +- `#`:匹配一个或多个词 +- `*`:匹配不多不少恰好**1个词** + +举例: + +- `item.#`:能够匹配`item.spu.insert` 或者 `item.spu` +- `item.*`:只能匹配`item.spu` + + + +### 基于注解声明交换机队列 + +以往我们都在 RabbitMQ 管理控制台手动创建队列和交换机,开发人员还得把所有配置整理一遍交给运维,既繁琐又容易出错。更好的做法是在应用启动时自动检测所需的队列和交换机,若不存在则直接创建。 + +**基于注解方式来声明** + +`durable="true"`:队列在 RabbitMQ 重启后依然存在。 + +`type` 默认交换机类型为ExchangeTypes.DIRECT + +```java +@RabbitListener(bindings = @QueueBinding( + value = @Queue(name = "direct.queue1",durable = "true"), + exchange = @Exchange(name = "hmall.direct", type = ExchangeTypes.DIRECT), + key = {"red", "blue"} +)) +public void listenDirectQueue1(String msg){ + System.out.println("消费者1接收到direct.queue1的消息:【" + msg + "】"); +} + +@RabbitListener(bindings = @QueueBinding( + value = @Queue(name = "direct.queue2"), + exchange = @Exchange(name = "hmall.direct", type = ExchangeTypes.DIRECT), + key = {"red", "yellow"} +)) +public void listenDirectQueue2(String msg){ + System.out.println("消费者2接收到direct.queue2的消息:【" + msg + "】"); +} +``` + +**检查队列** + +- 如果 RabbitMQ 中已经有名为 `direct.queue1` 的队列,就不会重复创建; +- 如果不存在,`RabbitAdmin` 会自动帮你创建一个。 + +**检查交换机** + +- 同理,会查看有没有名为 `hmall.direct`、类型为 `direct` 的交换机,若不存在就新建。 + +**检查绑定** + +- 最后再去声明绑定关系:把 `direct.queue1` 绑定到 `hmall.direct`,并且 routing-key 为 `"red"` 和 `"blue"`。 +- 如果已有相同的绑定(队列、交换机、路由键都一致),也不会再重复创建。 + + + +### 消息转换器 + +使用JSON方式来做序列化和反序列化,替换掉默认方式。 + +1)引入依赖 + +```XML + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + 2.9.10 + +``` + +2)配置消息转换器,在`publisher`和`consumer`两个服务的启动类中添加一个Bean即可: + +```Java +@Bean +public MessageConverter messageConverter(){ + // 1.定义消息转换器 + Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter(); + // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息 + jackson2JsonMessageConverter.setCreateMessageIds(true); + return jackson2JsonMessageConverter; +} +``` +