Четыре способа для Springcloud+Mybatis использовать несколько источников данных

задняя часть

Когда я некоторое время назад разрабатывал центр членства и систему промежуточного программного обеспечения, я много раз сталкивался с проблемами конфигурации нескольких источников данных, в основном с использованием четырех методов субподряда, параметризованного переключения, аннотации + АОП и динамического добавления. Вот резюме, делитесь опытом использования и ямами, на которые наступали.

Четыре способа сравнения

Статья относительно длинная.Во-первых, дается сравнение четырех методов реализации.Вы можете читать в соответствии с вашими потребностями.

Субподряд Параметрическое переключение Метод аннотации Метод динамического добавления
Применимая сцена Знайте, какой источник данных использовать при кодировании Среда выполнения определяет, какой источник данных использовать. Знайте, какой источник данных использовать при кодировании Динамически добавлять новые источники данных во время выполнения
режим переключения автоматический руководство автоматический руководство
путь картографа нужно заключать субподряд нет запроса нет запроса нет запроса
Нужно ли изменять класс конфигурации, чтобы добавить источник данных? нужно ненужный ненужный \
сложность реализации Простой сложный сложный сложный

Субподряд

файл конфигурации источника данных

В yml настройте два источника данных с идентификаторами master и s1 соответственно.

spring:
  datasource:
    master:
      jdbcUrl: jdbc:mysql://192.168.xxx.xxx:xxxx/db1?.........
      username: xxx
      password: xxx
      driverClassName: com.mysql.cj.jdbc.Driver
    s1:
      jdbcUrl: jdbc:mysql://192.168.xxx.xxx:xxxx/db2?........
      username: xxx
      password: xxx
      driverClassName: com.mysql.cj.jdbc.Driver

Класс конфигурации источника данных

класс конфигурации основного источника данных

будь осторожен:

Источник данных по умолчанию необходимо указывать с помощью аннотации @Primary, иначе Spring не знает, какой из них является основным источником данных;

@Configuration
@MapperScan(basePackages = "com.hosjoy.xxx.xxx.xxx.xxx.mapper.master", sqlSessionFactoryRef = "masterSqlSessionFactory")
public class MasterDataSourceConfig {

    //默认数据源
    @Bean(name = "masterDataSource")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public HikariDataSource masterDataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Bean(name = "masterSqlSessionFactory")
    @Primary
    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource datasource, PaginationInterceptor paginationInterceptor)
            throws Exception {
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(datasource);
        bean.setMapperLocations(
                // 设置mybatis的xml所在位置
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/master/**/**.xml"));
        bean.setPlugins(new Interceptor[]{paginationInterceptor});
        return bean.getObject();
    }

    @Bean(name = "masterSqlSessionTemplate")
    @Primary
    public SqlSessionTemplate masterSqlSessionTemplate(
            @Qualifier("masterSqlSessionFactory") SqlSessionFactory sessionfactory) {
        return new SqlSessionTemplate(sessionfactory);
    }
}

класс конфигурации источника данных s1

@Configuration
@MapperScan(basePackages = "com.hosjoy.xxx.xxx.xxx.xxx.mapper.s1", sqlSessionFactoryRef = "s1SqlSessionFactory")
public class S1DataSourceConfig {

    @Bean(name = "s1DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.s1")
    public HikariDataSource s1DataSource() {
        return DataSourceBuilder.create().type(HikariDataSource.class).build();
    }

