架構師總結:分佈式事務就是這麼簡單之RocketMQ解決方案

前言

​ 現在比較流行的分佈式架構而言,它雖然帶來一系列好處,比如支持高併發,高可用集羣。同時它也帶來一系列的挑戰,今天我們將的就是其中一種挑戰 - 分佈式事務 。

​ 在傳統的 all in 項目中單數據源的事務一致性依賴於單機事務,但是如果上升到分佈式項目中,那麼保證事務的一致性僅僅依靠單機事務是不能實現的,這時候就依賴於分佈式事務。

介紹

目前業界比較主流的分佈式事務解決方法大概可以分爲兩種

  • 強一致性
  • 最終一致性

強一致性

​ 主要解決方法代表有 2PC 、 Tcc 適用於 金融交易場景

最終一致性

​ 主要解決方法代表有 RocketMQ事務消息 適用於常見的積分訂單場景,1、比如創建訂單 2、如果訂單創建成功 3、增加買家積分 不管中途發生了什麼 只要訂單成功,那麼買家的積分就一定要增加。保證最終一致性

實現架構

術語介紹

  • HALF MESSAGE : 事務消息 也稱半消息 標識該消息處於"暫時不能投遞"狀態,不會被Comsumer所消費,待服務端收到生成者對該消息的commit或者rollback響應後,消息會被正常投遞或者回滾(丟棄)消息
  • RMQ_SYS_TRANS_HALF_TOPIC :半消息在被投遞到Mq服務器後,會存儲於Topic爲RMQ_SYS_TRANS_HALF_TOPIC的消費隊列中
  • RMQ_SYS_TRANS_OP_HALF_TOPIC : 在半消息被commit或者rollback處理後,會存儲到Topic爲RMQ_SYS_TRANS_OP_HALF_TOPIC的隊列中,標識半消息已被處理

在RocketMQ中 核心思路就是 **兩段提交 定時回查 **

流程圖如下:

1、首先事務發起者 給RocketMQ發送一個半消息

2、RocketMQ響應事務發起者 半消息發送成功

3、事務發起者提交本地事務

4、根據本地事務運行結果 響應RocketMQ 半消息是commit還是rollback

5、如果沒有收到第4步通知,則RocketMQ回查事務發起者。

6、事務發起者收到回查通知檢查本地消息狀態

7、將回查結果返回RocketMQ 根據結果commit/rollback半消息

8、如果broker收到commit 則將半消息從 trans_half隊列提交到真正的業務隊列中。如果收到rollback或者半消息過期 則提交到trans_op_half隊列中。

9、如果半消息被commit 則消息訂閱方法能讀取消費該消息,只要保證下游消費失敗重試,即可保證消息最終一致性。

分析一下 可能遇到的場景

1、半消息發送成功,本地事務運行失敗。rollback半消息,下游業務無感知,正常。

2、半消息發送成功,本地事務運行成功。但是第4步通知broker由於網絡原因發送失敗,但是broker有輪詢機制,根據唯一id查詢本地事務狀態,從而提交半消息。

通過以上幾步就實現了RocketMQ的事務消息。

實例

這裏通過一個實例來講一下RocketMQ實現分佈式事務具體編碼。

場景: 下單場景,當訂單支付成功之後,對應的買家對應的賬號需要增加積分。(暫時不考慮物流 庫存簡單分析。)

很明顯兩個服務, 1、訂單服務 2、積分服務

用戶付款成之後 1、修改訂單狀態已支付 2、通知積分服務 給對應的買家漲積分。

實體結構

訂單

/**
 * @author yukong
 * @date 2019-07-25 15:18
 * 訂單  省略其他字段
 */
@Data
public class Order {

    /**
     * 訂單號
     */
    private String orderNo;

    /**
     * 買家id
     */
    private Integer buyerId;

    /**
     * 支付狀態 0 已支付 1 未支付 2 已超時
     */
    private Integer payStatus;

    /**
     * 下單日期
     */
    private Date createDate;

    /**
     * 金額
     */
    private Long amount;



}
複製代碼

積分添加記錄

/**
 * @author yukong
 * @date 2019-07-25 15:32
 * 積分添加記錄表
 */
@Data
public class PointRecord {

    /**
     * 訂單號
     */
    private String orderNo;

    /**
     * 用戶id
     */
    private Integer userId;

}

複製代碼

首先我們需要實現業務代碼,也是修改訂單狀態,然後記錄一條積分添加記錄(可以用於事務回查,判斷本地事務是否允許成功)。

/**
 * @author yukong
 * @date 2019-07-25 15:14
 */
@Service("payService")
@Slf4j
public class PayService {


    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private PointRecordMapper pointRecordMapper;

    /**
     * 支付功能:
     *  如果支付成功 則下游業務 也就是積分服務對應的賬號需要增加積分
     *  如果支付失敗,則下游業務無感知
     */
    @Transactional(rollbackFor = Exception.class)
    public void pay(String orderNo, Integer buyerId) {
        // 1、構造積分添加記錄表
        PointRecord record = new PointRecord();
        record.setOrderNo(orderNo);
        record.setUserId(buyerId);
        // 2、存入數據庫
        pointRecordMapper.insert(record);
         // 3、修改訂單狀態 爲已支付
        Order order = new Order();
        order.setOrderNo(orderNo);
        order.setBuyerId(buyerId);
        //4、 更新訂單信息
        orderMapper.updateOrder(order);

        log.info("執行本地事務,pay() ");
    }

