1、可靠消息实现最终一致性
在前面我们学习过CAP理论,知道我们一般情况下会保证P和A,舍弃C,保证最终一致性。
分布式事务
使用消息队列实现最终一致性的核心就是将分布式事务拆分成多个本地事务,然后通过网络由消息队列协调完成所有的事务,实现最终一致性。
这个方案相信大家都很容易理解,但是也将面临不少问题:
1.消息发送方执行本地事务与发送消息的原子性问题,也就是说如何保证本地事务执行成功,消息一定发送成功
begin transaction
1.数据库操作
2.发送消息
commit transation
这种情况下,貌似没有问题,如果发送消息失败,就会抛出异常,导致数据库事务回滚。但如果是超时异常,数据库回滚,但此时消息已经正常发送了,同样会导致不一致。
2.消息接收方接收消息与本地事务的原子性问题,也就是说如何保证接收消息成功后,本地事务一定执行成功
3.由于消息可能会重复发送,这就要求消息接收方必须实现幂等性
由于在生产环境中,消费方很有可能是个集群,若某一个消费节点超时但是消费成功,会导致集群同组其他节点重复消费该消息。另外意外宕机后恢复,由于消费进度没有及时写入磁盘,会导致消费进度部分丢失,从而导致消息重复消费。
2、RocketMQ
RocketMQ 是一个来自阿里巴巴的分布式消息中间件,于 2012 年开源,并在 2017 年正式成为 Apache 顶级项目。Apache RocketMQ 4.3之后的版本正式支持事务消息,为分布式事务实现提供了便利性支持。因此,我们通过RocketMQ就可以解决前面的问题。
1.消息发送方执行本地事务与发送消息的原子性问题,也就是说如何保证本地事务执行成功,消息一定发送成功
RocketMQ中的Broker 与 发送方具备双向通信能力,使得 broker 天生可以作为一个事务协调者存在;并且RocketMQ 本身提供了存储机制,使得事务消息可以持久化保存;这些优秀的设计可以保证即使发生了异常,RocketMQ依然能够保证达成事务的最终一致性。
- 发送方发送一个事务消息给Broker,RocketMQ会将消息状态标记为“Prepared”,此时这条消息暂时不能被接收方消费。这样的消息称之为Half Message,即半消息。
- Broker返回发送成功给发送方
- 发送方执行本地事务,例如操作数据库
- 若本地事务执行成功,发送commit消息给Broker,RocketMQ会将消息状态标记为“可消费”,此时这条消息就可以被接收方消费;若本地事务执行失败,发送rollback消息给Broker,RocketMQ将删除该消息。
- 如果发送方在本地事务过程中,出现服务挂掉,网络闪断或者超时,那Broker将无法收到确认结果
- 此时RocketMQ将会不停的询问发送方来获取本地事务的执行状态(即事务回查)
- 根据事务回查的结果来决定Commit或Rollback,这样就保证了消息发送与本地事务同时成功或同时失败。
以上主干流程已由RocketMQ实现,对于我们来说只需要分别实现本地事务执行的方法以及本地事务回查的方法即可,具体来说就是实现下面这个接口:
public interface TransactionListener {
/**
- 发送prepare消息成功后回调该方法用于执行本地事务
- @param msg 回传的消息,利用transactionId即可获取到该消息的唯一Id
- @param arg 调用send方法时传递的参数,当send时候若有额外的参数可以传递到send方法中,这里能获取到
- @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:未知,需要回查
*/
LocalTransactionState executeLocalTransaction(final Message msg, final Object arg);
/**
- @param msg 通过获取transactionId来判断这条消息的本地事务执行状态
- @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:未知,需要回查
*/
LocalTransactionState checkLocalTransaction(final MessageExt msg);
}
2.消息接收方接收消息与本地事务的原子性问题,也就是说如何保证接收消息成功后,本地事务一定执行成功
如果是出现了异常,RocketMQ会通过重试机制,每隔一段时间消费消息,然后再执行本地事务;如果是超时,RocketMQ就会无限制的消费消息,不断的去执行本地事务,直到成功为止。
3、牛刀小试
环境要求
- 数据库:MySQL-5.7+
- JDK:64位 jdk1.8+
- 微服务:spring-boot-2.1.3、spring-cloud-Greenwich.RELEASE
- RocketMQ服务端:RocketMQ-4.5.0
- RocketMQ客户端:RocketMQ-spring-boot-starter.2.0.2-RELEASE
创建数据库
本案例需要两个数据库,一个是bank1,一个是bank2,无需创建,直接使用 Hmily 快速入门案例中的数据库即可。另外,为了实现幂等性,需要分别在bank1、bank2数据库中新增de_duplication表,即交易记录表(去重表)。
DROP TABLE IF EXISTS `de_duplication`;
CREATE TABLE `de_duplication` (
`tx_no` bigint(20) 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
启动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
Maven工程
创建两个maven工程,分别连接不同的数据库
功能实现
消息发送方bank1
- 定义一个类封装转账消息:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class AccountChangeEvent implements Serializable {
/**
* 账号
*/
private String accountNo;
/**
* 变动金额
*/
private double amount;
/**
* 事务号,时间戳
*/
private long txNo;
}
- 实现数据访问层,一共四个功能
@Mapper
@Component
public interface AccountInfoDao {
/**
* 修改某账号的余额
* @param accountNo 账号
* @param amount 变动金额
* @return
*/
@Update("update account_info set account_balance=account_balance+#{amount} where account_no=#{accountNo}")
int updateAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Double amount);
/**
* 查询某账号信息
* @param accountNo 账号
* @return
*/
@Select("select * from account_info where where account_no=#{accountNo}")
AccountInfo findByIdAccountNo(@Param("accountNo") String accountNo);
/**
* 查询某事务记录是否已执行
* @param txNo 事务编号
* @return
*/
@Select("select count(1) from de_duplication where tx_no = #{txNo}")
int isExistTx(long txNo);
/**
* 保存某事务执行记录
* @param txNo 事务编号
* @return
*/
@Insert("insert into de_duplication values(#{txNo},now());")
int addTx(long txNo);
}
- 实现发送转账消息
@Component
@Slf4j
public class BankMessageProducer {
@Resource
private RocketMQTemplate rocketMQTemplate;
public void sendAccountChangeEvent(AccountChangeEvent accountChangeEvent) {
// 1.构造消息
JSONObject object = new JSONObject();
object.put("accountChange", accountChangeEvent);
Message<String> msg = MessageBuilder.withPayload(object.toJSONString()).build();
// 2.发送消息
rocketMQTemplate.sendMessageInTransaction("producer_ensure_transfer",
"topic_ensure_transfer",
msg, null);
}
}
- 实现业务层代码,分别实现了发送事务消息与本地事务扣减金额,注意doUpdateAccountBalance的本地事务若执行成功,就会在交易记录去重表(de_duplication)保存数据。
public interface AccountInfoService {
/**
* 更新帐号余额-发送消息
* @param accountChange
*/
void updateAccountBalance(AccountChangeEvent accountChange);
/**
* 更新帐号余额-本地事务
* @param accountChange
*/
void doUpdateAccountBalance(AccountChangeEvent accountChange);
}
@Service
@Slf4j
public class AccountInfoServiceImpl implements AccountInfoService {
@Autowired
private BankMessageProducer bankMessageProducer;
@Autowired
private AccountInfoDao accountInfoDao;
/**
* 更新帐号余额-发送通知
* @param accountChange
*/
@Override
public void updateAccountBalance(AccountChangeEvent accountChange) {
bankMessageProducer.sendAccountChangeEvent(accountChange);
}
/**
* 更新帐号余额-本地事务
* @param accountChange
*/
@Override
@Transactional(isolation = Isolation.SERIALIZABLE)
public void doUpdateAccountBalance(AccountChangeEvent accountChange) {
accountInfoDao.updateAccountBalance(accountChange.getAccountNo(),accountChange.getAmount() * -1);
accountInfoDao.addTx(accountChange.getTxNo());
}
}
- 实现RocketMQ事务消息监听器,其中有两个功能:
(1)executeLocalTransaction,该方法执行本地事务,会被RocketMQ自动调用
(2)checkLocalTransaction,该方法实现事务回查,利用了交易记录去重表(de_duplication),会被RocketMQ自动调用
@Component
@Slf4j
@RocketMQTransactionListener(txProducerGroup = "producer_ensure_transfer")
public class TransferTransactionListenerImpl implements RocketMQLocalTransactionListener {
@Autowired
private AccountInfoService accountInfoService;
@Autowired
private AccountInfoDao accountInfoDao;
/**
* 执行本地事务
* @param msg
* @param arg
* @return
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
//1.接收并解析消息
final JSONObject jsonObject = JSON.parseObject(new String((byte[])
msg.getPayload()));
AccountChangeEvent accountChangeEvent =
JSONObject.parseObject(jsonObject.getString("accountChange"),AccountChangeEvent.
class);
//2.执行本地事务
Boolean isCommit = true;
try {
accountInfoService.doUpdateAccountBalance(accountChangeEvent);
}catch (Exception e){
isCommit = false;
}
//3.返回执行结果
if(isCommit){
return RocketMQLocalTransactionState.COMMIT;
}else {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
* 事务回查
* @param msg
* @return
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
//1.接收并解析消息
final JSONObject jsonObject = JSON.parseObject(new String((byte[])
msg.getPayload()));
AccountChangeEvent accountChangeEvent =
JSONObject.parseObject(jsonObject.getString("accountChange"),AccountChangeEvent.
class);
//2.查询de_duplication表
int isExistTx = accountInfoDao.isExistTx(accountChangeEvent.getTxNo());
//3.根据查询结果返回值
if(isExistTx>0){
return RocketMQLocalTransactionState.COMMIT;
}else {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
}
- 完善Controller代码
@RestController
@Slf4j
public class AccountInfoController {
@Autowired
private AccountInfoService accountInfoService;
@GetMapping(value = "/transfer")
public String transfer(){
accountInfoService.updateAccountBalance(new AccountChangeEvent("1",100,System.currentTimeMillis()));
return "转账成功";
}
}
消息接收方bank2
- 实现数据访问层,和bank1一样,可以直接拿来用
- 实现业务层功能, 增加账号余额,注意这里使用了交易记录去重表(de_duplication)实现幂等性控制
public interface AccountInfoService {
/**
* 更新帐号余额
* @param accountChange
*/
void updateAccountBalance(AccountChangeEvent accountChange);
}
@Service
@Slf4j
public class AccountInfoServiceImpl implements AccountInfoService {
@Autowired
private AccountInfoDao accountInfoDao;
@Override
@Transactional(isolation = Isolation.SERIALIZABLE)
public void updateAccountBalance(AccountChangeEvent accountChange) {
int isExistTx = accountInfoDao.isExistTx(accountChange.getTxNo());
if(isExistTx == 0){
accountInfoDao.updateAccountBalance(accountChange.getAccountNo(),accountChange.getAmount());
accountInfoDao.addTx(accountChange.getTxNo());
}
}
}
- 实现RocketMQ事务消息监听器, 收到消息后,解析消息,调用业务层进行处理
@Component
@RocketMQMessageListener(topic = "topic_ensure_transfer", consumerGroup = "consumer_ensure_transfer")
@Slf4j
public class EnsureMessageConsumer implements RocketMQListener<String>{
@Autowired
private AccountInfoService accountInfoService;
@Override
public void onMessage(String projectStr) {
System.out.println("开始消费消息:" + projectStr);
final JSONObject jsonObject = JSON.parseObject(projectStr);
AccountChangeEvent accountChangeEvent = JSONObject.parseObject(jsonObject.getString("accountChange"),AccountChangeEvent.class);
accountChangeEvent.setAccountNo("2");
accountInfoService.updateAccountBalance(accountChangeEvent);
}
}
功能测试
- bank1和bank2都成功
- bank1执行本地事务失败,则bank2接收不到转账消息。
- bank1执行完本地事务后,不返回任何信息,则Broker会进行事务回查。
- bank2执行本地事务失败,会进行重试消费。
可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入该机制后,同步的事务操作变为基于消息执行的异步操作, 避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。
-------------------------- 文章内容出自hmB站课程,学习使用 --------------------------