04-RabbitMQ

rabbitMQ 入门

MQ

04-RabbitMQ_第1张图片

MQ的优势

04-RabbitMQ_第2张图片
在这里插入图片描述

应用解耦

04-RabbitMQ_第3张图片
04-RabbitMQ_第4张图片

任务异步处理

04-RabbitMQ_第5张图片

削峰填谷

在这里插入图片描述
04-RabbitMQ_第6张图片

常见的 MQ 产品

04-RabbitMQ_第7张图片

RabbitMQ

官网
RabbitMQ 基础架构
04-RabbitMQ_第8张图片
04-RabbitMQ_第9张图片

RabbitMQ安装(Linux)

安装依赖环境

yum install build-essential openssl openssl-devel unixODBC unixODBC-devel make
gcc gcc-c++ kernel-devel m4 ncurses-devel tk tc xz

安装Erlang
准备三个文件
在这里插入图片描述
更新glibc版本

sudo yum install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlitedevel readline-devel tk-devel gcc make -y

下载rpm包

wget http://copr-be.cloud.fedoraproject.org/results/mosquito/myrepoel6/epel-6-x86_64/glibc-2.17-55.fc20/glibc-utils-2.17-55.el6.x86_64.rpm &
wget http://copr-be.cloud.fedoraproject.org/results/mosquito/myrepoel6/epel-6-x86_64/glibc-2.17-55.fc20/glibc-static-2.17-55.el6.x86_64.rpm &
wget http://copr-be.cloud.fedoraproject.org/results/mosquito/myrepoel6/epel-6-x86_64/glibc-2.17-55.fc20/glibc-2.17-55.el6.x86_64.rpm &
wget http://copr-be.cloud.fedoraproject.org/results/mosquito/myrepoel6/epel-6-x86_64/glibc-2.17-55.fc20/glibc-common-2.17-55.el6.x86_64.rpm &
wget http://copr-be.cloud.fedoraproject.org/results/mosquito/myrepoel6/epel-6-x86_64/glibc-2.17-55.fc20/glibc-devel-2.17-55.el6.x86_64.rpm &
wget http://copr-be.cloud.fedoraproject.org/results/mosquito/myrepoel6/epel-6-x86_64/glibc-2.17-55.fc20/glibc-headers-2.17-55.el6.x86_64.rpm &
wget http://copr-be.cloud.fedoraproject.org/results/mosquito/myrepoel6/epel-6-x86_64/glibc-2.17-55.fc20/nscd-2.17-55.el6.x86_64.rpm &

安装rpm

sudo rpm -Uvh socat-1.7.3.2-1.1.el7.x86_64.rpm --force --nodeps

查看glibc版本

strings /lib64/libc.so.6 | grep GLIBC

04-RabbitMQ_第10张图片

安装

rpm -ivh erlang-18.3-1.el7.centos.x86_64.rpm

安装RabbitMQ

rpm -ivh socat-1.7.3.2-1.1.el7.x86_64.rpm --force --nodeps

rpm -ivh rabbitmq-server-3.6.5-1.noarch.rpm

开启管理界⾯及配置

# 开启管理界⾯
rabbitmq-plugins enable rabbitmq_management
# 修改默认配置信息
vim /usr/lib/rabbitmq/lib/rabbitmq_server-3.6.5/ebin/rabbit.app
# ⽐如修改密码、配置等等,例如:loopback_users 中的 <<"guest">>,只保留guest

在这里插入图片描述
在这里插入图片描述

service rabbitmq-server start # 启动服务
service rabbitmq-server stop # 停⽌服务
service rabbitmq-server restart # 重启服务

设置配置文件

cd /usr/share/doc/rabbitmq-server-3.6.5/
cp rabbitmq.config.example /etc/rabbitmq/rabbitmq.config

账号密码guest
04-RabbitMQ_第11张图片

配置虚拟主机以及用户

添加新用户

04-RabbitMQ_第12张图片
04-RabbitMQ_第13张图片

配置虚拟主机

在这里插入图片描述

创建Virtual Hosts

04-RabbitMQ_第14张图片

点击/bbb设置权限
04-RabbitMQ_第15张图片

入门案例

04-RabbitMQ_第16张图片
依赖

    <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.6.0</version>
        </dependency>

创建消息生产者

public class Producer {

    static final String QUEUE_NAME = "simple_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建链接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //主机地址
        connectionFactory.setHost("192.168.238.129");
        //链接端口,默认5672
        connectionFactory.setPort(5672);
        //虚拟主机名称 默认guest
        connectionFactory.setVirtualHost("/bbb");
        //链接用户名,默认guest
        connectionFactory.setUsername("aaa");
        //链接密码,默认guest
        connectionFactory.setPassword("aaa");

        //创建链接
        Connection connection = connectionFactory.newConnection();

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

        //申明队列
        /**
         *队列名称
         * 师傅定义持久化队列
         * 是否独占本次链接
         * 是否在不适用的时候自动删除队列
         * 队列其他参数
         */
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);

        //消息
        String message = "你好,rabbitMQ";
        /**
         *交换机名称,默认Default Exchage
         * 路由key,简单模式可以传递队列名称
         * 消息其他属性
         * 消息内容
         */
        channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
        // 关闭资源
        channel.close();
        connection.close();

    }
}

