项目场景:
单元测试shardingsphere-jdbc的分布式事务碰到的问题集合
问题描述
最近在学习shardingsphere,没想到刚开始就给我绊一跟头,在这里集中记录一下所遇到的问题,引以为戒的同时,希望给其他遇到相同问题的小伙伴一个解决思路。
基础准备
言归正传,最开始的时候,按照网上一些教程,我做了如下准备:
两个测试库:db2022和sakila
两张测试表:position、position_detail
由于想要测试分库分表下的分布式事务,所以两个库都有position、position_detail表
表建好了,接下来就是配置:
首先是properties文件的配置:
spring.shardingsphere.datasource.names=ds0,ds1
spring.shardingsphere.datasource.ds0.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds0.jdbc-url=jdbc:mysql://localhost:3306/db2022?useSSL=false
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=root
spring.shardingsphere.datasource.ds1.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.ds1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.ds1.jdbc-url=jdbc:mysql://localhost:3306/sakila?useSSL=false
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=root
# 分片策略:database-strategy按数据库分片 以什么方式去查询:行表达式inline
spring.shardingsphere.sharding.tables.position.database-strategy.inline.sharding-column=id
# 路由规则:按主键mod2的值路由
spring.shardingsphere.sharding.tables.position.database-strategy.inline.algorithm-expression=ds${id % 2}
spring.shardingsphere.sharding.tables.position_detail.database-strategy.inline.sharding-column=pid
spring.shardingsphere.sharding.tables.position_detail.database-strategy.inline.algorithm-expression=ds${pid % 2}
接下来是写一个启动类,因为测试不需要前端参与,不需要以应用的方式一直运行,所以main方法可以省略,在test里进行指定启动类即可
@SpringBootApplication
@EnableTransactionManagement
public class RunBoot {
}
然后是基于JPA的dao层代码和entity层代码,这里就不赘述了
分布式事务的部分
先写一个test类:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {RunBoot.class})
public class TestShardingTransaction {
@Resource
private PositionRepository positionRepository;
@Resource
private PositionDetailRepository positionDetailRepository;
@Test
@Transactional
public void test1(){
for (int i = 1; i <= 3; i++) {
Position position = new Position();
position.setName("root" + i);
position.setSalary("1000000");
position.setCity("beijing");
positionRepository.save(position);
// if (i == 3) {
// throw new RuntimeException("人为制造异常");
// }
PositionDetail positionDetail = new PositionDetail();
positionDetail.setPid(position.getId());
positionDetail.setDescription("this is a root " + i);
positionDetailRepository.save(positionDetail);
}
}
}
然后引入shardingsphere的依赖
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>${shardingsphere.version}</version>
</dependency>
和XA的依赖
<!--XA模式 -->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-transaction-xa-core</artifactId>
<version>${shardingsphere.version}</version>
</dependency>
<!--Saga模式-->
<dependency>
<groupId>io.shardingsphere</groupId>
<artifactId>sharding-transaction-base-saga</artifactId>
<version>${shardingsphere-spi-impl.version}</version>
</dependency>
<!--Seata模式-->
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-transaction-base-seata-at</artifactId>
<version>${shardingsphere.version}</version>
</dependency>
当然,我们这次用到的只有XA模式
什么都不指定的情况下,正常插入数据库没有问题,然后我们把中间的异常代码放开
再次运行结果如图:
结果是符合预期的,没有配置的情况下,无法实现分布式事务的管理,此时的事务是失效的,只有最后抛出异常的那条数据没有插入,其余的数据都成功插入了数据库
XA模式的配置
测试代码形式的配置:
TransactionTypeHolder.set(TransactionType.XA);
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {RunBoot.class})
public class TestShardingTransaction {
@Resource
private PositionRepository positionRepository;
@Resource
private PositionDetailRepository positionDetailRepository;
@Resource
private TestService testService;
@Test
@Transactional
public void test1(){
// 指定事务模式为XA
TransactionTypeHolder.set(TransactionType.XA);
for (int i = 1; i <= 3; i++) {
Position position = new Position();
position.setName("root" + i);
position.setSalary("1000000");
position.setCity("beijing");
positionRepository.save(position);
if (i == 3) {
throw new RuntimeException("人为制造异常");
}
PositionDetail positionDetail = new PositionDetail();
positionDetail.setPid(position.getId());
positionDetail.setDescription("this is a root " + i);
positionDetailRepository.save(positionDetail);
}
}
}
可以看到,只有db2022数据库的数据回滚了,sakila的数据没有回滚
原因分析:
首先注意到,两个数据库的事务在结果上是独立的,因为最终报错的那条数据会写入db2022,所以db2022的事务回滚了,而写入sakila数据库的sql中并不需要回滚,所以成功写入,说明分布式事务是失效的,但是单库的事务是成功的
解决方案:
查询官方文档,发现需要配置事务管理器,于是新增以下配置类
@Configuration
@EnableTransactionManagement
public class TransactionConfiguration {
@Bean
public PlatformTransactionManager txManager(final DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public JdbcTemplate jdbcTemplate(final DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
然后就行了?当然不会这么顺利,控制台报错
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'transactionManager' available: No matching TransactionManager bean found for qualifier 'transactionManager' - neither qualifier match nor bean name match!
意思是我们的事务管理器需要命名为transactionManager
于是配置代码改成如下:
@Configuration
@EnableTransactionManagement
public class TransactionConfiguration {
@Bean(name = "transactionManager")
public PlatformTransactionManager txManager(final DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public JdbcTemplate jdbcTemplate(final DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
这次结果是正常的,两边的数据都回滚了
但是当我想要测试一下,正常插入有没有问题的时候,人傻眼了,控制台表示插入正常,但是两个数据库一条数据都没插进去。
此时控制台打印有这么条语句:
Rolled back transaction for test: [DefaultTestContext@79e2c065 testClass = TestShardingTransaction, testInstance = dao.TestShardingTransaction@1a482e36, testMethod = test1@TestShardingTransaction, testException = [null], mergedContextConfiguration = [MergedContextConfiguration@3a93b025 testClass = TestShardingTransaction, locations = '{}', classes = '{class com.lagou.RunBoot, class com.lagou.RunBoot}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@52f759d7, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@3ecd23d9, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@18bf3d14, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@4cf4d528], contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map[[empty]]]
查了一下资料,发现事务生效的情况下,Junit测试不会真正向数据库插入数据,每次插入数据都会自动回滚掉
于是加上@Rollback(false)注解,禁用自动回滚,最终结果正常
XA以外的分布式事务貌似原生的包不支持,还要下点别的东西,如果小伙伴们想用的话就自己研究研究