    @Bean(name = "s1SqlSessionFactory")
    public SqlSessionFactory s1SqlSessionFactory(@Qualifier("s1DataSource") DataSource datasource
            , PaginationInterceptor paginationInterceptor)
            throws Exception {
        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();
        bean.setDataSource(datasource);
        bean.setMapperLocations(
                // 设置mybatis的xml所在位置
                new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/s1/**/**.xml"));
        bean.setPlugins(new Interceptor[]{paginationInterceptor});
        return bean.getObject();
    }

    @Bean(name = "s1SqlSessionTemplate")
    public SqlSessionTemplate s1SqlSessionTemplate(
            @Qualifier("s1SqlSessionFactory") SqlSessionFactory sessionfactory) {
        return new SqlSessionTemplate(sessionfactory);
    }
}

использовать

Видно, что интерфейс картографа и файл xml должны быть упакованы в соответствии с разными источниками данных. При работе с базой данных внедряйте слой dao в класс обслуживания по мере необходимости.

Анализ функций

преимущество

Его просто реализовать, нужно только написать файл конфигурации источника данных и класс конфигурации, а также обратить внимание на субподряд интерфейса сопоставления и XML-файла.

недостаток

Очевидно, что если вы хотите добавить или удалить источники данных позже, вам нужно изменить не только файл конфигурации источника данных, но и класс конфигурации.

Например, чтобы добавить источник данных, вам нужно написать новый класс конфигурации для источника данных, а также рассмотреть возможность создания пакета интерфейса сопоставления, пакета xml и т. д., который не обеспечивает эффекта «горячей замены».

Метод параметрического переключения

Мысль

Параметрированное переключение источников данных означает, что бизнес-стороне необходимо динамически переключаться на разные источники данных в соответствии с текущими бизнес-параметрами.

Это отличается от идеи субподряда. Предпосылка субподряда заключается в том, что при написании кода вы уже знаете, какой источник данных вам нужно использовать, а параметризованное переключение источников данных требует от вас решить, какой источник данных использовать в соответствии с бизнес-параметрами.

Например, если значение параметра userType равно 1, необходимо переключиться на источник данных slave1, а когда значение userType параметра запроса равно 2, необходимо переключиться на источник данных slave2.

/**伪代码**/
int userType = reqUser.getType();
if (userType == 1){
    //切换到数据源slave1
    //数据库操作
} else if(userType == 2){
    //切换到数据源slave2
    //数据库操作
}

Идеи дизайна

Регистрация источника данных

Когда класс конфигурации источника данных создает источник данных, он считывает все конфигурации источника данных из файла конфигурации yml, автоматически создает каждый источник данных, регистрирует его в фабрике компонентов и AbstractRoutingDatasource (обсудим это позже) и возвращает мастер источника данных по умолчанию.

image-20200701112808482

переключатель источника данных

(1) Запрос обрабатывается через пул потоков, и каждый запрос имеет эксклюзивный поток, так что каждый поток не влияет друг на друга при переключении источников данных.

(2) получить идентификатор источника данных, который должен быть переключен в соответствии с бизнес-параметрами, и получить компонент источника данных из пула кэширования источника данных в соответствии с идентификатором;

(3) Сгенерировать текущий ключ источника данных потока;

(4) Установите ключ threadLocal;

(5) Поместите ключ и компонент источника данных в пул кеша источника данных;

(6) Перед выполнением метода сопоставления Spring вызовет метод defineCurrentLookupKey для получения ключа, затем перейдет к пулу кеша источника данных, чтобы получить источник данных в соответствии с ключом, а затем getConnection получит соединение с источником данных;

(7) использовать источник данных для выполнения операций с базой данных;

(8) Освободите источник данных текущего потока.

image-20200701112808482

Анализ исходного кода AbstractRoutingDataSource

Spring предоставляет нам абстрактный класс AbstractRoutingDataSource, который является ключом к динамическому переключению источников данных.

Давайте взглянем на диаграмму классов этого класса, реализующего интерфейс DataSource.

image-20200701151212528

Давайте посмотрим на логику его метода getConnection, который сначала вызывает defineTargetDataSource для получения источника данных, а затем получает соединение с базой данных. Несложно догадаться, что именно здесь решается, какой источник данных использовать.

image-20200701151449823

Войдя в метод defineTargetDataSource, мы видим, что он сначала вызывает defineCurrentLookupKey для получения lookupKey, а затем переходит к resolveDataSources для поиска соответствующего источника данных по этому ключу.

image-20200701151728497

Взгляните на несколько объектов, определенных в этом классе.

image-20200701151932020

Таким образом, ключом является логика получения этого lookupKey, которая определяет, какой источник данных в настоящее время получен, а затем выполняет ряд операций, таких как getConnection. DefinitionCurrentLookupKey — это абстрактный метод класса AbstractRoutingDataSource, возвращаемое им значение — значение ключа источника данных dataSource, который вы хотите использовать. ) Выньте из него соответствующий DataSource, и если не найдете, настройте источник данных по умолчанию.