消息消费者

    static final String QUEUE_NAME = "simple_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        //创建链接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //主机地址
        connectionFactory.setHost("192.168.238.129");
        //链接端口,默认5672
        connectionFactory.setPort(5672);
        //虚拟主机名称 默认guest
        connectionFactory.setVirtualHost("/bbb");
        //链接用户名,默认guest
        connectionFactory.setUsername("aaa");
        //链接密码,默认guest
        connectionFactory.setPassword("aaa");

        //创建链接
        Connection connection = connectionFactory.newConnection();

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

        //申明队列
        /**
         *队列名称
         * 是否定义持久化队列
         * 是否独占本次链接
         * 是否在不使用的时候自动删除队列
         * 队列其他参数
         */
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);

        //消息
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            /**
             * consumerTag 消息者标签,在channel.basicConsume时候可以指定
             * envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重 传标志(收到消息失败后是否需要重新发送)
             * properties 属性信息
             * body 消息
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //路由key
                System.out.println("路由key为:" + envelope.getRoutingKey());
                //交换机
                System.out.println("交换机为:" + envelope.getExchange());
                //消息id
                System.out.println("消息id为:" + envelope.getDeliveryTag());
                //收到的消息
                System.out.println("接收到的消息为:" + new String(body, "utf-8"));
            }
        };


        //监听消息
        /**
         *队列名称
         * 是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复 会删除消息,设置为false则需要手动确认
         * 消息接收到后回调
         */
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
        // 不关闭资源,应该一直监听消息
    }
在这里插入代码片

04-RabbitMQ_第17张图片

工作模式

Work queues工作队列模式

04-RabbitMQ_第18张图片
Work Queues 与入门程序的 简单模式 相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。

应用场景:对于 任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
链接工具类

public class ConnectionUtils {

    public static Connection getConnection() throws IOException, TimeoutException {
        //创建链接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //主机地址
        connectionFactory.setHost("192.168.238.129");
        //链接端口,默认5672
        connectionFactory.setPort(5672);
        //虚拟主机名称 默认guest
        connectionFactory.setVirtualHost("/bbb");
        //链接用户名,默认guest
        connectionFactory.setUsername("aaa");
        //链接密码,默认guest
        connectionFactory.setPassword("aaa");

        //创建链接
        Connection connection = connectionFactory.newConnection();

        return connection;
    }
}

消息生产者

public class Producer {

    static final String QUEUE_NAME = "work_queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        // 创建频道
        Channel channel = connection.createChannel();
        // 声明(创建)队列
        /***
        参数1:队列名称 *
        参数2:是否定义持久化队列 *
        参数3:是否独占本次连接 *
        参数4:是否在不使用的时候自动删除队列 *
        参数5:队列其它参数 */
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);

        for (int i = 1; i <= 30; i++) {
            String message = "你好;rabbit!work模式--" + i;
            /***
             * 参数1:交换机名称,如果没有指定则使用默认Default Exchage *
             * 参数2:路由key,简单模式可以传递队列名称 *
             * 参数3:消息其它属性 *
             * 参数4:消息内容 */
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            System.out.println("已发送消息:" + message);
        }
        // 关闭资源
        channel.close();
        connection.close();
    }
}

消息消费者1

public class Consumer1 {
    static final String QUEUE_NAME = Producer.QUEUE_NAME;

    public static void main(String[] args) throws IOException, TimeoutException {

        //创建链接
        Connection connection = ConnectionUtils.getConnection();

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

        //申明队列
        /**
         *队列名称
         * 是否定义持久化队列
         * 是否独占本次链接
         * 是否在不使用的时候自动删除队列
         * 队列其他参数
         */
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);

        //消息
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            /**
             * consumerTag 消息者标签,在channel.basicConsume时候可以指定
             * envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重 传标志(收到消息失败后是否需要重新发送)
             * properties 属性信息
             * body 消息
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //路由key
                System.out.println("路由key为:" + envelope.getRoutingKey());
                //交换机
                System.out.println("交换机为:" + envelope.getExchange());
                //消息id
                System.out.println("消息id为:" + envelope.getDeliveryTag());
                //收到的消息
                System.out.println("消费者1,接收到的消息为:" + new String(body, "utf-8"));
            }
        };


        //监听消息
        /**
         *队列名称
         * 是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复 会删除消息,设置为false则需要手动确认
         * 消息接收到后回调
         */
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
        // 不关闭资源,应该一直监听消息
    }
}
**消息消费者2**

