要实现SpringBoot+Mybatis+atomikos实现分布式事务(分包扫描),相对动态切换数据库来说简单很多。
1、引入依赖
2、配置文件
3、生成不同的数据库实例(bean)和相关工厂
1、引入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.19</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
2、配置文件
server:
port: 8025
spring:
application:
name: datasource_test
datasource:
master:
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.1:3306/citex_fusion?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=TRUE&serverTimezone=UTC&useSSL=true
slave1:
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.1:3306/citex_guess?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=TRUE&serverTimezone=UTC&useSSL=true
druid:
initial-size: 5
max-active: 20
min-idle: 5
test-on-borrow: true
max-wait: -1
min-evictable-idle-time-millis: 30000
max-evictable-idle-time-millis: 30000
time-between-eviction-runs-millis: 0
mybatis:
configuration:
map-underscore-to-camel-case: true
3、生成不同的数据库实例(bean)和相关工厂
扫描不同包,生成不同的数据库相关(bean)
工具类
public class PropertiesUtils {
/**
* 读取数据库配置
*
* @param env
* @param prefix
* @return
*/
public static Properties build(Environment env, String prefix) {
String druidStr = "spring.datasource.druid.";
Properties prop = new Properties();
prop.put("username", env.getProperty(prefix + "username"));
prop.put("password", env.getProperty(prefix + "password"));
prop.put("url", env.getProperty(prefix + "jdbc-url"));
prop.put("driverClassName", env.getProperty(prefix + "driver-class-name", ""));
prop.put("initialSize", env.getProperty(druidStr + "initial-size", Integer.class));
prop.put("maxActive", env.getProperty(druidStr + "max-active", Integer.class));
prop.put("minIdle", env.getProperty(druidStr + "min-idle", Integer.class));
prop.put("maxWait", env.getProperty(druidStr + "max-wait", Integer.class));
prop.put("testOnBorrow", env.getProperty(druidStr + "test-on-borrow", Boolean.class));
return prop;
}
}
主数据库的配置
@Configuration
@MapperScan(basePackages = "com.hongyu.mapper.fusion", sqlSessionFactoryRef = "masterSqlSessionFactory")
public class MasterDataSourceConfig {
@Primary
@Bean(name = "master")
public DataSource masterDataSource(Environment env) {
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
Properties prop = PropertiesUtils.build(env, "spring.datasource.master.");
ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
ds.setUniqueResourceName("master");
ds.setXaProperties(prop);
return ds;
}
@Primary
@Bean(name = "masterSqlSessionFactory")
public SqlSessionFactory testSqlSessionFactory(@Qualifier("master") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean.getObject();
}
@Primary
@Bean(name = "masterSqlSessionTemplate")
public SqlSessionTemplate testSqlSessionTemplate(
@Qualifier("masterSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
从数据库配置
@Configuration
@MapperScan(basePackages = "com.hongyu.mapper.guess", sqlSessionFactoryRef = "slaveOneSqlSessionFactory")
public class SlaveOneDataSourceConfig {
@Primary
@Bean(name = "slaveOne")
public DataSource masterDataSource(Environment env) {
AtomikosDataSourceBean ds = new AtomikosDataSourceBean();
Properties prop = PropertiesUtils.build(env, "spring.datasource.slave1.");
ds.setXaDataSourceClassName("com.alibaba.druid.pool.xa.DruidXADataSource");
ds.setUniqueResourceName("slaveOne");
ds.setXaProperties(prop);
return ds;
}
@Primary
@Bean(name = "slaveOneSqlSessionFactory")
public SqlSessionFactory testSqlSessionFactory(@Qualifier("slaveOne") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
return bean.getObject();
}
@Primary
@Bean(name = "slaveOneSqlSessionTemplate")
public SqlSessionTemplate testSqlSessionTemplate(
@Qualifier("slaveOneSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
下面我们编写测试的例子
public interface FusionMapper {
@Select("select * from fusion_invitee limit 10 ")
List<FusionInvite> getFusionInviteList();
@Insert("INSERT INTO fusion_invitee(user_id, invitee_id, code, create_time, update_time)\n"
+ "VALUES (50018, 20597, 'FV3NEA', NOW(), NOW())")
int insertfusionInvite(FusionInvite fusionInvite);
}
public interface GuessMapper {
@Select("select * from guess_account limit 10")
List<GuessAccount> getGuessAccountList();
@Insert("INSERT INTO guess_account(USER_ID, CURRENCY_ID, CURRENCY, AVAILABLE_QTY, FROZEN_QTY, CREATE_TIME, UPDATE_TIME) VALUES "
+ "(27491, 3, 'USDT', 0.007263000000000000, 0.000000000000000000, NOW(), NOW())")
int insertguessAccount(GuessAccount guessAccount);
}
@Transactional
public void testJtaTransactional() {
GuessAccount guessAccount = new GuessAccount();
int row = guessMapper.insertguessAccount(guessAccount);
FusionInvite fusionInvite = new FusionInvite();
int row2 = fusionMapper.insertfusionInvite(fusionInvite);
int i = 1 / 0;
}
特别说明,如果没有进行第7步的改造的话,会出现无法切换数据源的情况,是由于我们使用了 @Transactional注解。
为了保证事物的一致性,它需要保证同一个线程的数据库执行Connection和事物执行的Connection必须保持一致,因此去调用下一个Mapper时仍然保持了上一个Mapper的连接。所以就报错了。
需要解决这个问题,就需要实现事物中的Connection动态切换。这样我们两段提交协议才能生效。
在启动了上面必须要排除SpringBoot自带数据库配置加载
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableTransactionManagement
@MapperScan(basePackages = { "com.hongyu.mapper" })