RocketMQ事務消息方案
RocketMQ 事務消息設計則主要是為了解決 Producer 端的消息發送與本地事務執行的原子性問題,RocketMQ 的設計中 broker 與 producer 端的雙向通信能力,使得 broker 天生可以作為一個事務協調者存在;而 RocketMQ 本身提供的存儲機制為事務消息提供了持久化能力;RocketMQ 的高可用機制以及可靠消息設計則為事務消息在系統發生異常時依然能夠保證達成事務的最終一致性。
在RocketMQ 4.3后實現了完整的事務消息,實際上其實是對本地消息表的一個封裝,將本地消息表移動到了MQ內部,解決 Producer 端的消息發送與本地事務執行的原子性問題。
執行流程如下:
為方便理解我們還以注冊送積分的例子來描述 整個流程。
Producer 即MQ發送方,本例中是用戶服務,負責新增用戶。MQ訂閱方即消息消費方,本例中是積分服務,負責新增積分。
1、Producer 發送事務消息
Producer (MQ發送方)發送事務消息至MQ Server,MQ Server將消息狀態標記為Prepared(預備狀態),注意此時這條消息消費者(MQ訂閱方)是無法消費到的。
本例中,Producer 發送 ”增加積分消息“ 到MQ Server。
2、MQ Server回應消息發送成功
MQ Server接收到Producer 發送給的消息則回應發送成功表示MQ已接收到消息。
3、Producer 執行本地事務
Producer 端執行業務代碼邏輯,通過本地數據庫事務控制。
本例中,Producer 執行添加用戶操作。
4、消息投遞
若Producer 本地事務執行成功則自動向MQServer發送commit消息,MQ Server接收到commit消息后將”增加積分消息“ 狀態標記為可消費,此時MQ訂閱方(積分服務)即正常消費消息;
若Producer 本地事務執行失敗則自動向MQServer發送rollback消息,MQ Server接收到rollback消息后 將刪除”增加積分消息“ 。
MQ訂閱方(積分服務)消費消息,消費成功則向MQ回應ack,否則將重復接收消息。這里ack默認自動回應,即程序執行正常則自動回應ack。
5、事務回查
如果執行Producer端本地事務過程中,執行端掛掉,或者超時,MQ Server將會不停的詢問同組的其他 Producer來獲取事務執行狀態,這個過程叫事務回查。MQ Server會根據事務回查結果來決定是否投遞消息。
以上主干流程已由RocketMQ實現,對用戶側來說,用戶需要分別實現本地事務執行以及本地事務回查方法,因此只需關注本地事務的執行狀態即可。
RoacketMQ提供RocketMQLocalTransactionListener接口:
public interface RocketMQLocalTransactionListener { /** - 發送prepare消息成功此方法被回調,該方法用於執行本地事務 - @param msg 回傳的消息,利用transactionId即可獲取到該消息的唯一Id - @param arg 調用send方法時傳遞的參數,當send時候若有額外的參數可以傳遞到send方法中,這里能獲取到 - @return 返回事務狀態,COMMIT:提交 ROLLBACK:回滾 UNKNOW:回調 */ RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg); /** - @param msg 通過獲取transactionId來判斷這條消息的本地事務執行狀態 - @return 返回事務狀態,COMMIT:提交 ROLLBACK:回滾 UNKNOW:回調 */ RocketMQLocalTransactionState checkLocalTransaction(Message msg); }
發送事務消息:
以下是RocketMQ提供用於發送事務消息的API:
TransactionMQProducer producer = new TransactionMQProducer("ProducerGroup"); producer.setNamesrvAddr("127.0.0.1:9876"); producer.start(); //設置TransactionListener實現 producer.setTransactionListener(transactionListener); //發送事務消息 SendResult sendResult = producer.sendMessageInTransaction(msg, null);
案例說明
本實例通過RocketMQ中間件實現可靠消息最終一致性分布式事務,模擬兩個賬戶的轉賬交易過程。
兩個賬戶在分別在不同的銀行(張三在bank1、李四在bank2),bank1、bank2是兩個微服務。交易過程是,張三給李四轉賬指定金額。
本示例程序技術架構如下:
交互流程如下:
1、Bank1向MQ Server發送轉賬消息
2、Bank1執行本地事務,扣減金額
3、Bank2接收消息,執行本地事務,添加金額
創建數據庫
在bank1、bank2數據庫中新增de_duplication,交易記錄表(去重表),用於交易冪等控制。
DROP TABLE IF EXISTS `de_duplication`;
CREATE TABLE `de_duplication` (
`tx_no` varchar(64) COLLATE utf8_bin NOT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`tx_no`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;
啟動RocketMQ
(1)下載RocketMQ服務器
下載地址:http://mirrors.tuna.tsinghua.edu.cn/apache/rocketmq/4.5.0/rocketmq-all-4.5.0-bin-release.zip
(2)解壓並啟動
啟動nameserver:
set ROCKETMQ_HOME=[rocketmq服務端解壓路徑]
start [rocketmq服務端解壓路徑]/bin/mqnamesrv.cmd
啟動broker:
set ROCKETMQ_HOME=[rocketmq服務端解壓路徑] start [rocketmq服務端解壓路徑]/bin/mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true
(1)工程maven依賴說明
<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>2.0.2</version> </dependency>
(2)配置rocketMQ
在application-local.propertis中配置rocketMQ nameServer地址及生產組:
rocketmq.producer.group = producer_bank2
rocketmq.name-server = 127.0.0.1:9876
生產者實現如下功能:
1、扣減金額,提交本地事務。
2、向MQ發送轉賬消息。
2)Dao
@Mapper @Component public interface AccountInfoDao { @Update("update account_info set account_balance=account_balance+#{amount} where account_no=#{accountNo}") int updateAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double amount); @Select("select count(1) from de_duplication where tx_no = #{txNo}") int isExistTx(String txNo); @Insert("insert into de_duplication values(#{txNo},now());") int addTx(String txNo); }
3)AccountInfoService
@Service @Slf4j public class AccountInfoServiceImpl implements AccountInfoService { @Resource private RocketMQTemplate rocketMQTemplate; @Autowired private AccountInfoDao accountInfoDao; /** * 更新帳號余額-發送消息 * producer向MQ Server發送消息 * * @param accountChangeEvent */ @Override public void sendUpdateAccountBalance(AccountChangeEvent accountChangeEvent) { //構建消息體 JSONObject jsonObject = new JSONObject(); jsonObject.put("accountChange",accountChangeEvent); Message<String> message = MessageBuilder.withPayload(jsonObject.toJSONString()).build(); TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction("producer_group_txmsg_bank1", "topic_txmsg", message, null); log.info("send transcation message body={},result={}",message.getPayload(),sendResult.getSendStatus()); } /** * 更新帳號余額-本地事務 * producer發送消息完成后接收到MQ Server的回應即開始執行本地事務 * * @param accountChangeEvent */ @Transactional @Override public void doUpdateAccountBalance(AccountChangeEvent accountChangeEvent) { log.info("開始更新本地事務,事務號:{}",accountChangeEvent.getTxNo()); accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount() * -1); //為冪等作准備 accountInfoDao.addTx(accountChangeEvent.getTxNo()); if(accountChangeEvent.getAmount() == 2){ throw new RuntimeException("bank1更新本地事務時拋出異常"); } log.info("結束更新本地事務,事務號:{}",accountChangeEvent.getTxNo()); } }
4)RocketMQLocalTransactionListener
編寫RocketMQLocalTransactionListener接口實現類,實現執行本地事務和事務回查兩個方法。
@Component @Slf4j @RocketMQTransactionListener(txProducerGroup = "producer_group_txmsg_bank1") public class ProducerTxmsgListener implements RocketMQLocalTransactionListener { @Autowired AccountInfoService accountInfoService; @Autowired AccountInfoDao accountInfoDao; //消息發送成功回調此方法,此方法執行本地事務 @Override @Transactional public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object arg) { //解析消息內容 try { String jsonString = new String((byte[]) message.getPayload()); JSONObject jsonObject = JSONObject.parseObject(jsonString); AccountChangeEvent accountChangeEvent = JSONObject.parseObject(jsonObject.getString("accountChange"), AccountChangeEvent.class); //扣除金額 accountInfoService.doUpdateAccountBalance(accountChangeEvent); return RocketMQLocalTransactionState.COMMIT; } catch (Exception e) { log.error("executeLocalTransaction 事務執行失敗",e); e.printStackTrace(); return RocketMQLocalTransactionState.ROLLBACK; } } //此方法檢查事務執行狀態 @Override public RocketMQLocalTransactionState checkLocalTransaction(Message message) { RocketMQLocalTransactionState state; final JSONObject jsonObject = JSON.parseObject(new String((byte[]) message.getPayload())); AccountChangeEvent accountChangeEvent = JSONObject.parseObject(jsonObject.getString("accountChange"),AccountChangeEvent.class); //事務id String txNo = accountChangeEvent.getTxNo(); int isexistTx = accountInfoDao.isExistTx(txNo); log.info("回查事務,事務號: {} 結果: {}", accountChangeEvent.getTxNo(),isexistTx); if(isexistTx>0){ state= RocketMQLocalTransactionState.COMMIT; }else{ state= RocketMQLocalTransactionState.UNKNOWN; } return state; } }
5)Controller
@RestController @Slf4j public class AccountInfoController { @Autowired private AccountInfoService accountInfoService; @GetMapping(value = "/transfer") public String transfer(@RequestParam("accountNo")String accountNo,@RequestParam("amount") Double amount){ String tx_no = UUID.randomUUID().toString(); AccountChangeEvent accountChangeEvent = new AccountChangeEvent(accountNo,amount,tx_no); accountInfoService.sendUpdateAccountBalance(accountChangeEvent); return "轉賬成功"; } }
消費者需要實現如下功能:
1、監聽MQ,接收消息。
2、接收到消息增加賬戶金額。
1) Service
注意為避免消息重復發送,這里需要實現冪等。
@Service @Slf4j public class AccountInfoServiceImpl implements AccountInfoService { @Autowired AccountInfoDao accountInfoDao; /** * 消費消息,更新本地事務,添加金額 * @param accountChangeEvent */ @Override @Transactional public void addAccountInfoBalance(AccountChangeEvent accountChangeEvent) { log.info("bank2更新本地賬號,賬號:{},金額:{}",accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount()); //冪等校驗 int existTx = accountInfoDao.isExistTx(accountChangeEvent.getTxNo()); if(existTx<=0){ //執行更新 accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(),accountChangeEvent.getAmount()); //添加事務記錄 accountInfoDao.addTx(accountChangeEvent.getTxNo()); log.info("更新本地事務執行成功,本次事務號: {}", accountChangeEvent.getTxNo()); }else{ log.info("更新本地事務執行失敗,本次事務號: {}", accountChangeEvent.getTxNo()); } } }
2)MQ監聽類
@Component @RocketMQMessageListener(topic = "topic_txmsg",consumerGroup = "consumer_txmsg_group_bank2") @Slf4j public class TxmsgConsumer implements RocketMQListener<String> { @Autowired AccountInfoService accountInfoService; @Override public void onMessage(String s) { log.info("開始消費消息:{}",s); //解析消息為對象 final JSONObject jsonObject = JSON.parseObject(s); AccountChangeEvent accountChangeEvent = JSONObject.parseObject(jsonObject.getString("accountChange"),AccountChangeEvent.class); //調用service增加賬號金額 accountChangeEvent.setAccountNo("2"); accountInfoService.addAccountInfoBalance(accountChangeEvent); } }
測試場景
bank1本地事務失敗,則bank1不發送轉賬消息。
bank2接收轉賬消息失敗,會進行重試發送消息。
bank2多次消費同一個消息,實現冪等。
轉載源:https://blog.csdn.net/weixin_44062339/article/details/100180487