```python
public class Consumer2 {
    static final String QUEUE_NAME = Producer.QUEUE_NAME;

    public static void main(String[] args) throws IOException, TimeoutException {

        //创建链接
        Connection connection = ConnectionUtils.getConnection();

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

        //申明队列
        /**
         *队列名称
         * 是否定义持久化队列
         * 是否独占本次链接
         * 是否在不使用的时候自动删除队列
         * 队列其他参数
         */
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);

        //消息
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            /**
             * consumerTag 消息者标签,在channel.basicConsume时候可以指定
             * envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重 传标志(收到消息失败后是否需要重新发送)
             * properties 属性信息
             * body 消息
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //路由key
                System.out.println("路由key为:" + envelope.getRoutingKey());
                //交换机
                System.out.println("交换机为:" + envelope.getExchange());
                //消息id
                System.out.println("消息id为:" + envelope.getDeliveryTag());
                //收到的消息
                System.out.println("消费者2,接收到的消息为:" + new String(body, "utf-8"));
            }
        };


        //监听消息
        /**
         *队列名称
         * 是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复 会删除消息,设置为false则需要手动确认
         * 消息接收到后回调
         */
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
        // 不关闭资源,应该一直监听消息
    }
}

04-RabbitMQ_第19张图片

04-RabbitMQ_第20张图片

订阅模式

04-RabbitMQ_第21张图片
在这里插入图片描述

Publish/Subscribe发布与订阅模式

04-RabbitMQ_第22张图片
生产者

public class Producer {

    static final String FANOUT_EXCHANGE = "fanout_exchange";
    static final String FANOUT_QUEUE_NAME1 = "fanout_queue1";
    static final String FANOUT_QUEUE_NAME2 = "fanout_queue2";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        // 创建频道
        Channel channel = connection.createChannel();


        //声明交换机
        /**
         * 交换机名称
         * 交换机类型 fanout,topic,direct,headers
         */
        channel.exchangeDeclare(FANOUT_EXCHANGE, BuiltinExchangeType.FANOUT);
        channel.queueDeclare(FANOUT_QUEUE_NAME1,true,false,false,null);
        channel.queueDeclare(FANOUT_QUEUE_NAME2,true,false,false,null);


        channel.queueBind(FANOUT_QUEUE_NAME1,FANOUT_EXCHANGE,"");
        channel.queueBind(FANOUT_QUEUE_NAME2,FANOUT_EXCHANGE,"");


        for (int i = 1; i <= 10; i++) {
            String message = "你好;rabbit!work模式--" + i;
            /***
             * 参数1:交换机名称,如果没有指定则使用默认Default Exchage *
             * 参数2:路由key,简单模式可以传递队列名称 *
             * 参数3:消息其它属性 *
             * 参数4:消息内容 */
            channel.basicPublish(FANOUT_EXCHANGE, "", null, message.getBytes());
            System.out.println("已发送消息:" + message);
        }
        // 关闭资源
        channel.close();
        connection.close();
    }
}

消费者1

public class Consumer1 {
    static final String QUEUE_NAME = Producer.FANOUT_QUEUE_NAME1;

    public static void main(String[] args) throws IOException, TimeoutException {

        //创建链接
        Connection connection = ConnectionUtils.getConnection();

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

        //申明队列
        /**
         *队列名称
         * 是否定义持久化队列
         * 是否独占本次链接
         * 是否在不使用的时候自动删除队列
         * 队列其他参数
         */
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);

        //消息
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            /**
             * consumerTag 消息者标签,在channel.basicConsume时候可以指定
             * envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重 传标志(收到消息失败后是否需要重新发送)
             * properties 属性信息
             * body 消息
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //路由key
                System.out.println("路由key为:" + envelope.getRoutingKey());
                //交换机
                System.out.println("交换机为:" + envelope.getExchange());
                //消息id
                System.out.println("消息id为:" + envelope.getDeliveryTag());
                //收到的消息
                System.out.println("消费者1,接收到的消息为:" + new String(body, "utf-8"));
            }
        };


        //监听消息
        /**
         *队列名称
         * 是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复 会删除消息,设置为false则需要手动确认
         * 消息接收到后回调
         */
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
        // 不关闭资源,应该一直监听消息
    }
}
**消费者2**

```python
public class Consumer1 {
    static final String QUEUE_NAME = Producer.FANOUT_QUEUE_NAME2;

    public static void main(String[] args) throws IOException, TimeoutException {

        //创建链接
        Connection connection = ConnectionUtils.getConnection();

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

        //申明队列
        /**
         *队列名称
         * 是否定义持久化队列
         * 是否独占本次链接
         * 是否在不使用的时候自动删除队列
         * 队列其他参数
         */
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);

        //消息
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            /**
             * consumerTag 消息者标签,在channel.basicConsume时候可以指定
             * envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重 传标志(收到消息失败后是否需要重新发送)
             * properties 属性信息
             * body 消息
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //路由key
                System.out.println("路由key为:" + envelope.getRoutingKey());
                //交换机
                System.out.println("交换机为:" + envelope.getExchange());
                //消息id
                System.out.println("消息id为:" + envelope.getDeliveryTag());
                //收到的消息
                System.out.println("消费者1,接收到的消息为:" + new String(body, "utf-8"));
            }
        };


        //监听消息
        /**
         *队列名称
         * 是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复 会删除消息,设置为false则需要手动确认
         * 消息接收到后回调
         */
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
        // 不关闭资源,应该一直监听消息
    }
}

04-RabbitMQ_第23张图片
04-RabbitMQ_第24张图片
交换机需要与队列进行绑定,绑定之后;一个消息可以被多个消费者都收到
04-RabbitMQ_第25张图片

Routing路由模式

04-RabbitMQ_第26张图片
04-RabbitMQ_第27张图片
在编码上与 Publish/Subscribe发布与订阅模式 的区别是交换机的类型为:Direct,还有队列绑定交换机的时候需要指定routing key。
生产者

