微服务day06

MQ入门

同步处理业务:

异步处理:

将任务处理后交给MQ来进行分发处理。



MQ的相关知识

同步调用

同步调用的小结

异步调用

MQ技术选型

RabbitMQ

安装部署

其中包含几个概念:

  • publisher:生产者,也就是发送消息的一方

  • consumer:消费者,也就是消费消息的一方

  • queue:队列,存储消息。生产者投递的消息会暂存在消息队列中,等待消费者处理

  • exchange:交换机,负责消息路由。生产者发送的消息由交换机决定投递到哪个队列。

  • virtual host:虚拟主机,起到数据隔离的作用。每个虚拟主机相互独立,有各自的exchange、queue

上述这些东西都可以在RabbitMQ的管理控制台来管理,下一节我们就一起来学习控制台的使用。

查看详情:点这里icon-default.png?t=O83Ahttps://b11et3un53m.feishu.cn/wiki/OQH4weMbcimUSLkIzD6cCpN0nvc

数据隔离

创建新用户:

创建新的host:

Java客户端

快速入门

在控制台创建消息队列:

导入实例项目后,在发送者和接受者的pom文件中引入依赖:

 <!--AMQP依赖,包含RabbitMQ-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

修改两个模块的配置文件:

spring:
  rabbitmq:
    host: 192.168.21.101 # 你的虚拟机IP
    port: 5672 # 端口
    virtual-host: /hmall # 虚拟主机
    username: hmall # 用户名
    password: 123 # 密码

在发送者的启动类创建一个测试类:

package com.itheima.publisher;

import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class mqTest {

    //引入Rabbit提供的操作类
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Test
    public void test(){
        //设置要发送的字符串
        String massage = "hello rabbitmq";
        //设置要发个那个消息队列
        String name = "simple.queue";
        //调用工具类进行发送
        rabbitTemplate.convertAndSend(name,massage);
    }

}

在接收者创建一个接受的类:

package com.itheima.consumer.mq;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

//将类交给bean容器来进行管理,进行监听
@Component
@Slf4j
public class leatinMq {
    // 利用RabbitListener来声明要监听的队列信息
    // 将来一旦监听的队列中有了消息,就会推送给当前服务,调用当前方法,处理消息。
    // 可以看到方法体中接收的就是消息体的内容
    @RabbitListener(queues = "simple.queue")
    public void Leasion(String msg){
        log.info("接收到消息:{}",msg);
    }
}

输出结果:

