由浅至深的RabbitMQ学习之路

源于蚂蚁课堂的学习,点击这里查看(老余很给力)    

MQ背景

对于例如发送邮件或短信等行为,传统做法往往是自上而下执行,这样一来,增加用户等待响应时间,严重影响用户体验。
之后,开始将这些延时操作放入异步线程去处理,但是这样会增加CPU的开销。
故消息中间件横空出世,很好地解决了这一痛点,实现异步、解耦、流量削峰等功能

 市面主流的MQ

ActiveMQ 

历史悠久的开源项目,是Apache下的一个子项目。
已经在很多产品中得到应用,实现了JMS1.1规范,可以和spring-jms轻松融合,实现了多种协议,不够轻巧(源代码比RocketMQ多),
支持持久化到数据库,对队列数较多的情况支持不好。

RabbitMQ

结合erlang语言本身的并发优势,支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正是如此,使的它变的非常重量级,
更适合于企业级的开发。

RocketMQ 

阿里系下开源的一款分布式、队列模型的消息中间件,原名Metaq,3.0版本名称改为RocketMQ,是阿里参照kafka设计思想使用java
实现的一套mq。同时将阿里系内部多款mq产品(Notify、metaq)进行整合,只维护核心功能,去除了所有其他运行时依赖,保证核心功能
最简化,在此基础上配合阿里上述其他开源产品实现不同场景下mq的架构,目前主要多用于订单交易系统。

Kafka

Apache下的一个子项目,使用scala实现的一个高性能分布式Publish/Subscribe消息队列系统,具有以下特性:
高吞吐:在一台普通的服务器上既可以达到10W/s的吞吐速率;
高堆积:支持topic下消费者较长时间离线,消息堆积量大;

RabitMQ环境的基本安装 (windows)

1.下载并安装erlang,下载地址:http://www.erlang.org/download
2.配置erlang环境变量信息
  新增环境变量ERLANG_HOME=erlang的安装地址
  将%ERLANG_HOME%\bin加入到path中
3.下载并安装RabbitMQ,下载地址:http://www.rabbitmq.com/download.html
注意: RabbitMQ 它依赖于Erlang,需要先安装Erlang。

 RabitMQ管理平台中心

RabbitMQ 管理平台地址 http://127.0.0.1:15672
默认账号:guest/guest  用户可以自己创建新的账号

Virtual Hosts:
每个VirtualHost相当一个相对独立的RabbitMQ服务器,VirtualHost之间是相互隔离的。exchange、queue、message不能互通。

默认的端口15672:rabbitmq管理平台端口号
默认的端口5672: rabbitmq消息中间内部通讯的端口
默认的端口号25672  rabbitmq集群的端口号

 快速入门RabbitMQ简单队列

 Maven依赖

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

 获取连接

public class RabitMQConnection {
    /**
     * 获取连接
     *
     * @return
     */
    public static Connection getConnection() throws IOException, TimeoutException {
        // 1.创建连接
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 2.设置连接地址
        connectionFactory.setHost("127.0.0.1");
        // 3.设置端口号:
        connectionFactory.setPort(5672);
        // 4.设置账号和密码
        connectionFactory.setUsername("yanxiaohui");
        connectionFactory.setPassword("yanxiaohui");
        // 5.设置VirtualHost
        connectionFactory.setVirtualHost("/yxh");
        return connectionFactory.newConnection();
    }
}

 生产者

public class Producer {
    private static final String QUEUE_NAME = "rabbit_mq_demo";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        // 1.创建我们的连接
        Connection connection = RabitMQConnection.getConnection();
        // 2.创建我们通道
        Channel channel = connection.createChannel();
        // 开启了确认消息机制
        channel.confirmSelect();
        String msg = "这是一个mq的入门案例";
        channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
        if (channel.waitForConfirms()) {
            System.out.println("发送消息成功");
        } else {
            System.out.println("发送消息失败");
        }
        channel.close();
        connection.close();
    }
}

 消费者