public class Producer {

    static final String DIRECT_EXCHANGE = "direct_exchange";
    static final String DIRECT_QUEUE_NAME1 = "direct_queue1";
    static final String DIRECT_QUEUE_NAME2 = "direct_queue2";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        // 创建频道
        Channel channel = connection.createChannel();


        //声明交换机
        /**
         * 交换机名称
         * 交换机类型 fanout,topic,direct,headers
         */
        channel.exchangeDeclare(DIRECT_EXCHANGE, BuiltinExchangeType.DIRECT);


        channel.queueDeclare(DIRECT_QUEUE_NAME1,true,false,false,null);
        channel.queueDeclare(DIRECT_QUEUE_NAME2,true,false,false,null);


        channel.queueBind(DIRECT_QUEUE_NAME1,DIRECT_EXCHANGE,"insert");
        channel.queueBind(DIRECT_QUEUE_NAME2,DIRECT_EXCHANGE,"update");


        for (int i = 1; i <= 10; i++) {
            String message = "新增商品,路由模式,routing key insert" + i;
            /***
             * 参数1:交换机名称,如果没有指定则使用默认Default Exchage *
             * 参数2:路由key,简单模式可以传递队列名称 *
             * 参数3:消息其它属性 *
             * 参数4:消息内容 */
            channel.basicPublish(DIRECT_EXCHANGE, "insert", null, message.getBytes());
            System.out.println("已发送消息:" + message);
        }

        for (int i = 1; i <= 10; i++) {
            String message = "修改商品,路由模式,routing key update" + i;
            /***
             * 参数1:交换机名称,如果没有指定则使用默认Default Exchage *
             * 参数2:路由key,简单模式可以传递队列名称 *
             * 参数3:消息其它属性 *
             * 参数4:消息内容 */
            channel.basicPublish(DIRECT_EXCHANGE, "update", null, message.getBytes());
            System.out.println("已发送消息:" + message);
        }
        // 关闭资源
        channel.close();
        connection.close();
    }
}

消费者1

public class Consumer1 {
    static final String QUEUE_NAME = Producer.DIRECT_QUEUE_NAME1;
    static final String EXCHANGE_NAME = Producer.DIRECT_EXCHANGE;


    public static void main(String[] args) throws IOException, TimeoutException {

        //创建链接
        Connection connection = ConnectionUtils.getConnection();

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

        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);


        //申明队列
        /**
         *队列名称
         * 是否定义持久化队列
         * 是否独占本次链接
         * 是否在不使用的时候自动删除队列
         * 队列其他参数
         */


        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"insert");

        //消息
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            /**
             * consumerTag 消息者标签,在channel.basicConsume时候可以指定
             * envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重 传标志(收到消息失败后是否需要重新发送)
             * properties 属性信息
             * body 消息
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //路由key
                System.out.println("路由key为:" + envelope.getRoutingKey());
                //交换机
                System.out.println("交换机为:" + envelope.getExchange());
                //消息id
                System.out.println("消息id为:" + envelope.getDeliveryTag());
                //收到的消息
                System.out.println("消费者1,接收到的消息为:" + new String(body, "utf-8"));
            }
        };


        //监听消息
        /**
         *队列名称
         * 是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复 会删除消息,设置为false则需要手动确认
         * 消息接收到后回调
         */
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
        // 不关闭资源,应该一直监听消息
    }
}

消费者2

public class Consumer2 {
    static final String QUEUE_NAME = Producer.DIRECT_QUEUE_NAME2;
    static final String EXCHANGE_NAME = Producer.DIRECT_EXCHANGE;

    public static void main(String[] args) throws IOException, TimeoutException {

        //创建链接
        Connection connection = ConnectionUtils.getConnection();

        //创建频道
        Channel channel = connection.createChannel();
        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);

        //申明队列
        /**
         *队列名称
         * 是否定义持久化队列
         * 是否独占本次链接
         * 是否在不使用的时候自动删除队列
         * 队列其他参数
         */
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);

        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"update");

        //消息
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            /**
             * consumerTag 消息者标签,在channel.basicConsume时候可以指定
             * envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重 传标志(收到消息失败后是否需要重新发送)
             * properties 属性信息
             * body 消息
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //路由key
                System.out.println("路由key为:" + envelope.getRoutingKey());
                //交换机
                System.out.println("交换机为:" + envelope.getExchange());
                //消息id
                System.out.println("消息id为:" + envelope.getDeliveryTag());
                //收到的消息
                System.out.println("消费者2,接收到的消息为:" + new String(body, "utf-8"));
            }
        };


        //监听消息
        /**
         *队列名称
         * 是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复 会删除消息,设置为false则需要手动确认
         * 消息接收到后回调
         */
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);
        // 不关闭资源,应该一直监听消息
    }
}

04-RabbitMQ_第28张图片

04-RabbitMQ_第29张图片

Topics通配符模式

04-RabbitMQ_第30张图片
04-RabbitMQ_第31张图片
生产者

public class Producer {

    static final String TOPIC_EXCHANGE = "topic_exchange";
    static final String TOPIC_QUEUE_ALL = "topic_queue_all";
    static final String TOPIC_QUEUE_UPDATE_INSERT = "topic_queue_update_insert";

