RabbbitMQ基本使用及其五种工作模型

news2024/12/25 9:34:56

初识MQ

同步通讯和异步通讯

        什么是同步通讯呢?举个例子,你认识了一个小姐姐,聊的很火热,于是你们慢慢开始打电话,视频聊天,这种方式就成为同步通讯,那什么是一部通讯呢,同样的,你认识了多个小姐姐,你和他们进行文字聊天,这时候你一个人可以和多个人聊天,这就是异步通讯。我们之前进行服务间调用时使用的RestTemplaste,Feign就是同步调用。

同步调用的优缺点

        优点:时效性强,可以立即得到回复,就像你打视频一样

        缺点:假如你一个项目中存在很多业务,相互之间进行调用,如果你增加了新的需求,此时因为原本代码是同步调用,代码耦合度很高,于是乎修改代码变得十分繁琐,并且一个业务可能会调用很多服务,只有上一个服务调用完了,才能到下一个服务,等待的过程中就造成了资源浪费,性能下降,如果当前调用的服务失败,还可能会导致级联失败,服务雪崩。


异步调用的优缺点

优点:

1,代码耦合度低,因为异步调用是采取事件驱动来实现,当请求进来之后到达Broker之后Broker通知各自微服务去执行,服务间不在需要相互调用。

2,吞吐量提升,因为异步调用不像同步调用那样每个服务需要等待上游完成调用。

3,故障隔离,服务之间相互不进行调用,即使你挂了也跟我没关系。

4,流量削峰,假如同时又大量请求,但是你的服务处理请求能力是有限的,于是Broker会净请求先拦截,看服务又能力处理多少请求,就拿多少请求,不会一次性全部发布订阅。

缺点:

1,对Broker的依赖十分高,对他的可靠性,安全性,吞吐能力要求很高,万一他挂了......

2,服务之间相互调用关系不清晰,业务没有明显的流程线,代码出问题不容易排查。

所以,需要根据场景来选择同步还是异步,一般大多数需要同步。

什么是MQ?

MQ(MessageQueue),中文是消息队列,也就是存放消息的队列,也就是时间驱动中的Broker.

常见的MQ对比:

我们这里选择RabbitMQ

RabbitMQ安装

RabbitMQ是基于Erlang语言开发的开源消息中间件,因此它具备强大的并发处理能力

官网地址:RabbitMQ: One broker to queue them all | RabbitMQicon-default.png?t=N7T8https://www.rabbitmq.com/

这里我们只用Docker来安装RabbitMQ(快捷方便)

1,拉取RabbitMQ的镜像

docker pull rabbitmq:3-management 

2,运行RabbitMQ容器

docker run \
 -e RABBITMQ_DEFAULT_USER=你的账户名\
 -e RABBITMQ_DEFAULT_PASS=你的密码\
 --name mq \  
 --hostname mq1 \
 -p 15672:15672 \
 -p 5672:5672 \
 -d \
 rabbitmq:3-management

注意:账户名和密码是自己定义的,15672是RabbitMQ的管理端端口,5672是RabbitMQ通讯的端口。

3,在浏览器输入IP地址:15672,输入帐号名和密码登录

至此,安装成功

我们可以看到界面有好多目录,具体作用如下

channel:用来操作mq的工具

exchange:路由消息到队列中

queue:缓存消息

virtual host:虚拟主机,是对queue,exchange等资源的逻辑分组

MQ的整体结构:

消息发送者将消息发送到交换机,交换机将其路由到队列,消费者从队列中取走消息

MQ中常见消息模型

大致可以分为两类

第一类:基本消息队列(BasicQueue),工作消息队列(WorkQueue),他们都是最基本的消息队列

第二类:发布订阅(publish,Subscribe)根据交换机类型分为三种

Fanout Exchange(广播),Direct Exchange(路由),Topic Exchange(主题)

  

更多可参考官方文档: 

RabbitMQ Tutorials | RabbitMQ

RabbitMQ入门案例

我们使用RabbitMQ参考官方文档,完成一个hello world案例

引入依赖:

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

创建一个测试类publisher,用来发送消息代码如下:

public class PublisherTest {
    @Test
    public void testSendMessage() throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.*.*");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("***");
        factory.setPassword("****");
        // 1.2.建立连接
        Connection connection = factory.newConnection();
        // 2.创建通道Channel
        Channel channel = connection.createChannel();
        // 3.创建队列
        String queueName = "simple.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();

    }
}

Debug执行观察:

1,连接工厂初始化完成之后,他会创建一个连接,连接上RabbitMQ ,这时界面显示如下