11-10 20:42:27:552  INFO 22620 --- [ntContainer#0-1] com.itheima.consumer.mq.leatinMq         : 接收到消息:hello rabbitmq
11-10 20:42:42:000  INFO 22620 --- [ntContainer#0-1] com.itheima.consumer.mq.leatinMq         : 接收到消息:hello rabbitmq
Work Queues

创建队列:

修改发送方的测试函数,发送50条数据:

    //修改为连续发送50条数据到队列中
    @Test
    public void test2(){
        for (int i = 1; i <= 50; i++) {
            String massage = "hello rabbitmq_"+i;
            String name = "work.queue";
            rabbitTemplate.convertAndSend(name,massage);
        }
    }

建立两个监听来进行读取:

    @RabbitListener(queues = "work.queue")
    public void Leasion1(String msg){
        System.out.println("队列1接收到消息:"+msg+"_"+ LocalTime.now());
    }
    @RabbitListener(queues = "work.queue")
    public void Leasion2(String msg){
        System.err.println("队列2接收到消息:"+msg+"_"+ LocalTime.now());
    }

代码运行结果:

队列1接收到消息:hello rabbitmq_1_21:25:12.780
队列1接收到消息:hello rabbitmq_3_21:25:12.780
队列1接收到消息:hello rabbitmq_5_21:25:12.781
队列1接收到消息:hello rabbitmq_7_21:25:12.781
队列1接收到消息:hello rabbitmq_9_21:25:12.781
队列1接收到消息:hello rabbitmq_11_21:25:12.781
队列1接收到消息:hello rabbitmq_13_21:25:12.782
队列1接收到消息:hello rabbitmq_15_21:25:12.782
队列1接收到消息:hello rabbitmq_17_21:25:12.783
队列1接收到消息:hello rabbitmq_19_21:25:12.783
队列1接收到消息:hello rabbitmq_21_21:25:12.783
队列1接收到消息:hello rabbitmq_23_21:25:12.783
队列1接收到消息:hello rabbitmq_25_21:25:12.783
队列1接收到消息:hello rabbitmq_27_21:25:12.783
队列1接收到消息:hello rabbitmq_29_21:25:12.784
队列1接收到消息:hello rabbitmq_31_21:25:12.784
队列1接收到消息:hello rabbitmq_33_21:25:12.785
队列1接收到消息:hello rabbitmq_35_21:25:12.785
队列1接收到消息:hello rabbitmq_37_21:25:12.787
队列1接收到消息:hello rabbitmq_39_21:25:12.787
队列1接收到消息:hello rabbitmq_41_21:25:12.788
队列1接收到消息:hello rabbitmq_43_21:25:12.788
队列1接收到消息:hello rabbitmq_45_21:25:12.789
队列1接收到消息:hello rabbitmq_47_21:25:12.789
队列1接收到消息:hello rabbitmq_49_21:25:12.789
队列2接收到消息:hello rabbitmq_2_21:25:12.780
队列2接收到消息:hello rabbitmq_4_21:25:12.780
队列2接收到消息:hello rabbitmq_6_21:25:12.780
队列2接收到消息:hello rabbitmq_8_21:25:12.781
队列2接收到消息:hello rabbitmq_10_21:25:12.781
队列2接收到消息:hello rabbitmq_12_21:25:12.781
队列2接收到消息:hello rabbitmq_14_21:25:12.781
队列2接收到消息:hello rabbitmq_16_21:25:12.781
队列2接收到消息:hello rabbitmq_18_21:25:12.782
队列2接收到消息:hello rabbitmq_20_21:25:12.783
队列2接收到消息:hello rabbitmq_22_21:25:12.783
队列2接收到消息:hello rabbitmq_24_21:25:12.783
队列2接收到消息:hello rabbitmq_26_21:25:12.783
队列2接收到消息:hello rabbitmq_28_21:25:12.783
队列2接收到消息:hello rabbitmq_30_21:25:12.784
队列2接收到消息:hello rabbitmq_32_21:25:12.784
队列2接收到消息:hello rabbitmq_34_21:25:12.785
队列2接收到消息:hello rabbitmq_36_21:25:12.785
队列2接收到消息:hello rabbitmq_38_21:25:12.785
队列2接收到消息:hello rabbitmq_40_21:25:12.785
队列2接收到消息:hello rabbitmq_42_21:25:12.785
队列2接收到消息:hello rabbitmq_44_21:25:12.788
队列2接收到消息:hello rabbitmq_46_21:25:12.790
队列2接收到消息:hello rabbitmq_48_21:25:12.790
队列2接收到消息:hello rabbitmq_50_21:25:12.790

可以看出这两个监听者是轮流进行监听的。并且不考虑是否有运行速度的区别。

这个是将1监听设置线程休眠25毫秒即每秒中可处理40个,

这个是将1监听设置线程休眠200毫秒即每秒中可处理5个,的运行情况

队列1接收到消息:hello rabbitmq_1_21:31:42.712
队列1接收到消息:hello rabbitmq_3_21:31:42.737
队列1接收到消息:hello rabbitmq_5_21:31:42.762
队列1接收到消息:hello rabbitmq_7_21:31:42.787
队列1接收到消息:hello rabbitmq_9_21:31:42.813
队列1接收到消息:hello rabbitmq_11_21:31:42.838
队列1接收到消息:hello rabbitmq_13_21:31:42.864
队列2接收到消息:hello rabbitmq_2_21:31:42.885
队列1接收到消息:hello rabbitmq_15_21:31:42.890
队列1接收到消息:hello rabbitmq_17_21:31:42.915
队列1接收到消息:hello rabbitmq_19_21:31:42.941
队列1接收到消息:hello rabbitmq_21_21:31:42.967
队列1接收到消息:hello rabbitmq_23_21:31:42.993
队列1接收到消息:hello rabbitmq_25_21:31:43.019
队列1接收到消息:hello rabbitmq_27_21:31:43.045
队列1接收到消息:hello rabbitmq_29_21:31:43.070
队列2接收到消息:hello rabbitmq_4_21:31:43.086
队列1接收到消息:hello rabbitmq_31_21:31:43.097
队列1接收到消息:hello rabbitmq_33_21:31:43.122
队列1接收到消息:hello rabbitmq_35_21:31:43.148
队列1接收到消息:hello rabbitmq_37_21:31:43.173
队列1接收到消息:hello rabbitmq_39_21:31:43.198
队列1接收到消息:hello rabbitmq_41_21:31:43.223
队列1接收到消息:hello rabbitmq_43_21:31:43.249
队列1接收到消息:hello rabbitmq_45_21:31:43.274
队列2接收到消息:hello rabbitmq_6_21:31:43.286
队列1接收到消息:hello rabbitmq_47_21:31:43.300
队列1接收到消息:hello rabbitmq_49_21:31:43.326
队列2接收到消息:hello rabbitmq_8_21:31:43.487
队列2接收到消息:hello rabbitmq_10_21:31:43.687
队列2接收到消息:hello rabbitmq_12_21:31:43.887
队列2接收到消息:hello rabbitmq_14_21:31:44.089
队列2接收到消息:hello rabbitmq_16_21:31:44.289
队列2接收到消息:hello rabbitmq_18_21:31:44.490
队列2接收到消息:hello rabbitmq_20_21:31:44.691
队列2接收到消息:hello rabbitmq_22_21:31:44.891
队列2接收到消息:hello rabbitmq_24_21:31:45.092
队列2接收到消息:hello rabbitmq_26_21:31:45.293
队列2接收到消息:hello rabbitmq_28_21:31:45.495
队列2接收到消息:hello rabbitmq_30_21:31:45.695
队列2接收到消息:hello rabbitmq_32_21:31:45.896
队列2接收到消息:hello rabbitmq_34_21:31:46.098
队列2接收到消息:hello rabbitmq_36_21:31:46.299
队列2接收到消息:hello rabbitmq_38_21:31:46.499
队列2接收到消息:hello rabbitmq_40_21:31:46.699
队列2接收到消息:hello rabbitmq_42_21:31:46.900
队列2接收到消息:hello rabbitmq_44_21:31:47.101
队列2接收到消息:hello rabbitmq_46_21:31:47.303
队列2接收到消息:hello rabbitmq_48_21:31:47.504
队列2接收到消息:hello rabbitmq_50_21:31:47.704

下面将设置条件,能者多劳。

修改后的情况:

队列1接收到消息:hello rabbitmq_1_21:34:50.426
队列1接收到消息:hello rabbitmq_3_21:34:50.454
队列1接收到消息:hello rabbitmq_4_21:34:50.482
队列1接收到消息:hello rabbitmq_5_21:34:50.508
队列1接收到消息:hello rabbitmq_6_21:34:50.534
队列1接收到消息:hello rabbitmq_7_21:34:50.565
队列1接收到消息:hello rabbitmq_8_21:34:50.592
队列2接收到消息:hello rabbitmq_2_21:34:50.599
队列1接收到消息:hello rabbitmq_9_21:34:50.618
队列1接收到消息:hello rabbitmq_11_21:34:50.645
队列1接收到消息:hello rabbitmq_12_21:34:50.672
队列1接收到消息:hello rabbitmq_13_21:34:50.698
队列1接收到消息:hello rabbitmq_14_21:34:50.726
队列1接收到消息:hello rabbitmq_15_21:34:50.752
队列1接收到消息:hello rabbitmq_16_21:34:50.780
队列2接收到消息:hello rabbitmq_10_21:34:50.800
队列1接收到消息:hello rabbitmq_17_21:34:50.807
队列1接收到消息:hello rabbitmq_19_21:34:50.835
队列1接收到消息:hello rabbitmq_20_21:34:50.863
队列1接收到消息:hello rabbitmq_21_21:34:50.890
队列1接收到消息:hello rabbitmq_22_21:34:50.918
队列1接收到消息:hello rabbitmq_23_21:34:50.944
队列1接收到消息:hello rabbitmq_24_21:34:50.972
队列1接收到消息:hello rabbitmq_25_21:34:50.999
队列2接收到消息:hello rabbitmq_18_21:34:51.003
队列1接收到消息:hello rabbitmq_26_21:34:51.028
队列1接收到消息:hello rabbitmq_28_21:34:51.055
队列1接收到消息:hello rabbitmq_29_21:34:51.081
队列1接收到消息:hello rabbitmq_30_21:34:51.108
队列1接收到消息:hello rabbitmq_31_21:34:51.135
队列1接收到消息:hello rabbitmq_32_21:34:51.162
队列1接收到消息:hello rabbitmq_33_21:34:51.188
队列2接收到消息:hello rabbitmq_27_21:34:51.205
队列1接收到消息:hello rabbitmq_34_21:34:51.215
队列1接收到消息:hello rabbitmq_36_21:34:51.242
队列1接收到消息:hello rabbitmq_37_21:34:51.269
队列1接收到消息:hello rabbitmq_38_21:34:51.295
队列1接收到消息:hello rabbitmq_39_21:34:51.322
队列1接收到消息:hello rabbitmq_40_21:34:51.349
队列1接收到消息:hello rabbitmq_41_21:34:51.376
队列1接收到消息:hello rabbitmq_42_21:34:51.403
队列2接收到消息:hello rabbitmq_35_21:34:51.406
队列1接收到消息:hello rabbitmq_43_21:34:51.430
队列1接收到消息:hello rabbitmq_45_21:34:51.456
队列1接收到消息:hello rabbitmq_46_21:34:51.483
队列1接收到消息:hello rabbitmq_47_21:34:51.509
队列1接收到消息:hello rabbitmq_48_21:34:51.536
队列1接收到消息:hello rabbitmq_49_21:34:51.562
队列1接收到消息:hello rabbitmq_50_21:34:51.589
队列2接收到消息:hello rabbitmq_44_21:34:51.608

Fanout交换机

案例:

声明两个消息队列:

创建一个fanout模式的交换机:

将交换机和消息队列关联:

修改消费者的方法:

    @RabbitListener(queues = "fanout.queue1")
    public void Fanoutlisten1(String msg) throws InterruptedException {
        System.err.println("消费者1接收到队列fanout.queue1的消息:"+msg+"_"+ LocalTime.now());
    }

    @RabbitListener(queues = "fanout.queue2")
    public void Fanoutlisten2(String msg) throws InterruptedException {
        System.err.println("消费者2接收到队列fanout.queue2的消息:"+msg+"_"+ LocalTime.now());
    }

修改发送者的代码,使其发送到 hm.fanout 交换机:

    @Test
    public void testFanout(){
        String massage = "hello rabbitmq";
        //修改交换机的名字为hm.fanout
        String name = "hm.fanout";
        //由于是广播,所以发送到交换机,不需要指定路由键,将消息队列名称设置为null
        rabbitTemplate.convertAndSend(name,null,massage);
    }

结果:

消费者2接收到队列fanout.queue2的消息:hello rabbitmq_22:15:58.655
消费者1接收到队列fanout.queue1的消息:hello rabbitmq_22:15:58.655
交换机小结:

Direct队列
案例

创建队列:

创建交换机:

将交换机和队列联系起来:

修改接收者(消费者):

    @RabbitListener(queues = "direct.queue1")
    public void Directlisten1(String msg) throws InterruptedException {
        System.err.println("消费者1接收到队列direct.queue1的消息:"+msg+"_"+ LocalTime.now());
    }

    @RabbitListener(queues = "direct.queue2")
    public void Directlisten2(String msg) throws InterruptedException {
        System.err.println("消费者2接收到队列direct.queue2的消息:"+msg+"_"+ LocalTime.now());
    }

发送者:

    @Test
    public void testDirect1(){
        String massage = "红色:震惊男生宿舍后面发现女尸";
        //修改交换机的名字为hm.fanout
        String name = "hm.direct";
        //修改路由键为red
        rabbitTemplate.convertAndSend(name,"red",massage);
    }
    @Test
    public void testDirect2(){
        String massage = "蓝色:该女尸竟是硅胶制品";
        //修改交换机的名字为hm.fanout
        String name = "hm.direct";
        //修改路由键为blue
        rabbitTemplate.convertAndSend(name,"blue",massage);
    }

结果:

消费者1接收到队列direct.queue1的消息:红色:震惊男生宿舍后面发现女尸_22:40:27.927
消费者2接收到队列direct.queue2的消息:红色:震惊男生宿舍后面发现女尸_22:40:27.927
消费者1接收到队列direct.queue1的消息:蓝色:该女尸竟是硅胶制品_22:40:37.891
Topic交换机

案例

创建两个消息队列:

创建topic交换机:

关联交换机和消息队列:

修改发送者:

    @Test
    public void testTopic1(){
        String massage = "今天天气不错啊";
        //修改交换机的名字为hm.topic
        String name = "hm.topic";
        //修改路由键为red
        rabbitTemplate.convertAndSend(name,"china.news",massage);
    }
    @Test
    public void testTopic2(){
        String massage = "这是一个大新闻啊";
        //修改交换机的名字为hm.topic
        String name = "hm.topic";
        //修改路由键为blue
        rabbitTemplate.convertAndSend(name,"china.goods",massage);
    }

修改接收值:

    @RabbitListener(queues = "topic.queue1")
    public void Topiclisten1(String msg) throws InterruptedException {
        System.err.println("消费者1接收到队列topic.queue1的消息:"+msg+"_"+ LocalTime.now());
    }

    @RabbitListener(queues = "topic.queue2")
    public void Topiclisten2(String msg) throws InterruptedException {
        System.err.println("消费者2接收到队列topic.queue2的消息:"+msg+"_"+ LocalTime.now());
    }

运行结果:

消费者2接收到队列topic.queue2的消息:今天天气不错啊_09:08:04.351
消费者1接收到队列topic.queue1的消息:今天天气不错啊_09:08:04.351
消费者1接收到队列topic.queue1的消息:这是一个大新闻啊_09:08:12.309
Topic小结

声明队列交换机

 注意:       由于消息发送端通常只负责消息的发送,所以在通常情况下都是将消息队列和交换机的创建放在消息的接受端。

在接受端创建fanout交换机和队列:

1、删除已有的fanout交换机和队列。

2、编写代码:

        在消息接受者编写代码,创建一个配置类:

package com.itheima.consumer.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FanoutConfigrasion {
    //交给Bean注解来进行处理
    //创建交换机
    @Bean
    public FanoutExchange fanoutExchange(){
        //参数:交换机名称,是否持久化,是否自动删除,持久化默认为开启(持久化就是是否保存到磁盘)
//        return new FanoutExchange("hm.fanout");
        //使用build来创建交换机,durable(true)即是否持久化
        return ExchangeBuilder.fanoutExchange("hm.fanout").durable(true).build();
    }

    //创建消息队列
    @Bean
    public Queue fanoutQueue1(){
//        return new Queue("fanout.queue1");
        //使用build来创建消息队列
        return QueueBuilder.durable("fanout.queue1").build();
    }
    @Bean
    public Queue fanoutQueue2(){
//        return new Queue("fanout.queue1");
        //使用build来创建消息队列
        return QueueBuilder.durable("fanout.queue2").build();
    }

    // 绑定队列和交换机
    @Bean
    public Binding bindingfanoutQueue1(Queue fanoutQueue1, FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    }
    @Bean
    public Binding bindingfanoutQueue2(Queue fanoutQueue2, FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }
    
}

运行该模块就可以创建交换机和消息队列:

由于基于Bean注解的方式,需要每个key都要写一遍比较麻烦。

还提供基于@RabbitListener的声明方式。

使用配置类注解的方式:

package com.itheima.consumer.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class dircetConfigrasion {
    //交给Bean注解来进行处理
    //创建交换机
    @Bean
    public DirectExchange directExchange(){
        //参数:交换机名称,是否持久化,是否自动删除,持久化默认为开启(持久化就是是否保存到磁盘)
//        return new FanoutExchange("hm.fanout");
        //使用build来创建交换机,durable(true)即是否持久化
        return ExchangeBuilder.directExchange("hm.direct").durable(true).build();
    }

    //创建消息队列
    @Bean
    public Queue DirectQueue1(){
//        return new Queue("fanout.queue1");
        //使用build来创建消息队列
        return QueueBuilder.durable("direct.queue1").build();
    }
    @Bean
    public Queue DirectQueue2(){
//        return new Queue("fanout.queue1");
        //使用build来创建消息队列
        return QueueBuilder.durable("direct.queue2").build();
    }

    // 绑定队列和交换机
    @Bean
    public Binding bindingfanoutQueue1red(Queue directQueue1, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue1).to(directExchange).with("red");
    }
    @Bean
    public Binding bindingfanoutQueue1blue(Queue directQueue1, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue1).to(directExchange).with("blue");
    }
    @Bean
    public Binding bindingfanoutQueue2red(Queue directQueue2, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue2).to(directExchange).with("red");
    }
    @Bean
    public Binding bindingfanoutQueue2yellow(Queue directQueue2, DirectExchange directExchange){
        return BindingBuilder.bind(directQueue2).to(directExchange).with("yellow");
    }

}