    public static void main(String[] args) throws IOException, TimeoutException {
        Connection connection = ConnectionUtils.getConnection();
        // 创建频道
        Channel channel = connection.createChannel();


        //声明交换机
        /**
         * 交换机名称
         * 交换机类型 fanout,topic,direct,headers
         */
        channel.exchangeDeclare(TOPIC_EXCHANGE, BuiltinExchangeType.TOPIC);


        String message = "新增商品,路由模式,topic key item.insert";
        /***
         * 参数1:交换机名称,如果没有指定则使用默认Default Exchage *
         * 参数2:路由key,简单模式可以传递队列名称 *
         * 参数3:消息其它属性 *
         * 参数4:消息内容 */
        channel.basicPublish(TOPIC_EXCHANGE, "item.insert", null, message.getBytes());
        System.out.println("已发送消息:" + message);


        message = "修改商品,路由模式,topic key item.update";
        /***
         * 参数1:交换机名称,如果没有指定则使用默认Default Exchage *
         * 参数2:路由key,简单模式可以传递队列名称 *
         * 参数3:消息其它属性 *
         * 参数4:消息内容 */
        channel.basicPublish(TOPIC_EXCHANGE, "item.update", null, message.getBytes());
        System.out.println("已发送消息:" + message);

        message = "删除商品,路由模式,topic key item.delete";
        /***
         * 参数1:交换机名称,如果没有指定则使用默认Default Exchage *
         * 参数2:路由key,简单模式可以传递队列名称 *
         * 参数3:消息其它属性 *
         * 参数4:消息内容 */
        channel.basicPublish(TOPIC_EXCHANGE, "item.delete", null, message.getBytes());
        System.out.println("已发送消息:" + message);

        // 关闭资源
        channel.close();
        connection.close();
    }
}

消费者1

public class Consumer1 {


    public static void main(String[] args) throws IOException, TimeoutException {

        //创建链接
        Connection connection = ConnectionUtils.getConnection();

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

        //声明交换机
        channel.exchangeDeclare(Producer.TOPIC_EXCHANGE, BuiltinExchangeType.TOPIC);


        //申明队列
        /**
         *队列名称
         * 是否定义持久化队列
         * 是否独占本次链接
         * 是否在不使用的时候自动删除队列
         * 队列其他参数
         */


        channel.queueDeclare(Producer.TOPIC_QUEUE_ALL, true, false, false, null);
        channel.queueBind(Producer.TOPIC_QUEUE_ALL, Producer.TOPIC_EXCHANGE, "item.*");

        //消息
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            /**
             * consumerTag 消息者标签,在channel.basicConsume时候可以指定
             * envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重 传标志(收到消息失败后是否需要重新发送)
             * properties 属性信息
             * body 消息
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //路由key
                System.out.println("路由key为:" + envelope.getRoutingKey());
                //交换机
                System.out.println("交换机为:" + envelope.getExchange());
                //消息id
                System.out.println("消息id为:" + envelope.getDeliveryTag());
                //收到的消息
                System.out.println("消费者1,接收到的消息为:" + new String(body, "utf-8"));
            }
        };


        //监听消息
        /**
         *队列名称
         * 是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复 会删除消息,设置为false则需要手动确认
         * 消息接收到后回调
         */
        channel.basicConsume(Producer.TOPIC_QUEUE_ALL, true, defaultConsumer);
        // 不关闭资源,应该一直监听消息
    }
}

消费者2

public class Consumer2 {


    public static void main(String[] args) throws IOException, TimeoutException {

        //创建链接
        Connection connection = ConnectionUtils.getConnection();

        //创建频道
        Channel channel = connection.createChannel();
        //声明交换机
        channel.exchangeDeclare(Producer.TOPIC_EXCHANGE, BuiltinExchangeType.TOPIC);

        //申明队列
        /**
         *队列名称
         * 是否定义持久化队列
         * 是否独占本次链接
         * 是否在不使用的时候自动删除队列
         * 队列其他参数
         */
        channel.queueDeclare(Producer.TOPIC_QUEUE_UPDATE_INSERT, true, false, false, null);

        channel.queueBind(Producer.TOPIC_QUEUE_UPDATE_INSERT,Producer.TOPIC_EXCHANGE,"item.update");
        channel.queueBind(Producer.TOPIC_QUEUE_UPDATE_INSERT,Producer.TOPIC_EXCHANGE,"item.insert");

        //消息
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            /**
             * consumerTag 消息者标签,在channel.basicConsume时候可以指定
             * envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重 传标志(收到消息失败后是否需要重新发送)
             * properties 属性信息
             * body 消息
             */
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //路由key
                System.out.println("路由key为:" + envelope.getRoutingKey());
                //交换机
                System.out.println("交换机为:" + envelope.getExchange());
                //消息id
                System.out.println("消息id为:" + envelope.getDeliveryTag());
                //收到的消息
                System.out.println("消费者2,接收到的消息为:" + new String(body, "utf-8"));
            }
        };


        //监听消息
        /**
         *队列名称
         * 是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复 会删除消息,设置为false则需要手动确认
         * 消息接收到后回调
         */
        channel.basicConsume(Producer.TOPIC_QUEUE_UPDATE_INSERT, true, defaultConsumer);
        // 不关闭资源,应该一直监听消息
    }
}