2,之后他会创建一个Channel ,用于操作RabbbitMQ

3,之后会创建一个我们定义好的消息队列simple.queue:

4,之后发送消息hello,rabbitmq!

5,完成之后关闭连接,但是消息依旧存在与消息队列中

之后创建一个测试类consumer,用来接受消息代码如下:

public class ConsumerTest {

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.建立连接
        ConnectionFactory factory = new ConnectionFactory();
        // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
        factory.setHost("192.168.*.*");
        factory.setPort(5672);
        factory.setVirtualHost("/");
        factory.setUsername("****");
        factory.setPassword("****");
        // 1.2.建立连接
        Connection connection = factory.newConnection();
        // 2.创建通道Channel
        Channel channel = connection.createChannel();
        // 3.创建队列
        String queueName = "simple.queue";
        channel.queueDeclare(queueName, false, false, false, null);
        // 4.订阅消息
        channel.basicConsume(queueName, true, new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope,
                                       AMQP.BasicProperties properties, byte[] body) throws IOException {
                // 5.处理消息
                String message = new String(body);
                System.out.println("接收到消息:【" + message + "】");
            }
        });
        System.out.println("等待接收消息。。。。");
    }
}

 跟publisher一样创建工厂,建立连接,这里需要说明的是,之所以还要创建一个队列是因为在实际执行过程中,发布者和消费者又可能执行顺序不一致,所以我们消费者也需要创建一个队列,不过这个队列只会有一个,如果创建了就不再创建。之后消费者接受消息处理,消息队列中消息清空。

根据上述代码我们可以看到官方的demo确实是有带你复杂繁琐,实在是很不友好啊,于是我们使用一种简单的方式来操作RabbitMQ!


SpringAMQP

AMQP(Advanced Message Queuing Protocal):高级消息队列协议,是应用程序之间传递业务消息的开放标准/规范,和语言和平台无关。

SpringAMQP:是基于AMQP协议定义的一套API规范,提供了模板用来发送和接受消息,包含两部分,其中spring-amop是基础抽象,spring-rabbit是底层的默认实现。

SpringAMQP实现基础消息队列功能

消息发送

1,引入依赖

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

2,在配置文件中配置RabbitMQ的信息

spring:
  rabbitmq:
    host: 192.168.121.10 #主机名
    port: 5672 #端口
    virtual-host: /  #虚拟主机
    username: *** #用户名
    password: **** #密码
  

3,使用RabbitTemplate来发送消息(spring提供的发送消息的模板)

这里我们编写一个测试类来测试

@SpringBootTest
@RunWith(SpringRunner.class)
public class AMQPTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void sendMessage(){
        String queueName="simple.queue";//定义队列名
        String message="hello SpringAMQP!!";
        rabbitTemplate.convertAndSend(queueName,message);
    }
}

 可以看到消息成功发送

消息接收

依旧是先引依赖,添加RabbitMQ相关配置

之后书写监听消息代码,创建一个类,交给spring管理,定义一个方法加@RabbitListener注解指定接受消息的队列,可以传递数组,指定多个队列

@Component
public class ListenerMessage {
    @RabbitListener(queues = {"simple.queue"})//可以监听多个队列
    public void ListenerSimpleQueue(String message){
        System.out.println("接收到的消息是;"+message);
    }
}

可以看到成功接受消息

 WordQueue 工作队列

Work模型-多个消费者绑定到同一个队列,同一个消息只会被同一个消费者处理

我们来做一个测试,在一秒内发送50条消息,定义两个消费者,同时处理,一个消费者每秒处理50条消息,另一个消费者每秒处理20个消息,按照常理来说应该是多劳多得,也就是处理能力强的处理更多消息,弱的处理更少消息,我们修改之前的代码:

修改之后的消息发送代码:

@Test
    public void sendWorkMessage() throws InterruptedException {
        String queueName="simple.queue";//定义队列名
        String message="Work Message__";
        for (int i = 1; i <= 50; i++) {
            rabbitTemplate.convertAndSend(queueName,message+i);
            Thread.sleep(20);
        }
    }

修改之后的消息接收代码:

@Component
public class ListenerMessage {

    @RabbitListener(queues = {"simple.queue"})//可以监听多个队列
    public void ListenerWorkSimpleQueue1(String message) throws InterruptedException {
        System.out.println("consumer1接收到的消息是;"+message+"  时间:"+LocalDateTime.now());
        Thread.sleep(20);
    }
    @RabbitListener(queues = {"simple.queue"})//可以监听多个队列
    public void ListenerWorkSimpleQueue2(String message) throws InterruptedException {
        System.err.println("consumer2接收到的消息是;"+message+"  时间:"+LocalDateTime.now());
        Thread.sleep(200);
    }
}

