1. 业务说明
下面我们通过RocketMQ中间件实现最大努力通知型分布式事务来模拟充值过程。我们有账户系统和充值系统两个微服务,其中账户系统对应的数据库是bank1,其中包括一条张三账户记录。充值系统的数据库使用pay数据库,记录了账户的充值记录。业务流程图如下:
2. 开发环境
- 数据库:
MySQL-5.6
- 微服务框架:
spring-boot-2.2.2.RELEASE
、spring-cloud-Hoxton.SR1
、rocketmq-spring-boot-2.0.2
2.1 启动程序
源码地址:https://gitee.com/anbang713/distributed-transaction-study
启动程序之前,我们需要使用源码工程中对应的sql脚本创建相对应的数据库和表,以及插入测试数据。
(1)首先我们要启动registry-server
服务注册中心;
(2)分别启动notice-demo-bank
和notice-demo-pay
微服务;
(3)启动rocketmq服务(可参考:《RocketMQ:快速入门》)。
3. 技术架构
交互流程如下:
(1)用户请求充值系统进行充值。
(2)充值系统完成充值将充值结果发给MQ。
(3)账户系统监听MQ,接收充值结果通知。如果接收不到消息,MQ会重复发送通知。接收到充值结果通知账户系统增加充值金额。
(4)账户系统也可以主动查询充值系统的充值结果查询接口,增加金额。
4. 数据结构
- bank1数据库
DROP TABLE IF EXISTS `account_info`;
CREATE TABLE `account_info` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`account_name` VARCHAR(100) DEFAULT NULL,
`account_no` VARCHAR(100) DEFAULT NULL,
`account_balance` DOUBLE DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
CREATE TABLE de_duplication (
tx_no VARCHAR(64) NOT NULL COMMENT '事务id',
create_time DATETIME,
PRIMARY KEY (`tx_no`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
INSERT INTO `account_info`(`id`,`account_name`,`account_no`,`account_balance`) VALUES (1,'张三的账号','1',100);
- pay数据库
DROP TABLE IF EXISTS `account_pay`;
CREATE TABLE `account_pay` (
`id` VARCHAR(64) NOT NULL,
`result` VARCHAR(100) DEFAULT NULL,
`account_no` VARCHAR(100) DEFAULT NULL,
`pay_amount` DOUBLE DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
5. 核心代码介绍
5.1 notice-demo-bank的核心代码
(1)mq监听充值结果消息
public void onMessage(AccountPay accountPay) {
log.info("接收到消息:{}", JSON.toJSONString(accountPay));
if("success".equals(accountPay.getResult())){
//更新账户金额
AccountChangeEvent accountChangeEvent = new AccountChangeEvent();
accountChangeEvent.setAccountNo(accountPay.getAccountNo());
accountChangeEvent.setAmount(accountPay.getPayAmount());
accountChangeEvent.setTxNo(accountPay.getId());
accountInfoService.updateAccountBalance(accountChangeEvent);
}
log.info("处理消息完成:{}", JSON.toJSONString(accountPay));
}
(2)远程调用查询充值结果
public AccountPay queryPayResult(String tx_no) {
//远程调用
AccountPay payresult = payClient.payresult(tx_no);
if("success".equals(payresult.getResult())){
//更新账户金额
AccountChangeEvent accountChangeEvent = new AccountChangeEvent();
accountChangeEvent.setAccountNo(payresult.getAccountNo());//账号
accountChangeEvent.setAmount(payresult.getPayAmount());//金额
accountChangeEvent.setTxNo(payresult.getId());//充值事务号
updateAccountBalance(accountChangeEvent);
}
return payresult;
}
5.2 notice-demo-pay的核心代码
// 插入充值记录
@Override
public AccountPay save(AccountPay accountPay) {
accountPay = accountPayDao.save(accountPay);
//发送通知,使用普通消息发送通知
accountPay.setResult("success");
rocketMQTemplate.convertAndSend("topic_notifymsg",accountPay);
return accountPay;
}
// 查询充值记录,接收通知方调用此方法来查询充值结果
@Override
public AccountPay findById(String txNo) {
Optional<AccountPay> optional = accountPayDao.findById(txNo);
return optional.isPresent()?optional.get():null;
}
6. 测试
(1)充值
http://localhost:9011/pay/paydo
// 请求体
{
"accountNo":1,
"payAmount":1
}
(2)主动查询充值结果
http://localhost:9012/bank/payresult/{txNo}