RabbitMQ 快速入门 7 种简单模式

news2024/11/24 7:17:42

RabbitMQ 快速入门 7 种简单模式

  • 起步
    • 七种模式
    • 项目依赖
  • 1、"Hello World!"
    • (1) Connection 方式
    • (2) RabbitTemplate 方式
  • 2、Work Queues
    • 生产者
    • 消费者
  • 3、Publish/Subscribe
    • 关系绑定
    • 生产者
    • 消费者
  • 4、Routing
    • 消费者
    • 生产者
  • 5. Topics
    • 消费者
    • 生产者

起步

七种模式

这七种模型其实是 RabbitMQ 官方入门文档中给出的消息队运行模式

分别是:

  1. “Hello World!” (最初入门,一发一收)
  2. Work Queues (工作队列,一发多收)
  3. Publish/Subscribe (发布/订阅)
  4. Routing(消息路由)
  5. Topics(话题)
  6. RPC(请求/回复)
  7. Publisher Confirms(发布者确认)

官网地址:https://www.rabbitmq.com/getstarted.html

在这里插入图片描述

项目依赖

推荐创建两个 SpringBoot 项目,一个作为生产者,另一个作为消费者

也可使用 Maven 的继承聚合模式管理两个项目

项目中需要引入下面的依赖

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

1、“Hello World!”

最简单的模式就是一发一收,也是最基础的模式,所以官方称之为 “Hello World!”,表示最初的入门

在这里插入图片描述

(1) Connection 方式

RabbitMQ 中有几种概念,分别是:虚拟主机(virtualHost),通道(channel),队列(queue),还有一个交换机(exchanges)的概念在之后会遇到

服务先与虚拟主机建立连接,然后创建通道,声明或创建队列之后发送或接收消息,消息最终会在队列中传输

下面使用 connection 的方式来实现接发消息,以便于理解 RabbitMQ 的模式(不常用到,了解即可)

生产者测试类

@SpringBootTest
public class PublisherTest {
    @Test
    public void testSendMessage() throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.0.102");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("root");
        factory.setPassword("123456");
        // 1.2.建立连接
        Connection connection = factory.newConnection();

        // 2.创建通道 Channel
        Channel channel = connection.createChannel();

        // 3. 声明队列(不存在则创建)
        String queueName = "hello.world.queue";	// 队列名
        channel.queueDeclare(queueName, false, false, false, null);

        // 4.发送消息
        String message = "hello, rabbitmq!";
        channel.basicPublish("", queueName, null, message.getBytes());
        System.out.println("发送消息成功:" + message);

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

消费者测试类

@SpringBootTest
public class ConsumerTest {

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1. 建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1 设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.0.102");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("root");
        factory.setPassword("123456");
        // 1.2 建立连接
        Connection connection = factory.newConnection();

        // 2. 创建通道 Channel
        Channel channel = connection.createChannel();

        // 3. 声明队列(不存在则创建)
        String queueName = "hello.world.queue";	// 队列名
        channel.queueDeclare(queueName, false, false, false, null);

        // 4. 获取消息
        channel.basicConsume(queueName, true, new DefaultConsumer(channel) {
            @SneakyThrows
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) {
                // 5.处理消息
                String message = new String(body);
                System.out.println("接收到消息:" + message);
            }
        });
        System.out.println("等待接收消息......");
    }
}

我们可以在 RabbitMQ 的管理页面查看相关信息:

在这里插入图片描述

注意:

  1. 消费者接收消息是异步过程,而不会阻塞主线程
  2. queueDeclare 在不存在该队列时会创建队列,否则不创建
  3. 队列不会被自动删除,可以在管理页删除(点击队列名称,点击 Delete 选项)
  4. 消息只会被读取一次,未被读取的消息存放在队列中等待被消费
  5. 上例消费者没有关闭通道和连接的操作,不会只读取一条消息,而是一直等待不停读取
  6. RabbitMQ 重启后,队列因未持久化被删除,将 queueDeclare 第二个参数改为 true 以创建持久化队列(已存在的队列不可更改)
  7. RabbitMQ 重启后,虽有队列但消息没了,因为消息未持久化,发送消息时将 basicPublish 方法第三个参数改为 MessageProperties.PERSISTENT_TEXT_PLAIN 以持久化消息

持久化的队列在 Features 栏会有字母 D 标示,如图:

在这里插入图片描述
有持久化的消息可以看到 Properties 信息,未持久化则没有,如图:

在这里插入图片描述

(2) RabbitTemplate 方式