代码执行结果显示,consumer1和consumer2处理的消息是一样的并不会向我们设想的那样,能力强的处理多能力弱的处理少,是什么原因导致的呢??

这是由于RabbitMQ的消息预取机制,就是说在消息到达消息队列的时候两个消费者会分别从消息队列中一次性取完所有的消息理论上来说是无上限的,所以我们需要修改机制让消费者一次性例如说取一个消息,等这个消息处理完之后在去取下一个即可,

在配置文件中修改如下:

spring:
  rabbitmq:
    host: 192.168.101.100 #主机名
    port: 5672 #端口
    virtual-host: /  #虚拟主机
    username: qmlx #用户名
    password: QMLX-9999 #密码
    listener:
      direct:
        prefetch: 1  #每次取一个消息,取完之后在取

 发布(Publish)订阅(Subscribe)

发布订阅模式和之前案例的区别就是上述模型一个消息只能发送给一个consumer,而发布订阅模型则是将同一个消息发送给多个消费者,实现方式是假如exchange(交换机)

 

常见的exchange类型包括:

Fanout:广播

Direct:路由

Topic:话题

注意:exchange只会负责消息路由,而不是存储,路由失败则消息丢失

🧇发布订阅-Fanout Exchange

Fanout Exchange他会将接收到的消息路由到每一个与其绑定的queue,广播模式。

实现思路:

1,在consumer服务中,编写一个配置类,声明两个队列,一个交换机,并将交换机绑定到队列上


@Configuration
public class FanoutConfig {
    @Bean
    public FanoutExchange fanoutExchange(){
        return  new FanoutExchange("fanout.exchange");
    }

    //定义两个队列
    @Bean
    public Queue fanoutQueue1(){
        return new Queue("fanout.queue1");
    }
    @Bean
    public Queue fanoutQueue2(){
        return new Queue("fanout.queue2");
    }

    //将队列绑定在交换机上面
    @Bean
    public Binding bindingQueue1(Queue fanoutQueue1,FanoutExchange fanoutExchange){
    //按照类型和名称传入参数
        return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
    }

    @Bean
    public Binding bindingQueue2(Queue fanoutQueue2,FanoutExchange fanoutExchange){
        return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
    }


}

启动项目之后spring会自动加载交换机和队列

 2,编写consumer代码,监听定义好的两个队列


@Component
public class ListenerMessage {

     @RabbitListener(queues = {"fanout.queue1"})
    public void ListenerFanoutQueue1(String message){
        System.out.println("consumer1接受到的消息是:"+message);
    }

    @RabbitListener(queues = {"fanout.queue2"})
    public void ListenerFanoutQueue2(String message){
        System.err.println("consumer2接受到的消息是:"+message);
    }
}

3,编写publisher代码,发送消息

之前消息是发送到队列中,现在消息发送给交换机

    @Test
    public void sendFanoutMessage() throws InterruptedException {
        String exchangeName="fanout.exchange";//定义队列名
        String message="FanoutExchange Message!!";
        rabbitTemplate.convertAndSend(exchangeName,null,message);

    }

可以看到两个消费者同时接受到了消息!!!

交换机的作用:

1,接受publisher发送的消息

2,将消息按照规则路由发送给每一个与之绑定的队列

3,不能缓存信息,路由失败则消息丢失

🧇发布订阅-Direct Exchange

Direct Exchange:交换机将接收到的消息根据路由规则到指定的Queue,称之为路由模式(routes)

        每一个Queue都和Exchange设定一个BindingKey

        发布者发送消息时,指定消息的BindingKey

        Exchange将消息路由到BindKey和消息的BindingKey一致的队列

实现思路:

1,指定消息接收者绑定交换机和队列,但是需要指定BindingKey

这次直接使用注解实现,不必那莫繁琐

@Component
public class ListenerDirectMessage {

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue1"),
            exchange = @Exchange(name = "direct.exchange",type = ExchangeTypes.DIRECT),
            key = {"red","blue"}
    ))
    public void listensterDirectQueue1(String message){
        System.out.println("消费者1接收到Direct的消息是:"+message);
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "direct.queue2"),
            exchange = @Exchange(name = "direct.exchange",type = ExchangeTypes.DIRECT),
            key = {"red","blue"}
    ))
    public void listensterDirectQueue2(String message){
        System.out.println("消费者2接收到Direct的消息是:"+message);
    }
}

‘2,定义publisher发布消息