Поэтому, расширив класс AbstractRoutingDataSource и переписав метод определенияCurrentLookupKey(), можно реализовать переключение источника данных.

Код

Реализация кода ключа размещена ниже.

файл конфигурации источника данных

Здесь есть 3 источника данных, из которых основным источником данных является MySQL, а двумя вспомогательными источниками данных является sqlserver.

spring:
  datasource:
    master:
      jdbcUrl: jdbc:mysql://192.168.xx.xxx:xxx/db1?........
      username: xxx
      password: xxx
      driverClassName: com.mysql.cj.jdbc.Driver
    slave1:
      jdbcUrl: jdbc:sqlserver://192.168.xx.xxx:xxx;DatabaseName=db2
      username: xxx
      password: xxx
      driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
    slave2:
      jdbcUrl: jdbc:sqlserver://192.168.xx.xxx:xxx;DatabaseName=db3
      username: xxx
      password: xxx
      driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver

Определение источников динамических данных

В основном он наследует AbstractRoutingDataSource и реализует метод defineCurrentLookupKey.

public class DynamicDataSource extends AbstractRoutingDataSource {
    /*存储所有数据源*/
    private Map<Object, Object> backupTargetDataSources;

    public Map<Object, Object> getBackupTargetDataSources() {
        return backupTargetDataSources;
    }
    /*defaultDataSource为默认数据源*/
    public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSource) {
        backupTargetDataSources = targetDataSource;
        super.setDefaultTargetDataSource(defaultDataSource);
        super.setTargetDataSources(backupTargetDataSources);
        super.afterPropertiesSet();
    }
    public void addDataSource(String key, DataSource dataSource) {
        this.backupTargetDataSources.put(key, dataSource);
        super.setTargetDataSources(this.backupTargetDataSources);
        super.afterPropertiesSet();
    }
    /*返回当前线程的数据源的key*/
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getContextKey();
    }
}

Определить хранение переменной ключевого потока источника данных

Определите статическую переменную ThreadLocal, которая содержит связь между потоком и ключом источника данных потока. Когда мы хотим переключить источник данных, мы должны сначала сами сгенерировать ключ и сохранить ключ в переменной потока threadLocal; в то же время мы также должны получить объект источника данных из свойства backupTargetDataSources в объекте DynamicDataSource, и затем поместите ключ и объект источника данных вместе, поместите в backupTargetDataSources. Таким образом, Spring может извлечь объект источника данных, который мы только что установили, из BackupTargetDataSources в соответствии с ключом, возвращаемым методом DefiniteCurrentLookupKey, и выполнить ряд операций, таких как getConnection.

public class DynamicDataSourceContextHolder {
    /**
     * 存储线程和数据源key的映射关系
     */
    private static final ThreadLocal<String> DATASOURCE_CONTEXT_KEY_HOLDER = new ThreadLocal<>();

    /***
     * 设置当前线程数据源key
     */
    public static void setContextKey(String key) {
        DATASOURCE_CONTEXT_KEY_HOLDER.set(key);
    }
    /***
     * 获取当前线程数据源key
     */
    public static String getContextKey() {
        String key = DATASOURCE_CONTEXT_KEY_HOLDER.get();
        return key == null ? DataSourceConstants.DS_KEY_MASTER : key;
    }
    /***
     * 删除当前线程数据源key
     */
    public static void removeContextKey() {
        DynamicDataSource dynamicDataSource = RequestHandleMethodRegistry.getContext().getBean(DynamicDataSource.class);
        String currentKey = DATASOURCE_CONTEXT_KEY_HOLDER.get();
        if (StringUtils.isNotBlank(currentKey) && !"master".equals(currentKey)) {
            dynamicDataSource.getBackupTargetDataSources().remove(currentKey);
        }
        DATASOURCE_CONTEXT_KEY_HOLDER.remove();
    }
}