public class Consumer {
    private static final String QUEUE_NAME = "rabbit_mq_demo";
    private static int serviceTimeOut = 1000;

    public static void main(String[] args) throws IOException, TimeoutException {
        // 1.创建我们的连接
        Connection connection = RabitMQConnection.getConnection();
        // 2.创建我们通道
        final Channel channel = connection.createChannel();
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("消费消息msg:" + msg);
                // 手动ack应答模式
                channel.basicAck(envelope.getDeliveryTag(), false);
                try {
                    Thread.sleep(serviceTimeOut);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        // 3.创建我们的监听的消息
        channel.basicConsume(QUEUE_NAME, false, defaultConsumer);
    }
}

 RabbitMQ如何保证消息不丢失

生产者:投递消息时采用消息确认机制confirm,确保消息投递至消息中间件中,否则进行重新投递
消费者:采用手动ack的方式通知消息中间件删除消息,即消息成功消费后再将其删除
中间件:采用持久化的方式将消息持久化至硬盘中

 RabitMQ五种消息模式

点对点

消费者自动确认机制,即生成一个消息,推送一个消息至消费者,不管其是否消费成功,默认自动删除消息

 工作模式

消费者手动ack通知消息的服务器删除消息,然后推送下一个。
在有多个消费者时,默认轮询推送,但基于前一个消息删除后才会对其推送下个消息,故手动ack可以实现能者多劳的工作模式

 发布订阅(fanout)

引入交换机的概念,多个队列绑定至同一交换机上,生产者只需要将消息投放至交换机,交换机就会通过发布订阅的方式将
消息广播至其上队列。
交换机类型
    Fanout exchange(扇型交换机)默认
    Direct exchange(直连交换机)
    Topic exchange(主题交换机)
    Headers exchange(头交换机)

生产者 

public class ProducerFanout {

    private static final String EXCHANGE_NAME = "fanout_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        //  创建Connection
        Connection connection = RabitMQConnection.getConnection();
        // 创建Channel
        Channel channel = connection.createChannel();
        // 通道关联交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout", true);
        String msg = "发布订阅模式";
        channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes());
        channel.close();
        connection.close();
    }
}

 消费者1

public class Consumer1 {

    private static final String QUEUE_NAME = "consumerFanout_1";

    private static final String EXCHANGE_NAME = "fanout_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        System.out.println("消费者1...");
        // 创建我们的连接
        Connection connection = RabitMQConnection.getConnection();
        // 创建我们通道
        final Channel channel = connection.createChannel();
        // 关联队列消费者关联队列
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("消费者1获取消息:" + msg);
            }
        };
        // 开始监听消息 自动签收
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);

    }
}

 消费者2

public class Consumer2 {

    private static final String QUEUE_NAME = "consumerFanout_2";

    private static final String EXCHANGE_NAME = "fanout_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        System.out.println("消费者2...");
        // 创建我们的连接
        Connection connection = RabitMQConnection.getConnection();
        // 创建我们通道
        final Channel channel = connection.createChannel();
        // 关联队列消费者关联队列
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("消费者2获取消息:" + msg);
            }
        };
        // 开始监听消息 自动签收
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);

    }
}

路由(direct)

在发布订阅的基础上,引入路由key,每个队列都可以绑定多个路由key,生产者投递消息时,可以指定路由key,
交换机根据路由key
查找对应的队列进行消息投放。
即路由key类似于队列的一种属性,方便生成者投放消息时做查询过滤,又叫direct

 生产者

/**
 * 定义交换机的名称
 */
private static final String EXCHANGE_NAME = "direct_exchange";