@Test
    public void sendDirectMessage() throws InterruptedException {
        String exchangeName="direct.exchange";//定义队列名
        String message="DirectExchange Message!!";
        String routinfKey="blue";
        rabbitTemplate.convertAndSend(exchangeName,routinfKey,message);
        rabbitTemplate.convertAndSend(exchangeName,null,message);

    }

发送时需要指定routingKey即可,同一个队列定义时可指定多个BindingKey

注意:Direct Exchange只会发送给routingKeyBindingKey一致的队列

🧇发布订阅-Topic Exchange

Topic Exchange:Topic Exchange和Direct key类似,区别在于他的routingKey必须是多个单词的列表,并且必须是以 分割,并且Queue和Exchange指定时课使用通配符

实现思路:

1,实现消费者代码


@Component
public class ListenerTopicMessage {

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue1"),
            exchange = @Exchange(name = "topic.exchange",type = ExchangeTypes.TOPIC),
            key = "china.#"
    ))
    public void listensterDirectQueue1(String message){
        System.out.println("消费者1接收到topic的消息是:"+message);
    }

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "topic.queue2"),
            exchange = @Exchange(name = "topic.exchange",type = ExchangeTypes.TOPIC),
            key = "#.news"
    ))
    public void listensterDirectQueue2(String message){
        System.out.println("消费者2接收到topic的消息是:"+message);
    }
}

2,实现publisher代码

@Test
    public void sendtopictMessage() throws InterruptedException {
        String exchangeName="topic.exchange";//定义队列名
        String message="TopicExchange Message!!";
        String routinfKey="china.news";
        rabbitTemplate.convertAndSend(exchangeName,routinfKey,message);
        //rabbitTemplate.convertAndSend(exchangeName,null,message);

    }

至此RabbitMQ的安装使用及其五种基本工作模式搞定!!!!

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

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

相关文章

P1706 全排列问题

原题链接:全排列问题 - 洛谷 目录 1. 题目描述 2. 思路分析 3. 代码实现 1. 题目描述 2. 思路分析 dfs典题 3. 代码实现 #define _CRT_SECURE_NO_WARNINGS 1 #include<bits/stdc.h> using namespace std; #define ll long long #define endl \n const int N 2…

电脑技巧:Bandicam班迪录屏介绍

目录 一、 软件简介 二、软件功能 2.1 屏幕录制 2.2 游戏录制 2.3 设备录制 2.4实时编辑与截图 2.5 轻量级软件 三、软件用途 3.1 教育培训 3.2 游戏直播与分享 3.3 企业办公 3.4 在线教学与知识分享 四、总结 今天给大家推荐一款非常实用的电脑录屏软件&#xf…

Java实现单点登录(SSO)详解:从理论到实践

✨✨谢谢大家捧场&#xff0c;祝屏幕前的小伙伴们每天都有好运相伴左右&#xff0c;一定要天天开心哦&#xff01;✨✨ &#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; ✨✨ 帅哥美女们&#xff0c;我们共同加油&#xff01;一起进步&am…

提高大型语言模型 (LLM) 性能的四种数据清理技术

原文地址&#xff1a;four-data-cleaning-techniques-to-improve-large-language-model-llm-performance 2024 年 4 月 2 日 检索增强生成&#xff08;RAG&#xff09;过程因其增强对大语言模型&#xff08;LLM&#xff09;的理解、为它们提供上下文并帮助防止幻觉的潜力而受…

java-字符串

一、String概述 java.lang.String类代表字符串&#xff0c;java中所有字符串文字都是该类的对象 字符串的内容是不会发生变化的&#xff0c;它的对象在创建之后就不能被更改 二、创建String对象 1、直接赋值 语法&#xff1a; String 变量名内容&#xff1b; 2、使用构造…

Linux 磁盘存储挂载

fdisk -l 查看磁盘信息 mkfs.xfs /dev/vdc 格式化数据盘 mkdir vdev1 创建挂载点 mount /dev/vdc /vdev1 文件系统挂载点挂载 df -h 确认挂载成功 vi /etc/fstab 插入 /dev/vdc /vdev1 xfs defaults 0 0 /dev/vdc /vdev1 xfs defaults 0 0 保存就可以了 卸载实例&#…

韩顺平 | 零基础快速学Python(16) 文件处理

文件 输入与输出 输入&#xff1a;数据从数据源(文件)到程序(内存)&#xff1b; 输出&#xff1a;数据从程序(内存)到数据源(文件)。 #mermaid-svg-06PG6JZq4jJMV1oH {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-sv…

SpringBoot源码解析-01