上面的例子可以看出,大多数代码是重复的,所以 SpringAMQP 中封装了 RabbitTemplate 以便于进行消息队列的操作

首先在项目 yaml 配置文件中假如 RabbitMQ 的连接相关配置

spring:
  rabbitmq:
    host: 192.168.0.102	# RabbitMQ 服务 ip 地址
    port: 5672			# 消息服务端口
    username: root		# 用户名
    password: "123456"	# 密码
    virtual-host: /		#虚拟主机

然后就能自动装配 RabbitTemplate 类了

生产者测试类

@RunWith(SpringRunner.class)
@SpringBootTest()
public class SpringAmqpTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void helloWorldModeTest() {
        String queueName = "hello.world.queue";
        String message   = "Hello, springAMQP!";
        rabbitTemplate.convertAndSend(queueName, message);
    }
}

只需调用 convertAndSend 方法即可发送消息

注意:此操作不会创建队列,如果队列不存在则没有效果

若要创建队列,需要声明一个 Queue 类型的 bean 并受到 Spring 的管理

通常放在一个 Configuration 配置类中,示例如下:

@Configuration
public class RabbitMqConfig {
    @Bean
    public Queue simpleQueue() {
        return new Queue("hello.world.queue");	// 队列名与函数名无关
    }
}

如此启动项目时,bean 被创建,就会创建一个队列(若已存在则不再创建)

消费者不再在测试类中演示,而是使用监听队列的方式

只需在一个方法上注解 @RabbitListener,并指定队列名
同时方法所在的类也要被 Spring 管理(注解 @Component)

@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "hello.world.queue")
    public void listenSimpleQueue(String message) {
        System.out.printf("消费者接收到 hello.world.queue 的消息:【 %s 】\n", message);
    }

}

启动项目即可监听队列并处理接收到的消息

注意:如果监听的队列名不存在,则会报错Failed to declare queue(s):[hello.world.queue],解决方法同前面的配置里创建队列

注意:

  1. 此方式创建的队列默认持久化
  2. 此方式生产的消息默认持久化

2、Work Queues

Work Queues 工作队列其实就是一发多收的模式,当然也可以是多发多收

主要在于每一个消息可能就是一个要处理任务,所以多个消费者处理任务可以提高任务的执行效率

在这里插入图片描述

创建队列

创建队列的配置类可以只在生产者和消费者其一中配置,但有配置的一方应先启动,否则另一方会因不存在队列而失效

当然都配置也没问题,若队列已存在,也可以都不配置

@Configuration
public class RabbitMqConfig {
    @Bean
    public Queue simpleQueue() {
        return new Queue("work.queue");
    }
}

生产者

这里循环 50 次模拟多个任务的发布

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendMessage2WorkQueue() {
        String queueName = "work.queue";
        String message = "work message ---- ";
        for (int i = 1; i <= 50; i ++) {
            rabbitTemplate.convertAndSend(queueName, message + i + "th");
        }
    }

}

消费者

使用两个监听方法来模拟两个消费者,用线程的 sleep() 方法模拟处理任务花费的时间

@Component
public class SpringRabbitListener {

    private int count1 = 0;

    private int count2 = 0;

    @RabbitListener(queues = "work.queue")
    public void listenSimpleQueue1(String msg) throws InterruptedException {
        System.out.printf("消费者 1 第 %d 次接收消息:【 %s 】 %s", ++count1, msg, LocalTime.now().toString());
        Thread.sleep(50);
    }

    @RabbitListener(queues = "work.queue")
    public void listenSimpleQueue2(String msg) throws InterruptedException {
        System.err.printf("消费者 2 第 %d 次接收消息:【 %s 】 %s", ++count2, msg, LocalTime.now().toString());
        Thread.sleep(200);
    }

}

由于两个消费者睡眠时间不同,应该时速度快的消费者处理的任务更多些

但实际运行起来结果是两种处理了相同的任务数量,速度较快消费者 1 处理了一半任务便不在处理了

这和消息队列的预取机制有关

当消费者端存在未处理完成的消息时,队列依然会派发消息给消费者,这些消息会存放在消费者端的缓存中

解决方法就是限制消费者端的预取信息条数,在消费者的 yaml 配置文件中做如下配置即可:

spring:
  rabbitmq:
    listener:
      simple:
        prefetch: 1 # 预取信息条数

预取数量限制为 1 时,消费者只会在处理好当前消息后才会收到新的消息

3、Publish/Subscribe