04-RabbitMQ_第32张图片
04-RabbitMQ_第33张图片

模式总结

04-RabbitMQ_第34张图片

Spring Boot 整合RabbitMQ

生产者

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter</artifactId>
 </dependency>

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-test</artifactId>
     <scope>test</scope>
 </dependency>


 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-amqp</artifactId>
 </dependency>
spring:
  rabbitmq:
    host: 192.168.238.129
    port: 5672
    virtual-host: /bbb
    username: aaa
    password: aaa
@Configuration
public class RabbitMQConfig {

    //交换机名称
    public static final String ITEM_TOPIC_EXCHANGE = "springboot_item_topic_exchange";
    //队列名称
    public static final String ITEM_QUEUE = "springboot_item_queue";

    //声明交换机
    @Bean("itemTopicExchange")
    public Exchange topicExchange() {
        return ExchangeBuilder.topicExchange(ITEM_TOPIC_EXCHANGE).durable(true).build();
    }

    //申明队列
    @Bean("itemQueue")
    public Queue itemQueue() {
        return QueueBuilder.durable(ITEM_QUEUE).build();
    }

    //判断队列和交换机
    @Bean
    public Binding itemQueueExchange(@Qualifier("itemQueue") Queue queue, @Qualifier("itemTopicExchange") Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("item.#").noargs();
    }
}

@SpringBootTest
public class SpringbootRabbitmqApplicationTests {

	@Autowired
	private RabbitTemplate rabbitTemplate;

	@Test
	public void contextLoads() {
		rabbitTemplate.convertAndSend(RabbitMQConfig.ITEM_TOPIC_EXCHANGE,"item.insert", "商品新增,routing key 为item.insert");
		rabbitTemplate.convertAndSend(RabbitMQConfig.ITEM_TOPIC_EXCHANGE, "item.update", "商品修改,routing key 为item.update");
		rabbitTemplate.convertAndSend(RabbitMQConfig.ITEM_TOPIC_EXCHANGE, "item.delete", "商品删除,routing key 为item.delete");
	}

}

消费者

@Component
public class MyListener {
    /**
     *监听某个队列的消息
     */
    @RabbitListener(queues = "springboot_item_queue")
    public void myListener1(String message) {
        System.out.println("消费者接收到的消息为:" + message);
    }
}

04-RabbitMQ_第35张图片

高级特性

消息的可靠投递

04-RabbitMQ_第36张图片

确认模式

消息从 producer 到 exchange 则会返回一个 confirmCallback

确认模式开启:ConnectionFactory中开启publisher-confirms="true"

在rabbitTemplate定义ConfirmCallBack回调函数

rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             *
             * @param correlationData 相关配置信息
             * @param ack   exchange交换机 是否成功收到了消息。true 成功,false代表失败
             * @param cause 失败原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                System.out.println("confirm方法被执行了。。。");
                if (ack) {
                    System.out.println("成功接收消息:" + cause);
                } else {
                    System.out.println("接收消息失败:" + cause);
                    //业务处理
                }
            }
        });

        rabbitTemplate.convertAndSend("test_exchange_confirm22", "confirm", "message confirm....");

退回模式

消息从 exchange–>queue 投递失败则会返回一个 returnCallback

/**
     * 回退模式: 当消息发送给Exchange后,Exchange路由到Queue失败是 才会执行 ReturnCallBack
     * 步骤:
     * 1. 开启回退模式:publisher-returns="true"
     * 2. 设置ReturnCallBack
     * 3. 设置Exchange处理消息失败的模式:setMandatory

     * 1. 如果消息没有路由到Queue,则丢弃消息(默认)
     * 2. 如果消息没有路由到Queue,返回给消息发送方ReturnCallBack
     */

    @Test
    public void testReturn() {

        //设置交换机处理失败消息的模式
        rabbitTemplate.setMandatory(true);

        //2.设置ReturnCallBack
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             *
             * @param message   消息对象
             * @param replyCode 错误码
             * @param replyText 错误信息
             * @param exchange  交换机
             * @param routingKey 路由键
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("return 执行了....");

                System.out.println(message);
                System.out.println(replyCode);
                System.out.println(replyText);
                System.out.println(exchange);
                System.out.println(routingKey);

                //处理
            }
        });
        //3. 发送消息
        rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm", "message confirm....");
    }

Consumer Ack

04-RabbitMQ_第37张图片

@Component
public class AckListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliverTag = message.getMessageProperties().getDeliveryTag();

        try {
            System.out.println(new String(message.getBody()));
            System.out.println("处理业务逻辑");

            int i = 3/0;//模拟业务处理异常

            channel.basicAck(deliverTag, true);
        } catch (Exception e) {
            e.printStackTrace();

            //拒绝签收
            //requeue:true:表示重回队列
            channel.basicNack(deliverTag, true, true);
//            channel.basicReject(deliverTag, true);
        }
    }
}

消费端限流

04-RabbitMQ_第38张图片

@Component
public class QosListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {

        Thread.sleep(1000);
        //1.获取消息
        System.out.println(new String(message.getBody()));

        //2. 处理业务逻辑

        //3. 签收
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);

    }
}

