RabbitMQ系列(十二)基于RabbitMQ的分布式事务

1,460 阅读3分钟

分布式事务的本质是最终一致性。

分布式事务的几种解决方案

  1. 基于数据库XA/JTA协议的方式;
    需要数据库厂商支持;JAVA组件有atomikos等
  2. 异步校对数据的方式;
    支付宝、微信支付主动查询支付状态、对账单的形式;
  3. 基于可靠消息(MQ)的解决方案;
    异步场景、通用性较强、拓展性较高
  4. TCC编程式解决方案;
    严选、阿里、蚂蚁金服自己封装的DTX;

分布式系统的数据一致性问题

错误示例

@Transaction(rollbackFor=Exception.class)
public void createOrder(JSONObject orderInfo) throws Exception {
    // 1. 保存订单信息(订单系统)
    orderService.saveOrder(orderInfo);
    
    // 2. 通过HTTP接口发送订单信息到运单系统
    String result = callDispatchHttpApi(orderInfo);
    if(!"ok".equals(result)) {
        throw new Exception("订单创建失败,原因:运单接口调用失败!");
    }
}

分布式事务问题:
1. 接口调用成功,订单系统数据库事务提交失败,运单系统没有回滚,产生数据;
2. 接口调用超时,订单系统数据库事务回滚,运单系统接口继续执行,产生数据;

消息确认

分布式事务的实现方案

1. 可靠地发送消息(本地消息表 + 确认机制)

生产者(订单中心应用程序)在保存订单记录的同一个本地事务中,同时将要发送到运单中心的消息保存到本地消息表中;

public class OrderDBService {
    /**
     * 保存订单记录
     */
    public void saveOrder(JSONObject orderInfo) throws Exception {
        // 1. 保存订单记录
        String sql = "insert into t_order(order_id, user_id, order_content, create_time) values(?,?,?,now())";
        int count = jdbcTemplate.update(sql, orderInfo.get("orderId"), orderInfo.get("userId"), orderInfo.get("orderContent"));
        if(count != 1) {
            throw new Exception("订单创建失败,原因:数据库操作失败!");
        }
        
        // 2. 记录发往MQ消息记录
        saveMQLocalMessage(orderInfo);
    }
    
    /**
     * 记录发往MQ消息记录
     */
    public void saveMQLocalMessage(JSONObject orderInfo) throws Exception {
        String sql = "insert into t_mq_message(unique_id, msg_content, msg_status, create_time) values(?,?,?,now())";
        int count = jdbcTemplate.update(sql, orderInfo.get("orderId"), orderInfo.toJSONString(),0, now());
    }
}

保存订单后,将订单消息发送到RabbitMQ服务器中(开启发布确认机制);收到确认后更新本地消息表的状态;

spring:
    rabbitmq:
        host: XXX.XXX.XXX.XXX
        port: YYYY
        username: AAA
        password: BBB
        publisher-confirms: true  # 开启消息发送确认机制
        

public class MQService implements RabbitTemplate.ConfirmCallback {
    public void sendMsg(JSONObject orderInfo) throw Exception {
        // 发送消息到MQ
        // CorrelationData当收到消息回执时,会附带上这个参数(orderId)
        rabbitTemplate.convertAndSend("createOrderExchange", "", orderInfo.toJSONString(), new CorrelationData(orderInfo.getString("orderId"));
    }
    
    /**
     * 回调
     */
    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        System.out.println(" 回调id:" + correlationData);
        if (ack) { // 成功确认
            System.out.println("消息成功消费");
        } else { // 失败确认
            System.out.println("消息消费失败:" + cause);
            // TODO 补偿或者其他后置处理
        }
    }
}

注意:如果出现回执没收到、消息状态修改失败等特殊情况。
处理方案:定时检查消息表,超时没发送成功,再次重发;

2. 可靠地消费消息(幂等性 + 手动确认机制)

防止消息重复处理,需先校验数据是否已被处理。

开启手动ACK模式:由消费者控制消息的重发/清除/丢弃。
【正常处理确认】

【异常处理重发】

注意:出现异常一般会重试几次,由消费者自身记录重试次数,并进行次数控制(不会永远重试)

【失败丢弃或转移死信队列】

重试次数过多、消息内容格式错误等情况,通过线上预警机制通知运维人员。