前面两种模式中,消息只会被一个消费之接收一次,在这里则不同

Publish/Subscribe(发布 / 订阅)模式中,表示生产者的发布的消息会广播给所有的消费者,类似于博主与粉丝的关系

也称为 fanout 模式,因为是使用 fanout 交换机来实现的

在这里插入图片描述

关系绑定

@Configuration
public class FanoutConfig {

    // 创建 Fanout (广播)交换机
    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange("my.fanout");
    }

    // 创建队列 1
    @Bean
    public Queue fanoutQueue1() {
        return new Queue("fanout.queue1");
    }

    // 创建队列 2
    @Bean
    public Queue fanoutQueue2() {
        return new Queue("fanout.queue2");
    }

    // 创建绑定关系(Fanout交换机与队列 1)
    @Bean
    public Binding fanoutBinding1(FanoutExchange fanoutExchange, Queue fanoutQueue1) {
        return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    }

    // 创建绑定关系(Fanout交换机与队列 2)
    @Bean
    public Binding fanoutBinding2(FanoutExchange fanoutExchange, Queue fanoutQueue2) {
        return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }

}

生产者

此时生产者就不再是向队列发送消息,而是向 Fanout 交换机发送

Fanout 交换机会将消息广播给所有与之绑定的队列

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void fanoutMode() {
        String exchangeName = "my.fanout";
        String msg = "Hello, everyone!";
        rabbitTemplate.convertAndSend(exchangeName, "", msg);
    }

}

convertAndSend 向交换机发送消息时,第一个参数为交换机名称,第三个参数为消息内容

第二个参数为 routingKey 路由键,在之后的路由模式中会说到

消费者

消费者监听的依然是队列,没有什么特别的

@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "fanout.queue1")
    public void listenFanoutQueue1(String msg) {
        System.out.printf("消费者接收到 fanout.queue1 的消息:【 %s 】\n", msg);
    }

    @RabbitListener(queues = "fanout.queue2")
    public void listenFanoutQueue2(String msg) {
        System.out.printf("消费者接收到 fanout.queue2 的消息:【 %s 】\n", msg);
    }

}

4、Routing

Routing 也就是路由模式,根据 Direct 交换机实现,也称为 Direct 模式

交换机根据收到的消息的路由键,有条件的路由给不同的队列,且可以是多个队列
在这里插入图片描述

消费者

在前面的 Fanout 模式中,我们知道了在配置类中创建交换机与队列绑定的方式

其实在 @RabbitListener 注解中我们就可以完成这一步骤

先看消费者的实现:

@Component
public class SpringRabbitListener {

    @RabbitListener(bindings = @QueueBinding(   // 监听一个绑定关系
            value = @Queue("direct.queue1")     // 队列
            , exchange = @Exchange(name = "my.direct", type = ExchangeTypes.DIRECT) // 交换机(名称与类型)
            , key = {"info", "warning", "err"}  // 监听的路由键
    ))
    public void listenDirectQueue1(String msg) {
        System.out.printf("消费者接收 direct.queue1 的消息:【 %s 】", msg);
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue("direct.queue2")
            , exchange = @Exchange(name = "my.direct")  // 交换机默认类型即为 ExchangeTypes.DIRECT
            , key = {"err"}
    ))
    public void listenDirectQueue2(String msg) {
        System.err.printf("消费者接收 direct.queue2 的消息:【 %s 】", msg);
    }

}

注意:当注解中的交换机及队列不存在时,要先启动消费者来创建,然后再启动生产者

顺便一提:如若在配置里中声明 Binding 的 bean 时,使用 with() 来指定路由键

如:BindingBuilder.bind(queue).to(directExchange).with("info, err");

生产者

此时生产者向指定交换机发送消息时,指定路由键,即可将消息发送到对应的队列中

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendDirectExchange() {
        String exchangeName = "my.direct";
        String msg;

        msg = "Info message.";
        rabbitTemplate.convertAndSend(exchangeName, "info", msg);

        msg = "Warning message.";
        rabbitTemplate.convertAndSend(exchangeName, "warning", msg);

        msg = "Error message.";
        rabbitTemplate.convertAndSend(exchangeName, "err", msg);
    }

}

输出结果:

消费者接收 direct.queue2 的消息:【 Error message. 】
消费者接收 direct.queue1 的消息:【 Info message. 】
消费者接收 direct.queue1 的消息:【 Warning message. 】
消费者接收 direct.queue1 的消息:【 Error message. 】

queue2 最先输出是由于错误流与输出流的差异

