数据库层面上的分布式事务
XA事务:是DTP模型定义TM和RM之间通讯的接口规范。XA接口函数由数据库厂商提供。TM用它来通知数据库事务的开始、结束、提交、回滚。基于XA规范
衍生出下面的二阶段提交(2PC)、三阶段提交(3PC),sharding-jdbc和mycat都实现了XA事务
JTA:Java的事务API,需要第三方实现,目前JTA的实现有以下几种形式
- J2EE容器提供的JTA实现(Weblogic、JBoss );
- JOTM(Java Open Transaction Manager)、Atomikos,可独立于J2EE容器的环境下实现JTA
业务层面的事务解决方案
1、seata
角色划分:
- RM(ResourceManager 资源管理者)理解为 我们的一个一个的微服务 也叫做事务的参与者.
- TM(TranactionManager 事务管理者) 也是我们的一个微服务,但是该微服务是一个带头大哥,充当全局事务的发起者(决定了全局事务的开启,回滚,提交等),凡是我们的微服务中标注了@GlobalTransactional ,那么该微服务就会被看出一个TM。我们业务场景中订单微服务就是一个事务发起者,同时也是一个RM
- TC(全局事务的协调者):这里就是我们的Seata-server,用来保存全局事务,分支事务,全局锁等记录,然后会通知各个RM进行回滚或者提交.
AT模式:
当执行带有@GlobalTransactional注解的方法的时候,就会被认为是一个分布式事务,就会向TC(seata server)注册一个分布式全局事务,即往全局事务表中插入一条数据
执行分支逻辑,如:update product set name = 'GTS' where name = 'TXC';
一阶段:
- 获取前置镜像和后置镜像,生成插入undo日志表的sql,让插入undo日志表的sql和分支逻辑sql放在一起进行提交
- 在提交前,向TC(seata server)注册一个分支事务,即往分支事务表中插入有一条数据,还有要将分支逻辑sql受影响的数据添加到TC(seata server)的全局锁表中
- 提交之后将执行结果(成功和失败)上报给分支事务
二阶段-回滚:
收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
- 通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
- 数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
- 根据 UNDO LOG 中的前镜像和业务 SQL 的相关信息生成并执行回滚的语句:update product set name = 'TXC' where id = 1;
- 提交本地事务。并把本地事务的执行结果(即分支事务回滚的结果)上报给 TC。
二阶段-提交:
- 收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果
- 给 TC。异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录
缺点:隔离级别为读未提交,如果要实现可重复读,则select ... from for update,所以要保证数据强一致性,则性能会很慢
TCC模式:
根据两阶段行为模式的不同,我们将分支事务划分为 Automatic (Branch) Transaction Mode 和 Manual (Branch) Transaction Mode.
AT 模式(参考链接 TBD)基于 支持本地 ACID 事务 的 关系型数据库:
- 一阶段 prepare 行为:在本地事务中,一并提交业务数据更新和相应回滚日志记录。
- 二阶段 commit 行为:马上成功结束,自动 异步批量清理回滚日志。
- 二阶段 rollback 行为:通过回滚日志,自动 生成补偿操作,完成数据回滚。
相应的,TCC 模式,不依赖于底层数据资源的事务支持:
- 一阶段 prepare 行为:调用 自定义 的 prepare 逻辑。
- 二阶段 commit 行为:调用 自定义 的 commit 逻辑。
- 二阶段 rollback 行为:调用 自定义 的 rollback 逻辑。
所谓 TCC 模式,是指支持把 自定义 的分支事务纳入到全局事务的管理中。
2、消息队列rocketmq
事务消息模式:
TransactionListener transactionListener = new OrderTransactionListenerImpl();
//初始化生产者
TransactionMQProducer producer = new TransactionMQProducer("TxProducer");
producer.setTransactionListener(transactionListener);
producer.setNamesrvAddr("127.0.0.1:9876");
producer.setInstanceName("tx-instance");
producer.start();
try {
Object msg = new Object();
Message message = new Message("order-topic", JSON.toJSONString(msg).getBytes());
TransactionSendResult sendResult = producer.sendMessageInTransaction(message, null);
System.out.println(sendResult);
}
catch (Exception e) {
e.printStackTrace();
} finally {
producer.shutdown();
}
return false;
@SuppressWarnings("unused")
public class OrderTransactionListenerImpl implements TransactionListener {
private ConcurrentHashMap<String, Integer> countHashMap = new ConcurrentHashMap<>();
private final static int MAX_COUNT = 5;
@SneakyThrows
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
Connection connection = null;
connection.setAutoCommit(false);
try {
//执行本地业务逻辑
connection.commit();
}catch (Exception e){
connection.rollback();
return LocalTransactionState.UNKNOW;
}
return LocalTransactionState.COMMIT_MESSAGE;
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
try {
//回查成功,这里主要是本地事务运行成功了,但是生产者向broker发送事务确认由于网络原因,
// 没有发送成功,则broker中的消息状态一直是half状态,则等网络恢复后,提供broker调用的回查机制
}catch (Exception e){
return LocalTransactionState.UNKNOW;
}
return LocalTransactionState.COMMIT_MESSAGE;
}
}
rokectmq事务消息可以保证数据库写入和mq消息发送保持一致性
非事务消息模式:
Connection connection = null;
connection.setAutoCommit(false);
try {
//执行本地业务逻辑
//插入需要发送给mq的数据到t_message_transaction表中
connection.commit();
}catch (Exception e){
connection.rollback();
return LocalTransactionState.UNKNOW;
}
当本地事务执行成功,则插入需要发送给mq的数据到t_message_transaction表中,然后后台用一个定时任务,把t_message_transaction表中的数据发送给mq,注意:rocket mq必须要保证数据不被丢失,消费端必须一定要具备幂等性,否则会造成数据不一致