使用注解来创建:

1、注释掉Config注解使上面的配置类失效

2、代码:

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue1"),
            exchange = @Exchange(name = "hm.direct",type = ExchangeTypes.DIRECT),
            key = {"red","blue"}
    ))
    public void Directlisten1redblue(String msg) throws InterruptedException {
        System.err.println("消费者1接收到队列direct.queue1的消息:"+msg+"_"+ LocalTime.now());
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue2"),
            exchange = @Exchange(name = "hm.direct",type = ExchangeTypes.DIRECT),
            key = {"red","yellow"}
    ))
    public void Directlisten2redyellow(String msg) throws InterruptedException {
        System.err.println("消费者2接收到队列direct.queue2的消息:"+msg+"_"+ LocalTime.now());
    }

3、结果,运行项目创建成功。

消息转换器
案例:

    @Test
    public void testObgect(){
        //准备Map数据
        Map map = new HashMap();
        map.put("name","jack");
        map.put("age",21);
        rabbitTemplate.convertAndSend("obgect.queue",map);
    }

使用JSON序列化器:

引入依赖:

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    <version>2.9.10</version>
</dependency>

在两个模块都添加配置项:

@Bean
public MessageConverter messageConverter(){
    // 1.定义消息转换器
    Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
    // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
    jackson2JsonMessageConverter.setCreateMessageIds(true);
    return jackson2JsonMessageConverter;
}

