背景
前端时间公司的Oracle项目改造发觉需要用到分布式事务,顾继续回顾加强学习一下。
分布式事务
事务
要说分布式事务,首先还是从事务的基本特征说起。
- A 原子性 :在事务的执行过程中,要么全部执行成功,要么都不成功。
- C 一致性 :事务在执行前后,不能破坏数据的完整性。一致性更多的说的是通过 AID 来达到目的,数据应该符合预先的定义和约束,由应用层面来保证,还有的说法是 C 是强行为了 ACID 凑出来的。
- I 隔离性:多个事务之间是互相隔离的,事务之间不能互相干扰,涉及到不同事务的隔离级别的问题。
- D 持久性:一旦事务提交,数据库中数据的状态就应该是永久性的。.
分布式事务产生的原因
- 服务SOA化
- 数据库分库分表
XA
XA(eXtended Architecture)是指由 X/Open 组织提出的分布式事务处理的规范,他是一个规范或者说是协议,定义了事务管理器 TM(Transaction Manager),资源管理器RM(Resource Manager),和应用程序。
事务管理器 TM 就是事务的协调者,资源管理器 RM 可以认为就是一个数据库。
java代码:
A factory for XAConnection objects that is used internally. An object that implements the XADataSource interface is typically registered with a naming service that uses the Java Naming and Directory Interface™ (JNDI).
An implementation of XADataSource must include a public no-arg constructor.
Since:
1.4
public interface XADataSource extends CommonDataSource {
。。。
}
代码规范http://www.nssi.org.cn/nssi/front/3650570.html
XA与JTA
XA : XA是一个规范或是一个事务的协议.XA协议由Tuxedo首先提出的,并交给X/Open组织,作为资源管理器(数据库)与事务管理器的接口标准.
JTA(Java Transaction Manager) : 是Java规范,是XA在Java上的实现
- TransactionManager : 常用方法,可以开启,回滚,获取事务. begin(),rollback()…
- XAResouce : 资源管理,通过Session来进行事务管理,commit(xid)…
- XID : 每一个事务都分配一个特定的XID
实现例子
- 相关的依赖,spring-boot-starter-jta-atomikos 依赖,这是一个开源的事务管理器类
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
</dependencies>
- 定义数据源
/**
* Created by zhangjunwei on 2017/8/2.
*/
@Configuration
public class DataSourceConfig {
/**
* db1的 XA datasource
*
* @return
*/
@Bean(name = "symbolOrder")
@Primary
@Qualifier("symbolOrder")
public AtomikosDataSourceBean symbolOrderBean() {
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setUniqueResourceName("symbolOrder");
atomikosDataSourceBean.setXaDataSourceClassName(
"com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
Properties properties = new Properties();
properties.put("URL","jdbc:mysql://localhost:3306/datamanage");
properties.put("user", "root");
properties.put("password", "123456");
atomikosDataSourceBean.setXaProperties(properties);
return atomikosDataSourceBean;
}
/**
* db2的 XA datasource
*
* @return
*/
@Bean(name = "symbolPosition")
@Qualifier("symbolPosition")
public AtomikosDataSourceBean symbolPositionDataSourceBean() {
AtomikosDataSourceBean atomikosDataSourceBean = new AtomikosDataSourceBean();
atomikosDataSourceBean.setUniqueResourceName("symbolPosition");
atomikosDataSourceBean.setXaDataSourceClassName(
"com.mysql.jdbc.jdbc2.optional.MysqlXADataSource");
Properties properties = new Properties();
properties.put("URL", "jdbc:mysql://localhost:3306/symbol_position");
properties.put("user", "root");
properties.put("password", "123456");
atomikosDataSourceBean.setXaProperties(properties);
return atomikosDataSourceBean;
}
/**
* transaction manager
*
* @return
*/
@Bean(destroyMethod = "close", initMethod = "init")
public UserTransactionManager userTransactionManager() {
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(true);
return userTransactionManager;
}
/**
* jta transactionManager
*
* @return
*/
@Bean
public JtaTransactionManager transactionManager() {
JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
jtaTransactionManager.setTransactionManager(userTransactionManager());
return jtaTransactionManager;
}
}
//测试类
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = UserManageStart.class)
public class DataSouceTest {
@Autowired
@Qualifier("symbolOrder")
private AtomikosDataSourceBean symbolOrder;
@Autowired
@Qualifier("symbolPosition")
private AtomikosDataSourceBean symbolPosition;
@Transactional
@Test
public void test() {
//。。。
}
Jta特点
- JTA的特点就是能够支持多数据库事务同时事务管理,满足分布式系统中的数据的一致性.但是也有对应的弊端:
- 两阶段提交
- 事务时间太长,锁数据太长
- 低性能,低吞吐量
2PC
* XA 定义了规范,那么 2PC 和 3PC 就是它的具体实现方式。
* 2PC 叫做二阶段提交,分为投票阶段和执行阶段两个阶段。
-
投票阶段
TM 向所有的参与者发送 prepare 请求,询问是否可以执行事务,等待各个参与者的响应。
这个阶段可以认为只是执行了事务的 SQL 语句,但是还没有提交。
如果都执行成功了就返回 YES,否则返回 NO。
-
执行阶段
执行阶段就是真正的事务提交的阶段,但是要考虑到失败的情况。
如果所有的参与者都返回 YES,那么就执行发送 commit 命令,参与者收到之后执行提交事务。
反之,只要有任意一个参与者返回的是 NO 的话,就发送 rollback 命令,然后执行回滚的操作。
-
2PC 的缺陷
-
同步阻塞,可以看到,在执行事务的过程当中,所有数据库的资源都被锁定,如果这时候有其他人来访问这些资源,将会被阻塞,这是一个很大的性能问题。
-
TM 单点问题,只有一个 TM,一旦 TM 宕机,那么整个流程无法继续完成。
-
数据不一致,如果在执行阶段,参与者脑裂或者其他故障导致没有收到 commit 请求,部分提交事务,部分未提交,那么数据不一致的问题就产生了。
3PC
既然 2PC 有这么多问题,所以就衍生出了 3PC 的概念,也叫做三阶段提交,他把整个流程分成了 CanCommit、PreCommit、DoCommit 三个步骤,相比 2PC,增加的就是 CanCommit 阶段。
- CanCommit
这个阶段就是先询问数据库是否执行事务,发送一个 canCommit 的请求去询问,如果可以的话就返回 YES,反之返回 NO。
但是,这个地方的区别在于参与者有了超时机制,如果参与者超时未收到 doCommit 命令的话,将会默认去提交事务。 - DoCommit
DoCommit 阶段对应到2PC的执行阶段,如果上一个阶段都是收到 YES 的话,那么就发送 doCommit 命令去提交事务,反之则会发送 abort 命令去中断事务的执行。
- 相比 2PC 的改进
- 对于 2PC 的同步阻塞的问题,我们可以看到因为 3PC 加入了参与者的超时机制,所以原来 2PC 的如果某个参与者故障导致的同步阻塞的问题时间缩短了,这是一个优化,但是并没有完全避免。
- 第二个单点故障的问题,同样因为超时机制的引入,一定程度上也算是优化了。但是数据不一致的问题,这个始终没有得到解决。
- 举个栗子:
在 PreCommit 阶段,某个参与者发生脑裂,无法收到 TM 的请求,这时候其他参与者执行 abort 事务回滚,而脑 裂的参与者超时之后继续提交事务,还是有可能发生数据不一致的问题。 那么,为什么要加入 DoCommit 这个阶段呢?就是为了引入超时机制,事先我们先确认数据库是否都可以执行事务,如果都 OK,那么才会进入后面的步骤,所以既然都可以执行,那么超时之后说明发生了问题,就自动提交事务。
TCC
TCC 的模式叫做 Try、Confirm、Cancel,实际上也就是 2PC 的一个变种而已。
实现这个模式,一个事务的接口需要拆分成3个,也就是 Try 预占、Confirm 确认提交、最后Cancel回滚。
对于 TCC 来说,实际生产我基本上就没看见过有人用,考虑到原因,首先是程序员的本身素质参差不齐,多个团队协作你很难去约束别人按照你的规则来实现,另外一点就是太过于复杂。
SAGA
Saga 源于1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的如何处理 long lived transaction(长活事务)论文。
主要思想就是将长事务拆分成多个本地短事务。
如果全部执行成功,就正常完成了,反之,则会按照相反的顺序依次调用补偿。
SAGA模式有两种恢复策略:
- 向前恢复,这个模式偏向于一定要成功的场景,失败则会进行重试。
- 向后恢复,也就是发生异常的子事务依次回滚补偿。
消息队列
基于消息队列来实现最终一致性的方案,一般来说有两种方式,基于本地消息表和依赖 MQ 本身的事务消息。
本地消息表的这个方案其实更复杂,实际上也没看到过真正谁来用。这里以 RocketMQ 的事务消息来举例,这个方式相比本地消息表则更完全依赖MQ本身的特性做了解耦,释放了业务开发的复杂工作量。
- 业务发起方,调用远程接口,向 MQ 发送一条半事务消息,MQ 收到消息之后会返回给生产者一个 ACK。
- 生产者收到 ACK 之后,去执行事务,但是事务还没有提交。
- 生产者会根据事务的执行结果来决定发送 commit 提交或者 rollback 回滚到 MQ。
- 这一点是发生异常的情况,比如生产者宕机或者其他异常导致 MQ 长时间没有收到 commit 或者 rollback 的消息,这时候 MQ 会发起状态回查。
- MQ 如果收到的是 commit 的话就会去投递消息,消费者正常消费消息即可。如果是 rollback 的话,则会在设置的固定时间期限内去删除消息。
- 这个方案基于 MQ 来保证消息事务的最终一致性,还算是一个比较合理的解决方案,只要保证 MQ 的可靠性就可以正常实施应用,业务消费方根据本身的消息重试达到最终一致性。
主要代码实现:
- 实现
RocketMQLocalTransactionListener
@Component
@RocketMQTransactionListener(txProducerGroup = "rocket")
public class TransactionListener implements RocketMQLocalTransactionListener{
@Autowired
private PayLogService payLogService;
/****
* 当向RocketMQ的Broker发送Half消息成功之后,调用该方法
* @param msg:发送的消息
* @param arg:额外参数
* @return
*/
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
//========================本地事务控制===================
//消息
String result = new String((byte[]) msg.getPayload(),"UTF-8");
PayLog payLog = JSON.parseObject(result,PayLog.class);
payLogService.add(payLog);
//========================本地事务控制===================
} catch (Exception e) {
e.printStackTrace();
return RocketMQLocalTransactionState.ROLLBACK;
}
return RocketMQLocalTransactionState.COMMIT;
}
/***
* 超时回查
* @param msg
* @return
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
return RocketMQLocalTransactionState.COMMIT;
}
}
-
实现
RocketMQPushConsumerLifecycleListener
``
@Component
@RocketMQMessageListener(topic = “log”,consumerGroup = “resultgroup”)
public class OrderResultListener implements RocketMQListener,RocketMQPushConsumerLifecycleListener {
@Override
public void onMessage(Object message) {
}/****
- 消息监听
- @param consumer
*/
@Override
public void prepareStart(DefaultMQPushConsumer consumer) {
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) {
try {
for (MessageExt msg : msgs) {
String result = new String(msg.getBody(),“UTF-8”);
System.out.println(“result::::::”+result);
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
}
}
``
框架
GTS(Global Transaction Service 全局事务服务)是阿里云的中间件产品,只要你用阿里云,付钱就可以用 GTS。
Seata(Simple Extensible Autonomous Transaction Architecture)则是开源的分布式事务框架,提供了对 TCC、XA、Saga 以及 AT 模式的支持。(官网:https://seata.io/zh-cn/)
- AT 模式
- 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
- 二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
执行的流程如下:
- TM 向 TC 注册全局事务,获得 XID。
- RM 则会去代理 JDBC 数据源,生成镜像的 SQL,形成 UNDO_LOG,然后向 TC 注册分支事务,把数据更新和 UNDO_LOG 在本地事务中一起提交。
- TC 如果收到 commit 请求,则会异步去删除对应分支的 UNDO_LOG,如果是 rollback,就去查询对应分支的 UNDO_LOG,通过 UNDO_LOG 来执行回滚。
- seata实现步骤,网上Oracle相关的资料少,亲测demo可用
- 创建undo_log表
CREATE TABLE undo_log ( id NUMBER(19) NOT NULL, branch_id NUMBER(19) NOT NULL, xid VARCHAR2(100) NOT NULL, context VARCHAR2(128) NOT NULL, rollback_info BLOB NOT NULL, log_status NUMBER(10) NOT NULL, log_created TIMESTAMP(0) NOT NULL, log_modified TIMESTAMP(0) NOT NULL, PRIMARY KEY (id), CONSTRAINT ux_undo_log UNIQUE (xid, branch_id) );
- 启动seata服务
- 配置file.conf,registry.conf,和application.properties事务组,如:
properties spring.application.name=order-service server.port=8082 #spring.datasource.url=jdbc:mysql://localhost:3306/fescar?useSSL=false&serverTimezone=UTC #spring.datasource.username=root #spring.datasource.password=root spring.datasource.driver-class-name=oracle.jdbc.driver.OracleDriver spring.datasource.url=jdbc:oracle:thin:@47.107.253.254:1521/helowinXDB spring.datasource.username=system spring.datasource.password=system spring.cloud.alibaba.seata.tx-service-group=my_test_tx_group logging.level.org.springframework.cloud.alibaba.seata.web=debug logging.level.io.seata=debug eureka.instance.hostname=127.0.0.1 eureka.instance.prefer-ip-address=true eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:8761/eureka/ feign.hystrix.enabled=true spring.main.allow-bean-definition-overriding=true
- 需要用的代码打上
@GlobalTransactional
标签 - https://github.com/David0101/seata-samples.git
- 创建undo_log表