public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
    // 1.创建我们的连接
    Connection connection = RabitMQConnection.getConnection();
    // 2.创建我们通道
    Channel channel = connection.createChannel();
    // 不需要直接关心队列,只关心交换机
    channel.exchangeDeclare(EXCHANGE_NAME, "direct", true);
    String msg = "路由直连的消息"
    channel.basicPublish(EXCHANGE_NAME, "key1", null, msg.getBytes());
    channel.close();
    connection.close();
    // 如果交换机没有绑定队列,消息可能会丢失
}

 消费者1

public class Consumer1 {

    private static final String QUEUE_NAME = "consumer_direct_1";
    
    private static final String EXCHANGE_NAME = "direct_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 创建我们的连接
        Connection connection = RabitMQConnection.getConnection();
        // 创建我们通道
        final Channel channel = connection.createChannel();
        // 关联队列消费者关联队列
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key1");
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("消费者1获取消息:" + msg);
            }
        };
        // 开始监听消息 自动签收
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);

    }
}

 消费者2

public class Consumer2 {

    private static final String QUEUE_NAME = "consumer_direct_2";
    
    private static final String EXCHANGE_NAME = "direct_exchange";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 创建我们的连接
        Connection connection = RabitMQConnection.getConnection();
        // 创建我们通道
        final Channel channel = connection.createChannel();
        // 关联队列消费者关联队列
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "key2");
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("消费者2获取消息:" + msg);
            }
        };
        // 开始监听消息 自动签收
        channel.basicConsume(QUEUE_NAME, true, defaultConsumer);

    }
}

 主题(topic) 

在路由的基础上,实现通配符查询,即模糊查询,*代表一个单词,#代表多个单词
这样一来,队列不用去绑定一个又一个的路由key,直接绑定通配符的路由key即可

 SpringBoot整合RabbitMQ

maven依赖 

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
</parent>
<dependencies>

    <!-- springboot-web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 添加springboot对amqp的支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <!--fastjson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.49</version>
    </dependency>
</dependencies>

application.yml

spring:
  rabbitmq:
    ####连接地址
    host: 127.0.0.1
    ####端口号
    port: 5672
    ####账号
    username: yanxiaohui
    ####密码
    password: yanxiaohui
    ### 地址
    virtual-host: /yxh

配置类

@Component
public class RabbitMQConfig {

    /**
     * 定义交换机
     */
    private String EXCHANGE_SPRINGBOOT_NAME = "springboot_exchange";


    /**
     * 短信队列
     */
    private String FANOUT_SMS_QUEUE = "fanout_sms_queue";
    /**
     * 邮件队列
     */
    private String FANOUT_SMS_EMAIL = "fanout_email_queue";

    /**
     * 创建短信队列
     */
    @Bean
    public Queue smsQueue() {
        return new Queue(FANOUT_SMS_QUEUE);
    }

    /**
     * 创建邮件队列
     */
    @Bean
    public Queue emailQueue() {
        return new Queue(FANOUT_SMS_EMAIL);
    }

    /**
     * 创建交换机
     *
     * @return
     */
    @Bean
    public FanoutExchange fanoutExchange() {
        return new FanoutExchange(EXCHANGE_SPRINGBOOT_NAME);
    }

    /**
     * 定义短信队列绑定交换机
     */
    @Bean
    public Binding smsBindingExchange(Queue smsQueue, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(smsQueue).to(fanoutExchange);
    }

    /**
     * 定义邮件队列绑定交换机
     */
    @Bean
    public Binding emailBindingExchange(Queue emailQueue, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(emailQueue).to(fanoutExchange);
    }
}

 生产者

@RestController
public class FanoutProducer {
    @Autowired
    private AmqpTemplate amqpTemplate;

    @RequestMapping("/sendMsg")
    public String sendMsg(String msg) {
        // 参数1 交换机名称 、参数2路由key  参数3 消息
        amqpTemplate.convertAndSend("springboot_exchange", "", msg);
        return "success";
    }
}

 消费者

@Component
@RabbitListener(queues = "fanout_email_queue")
public class FanoutEmailConsumer {

    @RabbitHandler
    public void process(String msg) {
        System.out.println("邮件消费者消息msg:" + msg);
    }
}

