目录

  • 1 分布式数据一致性问题
  • 2 消息队列法
  • 2.1 可靠生产
  • 2.1.1 首先配置文件中开启确认机制
  • 2.1.2 业务服务中编写确认逻辑
  • 2.2 可靠消费
  • 2.2.1 更改配置文件手动应答
  • 2.2.2 创建队列绑定死信队列
  • 2.2.3 消费者逻辑
  • 2.2.4 为什么可靠消费可以实现分布式事务


1 分布式数据一致性问题

当业务拆分成多个异步微服务在不同的节点运行的时候,需要考虑数据一致性问题,单机上可以使用数据库的事务,遇到问题可以回滚,那么多机怎么办?

解决办法:

  • 两阶段提交(2PC):分成两个阶段,先由一方进行提议(propose)并收集其他节点的反馈(vote),再根据反馈决定提交(commit)或中止(abort)事务。我们将提议的节点称为协调者(coordinator),其他参与决议节点称为参与者(participants, 或cohorts):
  • 本地消息表:本地消息表和业务数据表处于同一个数据库中,利用本地事务来保证两个表的事务。
  • 消息队列法:利用消息队列实现分布式事务。

2 消息队列法

客户端把请求不是直接发送到服务端,而是发到消息队列,利用消息队列来实现分布式事务。

2.1 可靠生产

客户端把消息发送到消息队列,要知道这个消息确实发送到了消息队列,而没有丢失,所以可以在生产者端添加消息确认机制,即发送了消息,消息队列要回复客户端说确实已经到了,即消息确认。同时客户端需要有消息冗余表的存放,如果消息队列出了问题,客户端还是有数据,可以继续投放。
消息冗余表:{消息数据本身,消息状态},消息状态由消息队列确认机制来更新,确保消息确实到了消息队列。

2.1.1 首先配置文件中开启确认机制

# 配置RabbitMQ服务
spring:
  rabbitmq:
    publisher-confirm-type: correlated # 确认机制

2.1.2 业务服务中编写确认逻辑

// 消息确认机制
@PostConstruct // 用来修饰非静态void方法,在服务器加载servlet的时候运行,并且只执行一次,在构造函数之后,init()方法之前执行
public void CallBack(){
    // 消息发送成功以后,就有消息回执
    rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
        @Override
        public void confirm(CorrelationData correlationData, boolean ack, String cause) {
            // 如果不成功
            if(!ack) LOGGER.info("消息投递失败:"+correlationData.getId());
            //成功
            else LOGGER.info("消息投递成功"+correlationData.getId());
        }
    });
}

在消息确认之后可以修改相应数据库的状态值。

2.2 可靠消费

消费者消费完毕之后,会把ack消息发给消息队列,说这个消息确实被我消费完毕了,没有发生错误,如果发生错误,由消费者来控制这个消息的重发、清除和丢弃。
RabbitMQ消费者在消费消息的时候,如果出现异常,会不停反复重试,出现死循环。
解决方法:1.控制重发的次数 2.try-catch-手动ack 3.try-catch-手动ack+死信队列

2.2.1 更改配置文件手动应答

# 配置RabbitMQ服务
spring:
  rabbitmq:
    # 消费者手动ack
    listener:
      simple:
        acknowledge-mode: manual
        retry:
          enabled: true # 开启重试
          max-attempts: 5 # 最大重试次数
          initial-interval: 50ms # 重试间隔时间

2.2.2 创建队列绑定死信队列

// 2.声明队列
@Bean
public Queue emailQueue() {
    // 设置过期时间
    Map<String, Object> args = new HashMap<>();
    args.put("x-dead-letter-exchange", "lwj_dead_exchange");//添加死信队列
    // 名字、是否持久化
    return new Queue("email.fanout.queue", true, false, false, args);
}

消息发生错误的时候,把消息发送给死信交换机。

2.2.3 消费者逻辑

// 消费消息方法
@RabbitListener(queues = {"email.fanout.queue"})
public void receiveEmail(String message, Channel channel, CorrelationData correlationData,
                         @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {

    // try消费信息
    try {
        Order order = JSON.parseObject(message, Order.class);
        // 如果是王五就异常
        if(order.getUserName().equals("王五")) throw new Exception();
        // 不是王五正常消费
        LOGGER.info("消费一条邮件服务:" + message);
        // 手动确认
        channel.basicAck(tag,false);
    }catch (Exception e){
        // 捕捉到异常
        channel.basicNack(tag,false,false);// 不重试
    }
}

例子:当订单的消费者是王五的时候,就把消息发送到死信队列,不进行确认。其他情况都进行确认。

2.2.4 为什么可靠消费可以实现分布式事务

分布式事务实际上都是转换成本地事务,消息队列只是该本地事务是否被正常执行的一种通知机制,如果一个任务有三个服务,两个正常执行,一个不正常执行,不正常执行的消息会在本地事务回滚,然后把消息通道到死信队列,服务端的死信队列消费者会发现这个消息,然后启动回滚服务,告诉另外两个程序,也启动回滚事务,达到数据一致性。