发送者的配置项添加到启动类中:

package com.itheima.publisher;


import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class PublisherApplication {
    public static void main(String[] args) {
        SpringApplication.run(PublisherApplication.class);
    }

    @Bean
    public MessageConverter messageConverter(){
        // 1.定义消息转换器
        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
        jackson2JsonMessageConverter.setCreateMessageIds(true);
        return jackson2JsonMessageConverter;
    }
}

接受者代码:

配置类:

package com.itheima.consumer.config;

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JackionConfig {
    @Bean
    public MessageConverter messageConverter(){
        // 1.定义消息转换器
        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
        jackson2JsonMessageConverter.setCreateMessageIds(true);
        return jackson2JsonMessageConverter;
    }
}

接收监听:

    @RabbitListener(queues = "obgect.queue")
    public void Obgectlisten(Map msg) throws InterruptedException {
        System.err.println("消费者1接收到队列fanout.queue1的消息:"+msg);
    }
结果:
消费者1接收到队列fanout.queue1的消息:{name=jack, age=21}

业务改造:

给两个模块都引入依赖引入依赖:

        <!--消息发送-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
<!--        序列化器-->
        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
            <version>2.9.10</version>
        </dependency>

两个模块都设置配置文件

spring:
  rabbitmq:
    host: 192.168.21.101 # 你的虚拟机IP
    port: 5672 # 端口
    virtual-host: /hmall # 虚拟主机
    username: hmall # 用户名
    password: 123 # 密码