TTL

04-RabbitMQ_第39张图片
配置文件

    <!--ttl-->
    <rabbit:queue name="test_queue_ttl" id="test_queue_ttl">
        <rabbit:queue-arguments>
            <entry key="x-message-ttl" value="100000" value-type="java.lang.Integer"></entry>
        </rabbit:queue-arguments>
    </rabbit:queue>

    <rabbit:topic-exchange name="test_exchange_ttl">
        <rabbit:bindings>
            <rabbit:binding pattern="ttl.#" queue="test_queue_ttl"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:topic-exchange>
 /**
     * TTL:过期时间
     *  1. 队列统一过期
     *
     *  2. 消息单独过期
     *
     *
     * 如果设置了消息的过期时间,也设置了队列的过期时间,它以时间短的为准。
     * 队列过期后,会将队列所有消息全部移除。
     * 消息过期后,只有消息在队列顶端,才会判断其是否过期(移除掉)
     *
     */
    @Test
    public void testTtl() {

//        for (int i = 0; i < 10; i++) {
//            // 发送消息
//            rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl....");
//        }

//        // 消息后处理对象,设置一些消息的参数信息
        MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {

            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                //1.设置message的信息
                message.getMessageProperties().setExpiration("5000");//消息的过期时间
                //2.返回该消息
                return message;
            }
        };
//        //消息单独过期
//        rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl....", messagePostProcessor);


        for (int i = 0; i < 10; i++) {
            if(i == 5){
                //消息单独过期
                rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl....",messagePostProcessor);
            }else{
                //不过期的消息
                rabbitTemplate.convertAndSend("test_exchange_ttl", "ttl.hehe", "message ttl....");

            }

        }
    }

死信队列

04-RabbitMQ_第40张图片
配置文件

<!--
        1. 声明正常的队列(test_queue_dlx)和交换机(test_exchange_dlx)
    -->
    <rabbit:queue name="test_queue_dlx" id="test_queue_dlx">
        <!--3. 正常队列绑定死信交换机-->
        <rabbit:queue-arguments>
            <!--3.1 x-dead-letter-exchange:死信交换机名称-->
            <entry key="x-dead-letter-exchange" value="exchange_dlx" />

            <!--3.2 x-dead-letter-routing-key:发送给死信交换机的routingkey-->
            <entry key="x-dead-letter-routing-key" value="dlx.hehe" />

            <!--4.1 设置队列的过期时间 ttl-->
            <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer" />
            <!--4.2 设置队列的长度限制 max-length -->
            <entry key="x-max-length" value="10" value-type="java.lang.Integer" />
        </rabbit:queue-arguments>
    </rabbit:queue>
    <rabbit:topic-exchange name="test_exchange_dlx">
        <rabbit:bindings>
            <rabbit:binding pattern="test.dlx.#" queue="test_queue_dlx"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:topic-exchange>

    <!--
       2. 声明死信队列(queue_dlx)和死信交换机(exchange_dlx)
     -->
    <rabbit:queue name="queue_dlx" id="queue_dlx"></rabbit:queue>

    <rabbit:topic-exchange name="exchange_dlx">
        <rabbit:bindings>
            <rabbit:binding pattern="dlx.#" queue="queue_dlx"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:topic-exchange>
  /**
     * 发送测试死信消息:
     *  1. 过期时间
     *  2. 长度限制
     *  3. 消息拒收
     */
    @Test
    public void testDlx(){
        //1. 测试过期时间,死信消息
//        rabbitTemplate.convertAndSend("test_exchange_dlx","test.dlx.haha","我是一条消息,我会死吗?");

        //2. 测试长度限制后,消息死信
//        for (int i = 0; i < 20; i++) {
//            rabbitTemplate.convertAndSend("test_exchange_dlx","test.dlx.haha","我是一条消息,我会死吗?");
//        }

        //3. 测试消息拒收
        rabbitTemplate.convertAndSend("test_exchange_dlx","test.dlx.haha","我是一条消息,我会死吗?");

    }
@Component
public class DlxListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            //1.接收转换消息
            System.out.println(new String(message.getBody()));

            //2. 处理业务逻辑
            System.out.println("处理业务逻辑...");
            int i = 3/0;//出现错误
            //3. 手动签收
            channel.basicAck(deliveryTag,true);
        } catch (Exception e) {
            //e.printStackTrace();
            System.out.println("出现异常,拒绝接受");
            //4.拒绝签收,不重回队列 requeue=false
            channel.basicNack(deliveryTag,true,false);
        }
    }
}

延迟队列