Класс автоматической настройки с несколькими источниками данных

Здесь, читая конфигурацию всех источников данных в файле конфигурации yml, объект источника данных автоматически создается для каждого источника данных и регистрируется в фабрике компонентов. В то же время задайте для этих объектов источников данных значение AbstractRoutingDataSource.

Таким образом, если вам нужно добавить или изменить источник данных позже, вам не нужно добавлять или изменять класс конфигурации Java, просто перейдите в центр конфигурации, чтобы изменить файл yml.

@Configuration
@MapperScan(basePackages = "com.hosjoy.xxx.xxx.modules.xxx.mapper")
public class DynamicDataSourceConfig {
    @Autowired
    private BeanFactory beanFactory;
    @Autowired
    private DynamicDataSourceProperty dynamicDataSourceProperty;
    /**
     * 功能描述: <br>
     * 〈动态数据源bean 自动配置注册所有数据源〉
     *
     * @param
     * @return javax.sql.DataSource
     * @Author li.he
     * @Date 2020/6/4 16:47
     * @Modifier
     */
    @Bean
    @Primary
    public DataSource dynamicDataSource() {
        DefaultListableBeanFactory listableBeanFactory = (DefaultListableBeanFactory) beanFactory;
        /*获取yml所有数据源配置*/
        Map<String, Object> datasource = dynamicDataSourceProperty.getDatasource();
        Map<Object, Object> dataSourceMap = new HashMap<>(5);
        Optional.ofNullable(datasource).ifPresent(map -> {
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                //创建数据源对象
                HikariDataSource dataSource = (HikariDataSource) DataSourceBuilder.create().build();
                String dataSourceId = entry.getKey();
                configeDataSource(entry, dataSource);
                /*bean工厂注册每个数据源bean*/
                listableBeanFactory.registerSingleton(dataSourceId, dataSource);
                dataSourceMap.put(dataSourceId, dataSource);
            }
        });
        //AbstractRoutingDataSource设置主从数据源
        return new DynamicDataSource(beanFactory.getBean("master", DataSource.class),          dataSourceMap);
    }

    private void configeDataSource(Map.Entry<String, Object> entry, HikariDataSource dataSource) {
        Map<String, Object> dataSourceConfig = (Map<String, Object>) entry.getValue();
        dataSource.setJdbcUrl(MapUtils.getString(dataSourceConfig, "jdbcUrl"));
        dataSource.setDriverClassName(MapUtils.getString(dataSourceConfig, "driverClassName"));
        dataSource.setUsername(MapUtils.getString(dataSourceConfig, "username"));
        dataSource.setPassword(MapUtils.getString(dataSourceConfig, "password"));
    }

}

Класс инструментов переключения источников данных

Логика переключения:

(1) Сгенерируйте текущий ключ источника данных потока

(2) В соответствии с бизнес-условиями получить идентификатор источника данных, который необходимо переключить;

(3) Получить объект источника данных из пула буферов источников данных в соответствии с идентификатором и снова добавить его в пул буферов backupTargetDataSources;

(4) threadLocal устанавливает ключ источника данных, соответствующий текущему потоку;

(5) Перед выполнением операции с базой данных Spring вызовет метод defineCurrentLookupKey для получения ключа, затем перейдет к пулу кеша источника данных, чтобы получить источник данных в соответствии с ключом, а затем getConnection получит соединение с источником данных;

(6) использовать источник данных для выполнения операций с базой данных;