为两个模块设置序列化器:

package com.hmall.trade.config;

import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JackionConfigration {
    @Bean
    public MessageConverter messageConverter(){
        // 1.定义消息转换器
        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
        jackson2JsonMessageConverter.setCreateMessageIds(true);
        return jackson2JsonMessageConverter;
    }
}

在接受端设置接受代码:

package com.hmall.trade.listener;

import com.hmall.trade.service.IOrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class Orderlisten {

    private final IOrderService orderService;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "trade.pay.success.queue", durable = "true"),
            exchange = @Exchange(name = "pay.topic"),
            key = "pay.success"
    ))
    public void listenPaySuccess(Long orderId){
        orderService.markOrderPaySuccess(orderId);
    }
}

改造发送端的代码:

        //TODO 5.修改订单状态
        try {
            rabbitTemplate.convertAndSend("pay.direct", "pay.success", po.getBizOrderNo());
        } catch (Exception e) {
            log.error("支付成功的消息发送失败,支付单id:{}, 交易单id:{}", po.getId(), po.getBizOrderNo(), e);
        }
package com.hmall.pay.service.impl;

import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;

import com.hmall.api.client.UserClient;
import com.hmall.api.client.tradeClient;
import com.hmall.common.exception.BizIllegalException;
import com.hmall.common.utils.BeanUtils;
import com.hmall.common.utils.UserContext;
import com.hmall.pay.domain.dto.PayApplyDTO;
import com.hmall.pay.domain.dto.PayOrderFormDTO;
import com.hmall.pay.domain.po.PayOrder;
import com.hmall.pay.enums.PayStatus;
import com.hmall.pay.mapper.PayOrderMapper;
import com.hmall.pay.service.IPayOrderService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