    public Boolean checkPayStatus(String orderNo) {
        // 根據判斷是否有PointRecord這個記錄來 確實是否支付成成功 用於事務回查判斷本地事務是否執行成功
        return Objects.nonNull(pointRecordMapper.getPointRecordByOrderNo(orderNo));
    }

}
複製代碼

接下來要實現事務發起者的代碼,也是就是半消息發送者。

/**
 * @author yukong
 * @date 2019-07-25 14:48
 * 事務消息生產者
 */
@Component
@Slf4j
public class TransactionProducer implements InitializingBean {

    private  TransactionMQProducer producer;

    @Autowired
    private RocketMQProperties rocketMQProperties;

    @Autowired
    private TransactionListener transactionListener;


    /**
     * 構造生產者
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {

        producer = new TransactionMQProducer(rocketMQProperties.getTransactionProducerGroupName());
        producer.setNamesrvAddr(rocketMQProperties.getNamesrvAddr());
        ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("transaction-thread-name-%s").build();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 5, 60,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(30), threadFactory);
        producer.setExecutorService(executor);

        producer.setTransactionListener(transactionListener);

        producer.start();

    }

    /**
     * 真正的事物消息發送者
     */
    public void send() throws JsonProcessingException, UnsupportedEncodingException, MQClientException {

        ObjectMapper objectMapper = new ObjectMapper();

        // 模擬接受前臺的支付請求
        String orderNo = UUID.randomUUID().toString();
        Integer userId = 1;
        // 構造發送的事務 消息
        PointRecord record = new PointRecord();
        record.setUserId(userId);
        record.setOrderNo(orderNo);

        Message message = new Message(rocketMQProperties.getTopic(), "", record.getOrderNo(),
                objectMapper.writeValueAsString(record).getBytes(RemotingHelper.DEFAULT_CHARSET));

        producer.sendMessageInTransaction(message, null);

        log.info("發送事務消息, topic = {}, body = {}", rocketMQProperties.getTopic(), record);
    }
}

複製代碼

緊接着我們要實現,事務消息的二段提交與事務消息回查本地事務狀態的編碼。

/**
 * @author yukong
 * @date 2019-07-25 15:08
 * 事務消息 回調監聽器
 */
@Component
@Slf4j
public class PointTransactionListener implements TransactionListener {

    @Autowired
    private PayService payService;



    /**
     * 根據消息發送的結果 判斷是否執行本地事務
     * @param message
     * @param o
     * @return
     */
    @Override
    public LocalTransactionState executeLocalTransaction(Message message, Object o) {
        // 根據本地事務執行成與否判斷 事務消息是否需要commit與 rollback
        ObjectMapper objectMapper = new ObjectMapper();
        LocalTransactionState state = LocalTransactionState.UNKNOW;
        try {
            PointRecord record = objectMapper.readValue(message.getBody(), PointRecord.class);
            payService.pay(record.getOrderNo(), record.getUserId());
            state = LocalTransactionState.ROLLBACK_MESSAGE;
        } catch (UnsupportedEncodingException e) {
            log.error("反序列化消息 不支持的字符編碼:{}", e);
            state = LocalTransactionState.ROLLBACK_MESSAGE;
        } catch (IOException e) {
            log.error("反序列化消息失敗 io異常:{}", e);
            state = LocalTransactionState.ROLLBACK_MESSAGE;
        }
        return state;
    }

    /**
     * RocketMQ 回調 根據本地事務是否執行成功 告訴broker 此消息是否投遞成功
     * @param messageExt
     * @return
     */
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
        ObjectMapper objectMapper = new ObjectMapper();
        LocalTransactionState state = LocalTransactionState.UNKNOW;
        PointRecord record = null;
        try {
            record = objectMapper.readValue(messageExt.getBody(), PointRecord.class);
        } catch (IOException e) {
            log.error("回調檢查本地事務狀態異常: ={}", e);

        }
        try {
            //根據是否有transaction_id對應轉賬記錄 來判斷事務是否執行成功
            boolean isCommit = payService.checkPayStatus(record.getOrderNo());
            if (isCommit) {
                state = LocalTransactionState.COMMIT_MESSAGE;
            } else {
                state = LocalTransactionState.ROLLBACK_MESSAGE;
            }
        } catch (Exception e) {
            log.error("回調檢查本地事務狀態異常: ={}", e);
        }
        return state;

    }
}
複製代碼

這樣我們就實現了分佈式事務的最終一致性。

具體消費方代碼就不寫了,只要上游本地事務運行成功,且事務消息成功投遞給對應的topic,這樣下游業務對於上游是無感知,所以消費方只要保證冪等性即可。

羣內提供免費的Java架構學習資料,QQ羣:643459718
(裏面有高可用、高併發、高性能及分佈式、Jvm性能調優、Spring源碼,
MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多個知識點的架構資料)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章