(7) Освободите кеш: threadLocal очищает информацию об источнике данных текущего потока, а пул буферов источника данных очищает ключ источника данных текущего потока и объект источника данных, чтобы предотвратить утечку памяти.

@Slf4j
@Component
public class DataSourceUtil {
    @Autowired
    private DataSourceConfiger dataSourceConfiger;
    
    /*根据业务条件切换数据源*/
    public void switchDataSource(String key, Predicate<? super Map<String, Object>> predicate) {
        try {
            //生成当前线程数据源key
            String newDsKey = System.currentTimeMillis() + "";
            List<Map<String, Object>> configValues = dataSourceConfiger.getConfigValues(key);
            Map<String, Object> db = configValues.stream().filter(predicate)
                    .findFirst().get();
            String id = MapUtils.getString(db, "id");
            //根据ID从数据源缓存池中获取数据源对象,并再次添加到backupTargetDataSources
            addDataSource(newDsKey, id);
            //设置当前线程对应的数据源key
            DynamicDataSourceContextHolder.setContextKey(newDsKey);
            log.info("当前线程数据源切换成功,当前数据源ID:{}", id);

        }
        catch (Exception e) {
            log.error("切换数据源失败,请检查数据源配置文件。key:{}, 条件:{}", key, predicate.toString());
            throw new ClientException("切换数据源失败,请检查数据源配置", e);
        }
    }
    
    /*将数据源添加至多数据源缓存池中*/
    public static void addDataSource(String key, String dataSourceId) {
        DynamicDataSource dynamicDataSource = RequestHandleMethodRegistry.getContext().getBean(DynamicDataSource.class);
        DataSource dataSource = (DataSource) dynamicDataSource.getBackupTargetDataSources().get(dataSourceId);
        dynamicDataSource.addDataSource(key, dataSource);
    }
}

использовать

public void doExecute(ReqTestParams reqTestParams){
    //构造条件
    Predicate<? super Map<String, Object>> predicate =.........;
    //切换数据源
    dataSourceUtil.switchDataSource("testKey", predicate);
    //数据库操作
    mapper.testQuery();
    //清理缓存,避免内存泄漏
    DynamicDataSourceContextHolder.removeContextKey();
}

После использования каждого источника данных необходимо вызвать метод removeContextKey для очистки кеша, чтобы избежать утечек памяти.Здесь вы можете рассмотреть возможность использования АОП для перехвата определенных методов и использования пост-уведомления для выполнения очистки кеша для прокси метода выполнения.

@Aspect
@Component
@Slf4j
public class RequestHandleMethodAspect {
    @After("xxxxxxxxxxxxxxExecution表达式xxxxxxxxxxxxxxxxxx")
    public void afterRunning(JoinPoint joinPoint){
        String name = joinPoint.getSignature().toString();
        long id = Thread.currentThread().getId();
        log.info("方法执行完毕,开始清空当前线程数据源,线程id:{},代理方法:{}",id,name);
        DynamicDataSourceContextHolder.removeContextKey();
        log.info("当前线程数据源清空完毕,已返回至默认数据源:{}",id);
    }
}

Анализ функций

(1) Параметризованный метод переключения источника данных, начальная точка и метод субподряда различны, что подходит для определения того, какой источник данных использовать во время выполнения.

(2) Необходимо вручную выполнить операцию переключения источника данных;

(3) Нет необходимости в субподряде, пути mapper и xml определяются свободно;

(4) Чтобы добавить источник данных, нет необходимости изменять класс конфигурации Java, просто измените файл конфигурации источника данных.

Метод аннотации

Мысль

Этот метод использует идею аннотации + АОП, чтобы пометить пользовательскую аннотацию для метода, которому необходимо переключить источник данных, указать идентификатор источника данных для атрибута аннотации, а затем использовать аспект АОП для перехвата метода, отмеченного аннотацией, и переключиться на соответствующий источник данных перед выполнением метода; после выполнения переключиться на источник данных по умолчанию.