/**
 * <p>
 * 支付订单 服务实现类
 * </p>
 *
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class PayOrderServiceImpl extends ServiceImpl<PayOrderMapper, PayOrder> implements IPayOrderService {

    private final UserClient userClient;

    private final RabbitTemplate rabbitTemplate;

//    private final tradeClient tradeClient;

    @Override
    public String applyPayOrder(PayApplyDTO applyDTO) {
        // 1.幂等性校验
        PayOrder payOrder = checkIdempotent(applyDTO);
        // 2.返回结果
        return payOrder.getId().toString();
    }

    @Override
    @Transactional
    public void tryPayOrderByBalance(PayOrderFormDTO payOrderDTO) {
        // 1.查询支付单
        PayOrder po = getById(payOrderDTO.getId());
        // 2.判断状态
        if(!PayStatus.WAIT_BUYER_PAY.equalsValue(po.getStatus())){
            // 订单不是未支付,状态异常
            throw new BizIllegalException("交易已支付或关闭!");
        }
        // 3.尝试扣减余额
        userClient.deductMoney(payOrderDTO.getPw(), po.getAmount());
        // 4.修改支付单状态
        boolean success = markPayOrderSuccess(payOrderDTO.getId(), LocalDateTime.now());
        if (!success) {
            throw new BizIllegalException("交易已支付或关闭!");
        }
        //TODO 5.修改订单状态
        try {
            rabbitTemplate.convertAndSend("pay.direct", "pay.success", po.getBizOrderNo());
        } catch (Exception e) {
            log.error("支付成功的消息发送失败,支付单id:{}, 交易单id:{}", po.getId(), po.getBizOrderNo(), e);
        }
//        tradeClient.markOrderPaySuccess(po.getBizOrderNo());
    }

    public boolean markPayOrderSuccess(Long id, LocalDateTime successTime) {
        return lambdaUpdate()
                .set(PayOrder::getStatus, PayStatus.TRADE_SUCCESS.getValue())
                .set(PayOrder::getPaySuccessTime, successTime)
                .eq(PayOrder::getId, id)
                // 支付状态的乐观锁判断
                .in(PayOrder::getStatus, PayStatus.NOT_COMMIT.getValue(), PayStatus.WAIT_BUYER_PAY.getValue())
                .update();
    }


    private PayOrder checkIdempotent(PayApplyDTO applyDTO) {
        // 1.首先查询支付单
        PayOrder oldOrder = queryByBizOrderNo(applyDTO.getBizOrderNo());
        // 2.判断是否存在
        if (oldOrder == null) {
            // 不存在支付单,说明是第一次,写入新的支付单并返回
            PayOrder payOrder = buildPayOrder(applyDTO);
            payOrder.setPayOrderNo(IdWorker.getId());
            save(payOrder);
            return payOrder;
        }
        // 3.旧单已经存在,判断是否支付成功
        if (PayStatus.TRADE_SUCCESS.equalsValue(oldOrder.getStatus())) {
            // 已经支付成功,抛出异常
            throw new BizIllegalException("订单已经支付!");
        }
        // 4.旧单已经存在,判断是否已经关闭
        if (PayStatus.TRADE_CLOSED.equalsValue(oldOrder.getStatus())) {
            // 已经关闭,抛出异常
            throw new BizIllegalException("订单已关闭");
        }
        // 5.旧单已经存在,判断支付渠道是否一致
        if (!StringUtils.equals(oldOrder.getPayChannelCode(), applyDTO.getPayChannelCode())) {
            // 支付渠道不一致,需要重置数据,然后重新申请支付单
            PayOrder payOrder = buildPayOrder(applyDTO);
            payOrder.setId(oldOrder.getId());
            payOrder.setQrCodeUrl("");
            updateById(payOrder);
            payOrder.setPayOrderNo(oldOrder.getPayOrderNo());
            return payOrder;
        }
        // 6.旧单已经存在,且可能是未支付或未提交,且支付渠道一致,直接返回旧数据
        return oldOrder;
    }

    private PayOrder buildPayOrder(PayApplyDTO payApplyDTO) {
        // 1.数据转换
        PayOrder payOrder = BeanUtils.toBean(payApplyDTO, PayOrder.class);
        // 2.初始化数据
        payOrder.setPayOverTime(LocalDateTime.now().plusMinutes(120L));
        payOrder.setStatus(PayStatus.WAIT_BUYER_PAY.getValue());
        payOrder.setBizUserId(UserContext.getUser());
        return payOrder;
    }
    public PayOrder queryByBizOrderNo(Long bizOrderNo) {
        return lambdaQuery()
                .eq(PayOrder::getBizOrderNo, bizOrderNo)
                .one();
    }
}

业务改造完毕。

作业:

作业1

将MQ配置抽取到Nacos中管理,微服务中直接使用共享配置。

1、为pay-service模块引入依赖,统一配置管理和读取配置文件的依赖

        <!--统一配置管理-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--读取bootstrap文件-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>

2、在nacos中创建一个共享配置文件

spring:
  rabbitmq:
    host: ${hm.mq.host:192.168.21.101}
    port: ${hm.mq.port:5672} # 端口
    virtual-host: ${hm.mq.virtual-host:/hmall} # 虚拟主机
    username: ${hm.mq.username:hmall} # 用户名
    password: ${hm.mq.password:123} # 密码

3、修改模块中的配置文件

server:
  port: 8086
feign:
  okhttp:
    enabled: true # 开启OKHttp连接池支持
  sentinel:
    enabled: true # 开启feign对sentinel的支持
hm:
  swagger:
    title: 支付服务接口文档
    package: com.hmall.pay.controller
  db:
    database: hm-pay
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8090 #访问路径
      http-method-specify: true # 开启请求方式前缀
    nacos:
      server-addr: 192.168.21.101
  application:
    name: pay-service

4、添加引导配置文件

bootstrap.yml

spring:
  application:
    name: pay-service # 服务名称
  profiles:
    active: dev
  cloud:
    nacos:
      server-addr: 192.168.21.101 # nacos地址
      config:
        file-extension: yaml # 文件后缀名
        shared-configs: # 共享配置
          - dataId: shared-jdbc.yaml # 共享mybatis配置
          - dataId: shared-log.yaml # 共享日志配置
          - dataId: shared-swagger.yaml # 共享日志配置
          - dataId: shared-seata.yaml
          - dataId: shared-mq.yaml

作业二:改造下单功能

改造下单功能,将基于OpenFeign的清理购物车同步调用,改为基于RabbitMQ的异步通知:

  • 定义topic类型交换机,命名为trade.topic

  • 定义消息队列,命名为cart.clear.queue

  • cart.clear.queuetrade.topic绑定,BindingKeyorder.create

  • 下单成功时不再调用清理购物车接口,而是发送一条消息到trade.topic,发送消息的RoutingKeyorder.create,消息内容是下单的具体商品、当前登录用户信息

  • 购物车服务监听cart.clear.queue队列,接收到消息后清理指定用户的购物车中的指定商品

1、在car-service模块添加依赖:

        <!--消息发送-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

2、修改配置文件:

spring:
  application:
    name: cart-service # 服务名称
  profiles:
    active: dev
  cloud:
    nacos:
      server-addr: 192.168.21.101 # nacos地址
      config:
        file-extension: yaml # 文件后缀名
        shared-configs: # 共享配置
          - dataId: shared-jdbc.yaml # 共享mybatis配置
          - dataId: shared-log.yaml # 共享日志配置
          - dataId: shared-swagger.yaml # 共享日志配置
          - dataId: shared-seata.yaml # 共享日志配置
          - dataId: shared-mq.yaml # 共享日志配置

3、添加配置类配置序列化器

@Configuration
public class JackionConfigration {
    @Bean
    public MessageConverter messageConverter(){
        // 1.定义消息转换器
        Jackson2JsonMessageConverter jackson2JsonMessageConverter = new Jackson2JsonMessageConverter();
        // 2.配置自动创建消息id,用于识别不同消息,也可以在业务中基于ID判断是否是重复消息
        jackson2JsonMessageConverter.setCreateMessageIds(true);
        return jackson2JsonMessageConverter;
    }
}

4、创建监听:

package com.hmall.cart.listener;


import com.hmall.cart.service.impl.CartServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Set;

@Component
@RequiredArgsConstructor
public class Catlisten {

    private final CartServiceImpl cartService;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "cart.clear.queue", durable = "true"),
            exchange = @Exchange(name = "trade.topic"),
            key = "order.create"
    ))
    public void listenPaySuccess(Set<Long> orderIds){
        cartService.removeByItemIds(orderIds);
    }
}

修改发送端:trade-service

        //TODO 3.清理购物车商品
//        cartService.removeByItemIds(itemIds);
//        cartClient.deleteCartItemByIds(itemIds);
        rabbitTemplate.convertAndSend("trade.topic","order.create",itemIds);
    @Transactional
    @GlobalTransactional
    public Long createOrder(OrderFormDTO orderFormDTO) {
        // 1.订单数据
        Order order = new Order();
        // 1.1.查询商品
        List<OrderDetailDTO> detailDTOS = orderFormDTO.getDetails();
        // 1.2.获取商品id和数量的Map
        Map<Long, Integer> itemNumMap = detailDTOS.stream()
                .collect(Collectors.toMap(OrderDetailDTO::getItemId, OrderDetailDTO::getNum));
        Set<Long> itemIds = itemNumMap.keySet();
        // 1.3.查询商品
//        List<ItemDTO> items = itemService.queryItemByIds(itemIds);
        List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
        if (items == null || items.size() < itemIds.size()) {
            throw new BadRequestException("商品不存在");
        }
        // 1.4.基于商品价格、购买数量计算商品总价:totalFee
        int total = 0;
        for (ItemDTO item : items) {
            total += item.getPrice() * itemNumMap.get(item.getId());
        }
        order.setTotalFee(total);
        // 1.5.其它属性
        order.setPaymentType(orderFormDTO.getPaymentType());
        order.setUserId(UserContext.getUser());
        order.setStatus(1);
        // 1.6.将Order写入数据库order表中
        save(order);

        // 2.保存订单详情
        List<OrderDetail> details = buildDetails(order.getId(), items, itemNumMap);
        detailService.saveBatch(details);

        //TODO 3.清理购物车商品
//        cartService.removeByItemIds(itemIds);
//        cartClient.deleteCartItemByIds(itemIds);
        rabbitTemplate.convertAndSend("trade.topic","order.create",itemIds);
        // 4.扣减库存
        try {
            itemClient.deductStock(detailDTOS);
//            itemService.deductStock(detailDTOS);
        } catch (Exception e) {
            throw new RuntimeException("库存不足!");
        }
        return order.getId();
    }

修改完毕。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/913583.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

GIT:如何查找已删除的文件的历史记录

首先你得知道文件的名称和路径 然后打开 gitlab&#xff0c;到项目中&#xff0c;仓库-> 文件 查找文件 复制文件名到可能存在过这个文件的分支当中&#xff0c;就能看到了

PSINS工具箱,MATLAB例程,仅以速度为观测量的SINS/GNSS组合导航(滤波方式为EKF)

基于【PSINS工具箱】&#xff0c;提供一个MATLAB例程&#xff0c;仅以速度为观测量的SINS/GNSS组合导航&#xff08;滤波方式为EKF&#xff09; 文章目录 工具箱程序简述运行结果 代码程序讲解MATLAB 代码讲解&#xff1a;速度观测的 EKF 实现代码结构与功能EKF滤波过程结果处理…

hypermesh看模型内部

点击快捷键F5&#xff08;跳跃到mask部分&#xff09; 旋转即可

物品租赁+加盟系统+押金原路返回系统+酒店系统-一体化

一加盟商管理 二、加盟店押金管理 三、押金收银台 四、退押金 五、物品租赁系统领取 公众《未来之窗软件服务中心》 六、阿雪技术观 拥抱开源与共享&#xff0c;见证科技进步奇迹&#xff0c;畅享人类幸福时光&#xff01; 让我们积极投身于技术共享的浪潮中&#xff0c;不仅…

京韵作序 极见东方 玛格·极北京艺术旗舰店盛大开业

你好北京&#xff0c;问鼎高定 以极为信仰&#xff0c;坚持极致美学追求 在登峰造极的路上不断探索 玛格极大宅全案定制&#xff0c;耀启京城 实现人们对美好生活的向往 11月10日&#xff0c;玛格极北京艺术旗舰店在红星美凯龙全球家居1号店华丽亮相&#xff0c;来自家居行…

CSS如何改变滚动条的颜色样式粗细?

默认滚动条很丑怎么办&#xff1f;如何改版滚动条的粗细&#xff0c;颜色&#xff0c;让它更美观&#xff1f;CSS如何改变滚动条的粗细&#xff1f; 干货来了 /* Webkit内核浏览器的滚动条样式 */ ::-webkit-scrollbar {width: 4px; /* 设置滚动条的宽度 */ }::-webkit-scroll…

