一、资源管理器(RM)和事务管理器(TM )
- 资源管理器(resourceManager):管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
这个具体来说就是指数据库 - 事务管理器(transactionManager):定义全局事务的范围:开始全局事务、提交或回滚全局事务。这个就是指全局事务,可以操作多个资源管理器,进行协调所有资源完成数据一致。
(注:来源:《凤凰架构》)
二、tomcat配置数据源,使用全局事务框架进行多数据源分布式事务
代码地址:https://gitee.com/ClumsyBird/learn-demo/tree/master/jta/demo1/demo1
- 最终效果:
1.1 tomcat的context.xml配置
1.2 spring配置atomikos的全局事务
1.3 jndi配置加载数据源
1.4 测试类
两个数据源在一个事务里,第一次插入id=6都插入进去;删除1库的id=6数据,再次执行,2库由于有id=6的数据报主键冲突,查看1库也进行回滚没有执行插入。 - tomcat配置普通的数据源,并且集成jdbcTemplate
2.1 首先在tomcat的context.xml文件中配置:
<Resource name="jdbc_db1"
auth="Container"
type="javax.sql.DataSource"
driverClassName="com.mysql.cj.jdbc.Driver"
url="jdbc:mysql://localhost:8023/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false"
username="root"
password="root"
maxActive="500"
maxIdle="500"
maxWait="36000"
factory="com.alibaba.druid.pool.DruidDataSourceFactory"
/>
<Resource name="jdbc_db2"
auth="Container"
type="javax.sql.DataSource"
driverClassName="com.mysql.cj.jdbc.Driver"
url="jdbc:mysql://localhost:8023/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true"
username="root"
password="root"
maxActive="30"
maxIdle="20"
maxWait="36000"
factory="com.alibaba.druid.pool.DruidDataSourceFactory"
/>
说明:
2.1.1 此处使用的druid连接池,故而需要把druid的jar包以及mysql的jar包都放到tomcat的lib目录下,否则tomcat是找不到类报错的
2.1.2 在xml中 &符号要使用 & 转义字符进行拼接,否则会报错
2.1.3 是在节点下,弄错了不好使
2.1.4 链接参数要配置准确,不然报错也不太准确,可以先用jdbc试一下,再把链接参数放到这里
- 集成全局事务框架,进行多数据源事务配置(此处使用的atomikos)
3.1 先测试tomcat的配置能否正常使用:
public void testJNDI2() throws Exception{
Context ctx = new InitialContext();
DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc_db1");
Connection conn = ds.getConnection();
System.out.println(conn);
}
这个代码运行后不报错就,正常获取conn就ok
3.2 spring的配置文件中加入:
<bean id="db1" name="db1" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="java:comp/env/jdbc_db1" />
<!-- <property name="resourceRef" value="true"/>-->
</bean>
<bean id="db2" name="db2" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="jdbc_db2" />
<property name="resourceRef" value="true"/>
</bean>
注:jndiName:第一步中所配置单独JNDI名称。
resourceRef:可以让我们指定jndiName时,省略前缀“java:comp/env/”。
坑:这里的JndiObjectFactoryBean不是这个bean的类型,这玩意儿的类型是tomcat的context.xml配置的工厂生成的datasource… 然而idea中提示的这个配置的类型是JndiObjectFactoryBean,而且自动生成的代码也是这个类型,接着就报错了,故而ref这个bean的时候要写datasource的类型,写JndiObjectFactoryBean类型就报错了。
3.3 这玩意是datasource类型,那么就可以直接塞到jdbctemplate
<bean id="jdbcTemplate1" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="db1" />
</bean>
<bean id="jdbcTemplate2" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="db2" />
</bean>
DruidDataSource db1 = applicationContextHelper.popBean("db1", DruidDataSource.class);
this.jdbcTemplate1 = new JdbcTemplate(db1);
DruidDataSource db2 = applicationContextHelper.popBean("db2", DruidDataSource.class);
this.jdbcTemplate2 = new JdbcTemplate(db2);
以上xml配置或者代码塞进去都可以,idea中xml配置db1、db2会飘红,是不受影响的
3.4 接着正常测试就可以了
String sql = "insert into test(id, name) values(6, '444')";
jdbcTemplate1.execute(sql); // 1库
jdbcTemplate2.execute(sql); // 2库
- 集成全局事务框架,进行多数据源事务配置(此处使用的atomikos)
4.1 pom文件
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions-jdbc</artifactId>
<version>5.0.9</version>
</dependency>
<dependency>
<groupId>com.atomikos</groupId>
<artifactId>transactions-jdbc</artifactId>
<version>5.0.9</version>
</dependency>
4.2 spring中配置分布式事务
<!-- 分布式事务 -->
<bean id="atomikosTransactionManager" class="com.atomikos.icatch.jta.UserTransactionManager" init-method="init" destroy-method="close">
<property name="forceShutdown" value="true"/>
</bean>
<bean id="atomikosUserTransaction" class="com.atomikos.icatch.jta.UserTransactionImp">
<property name="transactionTimeout" value="300"/>
</bean>
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManager" ref="atomikosTransactionManager"/>
<property name="userTransaction" ref="atomikosUserTransaction"/>
</bean>
<!--配置TX注解解析器-->
<tx:annotation-driven transaction-manager="transactionManager"/>
<mvc:annotation-driven></mvc:annotation-driven>
4.3 注解也加进去
<!--配置DI注解解析器-->
<context:annotation-config/>
<!--配置IoC注解解析器-->
<context:component-scan base-package="com"></context:component-scan>
<!-- 这个玩意是能够用类注入,默认是jdk的接口代理注入 -->
<aop:config proxy-target-class="true"></aop:config>
4.4 无论@Transactional还是硬编码UserTransaction userTransaction = transactionManager.getUserTransaction();此处配置完测试是失败的,两个库直接数据报异常后并没有在一个事务管理器里
- tomcat配置AXDataSource数据源,加入atomikosDataSourceBean
5.1 atomikosDataSourceBean中设置的是XADatasource类型,所以tomcat之中要先把生成dataSource转为生成XADataSource类型,最终的配置:
<Resource
name="jdbc_db1" factory="org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory"
type="com.mysql.cj.jdbc.MysqlXADataSource"
user="root"
password="root"
url="jdbc:mysql://localhost:8023/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true"
driverClassName="com.mysql.cj.jdbc.Driver"
/>
<Resource
name="jdbc_db2"
factory="org.apache.tomcat.jdbc.naming.GenericNamingResourcesFactory"
type="com.mysql.cj.jdbc.MysqlXADataSource"
user="root"
password="root"
url="jdbc:mysql://localhost:8024/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true"
driverClassName="com.mysql.cj.jdbc.Driver"
/>
注:
GenericNamingResourcesFactory是灵活依据类型生成的工厂,这个工厂可以生成DataSource、XADatasource或者其他;type中要配置com.mysql.cj.jdbc.MysqlXADataSource,配置javax.sql.XADataSource是不生效的;DataSource中配置的是username,而此处XADatasource配置的是user;链接字符串中要加上allowPublicKeyRetrieval,否则链接时候是报错的。
百度有些配置factory="com.atomikos.tomcat.EnhancedTomcatAtomikosBeanFactory"说是可以实现,但是没有找到这个jar是哪个。
5.2 测试XADatasource是否配置正确
需要先在mysql执行:GRANT XA_RECOVER_ADMIN ON *.* TO root@'%' ;
(否则会报错的)
public void testJNDI() throws Exception{
Context ctx = new InitialContext();
XADataSource ds = (XADataSource) ctx.lookup("java:comp/env/jdbc_db1");
XAConnection conn = ds.getXAConnection();
System.out.println(conn.getConnection());
}
运行完无报错,正常输入链接就ok了
5.3 spring配置文件配置atomikosDataSourceBean
<bean id="atomikosDataSourceBean1" class="com.atomikos.jdbc.AtomikosDataSourceBean">
<property name="xaDataSource" ref="db1" />
<property name="uniqueResourceName" value="db1" />
<property name="minPoolSize" value="5" />
<property name="maxPoolSize" value="5" />
</bean>
<bean id="jdbcTemplate1" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="atomikosDataSourceBean1" />
</bean>
<bean id="atomikosDataSourceBean2" class="com.atomikos.jdbc.AtomikosDataSourceBean">
<property name="xaDataSource" ref="db2" />
<property name="uniqueResourceName" value="db2" />
<property name="minPoolSize" value="5" />
<property name="maxPoolSize" value="5" />
</bean>
<bean id="jdbcTemplate2" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="atomikosDataSourceBean2" />
</bean>
5.4 测试全局事务
public void test4() throws Exception{
// GRANT XA_RECOVER_ADMIN ON *.* TO root@'%' ;
// Object db1 = applicationContextHelper.getApplicationContext().getBean("db1");
// XADataSource s= (XADataSource)db1;
// XAConnection conn = s.getXAConnection();
// System.out.println(conn.getConnection());
UserTransaction userTransaction = transactionManager.getUserTransaction();
userTransaction.begin();
try {
String sql = "insert into test(id, name) values(6, '444')";
jdbcTemplate1.execute(sql);
jdbcTemplate2.execute(sql);
userTransaction.commit();
} catch (Exception e) {
userTransaction.rollback();
e.printStackTrace();
}
}
@Transactional
public void test() throws Exception{
String sql = "insert into test(id, name) values(6, '444')";
jdbcTemplate1.execute(sql); // 1库
jdbcTemplate2.execute(sql); // 2库
}
无论使用userTransaction还是@Transactional注解都是可以达到正常全局事务预期;第一次插入两个库均有数据,删除1库的id=6数据,再次执行,2库会有主键冲突,发现1库的数据也未插入。
三、分布式两阶段三阶段的弊端
- 两阶段:
1.1 单点问题:协调者一旦宕机,所有参与者都会受到影响。如果协调者一直没 有恢复,没有正常发送 Commit 或者 Rollback 的指令,那所有参与者都必须一直等待。
1.2 性能问题:两段提交过程中, 要经过两次远程服务调用,三次数据持久化(准备阶段写重做日志,协调者做状态持久 化,提交阶段在日志写入 Commit Record),整个过程将持续到参与者集群中最慢的那一个处理操作结束为止,这决定了两段式提交的性能通常都较差。
1.3 一致性风险:两段式提交的成立是有前提条件的,当网络稳定性和宕机 恢复能力的假设不成立时,仍可能出现一致性问题。如果协调者在发出准备指令后,根据 收到各个参与者发回的信息确定事务状态是可以提交的,协调者会先持久化事务状态,提交自己的事务,如果这时候网络忽然被断开,无法再通过网络向所有参与者发出 C ommit 指令的话,就会导致部分数据(协调者的)已提交,但部分数据(参与者的)既 未提交,也没有办法回滚,产生了数据不一致的问题。 - 三阶段:
2.1 三段式提交对单点问题和回滚时的性能问题有所改善,因为新增了 CanCommit, 是一个询问阶段
2.2 在事务能够正常提交的场景中,三段式因为多了一次询问,还要比二段式稍微更差一些
2.3 一致性风险问题对比二段式并未有任何改进,在这方面它面临的风险甚至反而是略有增加了的。譬如, 进入 PreCommit 阶段之后,协调者发出的指令不是 Ack 而是 Abort,而此时因网络问题, 有部分参与者直至超时都未能收到协调者的 Abort 指令的话,这些参与者将会错误地提交 事务,这就产生了不同参与者之间数据不一致的问题
参考链接:
分布式事务两段提交原理
分布式事务
两段和三段式事务