Следует отметить, что приоритет пользовательского аспекта должен быть выше, чем приоритет аспекта, соответствующего аннотации @Transactional.

В противном случае, когда пользовательская аннотация и @Transactional используются одновременно, аспект @Transactional будет выполнен первым.Когда аспект вызывает метод getConnection, он вызывает метод AbstractRoutingDataSource.determineCurrentLookupKey, и мастер источника данных по умолчанию будет получены в это время. В это время, даже если аспект, соответствующий @UsingDataSource, устанавливает ключ источника данных текущего потока, он не будет вызывать метод defineCurrentLookupKey для переключения источника данных.

Идеи дизайна

Регистрация источника данных

То же.

переключатель источника данных

Используйте аспект для перехвата всех методов, помеченных аннотацией @UsingDataSource, по атрибуту dataSourceId перед выполнением метода переключитесь на соответствующий источник данных, после выполнения метода очистите кеш и переключитесь на источник данных по умолчанию.

image-20200717094314674

Код

файл конфигурации источника данных

То же.

Определение источников динамических данных

То же.

Определить хранение переменной ключевого потока источника данных

То же.

Класс автоматической настройки с несколькими источниками данных

То же.

Класс инструментов переключения источников данных

Логика переключения:

(1) Сгенерируйте текущий ключ источника данных потока

(3) Получить объект источника данных из пула буферов источников данных в соответствии с идентификатором и снова добавить его в пул буферов backupTargetDataSources;

(4) threadLocal устанавливает ключ источника данных, соответствующий текущему потоку;

(5) Перед выполнением операции с базой данных Spring вызовет метод defineCurrentLookupKey для получения ключа, затем перейдет к пулу кеша источника данных, чтобы получить источник данных в соответствии с ключом, а затем getConnection получит соединение с источником данных;

(6) использовать источник данных для выполнения операций с базой данных;

(7) Освободите кеш: threadLocal очищает информацию об источнике данных текущего потока, а пул буферов источника данных очищает ключ источника данных текущего потока и объект источника данных.

public static void switchDataSource(String dataSourceId) {
    if (StringUtils.isBlank(dataSourceId)) {
        throw new ClientException("切换数据源失败,数据源ID不能为空");
    }
    try {
        String threadDataSourceKey = UUID.randomUUID().toString();
        DataSourceUtil.addDataSource(threadDataSourceKey, dataSourceId);
        DynamicDataSourceContextHolder.setContextKey(threadDataSourceKey);
    }
    catch (Exception e) {
        log.error("切换数据源失败,未找到指定的数据源,请确保所指定的数据源ID已在配置文件中配置。dataSourceId:{}", dataSourceId);
        throw new ClientException("切换数据源失败,未找到指定的数据源,请确保所指定的数据源ID已在配置文件中配置。dataSourceId:" + dataSourceId, e);
    }
}

пользовательская аннотация

Пользовательская аннотация отмечает источник данных, используемый текущим методом, который по умолчанию является основным источником данных.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UsingDataSource {

    String dataSourceId() default "master";
}

раздел

В основном он определяет предварительное и постуведомление, перехватывает метод, помеченный аннотацией UsingDataSource, переключает источник данных перед выполнением метода и очищает кеш источника данных после выполнения метода.

Аспект, который необходимо пометить, имеет более высокий приоритет, чем аспект, соответствующий аннотации @Transaction. В противном случае, когда пользовательская аннотация и @Transactional используются одновременно, аспект @Transactional будет выполнен первым.Когда аспект вызывает метод getConnection, он вызывает метод AbstractRoutingDataSource.determineCurrentLookupKey, и мастер источника данных по умолчанию будет получены в это время. В это время, даже если аспект, соответствующий @UsingDataSource, устанавливает ключ источника данных текущего потока, он не будет вызывать метод defineCurrentLookupKey для переключения источника данных.

@Aspect
@Component
@Slf4j
@Order(value = 1)
public class DynamicDataSourceAspect {