5. Topics

Topics 即话题模式,它和 Routing 模式很相似,都是根据路由键来路由给符合条件的队列,但 Topic 的路由键支持通配符

在这里插入图片描述

消费者

@Component
public class SpringRabbitListener {

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue1")
            , exchange = @Exchange(name = "my.topic", type = ExchangeTypes.TOPIC)	// 交换机类型为 TOPIC
            , key = {"cn.*"}	// * 为通配符,只能匹配到下一个点的任意字符串(如 cn.news 可匹配, cn.news.today 不可匹配)
    ))
    public void listenTopicQueue1(String msg) {
        System.out.printf("消费者接收 cn.* 的消息:【 %s 】\n", msg);
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue2")
            , exchange = @Exchange(name = "my.topic", type = ExchangeTypes.TOPIC)
            , key = {"#.news"}	// 也可用 # 做通配符,效果同 *
    ))
    public void listenTopicQueue2(String msg) {
        System.out.printf("消费者接收 *.news 的消息:【 %s 】\n", msg);
    }

}

生产者

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendTopicExchange() {
        String exchangeName = "my.topic";
        String message = "富强民主文明和谐自由平等公正法制爱国敬业诚信友善";
        rabbitTemplate.convertAndSend(exchangeName, "cn.news", message);
    }

}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ch1_系统启动_bootsect.s

这里介绍&#xff0c; 操作系统启动中的 bootsect.S 文件 1. bootsect.s的作用概览 bootsect.s代码是磁盘引导块程序&#xff0c;驻留在磁盘的第一个扇区中(引导扇区&#xff0c;0磁道(柱面),0磁头&#xff0c;第一个扇区&#xff0c; 如图中的黄色区域&#xff09; 图一&…

springboot+jsp学生心理健康测评网

基于JSP技术设计并实现了学生心理健康网。该系统基于B/S即所谓浏览器/服务器模式&#xff0c;应用SSM框架&#xff0c;选择MySQL作为后台数据库。系统主要包括个人中心、用户管理、知识分类管理、知识信息管理、心理测试管理、交流论坛、试卷管理、系统管理、考试管理等功能模块…

Redis实战——优惠券秒杀(超卖问题)

1 实现优惠券秒杀功能 下单时需要判断两点&#xff1a;1.秒杀是否开始或者结束2.库存是否充足 所以&#xff0c;我们的业务逻辑如下 1. 通过优惠券id获取优惠券信息 2.判断秒杀是否开始&#xff0c;如果未返回错误信息 3.判断秒杀是否结束&#xff0c;如果已经结束返回错误…

第十三章《集合》第2节:List集合

List这个单词意为“列表”,List类型的集合的特点是:元素呈线性排列,致密且有序。下面的图13-3展示了List类型集合的特点。 图13-3 List类型集合 图13-3中的每一个小圆形代表一个元素,可以看到,这些元素被放到List集合中后被排成一列,这就是“线性排列”。List集合中的元…

美国海运专线有哪些港口?美国海运专线有哪些路线?

随着全球物流的日益密切和跨境电商的活跃&#xff0c;跨境物流连接买卖双方的作用越来越重要。美国是中国亚马逊卖家的重要市场之一&#xff0c;那么美国海运专线有哪些港口?又有哪些路线?方联物流以亚马逊平台为例&#xff0c;跟大家聊聊美国海运专线那点事。美国海运专线有…

Android Studio Gradle相关

一、区分gradle version与gradle plugin version 参考博客 gradle是一个构建工具&#xff0c;理论上来说&#xff0c;它可以用来构建任何项目&#xff08;如java项目&#xff0c;ios项目&#xff09;。它可以与任何类型的IDE集成&#xff08;如ecllipse&#xff0c;android st…

2023年上半年软考中级数据库系统工程师如何高效备考?难吗?

考试题型介绍&#xff1a; &#xff08;1&#xff09;基础知识&#xff0c;考试时间为150分钟&#xff0c;笔试&#xff0c;满分75分。45分及格。 &#xff08;2&#xff09;应用技术&#xff0c;考试时间为150分钟&#xff0c;笔试&#xff0c;满分75分。45分及格。 &#…

思维导图之设计原则

设计原则是指导我们代码设计的一些经验总结&#xff0c;对于某些场景下&#xff0c;是否应该应用某种设计模式&#xff0c;具有指导意义。 设计原则总结如下图所示&#xff1a;

[附源码]SSM计算机毕业设计学生互评的在线作业管理系统JAVA

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

