SpringBoot 使用多SqlSessionFactory下的事务问题

如下配置了两个数据源:

spring:
  datasource:
    ds1:
      jdbc-url: jdbc:mysql://localhost:3307/spring-boot-demos?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: password
    ds2:
      jdbc-url: jdbc:mysql://127.0.0.1:3308/spring-boot-demos?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: password

同时,配置了响应的数据源配置:

@Configuration
public class DataSourceConfigurations {

    @Configuration
    @MapperScan(basePackages = "com.example.mapper.ds1",
            sqlSessionFactoryRef = "ds1SqlSessionFactory")
    public static class Ds1Configuration {
        public static final String DS1_MAPPER_LOCATION = "classpath*:mapper/ds1/*.xml";


        @Primary
        @Bean("ds1DataSource")
        @ConfigurationProperties(prefix = "spring.datasource.ds1")
        public DataSource ds1DataSource() {
            return DataSourceBuilder.create().build();
        }

        @Primary
        @Bean("ds1TransactionManager")
        public PlatformTransactionManager ds1TransactionManager(@Qualifier("ds1DataSource") DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }

        @Primary
        @Bean(name = "ds1SqlSessionFactory")
        public SqlSessionFactory ds1SqlSessionFactory(@Qualifier("ds1DataSource") DataSource dataSource) throws Exception {
            MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean();
            sessionFactoryBean.setDataSource(dataSource);
            sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(DS1_MAPPER_LOCATION));
            return sessionFactoryBean.getObject();
        }
    }

    @Configuration
    @MapperScan(basePackages = "com.example.ds2",
            sqlSessionFactoryRef = "ds2SqlSessionFactory")
    public static class Ds2Configuration {
        public static final String DS2_MAPPER_LOCATION = "classpath*:mapper/ds2/*.xml";

        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.ds2")
        public DataSource ds2DataSource() {
            return DataSourceBuilder.create().build();
        }

        @Bean
        public PlatformTransactionManager ds2TransactionManager(@Qualifier("ds2DataSource") DataSource dataSource) {
            return new DataSourceTransactionManager(dataSource);
        }

        @Bean(name = "ds2SqlSessionFactory")
        public SqlSessionFactory ds2SqlSessionFactory(@Qualifier("ds2DataSource") DataSource dataSource) throws Exception {
            MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean();
            sessionFactoryBean.setDataSource(dataSource);
            sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(DS2_MAPPER_LOCATION));
            return sessionFactoryBean.getObject();
        }

    }
}

相对应的,有两个Mapper:

@Data
@TableName("tb_test1")
public class Test1DO {

    @TableId
    private Integer id;
    private LocalDateTime createTime;
    private boolean deleted;
    private String name;
    private Short status;
}

@Mapper
public interface Ds1Test1Mapper extends BaseMapper<Test1DO> {

}

@Mapper
public interface Ds2Test1Mapper extends BaseMapper<Test1DO> {

}

首先,先向ds2数据库的tb_test1表中插入id=1的数据。然后做如下操作:

@RequiredArgsConstructor
@Service
public class Test1ServiceImpl implements ITest1Service {
    private final Ds1Test1Mapper ds1Test1Mapper;
    private final Ds2Test1Mapper ds2Test1Mapper;

    @Override
    @Transactional(rollbackFor = RuntimeException.class)
    public void save() {
        Test1DO test1DO = new Test1DO();
        test1DO.setId(1);
        test1DO.setName("test ds2");
        test1DO.setStatus((short) 1);
        ds1Test1Mapper.insert(test1DO);
        ds2Test1Mapper.insert(test1DO);
    }

    @Override
    public void save1() {
        Test1DO test1DO = new Test1DO();
        test1DO.setId(1);
        test1DO.setName("test ds2");
        test1DO.setStatus((short) 1);
        ds1Test1Mapper.insert(test1DO);
        ds2Test1Mapper.insert(test1DO);
    }
}

当调用save1()时,ds1中插入了一条数据,而ds2中由于id冲突而失败。
如果调用save()时,ds1和ds2中都插入失败(ds1的数要清掉).
这个时候对save()进行断点:

//TransactionAspectSupport.java 377行 (springboot版本2.7.17)
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);

发现ptm是上面配置的Primary TransactionManager。

如果将两个数据库表数据都清空,然后执行save(),对 commit 进行断点:

// DataSourceTransactionManager.doCommit()
protected void doCommit(DefaultTransactionStatus status) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
		Connection con = txObject.getConnectionHolder().getConnection();
		if (status.isDebug()) {
			logger.debug("Committing JDBC transaction on Connection [" + con + "]");
		}
		try {
			con.commit();
		}
		catch (SQLException ex) {
			throw translateException("JDBC commit", ex);
		}
	}

对 con 进行断点发现,其为3307的数据库的连接,然后整个过程没有别的连接了。并且3308的数据也能正常保存。

以上也就是两点:
1、当使用多个SqlSessionFactory配置数据源时,公用同一个TransactionManager,和同一个Connection(3307),这个时候,3308数据正常提交。
2、当提交3308的数据发生异常时,3307的数据也不会插入,即有事务属性。

目前对于上述两点不是很理解,为何只有3307的Connection,3308的数据也能提交,而且为何会有事务性。

可能是作者对SpringBoot的事务不是理解的很深入,希望有大佬能解惑。

你可能感兴趣的:(spring,boot,后端,java)