    //拦截UsingDataSource注解标记的方法,方法执行前切换数据源
    @Before(value = "@annotation(usingDataSource)")
    public void before(JoinPoint joinPoint, UsingDataSource usingDataSource) {
        String dataSourceId = usingDataSource.dataSourceId();
        log.info("执行目标方法前开始切换数据源,目标方法:{}, dataSourceId:{}", joinPoint.getSignature().toString(), dataSourceId);
        try {
            DataSourceUtil.switchDataSource(dataSourceId);
        }
        catch (Exception e) {
            log.error("切换数据源失败!数据源可能未配置或不可用,数据源ID:{}", dataSourceId, e);
            throw new ClientException("切换数据源失败!数据源可能未配置或不可用,数据源ID:" + dataSourceId, e);
        }
        log.info("目标方法:{} , 已切换至数据源:{}", joinPoint.getSignature().toString(), dataSourceId);
    }

    //拦截UsingDataSource注解标记的方法,方法执行后清理数据源,防止内存泄漏
    @After(value = "@annotation(com.hosjoy.hbp.dts.common.annotation.UsingDataSource)")
    public void after(JoinPoint joinPoint) {
        log.info("目标方法执行完毕,执行清理,切换至默认数据源,目标方法:{}", joinPoint.getSignature().toString());
        try {
            DynamicDataSourceContextHolder.removeContextKey();
        }
        catch (Exception e) {
            log.error("清理数据源失败", e);
            throw new ClientException("清理数据源失败", e);
        }
        log.info("目标方法:{} , 数据源清理完毕,已返回默认数据源", joinPoint.getSignature().toString());
    }
}

использовать

@UsingDataSource(dataSourceId = "slave1")
@Transactional
public void test(){
    AddressPo po = new AddressPo();
    po.setMemberCode("asldgjlk");
    po.setName("lihe");
    po.setPhone("13544986666");
    po.setProvince("asdgjwlkgj");
    addressMapper.insert(po);
    int i = 1 / 0;
}

Метод динамического добавления (очень полезный)

Описание бизнес-сценария

Такой бизнес-сценарий не очень распространен, но кто-то наверняка сталкивался с ним.

В проекте настроен только один источник данных по умолчанию, и новые источники данных необходимо динамически добавлять во время определенной среды выполнения, а не настроенные статические несколько источников данных. Например, вам нужно перейти на сервер, чтобы прочитать информацию о конфигурации источника данных в режиме реального времени (не настроенную локально), а затем выполнить операции с базой данных.

В этом бизнес-сценарии три вышеуказанных метода неприменимы, поскольку указанные выше источники данных заранее подготовлены в yml-файле.

Реализовать идеи

За исключением шага 6, этого можно добиться с помощью кода, написанного ранее.

Идея такова:

(1) Создайте новый источник данных;

(2) DynamicDataSource регистрирует новый источник данных;

(3) Переключатель: установить текущий ключ источника данных потока, добавить временный источник данных;

(4) операции с базой данных (должны быть реализованы в другом сервисе, иначе транзакция не может контролироваться);

(5) Очистите текущий ключ источника данных потока и очистите временный источник данных;

(6) Очистить только что зарегистрированный источник данных;

(7) На данный момент он вернулся к источнику данных по умолчанию.

код

Код относительно грубый, но шаблон, вероятно, такой, в основном для того, чтобы выразить способ реализации.

Service A:

public String testUsingNewDataSource(){
        DynamicDataSource dynamicDataSource = RequestHandleMethodRegistry.getContext().getBean("dynamicDataSource", DynamicDataSource.class);
        try {
            //模拟从服务器读取数据源信息
            //..........................
            //....................
            
            //创建新数据源
            HikariDataSource dataSource = (HikariDataSource)                   DataSourceBuilder.create().build();
            dataSource.setJdbcUrl("jdbc:mysql://192.168.xxx.xxx:xxxx/xxxxx?......");
            dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
            dataSource.setUsername("xxx");
            dataSource.setPassword("xxx");
            
            //DynamicDataSource注册新数据源
            dynamicDataSource.addDataSource("test_ds_id", dataSource);

            //设置当前线程数据源key、添加临时数据源
            DataSourceUtil.switchDataSource("test_ds_id");

            //数据库操作(必须在另一个service实现,否则无法控制事务)
            serviceB.testInsert();
        }
        finally {
            //清理当前线程数据源key
            DynamicDataSourceContextHolder.removeContextKey();

            //清理刚刚注册的数据源
            dynamicDataSource.removeDataSource("test_ds_id");

        }
        return "aa";
    }

Service B:

@Transactional(rollbackFor = Exception.class)
    public void testInsert() {
        AddressPo po = new AddressPo();
        po.setMemberCode("555555555");
        po.setName("李郃");
        po.setPhone("16651694996");
        po.setProvince("江苏省");
        po.setCity("南京市");
        po.setArea("浦口区");
        po.setAddress("南京市浦口区宁六路219号");
        po.setDef(false);
        po.setCreateBy("23958");
        addressMapper.insert(po);
        //测试事务回滚
        int i = 1 / 0;
    }

DynamicDataSource: добавьте метод removeDataSource для очистки зарегистрированного нового источника данных.

public class DynamicDataSource extends AbstractRoutingDataSource {
    
            .................
            .................
            .................
    public void removeDataSource(String key){
        this.backupTargetDataSources.remove(key);
        super.setTargetDataSources(this.backupTargetDataSources);
        super.afterPropertiesSet();
    }
    
            .................
            .................
            .................
}

Четыре способа сравнения

Субподряд Параметрическое переключение Метод аннотации Метод динамического добавления
Применимая сцена Знайте, какой источник данных использовать при кодировании Среда выполнения определяет, какой источник данных использовать. Знайте, какой источник данных использовать при кодировании Динамически добавлять новые источники данных во время выполнения
режим переключения автоматический руководство автоматический руководство
путь картографа нужно заключать субподряд нет запроса нет запроса нет запроса
Нужно ли изменять класс конфигурации, чтобы добавить источник данных? нужно ненужный ненужный \
сложность реализации Простой сложный сложный сложный

деловая проблема

Используя описанный выше метод настройки источника данных, можно реализовать управление транзакциями с одним источником данных.

Например, в сервисном методе, когда для выполнения CUD необходимо использовать несколько источников данных, может быть реализовано управление транзакциями одного источника данных. Метод заключается в следующем: отдельное извлечение методов, которым требуется управление транзакциями, в другую службу, которая может реализовать управление транзакциями одного метода транзакции.

ServiceA:

public void updateMuilty(){
     serviceB.updateDb1();
     serviceB.updateDb2();
}

ServiceB:

@UsingDataSource(dataSourceId = "slave1")
@Transactional
public void updateDb1(){
    //业务逻辑......
}

@UsingDataSource(dataSourceId = "slave2")
@Transactional
public void updateDb2(){
    //业务逻辑......
}

Однако управлять транзакциями нескольких источников данных одним и тем же методом не так просто.Это относится к области распределенных транзакций.Вы можете рассмотреть возможность использования проекта с открытым исходным кодом atomikos для реализации распределенной обработки транзакций JTA или фреймворка Ali Fescar.

Из-за того, что задействован распределенный контроль транзакций, реализация усложняется, эта проблема только поднимается здесь, и я займу время, чтобы восполнить ее позже.

Справочная статья

1.Ляо Сюэфэн.com/article/118…Конфигурация базы данных Spring master-slave и принцип динамического переключения источников данных

2.blog.CSDN.net/Drink Coffee 2010/AR…Springcloud большинство библиотек Интеграция с несколькими источниками данных, база данных с динамическим переключением запросов

3.blog.CSDN.net/Вторник?/Ах…Два метода интеграции нескольких источников данных springboot-mybatis