pycare检测时间序列异常

1. 获取时间序列数据&#xff08;官方文档是多特征&#xff0c;本文举例是单特征&#xff09; 未安装pycaret的先安装&#xff1a;pip install --pre pycaret &#xff08;也可以pip install pycaret&#xff09; 2.用pycaret检测异常一般用pycaret.anomaly pycaret.anomaly…

七、【React-Router5】嵌套路由

文章目录1、实现效果2、嵌套路由规则3、修改上一节代码3.1、项目结构变更如下3.2、CODE3.2.1、Home.jsx3.2.2、Message.jsx3.2.3、News.jsx3.3、Result1、实现效果 2、嵌套路由规则 注册子路由时要写上父路由的path值路由的匹配是按照注册路由的顺序进行的 3、修改上一节代码…

SparkMlib 之决策树及其案例

文章目录什么是决策树&#xff1f;决策树的优缺点决策树示例——鸢尾花分类什么是决策树&#xff1f; 决策树及其集成是分类和回归机器学习任务的流行方法。决策树被广泛使用&#xff0c;因为它们易于解释&#xff0c;处理分类特征&#xff0c;扩展到多类分类设置&#xff0c;…

信号类型(雷达)——连续波雷达(二)

系列文章目录 《信号类型&#xff08;雷达通信&#xff09;》 《信号类型&#xff08;雷达&#xff09;——雷达波形认识&#xff08;一&#xff09;》 文章目录 目录 一、连续波雷达信号 1.1 单频雷达信号 1.2 调频连续波雷达 1.3 相位编码连续波雷达 1.4 多频连续波雷…

【AI学习笔记】TensorFlow GPU版本的安装(超详细)

安装步骤&#xff1a;1. 确认显卡是否支持CUDA2. 安装CUDA3. 安装cuDNN3.1 安装 cudnn3.2 将cudnn64_8.dll存放的位置加入Path环境变量4. 安装TensorFlow GPU版本4.1 在Anaconda建立TensorFlow GPU虚拟环境4.2 安装Tensorflow-gpu4.3 安装Keras总结1. 确认显卡是否支持CUDA 在…

【计算机】可信平台模块Trusted Platform Module - TPM

简述 Brief Introduction TPM内部功能模块示意图&#xff1a; 引述 Trusted Platform Module Technology Overview (Windows) | Microsoft Learn&#xff1a; Trusted Platform Module (TPM) technology is designed to provide hardware-based, security-related functions.…

速锐得解码理想汽车L8方向盘转向角度应用随动大灯照明升级

前日&#xff0c;速锐得解码了理想汽车L8车型&#xff0c;由于理想L8是新款车型&#xff0c;架构和理想L9十分相似&#xff0c;与理想ONE这一代有比较大的差异&#xff0c;这恰恰也是我们很好的一次学习机会&#xff0c;也让我们重新认识了理想汽车。 我这里&#xff0c;只挑有…

PCL 点云的法向量

一&#xff0c;点的法向量 点云法线 法向量的概念是很小的时候我们就已经说的&#xff0c;法向量是我们点云中一个非常重要的属性&#xff0c;诸如饿哦们常说的三维重建、点云分割&#xff0c;点云去噪 以及特种描述算法等。 特性&#xff1a; 点云中每一点的法向量夹角和曲率…

【即将开源】⽤于3D激光雷达SLAM闭环检测的词袋模型BoW3D

​以下内容来自从零开始机器人SLAM知识星球 每日更新内容 点击领取学习资料 → 机器人SLAM学习资料大礼包 #论文# BoW3D: Bag of Words for Real-time Loop Closing in 3D LiDAR SLAM 论文地址&#xff1a;https://arxiv.org/abs/2208.07473 作者单位&#xff1a;中国沈阳自动…

C++基础语法

cout输出 cin是键盘输入 //i input 输入 o output输出 stream流 输入输出流头文件&#xff08;类似stdio.h&#xff09; 2 #include <iostream> 3 4 //std(标准) 使用标准的命名空间using namespace std;//命名空间,此标识符作为此组群的名字 5 using namespace std; 6…

A*算法-Python实现

好久没有在CSDN上发文章了&#xff0c;快一年了吧。这两天重新登录了一下&#xff0c;不看不知道&#xff0c;一看吓一跳&#xff0c;没想到访问量快13万了。 之前写博客的时候&#xff0c;想着把一些有用的东西写下来&#xff0c;一方面是当做笔记了&#xff0c;免得以后忘记…