前言
現在比較流行的分佈式架構而言,它雖然帶來一系列好處,比如支持高併發,高可用集羣。同時它也帶來一系列的挑戰,今天我們將的就是其中一種挑戰 - 分佈式事務 。
在傳統的 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等多個知識點的架構資料)