频道
bg

Spring多数据源与事务

coding十二月 05, 20171mins
Spring Framework

多数据源H2

实际业务中经常会遇到需要访问多个数据库的情况,有些情况是表和数据库之间的关系是固定的,而有些情况同一张表会存在于多个数据库中。

CASE 1H3

针对第一种情况,通过Spring的配置就能完成,例如

bash

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "entityManagerFactory",
basePackages = { "com.foobar.foo.repo" }
)
public class FooDbConfig {
@Primary
@Bean(name = "dataSource")
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Primary
@Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean
entityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("dataSource") DataSource dataSource
) {
return builder
.dataSource(dataSource)
.packages("com.foobar.foo.domain")
.persistenceUnit("foo")
.build();
}
@Primary
@Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager(
@Qualifier("entityManagerFactory") EntityManagerFactory
entityManagerFactory
) {
return new JpaTransactionManager(entityManagerFactory);
}
}

bash

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
entityManagerFactoryRef = "barEntityManagerFactory",
transactionManagerRef = "barTransactionManager",
basePackages = { "com.foobar.bar.repo" }
)
public class BarDbConfig {
@Bean(name = "barDataSource")
@ConfigurationProperties(prefix = "bar.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "barEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean
barEntityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("barDataSource") DataSource dataSource
) {
return
builder
.dataSource(dataSource)
.packages("com.foobar.bar.domain")
.persistenceUnit("bar")
.build();
}
@Bean(name = "barTransactionManager")
public PlatformTransactionManager barTransactionManager(
@Qualifier("barEntityManagerFactory") EntityManagerFactory
barEntityManagerFactory
) {
return new JpaTransactionManager(barEntityManagerFactory);
}
}

CASE 2H3

第二种情况,则通过框架提供的AbstractRoutingDataSource来解决。通常的方案是根据自己的业务需求把要访问的数据库的标识存在TheadLocal中,然后在determineCurrentLookupKey去获取这个值。

bash

public class ContextRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DatabaseContextHolder.getContext();
}
}
public class DatabaseContextHolder {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
public static void setContext(String contextName) {
Assert.notNull(contextName, "Context cannot be null");
contextHolder.set(contextName);
}
public static String getContext() {
return contextHolder.get();
}
public static void clearContext() {
contextHolder.remove();
}
}

事务的处理H2

多个数据源之间的访问,有些情况需要保证在一个事务中进行,这就涉及到分布式事务。Atomikos 是一个维护的较好的内嵌的事务管理器,能够直接集成到Spring应用中。

下面是在 Spring Boot 中配置Datasource的示例,主要在于需要用AtomikosDataSourceBean来包装各个数据库的XA驱动

bash

private DataSource createXaDataSource(DataSourceProperties dataSourceProperties) throws Exception {
String className = dataSourceProperties.getXa().getDataSourceClassName();
if (!StringUtils.hasLength(className)) {
className = DatabaseDriver.fromJdbcUrl(dataSourceProperties.determineUrl())
.getXaDataSourceClassName();
}
Assert.state(StringUtils.hasLength(className),
"No XA DataSource class name specified");
XADataSource dataSource = createXaDataSourceInstance(className);
bindXaProperties(dataSource, dataSourceProperties);
return wrapDataSource(dataSourceProperties, dataSource);
}
private XADataSource createXaDataSourceInstance(String className) {
try {
Class<?> dataSourceClass = ClassUtils.forName(className, this.classLoader);
Object instance = BeanUtils.instantiate(dataSourceClass);
Assert.isInstanceOf(XADataSource.class, instance);
return (XADataSource) instance;
} catch (Exception ex) {
throw new IllegalStateException(
"Unable to create XADataSource instance from '" + className + "'");
}
}
private void bindXaProperties(XADataSource target, DataSourceProperties dataSourceProperties) {
MutablePropertyValues values = new MutablePropertyValues();
values.add("user", dataSourceProperties.determineUsername());
values.add("password", dataSourceProperties.determinePassword());
values.add("url", dataSourceProperties.determineUrl());
values.addPropertyValues(dataSourceProperties.getXa().getProperties());
new RelaxedDataBinder(target).withAlias("user", "username").bind(values);
}
private DataSource wrapDataSource(DataSourceProperties dataSourceProperties, XADataSource dataSource) throws Exception {
DataSource wrappedDataSource = this.wrapper.wrapDataSource(dataSource);
if (wrappedDataSource instanceof AtomikosDataSourceBean) {
((AtomikosDataSourceBean) wrappedDataSource).setUniqueResourceName(dataSourceProperties.getName());
((AtomikosDataSourceBean) wrappedDataSource).setMaxPoolSize(
env.getProperty("spring.jta.atomikos.datasource.max-pool-size", Integer.class));
((AtomikosDataSourceBean) wrappedDataSource).setMinPoolSize(
env.getProperty("spring.jta.atomikos.datasource.min-pool-size", Integer.class));
((AtomikosDataSourceBean) wrappedDataSource).setTestQuery(
env.getProperty("spring.jta.atomikos.datasource.test-query"));
}
return wrappedDataSource;
}

Spring框架中一个事务中用到的EntityManger、Connection等都会绑定到TheadLocal中进行复用,这就导致一个问题。为了让两个数据库的操作在一些事务中进行,我们需要用@Transactional注解标示事务,但是在一个事务中由于绑定了ThreadLocal,切换数据源的设置又会无效。

解决的办法是手动清除掉绑定到ThreadLocal的一些对象,实际只要清除EntityManagerFactory即可

bash

TransactionSynchronizationManager.unbindResource(emf);

事务结束时会自动unbind 所有注册的TransactionSynchronization中引用的 EntityManagerFactoryTransactionSynchronization对象是获取EntityManagerFactory后注册的。

由于我们手动清除了一次EntityManagerFactory,再次生成EntityManagerFactory对象时会再registerSynchronization一次,导致synchronizations中两个TransactionSynchronization中引用了同一个TransactionSynchronization,事务结束时会对这个EntityManagerFactory对象unbind两次,导致错误。

所以需要在unbindResource之后把之前的TransactionSynchronization对象清除掉

bash

TransactionSynchronizationManager.clearSynchronization();
TransactionSynchronizationManager.initSynchronization();

评论


新的评论

匹配您的Gravatar头像

Joen Yu

@2022 JoenYu, all rights reserved. Made with love.