死信队列

产生的背景  

俗称备胎队列,用于存放消息多次消费失败、消费超时、超过队列最大长度被拒绝接收的消息。
消息中间件因为某种原因拒收该消息后,可以转移到死信队列中存放,死信队列也可以有交换机和路由key等。

 SpringBoot整合死信队列

maven依赖 

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.0.RELEASE</version>
</parent>
<dependencies>

    <!-- springboot-web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 添加springboot对amqp的支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <!--fastjson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.49</version>
    </dependency>
</dependencies>

application.yml

spring:
  rabbitmq:
    ####连接地址
    host: 127.0.0.1
    ####端口号
    port: 5672
    ####账号
    username: yanxiaohui
    ####密码
    password: yanxiaohui
    ### 地址
    virtual-host: /yxh
server:
  port: 8080

###模拟演示死信队列
yxh:
  dlx:
    exchange: yxh_dlx_exchange
    queue: yxh_order_dlx_queue
    routingKey: dlx
  ###备胎交换机
  order:
    exchange: yxh_order_exchange
    queue: yxh_order_queue
    routingKey: yxh.order

 信队列配置

@Component
public class DeadLetterMQConfig {
    /**
     * 订单交换机
     */
    @Value("${yxh.order.exchange}")
    private String orderExchange;

    /**
     * 订单队列
     */
    @Value("${yxh.order.queue}")
    private String orderQueue;

    /**
     * 订单路由key
     */
    @Value("${yxh.order.routingKey}")
    private String orderRoutingKey;
    /**
     * 死信交换机
     */
    @Value("${yxh.dlx.exchange}")
    private String dlxExchange;

    /**
     * 死信队列
     */
    @Value("${yxh.dlx.queue}")
    private String dlxQueue;
    /**
     * 死信路由
     */
    @Value("${yxh.dlx.routingKey}")
    private String dlxRoutingKey;

    /**
     * 声明死信交换机
     *
     * @return DirectExchange
     */
    @Bean
    public DirectExchange dlxExchange() {
        return new DirectExchange(dlxExchange);
    }

    /**
     * 声明死信队列
     *
     * @return Queue
     */
    @Bean
    public Queue dlxQueue() {
        return new Queue(dlxQueue);
    }

    /**
     * 声明订单业务交换机
     *
     * @return DirectExchange
     */
    @Bean
    public DirectExchange orderExchange() {
        return new DirectExchange(orderExchange);
    }

    /**
     * 绑定死信队列到死信交换机
     *
     * @return Binding
     */
    @Bean
    public Binding binding() {
        return BindingBuilder.bind(dlxQueue())
                .to(dlxExchange())
                .with(dlxRoutingKey);
    }

    /**
     * 声明订单队列
     *
     * @return Queue
     */
    @Bean
    public Queue orderQueue() {
        // 订单队列绑定我们的死信交换机
        Map<String, Object> arguments = new HashMap<>(2);

        arguments.put("x-dead-letter-exchange", dlxExchange);
        arguments.put("x-dead-letter-routing-key", dlxRoutingKey);
        return new Queue(orderQueue, true, false, false, arguments);
    }

    /**
     * 绑定订单队列到订单交换机
     *
     * @return Binding
     */
    @Bean
    public Binding orderBinding() {
        return BindingBuilder.bind(orderQueue())
                .to(orderExchange())
                .with(orderRoutingKey);
    }

 消费者

@Component
public class OrderDlxConsumer {

    /**
     * 死信队列监听队列回调的方法
     *
     * @param msg
     */
    @RabbitListener(queues = "yxh_order_dlx_queue")
    public void orderConsumer(String msg) {
        System.out.println("死信队列消费订单消息" + msg);
    }
}

 订单消费者

@Component
public class OrderConsumer {