04-RabbitMQ_第41张图片
04-RabbitMQ_第42张图片

 <!--
        延迟队列:
            1. 定义正常交换机(order_exchange)和队列(order_queue)
            2. 定义死信交换机(order_exchange_dlx)和队列(order_queue_dlx)
            3. 绑定,设置正常队列过期时间为30分钟
    -->
    <!-- 1. 定义正常交换机(order_exchange)和队列(order_queue)-->
    <rabbit:queue id="order_queue" name="order_queue">
        <!-- 3. 绑定,设置正常队列过期时间为30分钟-->
        <rabbit:queue-arguments>
            <entry key="x-dead-letter-exchange" value="order_exchange_dlx" />
            <entry key="x-dead-letter-routing-key" value="dlx.order.cancel" />
            <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer" />
        </rabbit:queue-arguments>
    </rabbit:queue>
    <rabbit:topic-exchange name="order_exchange">
        <rabbit:bindings>
            <rabbit:binding pattern="order.#" queue="order_queue"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:topic-exchange>

    <!--  2. 定义死信交换机(order_exchange_dlx)和队列(order_queue_dlx)-->
    <rabbit:queue id="order_queue_dlx" name="order_queue_dlx"></rabbit:queue>

    <rabbit:topic-exchange name="order_exchange_dlx">
        <rabbit:bindings>
            <rabbit:binding pattern="dlx.order.#" queue="order_queue_dlx"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:topic-exchange>
    @Test
    public  void testDelay() throws InterruptedException {
        //1.发送订单消息。 将来是在订单系统中,下单成功后,发送消息
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        rabbitTemplate.convertAndSend("order_exchange","order.msg","订单信息:id=1,time=" + sdf.format(new Date()));

    /*//2.打印倒计时10秒
    for (int i = 10; i > 0 ; i--) {
        System.out.println(i+"...");
        Thread.sleep(1000);
    }*/

    }
@Component
public class OrderListener implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            //1.接收转换消息
            System.out.println(new String(message.getBody()));

            //2. 处理业务逻辑
            System.out.println("处理业务逻辑...");
            System.out.println("根据订单id查询其状态...");
            System.out.println("判断状态是否为支付成功");
            System.out.println("取消订单,回滚库存....");
            //3. 手动签收
            channel.basicAck(deliveryTag,true);
        } catch (Exception e) {
            //e.printStackTrace();
            System.out.println("出现异常,拒绝接受");
            //4.拒绝签收,不重回队列 requeue=false
            channel.basicNack(deliveryTag,true,false);
        }
    }
}
<rabbit:listener ref="orderListener" queue-names="order_queue_dlx"> </rabbit:listener>

springboot整合

@Configuration
public class MqConfig {

    @Autowired
    private Environment env;

    /**
     * 正常交换机
     * @return
     */
    @Bean
    public Exchange ttlExchange() {
        return ExchangeBuilder.directExchange(env.getProperty("mq.order.exchange.ttl")).durable(true).build();
    }

    /**
     * 死信交换机
     * @return
     */
    @Bean
    public Exchange dlxExchange() {
        return ExchangeBuilder.directExchange(env.getProperty("mq.order.exchange.dlx")).durable(true).build();
    }

    /**
     * 死信队列
     * @return
     */
    @Bean
    public Queue dlxQueue() {
        return QueueBuilder.durable(env.getProperty("mq.order.queue.dlx")).build();
    }

    /**
     * 正常队列
     * @return
     */
    @Bean
    public Queue ttlQueue() {
        return QueueBuilder
                .durable(env.getProperty("mq.order.queue.ttl"))
                //队列的过期时间,10秒过期
                .withArgument("x-message-ttl", 10000)
                //指定死信交换机
                .withArgument("x-dead-letter-exchange", env.getProperty("mq.order.exchange.dlx"))
                .withArgument("x-dead-letter-routing-key", env.getProperty("mq.order.routing.dlx"))
                .build();
    }


    /**
     * 正常交换机,队列绑定
     * @param queue
     * @param exchange
     * @return
     */
    @Bean
    public Binding ttlBinding(@Qualifier("ttlQueue") Queue queue, @Qualifier("ttlExchange")  Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(env.getProperty("mq.order.routing.ttl")).noargs();
    }

    /**
     * 死信交换机,队列绑定
     * @param queue
     * @param exchange
     * @return
     */
    @Bean
    public Binding dlxBinding(@Qualifier("dlxQueue") Queue queue, @Qualifier("dlxExchange")  Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with(env.getProperty("mq.order.routing.dlx")).noargs();
    }


}

@Component
@RabbitListener(queues = "${mq.pay.queue.order}")
public class OrderPayStatusListener {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private IOrderService orderService;

    @RabbitHandler
    public void handlerData(String msg) {
        //反序列化消息数据
        Map<String, String> map = null;
        try {
            map = objectMapper.readValue(msg, Map.class);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //更新订单状态
        if (map != null) {
            if (map.get("trade_status").equals("TRADE_SUCCESS")) {
                orderService.updatePayStatus(map.get("out_trade_no"), map.get("trade_no"));
            } else {
                //删除订单,支付失败,回滚库存(作业)
            }
        }
    }

}
@Component
@RabbitListener(queues = "${mq.order.queue.dlx}")
public class OrderLazyStatusListener {

    @Autowired
    private IOrderService orderService;

    @RabbitHandler
    public void handlerData(String msg) {
        if (StringUtils.isNotEmpty(msg)) {
            Long id = Long.parseLong(msg);
            Order order = orderService.getById(id);
            order.setOrderStatus("3");
            orderService.updateById(order);
            //回滚库存(作业)
        }
    }

}
rabbitTemplate.convertAndSend(env.getProperty("mq.order.exchange.ttl"), env.getProperty("mq.order.routing.ttl"), order.getId().toString());

你可能感兴趣的:(#,springCloud,rabbitmq,中间件,分布式)