目次
I.はじめに
ビジネスの量が増えると、データベースのパフォーマンスは最終的にボトルネックになります。そのため、ビジネスの一部をデータベースに分割する必要があります。同時に、Alibaba Cloudのデータなど、一部のクエリをパフォーマンスの高いデータベースに切り替える必要があります。ウェアハウス。データソースのシナリオ。もちろん、現時点では、異なるデータソース間のトランザクションの一貫性が特に重要です。この記事では、SpringBoot + Mybatisがマルチデータソースのトランザクションの一貫性を実現する小さな事例を記録します。
2つのマルチデータソーストランザクション
将来の参考のために記録する2つの方法があります
オプション1
主なアイデアは、さまざまなデータソースが指定されたパスのマッパーファイルを管理し、さまざまなトランザクション管理クラスを使用してトランザクションを区別し、Springのデフォルトのトランザクション伝播動作の実装に依存することです。つまり、Springは最初にアクティブなトランザクションがあるかどうかをチェックします。現在のスレッドコンテキストで、存在する場合は結合し、存在しない場合は作成します。
1.データテーブルを作成します
異なるデータベースにテーブルを作成する
create table user_test_0
(
id serial not null
constraint user_0_pk
primary key,
name varchar,
tenant_id varchar
);
2.データソースの構成
first.datasource.url=jdbc:postgresql://
first.datasource.username=
first.datasource.password=
second.datasource.url=jdbc:postgresql://
second.datasource.username=
second.datasource.password=
3.マッピングを構成します
@Configuration
@ConfigurationProperties(prefix = FirstDataSourceProperties.PREFIX)
public class FirstDataSourceProperties extends BaseDataSourceProperties {
public static final String PREFIX = "first.datasource";
}
@Configuration
@ConfigurationProperties(prefix = SecondDataSourceProperties.PREFIX)
public class SecondDataSourceProperties extends BaseDataSourceProperties {
public static final String PREFIX = "second.datasource";
}
次に、さまざまなデータソース(DataSource)、トランザクション管理クラス(DataSourceTransactionManager)、およびMybatisセッション関連クラス(SqlSessionFactory、SqlSessionTemplate)をさまざまなデータソース用に作成する必要があります。
@Configuration
@MapperScan(basePackages = "com.database.subtable.dao", sqlSessionTemplateRef = "firstSessionTemplate")
public class FirstConfiguration extends BaseDataSourceConfiguration {
private static final Logger logger = LoggerFactory.getLogger(FirstConfiguration.class);
@Autowired
FirstDataSourceProperties properties;
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
logger.info("initialize threadPoolTaskExecutor");
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(5);
threadPoolTaskExecutor.setMaxPoolSize(10);
threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
return threadPoolTaskExecutor;
}
@Bean
public WallConfig getWallConfig() {
WallConfig wallConfig = new WallConfig();
wallConfig.setStrictSyntaxCheck(false);
wallConfig.setDir("META-INF/druid/wall/postgres");
wallConfig.init();
return wallConfig;
}
@Bean
public WallFilter getWallFilter() {
WallFilter wallFilter = new WallFilter();
wallFilter.setDbType(properties.getDbType());
wallFilter.setConfig(getWallConfig());
return wallFilter;
}
@Bean(name = "firstDataSource")
public DataSource getDataSource() throws SQLException {
DataSource dataSource = buildDataSource(properties);
logger.info("firstDataSource init");
return dataSource;
}
@Bean(name = "firstDataSourceTransactionManager")
public DataSourceTransactionManager transactionManager(@Qualifier("firstDataSource") DataSource dataSource) throws SQLException {
return new DataSourceTransactionManager(dataSource);
}
@Bean("firstSqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(@Qualifier("firstDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath:com/database/subtable/dao/*.xml"));
// sessionFactory.setTypeHandlersPackage("");
return sessionFactory.getObject();
}
@Bean(name = "firstSessionTemplate")
public SqlSessionTemplate sessionTemplate(
@Qualifier("firstSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
@Configuration
@MapperScan(basePackages = "com.database.subtable.mybatis.second.mapper", sqlSessionTemplateRef = "secondSessionTemplate")
public class SecondConfiguration extends BaseDataSourceConfiguration {
private static final Logger logger = LoggerFactory.getLogger(SecondConfiguration.class);
@Autowired
SecondDataSourceProperties properties;
@Bean(name = "secondDataSource")
@Primary
public DataSource getDataSource() throws SQLException {
DataSource dataSource = buildDataSource(properties);
logger.info("secondDataSource init,url");
return dataSource;
}
@Bean(name = "secondDataSourceTransactionManager")
public DataSourceTransactionManager transactionManager(@Qualifier("secondDataSource") DataSource dataSource) throws SQLException {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "secondSqlSessionFactory")
@Primary
public SqlSessionFactory sqlSessionFactory(@Qualifier("secondDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dataSource);
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath:com/database/subtable/mybatis/second/mapper/*.xml"));
// sessionFactory.setPlugins(new Interceptor[]{new ShardTableInterceptor()});
return sessionFactory.getObject();
}
@Bean(name = "secondSessionTemplate")
public SqlSessionTemplate sessionTemplate(
@Qualifier("secondSqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
4. @ Transactionalアノテーションを使用します
次に、トランザクションを使用する必要があるクラスまたはメソッドに@Transactionalアノテーションをマークし、異なるデータソースを分離することを忘れないでください。
@Service
public class MultiDataSourcesImpl implements MultiDataSources {
@Resource UserMapper firstUserMapper;
@Resource SecondUserMapper secondUserMapper;
@Autowired private MultiDataSources multiDataSources;
@Override
@Transactional(transactionManager = "firstDataSourceTransactionManager")
public void test() {
User user = new User("张三", "腾讯");
firstUserMapper.insertUser(user);
multiDataSources.insertSecond();
}
@Override
@Transactional(transactionManager = "secondDataSourceTransactionManager")
public void insertSecond() {
User user = new User("李四", "百度");
secondUserMapper.insertUser(user);
int i = 1/0;
}
}
新しいテストクラスを作成すると、insertSecond()メソッドが例外をスローすると、2つのデータベースが同時にロールバックされることがわかります。興味がある場合は、春のトランザクションインターセプター(TransactionInterceptor)からゆっくりと開始できます。
オプションII
MybatisはSpringManagerをDataSourceに渡して接続を取得し、Springにはルーティング機能を備えたDataSourceがあるため、キー、つまりAbstractRoutingDataSourceを検索することでさまざまなデータソースを呼び出すことができます。したがって、主なアイデアは、このクラスのdetermineCurrentLookupKeyメソッドを書き直し、ThreadLocalを使用して現在のスレッドのデータソースタイプを格納し、ビジネスダイナミクスに従ってデータソース名を返すことです。
1.最初に複数のデータソースを構成します
1つ目は、マルチデータソース構成をロードしてから、SpringManagementに渡すことです。
@Configuration
@MapperScan("com.database.subtable")
public class MybatisConfiguration extends BaseDataSourceConfiguration {
public static final String FIRST = "first";
public static final String SECOND = "second";
private static final Logger logger = LoggerFactory.getLogger(MybatisConfiguration.class);
@Autowired private FirstDataSourceProperties firstDataSourceProperties;
@Autowired(required = false) private SecondDataSourceProperties secondDataSourceProperties;
@Bean
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
logger.info("initialize threadPoolTaskExecutor");
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(5);
threadPoolTaskExecutor.setMaxPoolSize(10);
threadPoolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
return threadPoolTaskExecutor;
}
@Bean
public WallConfig getWallConfig() {
WallConfig wallConfig = new WallConfig();
wallConfig.setStrictSyntaxCheck(false);
wallConfig.setDir("META-INF/druid/wall/postgres");
wallConfig.init();
return wallConfig;
}
@Bean
public WallFilter getWallFilter() {
WallFilter wallFilter = new WallFilter();
wallFilter.setDbType(firstDataSourceProperties.getDbType());
wallFilter.setConfig(getWallConfig());
return wallFilter;
}
@Primary
@Bean(name = "firstDataSource")
public DataSource firstDataSource() throws SQLException {
DataSource dataSource = buildDataSource(firstDataSourceProperties);
logger.info("firstDataSource init");
return dataSource;
}
/**
* 条件注解生效时才注入当前数据源
* @return
* @throws SQLException
*/
@ConditionalOnBean(SecondDataSourceProperties.class)
@Bean(name = "secondDataSource")
public DataSource secondDataSource() throws SQLException {
DataSource dataSource = buildDataSource(secondDataSourceProperties);
logger.info("secondDataSource init,url");
return dataSource;
}
@Bean("dynamicDataSource")
public DataSource dynamicDataSource() throws SQLException{
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> dataSourceMap = new HashMap<>(2);
dataSourceMap.put(FIRST, firstDataSource());
if (secondDataSourceProperties != null) {
dataSourceMap.put(SECOND, secondDataSource());
} else {
logger.info("this env don't support second datasource.");
}
// 将 first 数据源作为默认指定的数据源
dynamicDataSource.setDefaultDataSource(firstDataSource());
// 将 first 和 second 数据源作为指定的数据源
dynamicDataSource.setDataSources(dataSourceMap);
return dynamicDataSource;
}
@Bean(name = "jdbcTransactionManager")
public DataSourceTransactionManager jdbcTransactionManager() throws SQLException {
return new DataSourceTransactionManager(dynamicDataSource());
}
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
sessionFactory.setDataSource(dynamicDataSource());
sessionFactory.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath:com/database/subtable/*/*.xml"));
// sessionFactory.setPlugins(new Interceptor[]{new ShardTableInterceptor()});
return sessionFactory.getObject();
}
}
备用数据源可以设置开关,并不是每个环境都需要
@Configuration
@ConfigurationProperties(prefix = SecondDataSourceProperties.PREFIX)
@ConditionalOnProperty(prefix = SecondDataSourceProperties.PREFIX, name = "enabled", havingValue = "true") // 动态数据源时使用
public class SecondDataSourceProperties extends BaseDataSourceProperties {
public static final String PREFIX = "second.datasource";
}
2.determineCurrentLookupKeyメソッドを書き直します
コアは、データソースを初期化するときに、複数のデータソースをマップコレクションに設定することです。その後、キーに応じてさまざまなデータソースを取得できます。
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSource.class);
@Override
protected DataSource determineTargetDataSource() {
return super.determineTargetDataSource();
}
@Override
protected Object determineCurrentLookupKey() {
String dataSourceKey = DynamicDataSourceContextHolder.getDataSourceKey();
if (StringUtils.equals(dataSourceKey, MybatisConfiguration.SECOND)) {
logger.info("get data from second");
}
return dataSourceKey;
}
public void setDefaultDataSource(Object defaultDataSource) {
super.setDefaultTargetDataSource(defaultDataSource);
}
public void setDataSources(Map<Object, Object> dataSources) {
super.setTargetDataSources(dataSources);
DynamicDataSourceContextHolder.addDataSourceKeys(dataSources.keySet());
}
}
3.現在のスレッドのデータソースタイプを保存します
public class DynamicDataSourceContextHolder {
private static final ThreadLocal<String> contextHolder = ThreadLocal.withInitial(() -> MybatisConfiguration.FIRST);
public static List<Object> dataSourceKeys = new ArrayList<>();
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
public static String getDataSourceKey() {
return contextHolder.get();
}
public static void clearDataSourceKey() {
contextHolder.remove();
}
public static boolean containDataSourceKey(String key) {
return dataSourceKeys.contains(key);
}
public static boolean addDataSourceKeys(Collection<? extends Object> keys) {
return dataSourceKeys.addAll(keys);
}
}
4.データソースを変換します
すべての構成が完了した後、データソースは注釈とアスペクトを介してビジネスに動的に設定できます
public class DynamicDataSourceAspect {
private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class);
/**
* 切换数据源
* @param point
* @param dataSource
*/
@Before("@annotation(dataSource))")
public void switchDataSource(JoinPoint point, DataSource dataSource) {
try {
if (!DynamicDataSourceContextHolder.containDataSourceKey(dataSource.value())) {
logger.warn("DataSource [{}] doesn't exist, so auto use default DataSource", dataSource.value());
return;
}
DynamicDataSourceContextHolder.setDataSourceKey(dataSource.value());
logger.info("Switch DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.getDataSourceKey(),
point.getSignature());
} catch (Exception e) {
logger.warn("switchDataSource error,e :", e);
}
}
/**
* 重置数据源
* @param point
* @param dataSource
*/
@After("@annotation(dataSource))")
public void restoreDataSource(JoinPoint point, DataSource dataSource) {
DynamicDataSourceContextHolder.clearDataSourceKey();
logger.info("Restore DataSource to [{}] in Method [{}]", DynamicDataSourceContextHolder.getDataSourceKey(),
point.getSignature());
}
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Transactional(propagation = Propagation.REQUIRES_NEW)
public @interface DataSource {
/**
* 数据源key值
* @return
*/
String value();
}
この場合、トランザクションが機能しない可能性があります...