    /**
     * 监听队列回调的方法
     *
     * @param msg
     */
    @RabbitListener(queues = "yxh_order_queue")
    public void orderConsumer(String msg) {
        System.out.println("正常订单消费者消息msg:" + msg);
    }
}

 生产者投递消息

@RestController
public class DeadLetterProducer {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    /**
     * 订单交换机
     */
    @Value("${yxh.order.exchange}")
    private String orderExchange;
    /**
     * 订单路由key
     */
    @Value("${yxh.order.routingKey}")
    private String orderRoutingKey;

    @RequestMapping("/sendOrder")
    public String sendOrder() {
        String msg = "死信队列的demo";
        rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, msg, new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                message.getMessageProperties().setExpiration("10000");
                return message;
            }
        });
        return "succcess";
    }
}

 消息中间件如何获取消费结果

由于中间件的操作是异步的,所以想要获取其操作结果,只有通过主动查询的方式,才能知道消息是否已消费。
即通过业务生成全局的消息ID,使用消息ID去数据库查找对应的业务是否发生了预期的变化,进而得出消息是否成功消费。
此方法也是其解决消息幂等性的思路

RabbitMQ消息幂等问题 

消息自动重试机制

在消费者消费消息的代码中出现异常后,默认是重复执行的,(默认无数次)。
如果代码本身就有问题,而非外部因素(网络抖动等)影响,那么这种重试本质无意义。
所以应该对重试机制设置重试次数和时间间隔,超过重试机制还是抛出异常的,我们可以将消息放入死信或者数据库将其记录,方便补偿。

SpringBoot开启重试策略

spring:
  rabbitmq:
    ####连接地址
    host: 127.0.0.1
    ####端口号
    port: 5672
    ####账号
    username: yanxiaohui
    ####密码
    password: yanxiaohui
    ### 地址
    virtual-host: /yxh
    listener:
      simple:
        retry:
          ####开启消费者(程序出现异常的情况下会)进行重试
          enabled: true
          ####最大重试次数
          max-attempts: 5
          ####重试间隔次数
          initial-interval: 3000

消费者开启重试策略

System.out.println("消费者消息msg:" + msg);
JSONObject msgJson = JSONObject.parseObject(msg);
String email = msgJson.getString("email");
String emailUrl = "http://127.0.0.1:8081/sendEmail?email=" + email;
JSONObject jsonObject = null;
try {
    jsonObject = HttpClientUtils.httpGet(emailUrl);
} catch (Exception e) {
    String errorMsg = email + ",调用第三方邮件接口失败:" + ",错误原因:" + e.getMessage();
    throw new Exception(errorMsg);
}
System.out.println("邮件消费者调用第三方接口结果:" + jsonObject);

rabbitMQ如何解决消息幂等问题 

采用消息全局id根据业务来定

生产者

@RequestMapping("/sendOrderMsg")
    public String sendOrderMsg() {
        // 1.生产订单id
        String orderId = System.currentTimeMillis() + "";
        String orderName = "生成消息幂等的订单";
        OrderEntity orderEntity = new OrderEntity(orderName, orderId);
        String msg = JSONObject.toJSONString(orderEntity);
        sendMsg(msg, orderId);
        return orderId;
        // 后期客户端主动使用orderId调用服务器接口 查询该订单id是否在数据库中存在数据 消费成功 消费失败
    }

    @Async
    public void sendMsg(String msg, String orderId) {
        rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, msg,
                new MessagePostProcessor() {
                    @Override
                    public Message postProcessMessage(Message message) throws AmqpException {
//                        message.getMessageProperties().setExpiration("10000");
                        message.getMessageProperties().setMessageId(orderId);
                        return message;
                    }
                });
        // 消息投递失败
    }

消费者 