SpringBoot3详细版 一&#xff1a;SpringBoot引入&#xff1a; 1.1.什么是Springboot&#xff1a; SpringBoot 帮我们简单、快速地创建一个独立的、生产级别的 Spring 应用&#xff08;说明&#xff1a;SpringBoot底层是Spring&#xff09; 大多数 SpringBoot 应用只需要编…

洛谷 P9532 [YsOI2023] 前缀和

题目背景 Ysuperman 模板测试的试机题。 小心立秋&#xff0c;小心秋丽。 题目描述 立秋有一个长度为 n 的数组 a&#xff0c;所有数字都是正整数&#xff0c;并且除了其中第一个数字以外其它数字都等于前面所有数字的和。 例如&#xff0c;数组 [1,1,2,4,8,16] 就有可能是…

如何入行产品经理?

转产品经理第一点要先学基础理论知识&#xff0c;学了理论再去实践&#xff0c;转行&#xff0c;跳槽&#xff01; 学理论比较好的就是去报NPDP的系统班&#xff0c;考后也会有面试指导课&#xff0c;跟职场晋升课程&#xff0c;对小白来说非常合适了~&#xff08;可以去哔站找…

【程序分享】AACSD 程序: 用于晶体结构和缺陷的原子分析平台

分享一个 AACSD 程序: 用于晶体结构和缺陷的原子分析平台。 感谢论文的原作者&#xff01; 主要内容 “我们开发了一个名为 AACSD&#xff08;晶体结构和缺陷原子分析器&#xff09;的高效命令行程序&#xff0c;用于对各种原子模拟代码生成的原子配置进行后期分析。该程序不…

利用栈删除数组中重复元素

先将数据排序&#xff08;降序或升序&#xff09; 建立一个“栈”&#xff0c;三种情况&#xff1a; 1.栈为空&#xff1a;压入一个元素 2.栈不为空 且 栈顶元素不等于将入栈元素&#xff1a;压入一个元素 3.栈不为空 且 栈顶元素等于将入栈元素&#xff1a;删除将压入元素…

循环双链表的操作

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd; 每一个裂缝都是为透出光而努力&#…

Redis消息队列-基于PubSub的消息队列

7.3 Redis消息队列-基于PubSub的消息队列 PubSub&#xff08;发布订阅&#xff09;是Redis2.0版本引入的消息传递模型。顾名思义&#xff0c;消费者可以订阅一个或多个channel&#xff0c;生产者向对应channel发送消息后&#xff0c;所有订阅者都能收到相关消息。 SUBSCRIBE …

mysql dump导出导入数据

前言 mysqldump是MySQL数据库中一个非常有用的命令行工具&#xff0c;用于备份和还原数据库。它可以将整个数据库或者特定的表导出为一个SQL文件&#xff0c;以便在需要时进行恢复或迁移。 使用mysqldump可以执行以下操作&#xff1a; 备份数据库&#xff1a;可以使用mysqld…

【Linux 驱动基础】设备树中断

# 前置知识 interrupts 文档 Specifying interrupt information for devices 1) Interrupt client nodes -------------------------Nodes that describe devices which generate interrupts must contain an "interrupts" property, an "interrupts-extende…

【数据库】表的增删改(CUD)

目录 一、insert 插入 1.单行插入&#xff1a; 2.多行插入&#xff1a; (1) insert into 插入&#xff1a; (2) replace into 替换插入&#xff1a; (3) 图片插入 &#xff1a; 二、update 修改 三、delete 删除 一、insert 插入 语法&#xff1a; INSERT INTO table_name…

ue4打包多模块

首先&#xff0c;每个模块&#xff0c;包含插件内的模块在内&#xff0c;都要用IMPLEMENT_MODULE(类名, 模块名)的方式&#xff0c;模块名就是带.build.cs的第一个单词。 build.cs里就说了这个模块该怎么用&#xff0c;用c#编写。 打包中要考虑到target.cs,将工程中相应的模块…

go语言并发实战——日志收集系统(一) 项目前言

-goroutine- 简介 go并发编程的练手项目 项目背景 一般来说业务系统都有自己的日志,当系统出现问题时,我们一般需要通过日志信息来定位与解决问题&#xff0c;当系统机器较少时我们可以登录服务器来查看,但是当系统机器较多时,我们通过服务器来查看日志的成本就会变得很大,…

leetcode刷题日记之全排列

题目描述 题目解释 这个题类似于之前做的某一道题&#xff0c;其实算法还是要追踪到树的深度遍历&#xff0c;相当于便利叶子节点的路径记录。不过递归的过程就相当于件数根据树进行遍历了。 代码如下 class Solution:def permute(self, nums: List[int]) -> List[List[i…