使用GPT-SoVITS训练语音模型

1.项目演示 阅读单句话 1725352713141 读古诗 1725353700203 2.项目环境 开发环境&#xff1a;linux 机器配置如下&#xff1a;实际使用率百分之二十几&#xff0c; 3.开发步骤 1.首先是准备数据集&#xff0c;要求是wav格式&#xff0c;一到两个小时即可&#xff0c; 2.…

AI生活之我用AI处理Excel表格

AI生活之我用AI处理Excel表格 场景再现AI提问词AI代码运行调试结果心得感受 场景再现 因学习需要&#xff0c;整理了某个题库&#xff0c;方便自己刷题使用。 已将每套题打上了制定标签&#xff0c;得到一个Excel表格。截图如下&#xff1a; 需求是&#xff1a;一共35套题&…

PySpark本地开发环境搭建

一.前置事项 请注意&#xff0c;需要先实现Windows的本地JDK和Hadoop的安装。 二.windows安装Anaconda 资源&#xff1a;Miniconda3-py38-4.11.0-Windows-x86-64&#xff0c;在window使用的Anaconda资源-CSDN文库 右键以管理员身份运行&#xff0c;选择你的安装路径&#x…

virtualBox安装拓展包extension pack失败的超级详细解决办法

virtualBox安装拓展包extension pack时&#xff0c;网上的博主们都说直接进官网下载包&#xff0c;安装就行。下载网站是 https://www.virtualbox.org/wiki/Downloads 但是&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 默认下载的是最新版本的包&#xff0c…