String msg = new String(message.getBody());
System.out.println("订单队列获取消息:" + msg);
OrderEntity orderEntity = JSONObject.parseObject(msg, OrderEntity.class);
if (orderEntity == null) {
    return;
}
// messageId根据具体业务来定,如果已经在数据表中插入过数据,则不会插入
String orderId = message.getMessageProperties().getMessageId();
if (StringUtils.isEmpty(orderId)) {
    // 开启消息确认机制
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    return;
}
OrderEntity dbOrderEntity = orderMapper.getOrder(orderId);
if (dbOrderEntity != null) {
    // 说明已经处理过请求
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    return;
}

int result = orderMapper.addOrder(orderEntity);
if (result >= 0) {
    // 开启消息确认机制
    channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}

RabbitMQ如何解决分布式事务 

什么是分布式事务

官方说法:在分布式系统中,因为跨服务调用接口,存在多个不同的事务,每个事务都互不影响。就存在分布式事务的问题。
说白了就是:在一个事务中,出现rpc远程调用,其中对数据的变更脱离当前事务的管理,导致当前事务回滚时,无法将远程事务一并回滚。

解决分布式事务核心思想

最终一致性。分布式领域不存在强一致性,对于短暂期间的不一致,可以允许通过补偿或延时使其最终数据保持一致。

RabbitMQ解决分布式事务的思路

1.通过消息确认机制confirm确保消息一定投递至消息中间中
2.消费者手动ack确定消息的消费成功
3.对于消费成功但事务回滚的操作,需要进行补充,即将整个业务操作(除去消息投递)都记录至补偿队列,然后补偿业务的数据缺失。

 Maven依赖

<dependencies>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.1.1</version>
    </dependency>
    <!-- mysql 依赖 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <!-- 阿里巴巴数据源 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.0.14</version>
    </dependency>
    <!-- SpringBoot整合Web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- 添加springboot对amqp的支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <!--fastjson -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.49</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

application.yml 

spring:
  rabbitmq:
    ####连接地址
    host: 127.0.0.1
    ####端口号
    port: 5672
    ####账号
    username: yanxiaohui
    ####密码
    password: yanxiaohui
    ### 地址
    virtual-host: /yxh
    ###开启消息确认机制 confirms
    publisher-confirms: true
    publisher-returns: true
    listener:
      simple:
        retry:
          ####开启消费者(程序出现异常的情况下会)进行重试
          enabled: true
          ####最大重试次数
          max-attempts: 5
          ####重试间隔次数
          initial-interval: 3000
          ###开启ack模式
        acknowledge-mode: manual
  datasource:
    url: jdbc:mysql://localhost:3306/order?useUnicode=true&characterEncoding=UTF-8
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
server:
  port: 8080

mq配置

@Component
public class OrderRabbitMQConfig {

    /**
     * 派单队列
     */
    public static final String ORDER_DIC_QUEUE = "order_dic_queue";
    /**
     * 补单对接
     */
    public static final String ORDER_CREATE_QUEUE = "order_create_queue";
    /**
     * 派单交换机
     */
    private static final String ORDER_EXCHANGE_NAME = "order_exchange_name";

    /**
     * 定义派单队列
     *
     * @return
     */
    @Bean
    public Queue directOrderDicQueue() {
        return new Queue(ORDER_DIC_QUEUE);
    }

    /**
     * 定义补派单队列
     *
     * @return
     */
    @Bean
    public Queue directCreateOrderQueue() {
        return new Queue(ORDER_CREATE_QUEUE);
    }


    /**
     * 定义订单交换机
     *
     * @return
     */
    @Bean
    DirectExchange directOrderExchange() {
        return new DirectExchange(ORDER_EXCHANGE_NAME);
    }


    /**
     * 派单队列与交换机绑定
     *
     * @return
     */
    @Bean
    Binding bindingExchangeOrderDicQueue() {
        return BindingBuilder.bind(directOrderDicQueue()).to(directOrderExchange()).with("orderRoutingKey");
    }

    /**
     * 补单队列与交换机绑定
     *
     * @return
     */
    @Bean
    Binding bindingExchangeCreateOrder() {
        return BindingBuilder.bind(directCreateOrderQueue()).to(directOrderExchange()).with("orderRoutingKey");
    }
}

