На самом деле при проектировании системы один проект должен стараться избегать доступа к нескольким источникам данных. Мы должны сделать все возможное, чтобы сблизить пользователя базы данных, чтобы можно было снизить риск и трудности при последующем переносе данных, реконструкции базы данных и других работах. Конечно, это не абсолютная ситуация, так называемое «существование разумно». С другой стороны, использование нескольких источников данных может значительно снизить удобство кодирования. Нам больше не нужно получать актуальные данные из других систем через Dubbo, SpringCloud и т.д. Так случилось, что недавно я столкнулся с соответствующим сценарием использования в своей работе, поэтому поделюсь двумя решениями, которые я рассматривал:
Решения с несколькими источниками данных
На самом деле это решение, о котором можно подумать с первого раза.Мы напрямую внедряем несколько SqlSessionFactory в проект (если вы используете Mybatis) и можете хранить домены и dao нескольких источников данных по пакетам. Таким образом, вы можете получить доступ к разным базам данных, настроив @MapperScan и setTypeAliasesPackage разных SqlSessionFactory. Следующий фрагмент кода настраивает параметры одного из источников данных:
/**
* db_b2b数据源
* @return b2b库数据源
*/
@Bean(name = "b2b")
public DataSource b2b () throws SQLException {
MysqlXADataSource mysqlXADataSource=new MysqlXADataSource();
mysqlXADataSource.setUrl((dbB2BProperties.getUrl()));
mysqlXADataSource.setPinGlobalTxToPhysicalConnection(true);
mysqlXADataSource.setPassword((dbB2BProperties.getPassword()));
mysqlXADataSource.setUser((dbB2BProperties.getUserName()));
mysqlXADataSource.setPinGlobalTxToPhysicalConnection(true);
AtomikosDataSourceBean xaDataSource=new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(mysqlXADataSource);
xaDataSource.setUniqueResourceName("b2b");
return xaDataSource;
}
@Bean(name = "sqlSessionFactoryB2B")
public SqlSessionFactory sqlSessionFactoryB2B(@Qualifier("b2b")DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setTypeAliasesPackage("com.mhc.lite.dal.domain.b2b");
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
bean.setConfiguration(configuration);
bean.setPlugins(new Interceptor[]{
paginationInterceptor //添加分页功能
});
return bean.getObject();
}
@Bean(name = "sqlSessionTemplateB2B")
public SqlSessionTemplate sqlSessionTemplateB2B(
@Qualifier("sqlSessionFactoryB2B") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
Некоторые люди могут быть незнакомы с MysqlXADataSource и AtomikosDataSourceBean, которые на самом деле являются двумя классами, используемыми в распределенных транзакциях JTA, которые будут объяснены в конце статьи. Приведенная выше конфигурация завершает добавление источника данных, и другие источники данных могут быть скопированы в соответствии с этим шаблоном, но стоит отметить, что имена bean-компонентов каждого DataSource и SqlSessionFactory необходимо различать и выбирать с помощью @Qualifier, иначе он будет привести к различным источникам данных.Вызвать путаницу между источниками.
Сценарий источника динамических данных
На самом деле, подумайте об этом с другой точки зрения, когда мы используем несколько SqlSessionFactory для подключения к разным источникам данных, это очень ограничено. Когда у нас есть большое количество источников данных, код шаблона, подобный приведенному выше, заполнит весь проект, и его будет сложно настроить. Более того, представьте, что нам не нужно постоянно работать с каждым источником данных, и каждый источник данных будет поддерживать базовое количество незанятых соединений. Это тратит впустую такие ресурсы, как системная память и ЦП, которые и без того ценны, поэтому появилось второе решение — динамический источник данных. Позвольте мне привести более яркий пример из жизни: буровое долото, используемое рабочими, на самом деле требуется только одна буровая установка Нам нужно только заменить разные буровые долота на буровой установке в соответствии с различными материалами стен и формами отверстий для адаптации к каждой сцене. И то, что мы сделали выше, заключалось в том, чтобы купить две или более буровых установки (действительно, немного роскоши!). Давайте посмотрим, что делать:
/**
* db_base数据源
* @return
*/
@Bean(name = "base")
@ConfigurationProperties(prefix = "spring.datasource.druid.base" )
public DataSource base () {
return DruidDataSourceBuilder.create().build();
}
/**
* db_b2b数据源
* @return
*/
@Bean(name = "b2b")
@ConfigurationProperties(prefix = "spring.datasource.druid.b2b" )
public DataSource b2b () {
return DruidDataSourceBuilder.create().build();
}
/**
* 动态数据源配置
* @return
*/
@Bean
@Primary
public DataSource multipleDataSource (@Qualifier("base") DataSource base,
@Qualifier("b2b") DataSource b2b ) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map< Object, Object > targetDataSources = new HashMap<>();
targetDataSources.put(DBTypeEnum.DB_BASE.getValue(), base );
targetDataSources.put(DBTypeEnum.DB_B2B.getValue(), b2b);
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(base);
return dynamicDataSource;
}
@Bean("sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
sqlSessionFactory.setDataSource(multipleDataSource(base(),b2b()));
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
sqlSessionFactory.setConfiguration(configuration);
sqlSessionFactory.setPlugins(new Interceptor[]{ //PerformanceInterceptor(),OptimisticLockerInterceptor()
paginationInterceptor() //添加分页功能
});
sqlSessionFactory.setGlobalConfig(globalConfiguration());
return sqlSessionFactory.getObject();
}
Теперь нам нужна только одна SqlSessionFactory ("rig"), но еще один DynamicDataSource. На самом деле его можно рассматривать как список источников данных, и позже мы сможем динамически переключаться в соответствии с именами источников данных в списке. Итак, снова возникает вопрос, как узнать, когда какую базу данных использовать? Затем см.:
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* 核心方法,切换数据源上下文
*/
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
}
public class DbContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal<>();
/**
* 设置数据源
*/
public static void setDbType(DBTypeEnum dbTypeEnum) {
contextHolder.set(dbTypeEnum.getValue());
}
/**
* 取得当前数据源
*/
private static String getDbType() {
return (String) contextHolder.get();
}
/**
* 清除上下文数据
*/
private static void clearDbType() {
contextHolder.remove();
}
}
@Slf4j
@Aspect
@Order(-100)
@Component
public class DataSourceSwitchAspect {
/**
* 自己编写的manager method
*/
@Pointcut("execution(* com.mhc.polestar.dal.manager.*.*(..))")
private void ownMethod(){}
/**
* MP生成的CRUD method
*/
@Pointcut("execution(* com.baomidou.mybatisplus.service.*.*(..))")
private void mpMethod() {}
@Before( "ownMethod() || mpMethod()" )
public void base(JoinPoint joinPoint) {
String name = joinPoint.getTarget().getClass().getName();
if (name.contains("B2b")){
log.debug("切换到b2b数据源...");
DbContextHolder.setDbType(DBTypeEnum.DB_B2B);
}else {
log.debug("切换到base数据源...");
DbContextHolder.setDbType(DBTypeEnum.DB_BASE);
}
}
}
Помните идею фасетно-ориентированного программирования?На самом деле каждый раз, когда мы хотим «сменить дрель», действие можно абстрагировать через фасеты. Мы выбираем, на какой источник данных переключиться, в соответствии с информацией, такой как имя и путь менеджера, используемого в pointcut (необходимо заранее сформулировать некоторые правила), чтобы он мог нормально работать. В то же время используйте @Order, чтобы убедиться, что порядок выполнения аспекта имеет приоритет. Сказав это, общая идея также выражена, но я столкнулся с проблемой в процессе фактического использования. Выполнение вышеуказанного может обеспечить нулевое вторжение в код, рассмотрите следующую ситуацию:
@Override
@ValidateDTO
@Transactional(rollbackFor = Exception.class)
public APIResult<Boolean> addPartner(PartnerParamDTO paramDTO) {...}
Нам часто нужно открывать транзакцию для обеспечения бизнес-консистентности, но если мы открываем транзакцию на вызывающем Сервисе Менеджера, то точка сохранения будет установлена на сервисном уровне, то есть контекст его нижнего уровня должен быть определен и не может быть изменено, и это происходит. Чтобы обеспечить согласованность, при возникновении исключения выполняется откат. Однако наш аспект установлен в слое Manger, поэтому мы не можем переключить источник данных, и ошибки обязательно возникнут! Позже я решил проблему переключения источника данных в сервисном слое настройкой аннотаций, но этот способ будет навязчив к коду (нужно добавить аннотации во все места, где переключается источник данных. , это не к чему сделать с исходным бизнесом, и нам нужно использовать несколько источников данных в методе службы, тогда нам нужно еще больше углубить неприятный запах этого кода на уровень менеджера и использовать DbContextHolder.setDbType() для ручного переключения). На данный момент эта проблема, с которой я столкнулся, - это почти конец описания.
Распределенная транзакция
MysqlXADataSource и AtomikosDataSourceBean упоминались выше, когда речь шла о решении с несколькими источниками данных, которые на самом деле являются двумя ключами в распределенных транзакциях JTA. Как мы все знаем, традиционная транзакция Spring может управлять согласованностью только одного источника данных. Теперь мы используем несколько источников данных или динамические источники данных в нашем проекте.Если мы хотим продолжать использовать транзакции, мы должны рассмотреть распределенные транзакции JTA. Это необходимо для обеспечения синхронизации транзакций между несколькими источниками данных.Здесь я использую метод атомико для простой демонстрации:
@Configuration
@EnableTransactionManagement
public class TxManagerConfig {
@Bean(name = "userTransaction")
public UserTransaction userTransaction() throws Throwable {
UserTransactionImp userTransactionImp = new UserTransactionImp();
userTransactionImp.setTransactionTimeout(10000);
return userTransactionImp;
}
@Bean(name = "atomikosTransactionManager", initMethod = "init" , destroyMethod = "close")
public TransactionManager atomikosTransactionManager() {
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(false);
return userTransactionManager;
}
@Bean(name = "transactionManager")
@DependsOn({ "userTransaction", "atomikosTransactionManager" })
public PlatformTransactionManager transactionManager() throws Throwable {
return new JtaTransactionManager(userTransaction(),atomikosTransactionManager());
}
}
/**
* db_b2b数据源
* @return b2b库数据源
*/
@Bean(name = "b2b")
public DataSource b2b () throws SQLException {
MysqlXADataSource mysqlXADataSource=new MysqlXADataSource();
mysqlXADataSource.setUrl((dbB2BProperties.getUrl()));
mysqlXADataSource.setPinGlobalTxToPhysicalConnection(true);
mysqlXADataSource.setPassword((dbB2BProperties.getPassword()));
mysqlXADataSource.setUser((dbB2BProperties.getUserName()));
mysqlXADataSource.setPinGlobalTxToPhysicalConnection(true);
AtomikosDataSourceBean xaDataSource=new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(mysqlXADataSource);
xaDataSource.setUniqueResourceName("b2b");
return xaDataSource;
}
Вы правильно прочитали, распределенные транзакции могут быть достигнуты с помощью такой простой конфигурации в проекте SpringBoot. Мы используем MysqlXADataSource и AtomikosDataSourceBean для определения источников данных, которыми необходимо управлять, а затем мы можем использовать PlatformTransactionManager, предоставляемый JTA, для управления транзакциями.
компромиссы
1. Преимущество решения с несколькими источниками данных в простоте настройки и минимальном вторжении в бизнес-код Минус также очевиден: нам нужно занимать некоторые ресурсы в системе, а эти ресурсы не всегда нужны , что в определенной степени приведет к пустой трате ресурсов. Если вам нужно использовать данные из нескольких источников данных одновременно в куске бизнес-кода и учитывать атомарность (транзакцию) операции, то это решение, несомненно, вам подойдет.
2. Конфигурация схемы источника динамических данных кажется немного более сложной, но она соответствует принципу построения «как только вы его используете, и вы возвращаете его по мере использования». Мы рассматриваем множественные данные. источники как пул, а затем потреблять. Его недостатки указаны выше: нам часто приходится идти на компромиссы в соответствии с потребностями сделки. Более того, из-за необходимости переключения контекста среды могут возникать проблемы с живучестью, такие как взаимоблокировка, когда конкуренция за ресурсы выполняется в системе с высокой степенью параллелизма. Мы часто используем его для «разделения чтения-записи» базы данных, и в бизнесе нет необходимости работать с несколькими источниками данных одновременно.
3. Если вам нужно использовать транзакции, вы должны помнить об использовании распределенных транзакций для замены собственного управления транзакциями Spring, иначе контроль согласованности будет невозможен!
На этом статья подходит к концу.Давно не писал статьи.Многое не очень подробно рассматривается.Спасибо за критику и поправку!