基于标签相关性的多标签学习

基于标签相关性的多标签学习 论文概述什么是多标签学习论文贡献 算法流程挖掘“主题“——提取标签相关性训练 M T M_T MT​模型——拟合{特征集, 主题集合}用标记相关性扩增数据集再次训练拟合 M M M模型——对真实帕金森病例进行筛查 实验结果核心代码复现main.py文件multi_l…

阿里公告:停止 EasyExcel 更新与维护

最近&#xff0c;阿里发布公告通知&#xff0c;将停止对知名 Java Excel 工具库 EasyExcel 的更新和维护。EasyExcel 由阿里巴巴开源&#xff0c;作者是玉箫&#xff0c;在 GitHub 上拥有 30k stars、7.5k forks 的高人气。 据悉&#xff0c;EasyExcel 作者玉箫去年已从阿里离…

稳恒磁场(2)

物理概念 电流元 IdL要足够小 物理理论 毕奥——萨伐尔定律 电流元在空间产生的磁场&#xff0c;磁感应强度与电流I长正比&#xff0c;与半径的平方成反比 后由拉普拉斯总结得出数学表达式&#xff1a; 其中 &#xff0c;μ0 4π*10^-7 N*A^-2 &#xff0c; r^为r反向上…

C语言 | Leetcode C语言题解之第553题最优除法

题目&#xff1a; 题解&#xff1a; #define MAX_STR_LEN 64char * optimalDivision(int* nums, int numsSize) {char * res (char *)malloc(sizeof(char) * MAX_STR_LEN);if (numsSize 1) {sprintf(res, "%d", nums[0]);return res;}if (numsSize 2) {sprintf(r…

IntelliJ+SpringBoot项目实战(二)---构建项目多层级模块

本人上一篇文件介绍了使用IntelliJ开发工具构建新的SpringBoot项目。本节介绍SpringBoot项目的模块、子模块如何搭建。 首先我们根据项目的需求&#xff0c;可以将在顶级工程下创建多个模块&#xff0c;已经不同模块下还有子模块。 仍使用上节的OpenJWeb项目作为示例&#xff0…

达梦数据库安装与初始化超教程

达梦数据库&#xff08;DM Database&#xff09;作为中国自主研发的数据库系统&#xff0c;因其高效的性能和稳定的支持广泛应用于政府、金融、能源等关键领域。面对日益增长的数据管理需求&#xff0c;如何快速安装与初始化达梦数据库成了许多新手的第一步。这篇教程&#xff…

Mac中禁用系统更新

Mac中禁用系统更新 文章目录 Mac中禁用系统更新1. 修改hosts&#xff0c;屏蔽系统更新检测联网1. 去除系统偏好设置--系统更新已有的小红点标记 1. 修改hosts&#xff0c;屏蔽系统更新检测联网 打开终端&#xff0c;执行命令&#xff1a; sudo vim /etc/hosts127.0.0.1 swdis…

黑芝麻智能:技术+生态+量产三大先发优势,领跑智驾“芯”成长

黑芝麻智能&#xff1a;定位智驾芯片供应商&#xff0c;卓越研发团队打造强劲产品力。 1&#xff09;公司定位&#xff1a;Tier2 芯片供应商&#xff0c;提供车规级计算 SoC 及基于 SoC 的智能汽车解决方案。2&#xff09;产品结构&#xff1a;公司具有两个车规级 SoC 系列——…

Yolo11改进策略:上采样改进|CARAFE,轻量级上采样|即插即用|附改进方法+代码

论文介绍 CARAFE模块概述&#xff1a;本文介绍了一种名为CARAFE&#xff08;Content-Aware ReAssembly of FEatures&#xff09;的模块&#xff0c;它是一种用于特征上采样的新方法。应用场景&#xff1a;CARAFE模块旨在改进图像处理和计算机视觉任务中的上采样过程&#xff0…

一文了解git TAG

Git标签&#xff08;Tag&#xff09;用于为 Git 历史中的特定提交打上标记&#xff0c;通常用于标识重要的节点&#xff0c;比如发布版本。标签让开发者可以很方便地对特定版本进行引用&#xff0c;且不会随时间变动。Git 中的标签分为两种&#xff1a;轻量标签和附注标签。 …