生产者 

@Component
@Slf4j
public class OrderProducer implements RabbitTemplate.ConfirmCallback {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private RabbitTemplate rabbitTemplate;


    @Transactional
    public String send() {
        // 1.创建订单
        String orderId = System.currentTimeMillis() + "";
        OrderEntity orderEntity = createOrder(orderId);
        //2.将订单添加到数据库中(步骤一 先往数据库中添加一条数据)
        int result = orderMapper.addOrder(orderEntity);
        if (result <= 0) {
            return orderId;
        }
        //3.使用消息中间件异步 ,分配订单
        String sendMsgJson = JSONObject.toJSONString(orderEntity);
        send(sendMsgJson);
        int i = 1 / 0;
        return orderId;
    }

    public OrderEntity createOrder(String orderId) {
        OrderEntity orderEntity = new OrderEntity();
        orderEntity.setName("分布式事务");
        orderEntity.setOrderCreatetime(new Date());
        // 价格是300元
        orderEntity.setOrderMoney(300d);
        // 状态为 未支付
        orderEntity.setOrderState(0);
        Long commodityId = 30L;
        // 商品id
        orderEntity.setCommodityId(commodityId);
        orderEntity.setOrderId(orderId);
        return orderEntity;
    }

    private void send(String sendMsg) {

        log.info(">>>生产者发送订单数据:" + sendMsg);
        // 设置生产者消息确认机制
        this.rabbitTemplate.setMandatory(true);
        this.rabbitTemplate.setConfirmCallback(this);
        // 构建回调返回参数
        CorrelationData correlationData = new CorrelationData(sendMsg);
        String orderExchange = "order_exchange_name";
        String orderRoutingKey = "orderRoutingKey";
        rabbitTemplate.convertAndSend(orderExchange, orderRoutingKey, sendMsg, correlationData);
    }

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String s) {
        String sendMsg = correlationData.getId();
        System.out.println("生产者开始消息确认orderId:" + sendMsg);
        if (!ack) {
            // 递归调用发送
            send(sendMsg);
            return;
        }
        System.out.println("生产者消息确认orderId:" + sendMsg);
    }
}

消费者

@Component
public class DistriLeafleConsumer {

    @Autowired
    private DispatchMapper dispatchMapper;

    @RabbitListener(queues = "order_dic_queue")
    public void distriLeafleConsumer(Message message, Channel channel) throws IOException {
        String msg = new String(message.getBody());
        System.out.println("派代服务平台msg:" + msg);
        JSONObject jsonObject = JSONObject.parseObject(msg);
        // 订单id
        String orderId = jsonObject.getString("orderId");
        // 假设派单userID 1234
        Long userId = 1234L;
        DispatchEntity dispatchEntity = new DispatchEntity(orderId, userId);
        int result = dispatchMapper.insertDistribute(dispatchEntity);
        if (result >= 0) {
            // 手动ack 删除该消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        }
    }
}

补单消费者(补偿队列的消费)

@Component
public class CreateOrderConsumer {

    @Autowired
    private OrderMapper orderMapper;

    @RabbitListener(queues = "order_create_queue")
    public void createOrderConsumer(Message message, Channel channel) throws IOException {
        String msg = new String(message.getBody());
        OrderEntity orderEntity = JSONObject.parseObject(msg, OrderEntity.class);
        String orderId = orderEntity.getOrderId();

        // 根据订单号码查询该笔订单是否创建
        OrderEntity dbOrderEntity = orderMapper.findOrderId(orderId);
        if (dbOrderEntity != null) {
            // 手动ack 删除该消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            return;
        }
        int result = orderMapper.addOrder(orderEntity);
        if (result >= 0) {
            // 手动ack 删除该消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        }
    }
}

猜你喜欢

转载自blog.csdn.net/yxh13521338301/article/details/106997361
今日推荐