Анализ исходного кода dynamic-datasource-spring-boot-starter

Java

анализ исходного кода динамических источников данных

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

Это решение с несколькими источниками данных, разработанное MP (Mybatis-plus), которое используется многими людьми.

Шаги анализа

Автоматическая конфигурация

  1. Во-первых, это стартер SpringBoot, так что давайте начнем сspring.factoriesНачать.

    • Найдено, чтобы помочь нам автоматически настроитьDynamicDataSourceAutoConfiguration
  2. ПроверятьDynamicDataSourceAutoConfigurationКласс конфигурации.

    • Давайте сначала рассмотрим более важные заметки
    @EnableConfigurationProperties(DynamicDataSourceProperties.class)
    @AutoConfigureBefore(DataSourceAutoConfiguration.class)
    @Import(value = {DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class})
    @ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
    
    • Давайте взглянем

      • DynamicDataSourceProperties — это значения свойств, которые мы можем настроить в yml, и эти конфигурации будут сопоставлены с этим классом объектов.
      • AutoConfigureBefore, чтобы предотвратить запуск SpringBoot по умолчаниюDataSourceAutoConfigurationконфликт, установите эту конфигурацию для настройки перед ее автоматической настройкой.
      • ИМПОРТ, ввод 2-х конфигураций в контейнеры: beandefinition:
        • DruidDynamicDataSourceConfiguration: повторно использовать автоматическую конфигурацию Druid.
        • DynamicDataSourceCreatorAutoConfiguration: этот класс конфигурации в основном используется для внедрения bean-компонентов создателя DataSource в контейнер. Есть 4 создателя (по умолчанию, JNDI, Druid, Hikari)
      • ConditionalOnProperty указывает, что его можно настроить с помощьюspring.datasource.dynamic.enable=falseчтобы отключить настройку динамического источника данных
    • Затем измените класс конфигурации, чтобы внедрить в контейнер следующие bean-компоненты:

      • DynamicDataSourceProvider: провайдер принимает информацию о конфигурации нескольких источников данных в файле конфигурации и предоставляет методloadDataSourcesИспользуется для загрузки нескольких источников данных.

        @AllArgsConstructor
        public class YmlDynamicDataSourceProvider extends AbstractDataSourceProvider {
            /**
             * 配置文件中数据源的配置信息
             */
            private final Map<String, DataSourceProperty> dataSourcePropertiesMap;
          	// 可以调用该方法,来加载DataSource对象。具体的创建是由creator来创建的
          	// 这个creator是DynamicDataSourceCreatorAutoConfiguration注入的
            @Override
            public Map<String, DataSource> loadDataSources() {
                return createDataSourceMap(dataSourcePropertiesMap);
            }
        }
        
      • DataSource: Это схема реализации динамического источника данных.В этой схеме есть только один DataSource глобально, который является пользовательским DataSource:DynamicRoutingDataSource.

        • Суть пользовательского источника данных заключается в том, чтобы хранить все источники данных во внутренней карте, а затем использовать первичный источник для определения того, какой источник данных используется по умолчанию.
        • Создание этого источника данных зависит от поставщика, созданного выше. После установки всех свойств DynamicRoutingDataSource он будет вызыватьсяloadDataSourcesСпособ получения источника данных.
      • DynamicDataSourceAnnotationAdvisor: внедрить в контейнер аспект АОП: в основном Interceptor и указанный PointCut, основная логика переключения источника данных находится здесь.

      • Advisor:dynamicTransactionAdvisor, Поддержка транзакций в нескольких источниках данных.В первые дни несколько источников данных не поддерживали транзакции: если вам нужны транзакции, вы должны использовать seata для распределенных транзакций. Это подробно обсуждается ниже.

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

    • На этом автоматическая настройка завершена.

  3. Мы его погладили, автоматически настроили на участие в нескольких важных объектах

    1. создатель: Создатель, который фактически создает объект DataSource источника данных, инкапсулирует метод анализа DataSourceProperty и создания его в объекте DataSource.
    2. поставщик: поставщик источника данных, который внутренне удерживает создателя для создания объектов и предоставляетloadDataSourcesМетод возвращает все источники данных.
    3. АОП-аспект: один для перехвата переключения источника данных и один для обработки транзакций.
    4. процессор: процессор, в основном анализируемый из аннотаций, информация об источнике данных, которую необходимо переключить.

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

Переключение источника данных Baomidou в основном осуществляется с помощью аннотации @DS для переключения источника данных. Эта часть логики в основном использует аспектную логику АОП. Наша запись в основном относится к первому классу аспектов, упомянутому в приведенной выше автоматической конфигурации, которая в основном включает два класса:DynamicDataSourceAnnotationAdvisor, DynamicDataSourceAnnotationInterceptor.

DynamicDataSourceAnnotationAdvisor

Этот класс в основном определяет точки:

    private Pointcut buildPointcut() {
        Pointcut cpc = new AnnotationMatchingPointcut(DS.class, true);
        Pointcut mpc = new AnnotationMethodPoint(DS.class);
        return new ComposablePointcut(cpc).union(mpc);
    }

Вы можете видеть, что здесь перехвачена аннотация @DS. (Метод без аннотации, принятый реализацией аспекта АОП Baomidou, но метод Advisor)

Конкретный метод обработки перехвата выглядит следующим образомDynamicDataSourceAnnotationInterceptorв.

DynamicDataSourceAnnotationInterceptor

Содержит 2 важных свойства:

//加入扩展, 给外部一个修改aop条件的机会 (这个机会其实就是让我们可以配置是否只处理public的方法)
private final DataSourceClassResolver dataSourceClassResolver;
// 这是我们上面提到的,主要为了解析@DS里面内容的(因为DS可能是个表达式)
private final DsProcessor dsProcessor;

Этот класс реализуетMethodInterceptor, поэтому наша основная логика включения таковаinvokeметод.

@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
  	// 拿到需要切换数据源的key
    String dsKey = determineDatasourceKey(invocation);
  	// 设置当前线程使用的DataSource为该key的DataSource
    DynamicDataSourceContextHolder.push(dsKey);
    try {
      	// 执行原逻辑
        return invocation.proceed();
    } finally {
        // 使用完了,弹出,因为@DS是可以嵌套的,我们应该将数据源设置为之前的数据源
        DynamicDataSourceContextHolder.poll();
    }
}

Вот суть переключения источников данных! ! !

ключевая логика
как определить источник данных
private String determineDatasourceKey(MethodInvocation invocation) {
    String key = dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());
    return (!key.isEmpty() && key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;
}

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

Логика разбора ключей:

public String findDSKey(Method method, Object targetObject) {
    if (method.getDeclaringClass() == Object.class) {
        return "";
    }
    Object cacheKey = new MethodClassKey(method, targetObject.getClass());
    String ds = this.dsCache.get(cacheKey);
    if (ds == null) {
        ds = computeDatasource(method, targetObject);
        if (ds == null) {
            ds = "";
        }
        this.dsCache.put(cacheKey, ds);
    }
    return ds;
}

Видно, что ядроds = computeDatasource(method, targetObject);этот абзац. Кроме того, Bomidou кэширует все методы для повышения эффективности парсинга ключей.

computeDatasourceМетод относительно прост: найти аннотацию @DS уровень за уровнем (от текущего метода к объекту), а затем отразить значение аннотации.

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

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

Ядром переключения являетсяDynamicDataSourceContextHolderв классе! Этот класс внутренне содержит ThreadLocal:

private static final ThreadLocal<Deque<String>> LOOKUP_KEY_HOLDER = new NamedThreadLocal<Deque<String>>("dynamic-datasource") {
    @Override
    protected Deque<String> initialValue() {
        return new ArrayDeque<>();
    }
};

ThreadLocal эквивалентен назначению каждому потоку очереди ArrayDeque.Хотя это и очередь, она используется как стек. Что касаетсяпричина, потому что ArrayDeque более эффективен, чем Stack.

Почему это должен быть стек

Поскольку наши вызовы часто являются вложенными: A->B->C, когда выполнение C завершено, источник данных должен быть урезан до источника данных B, и поэтому он должен быть реализован в структуре стека.

Обработка транзакции

В предыдущей версии динамический источник данных поддерживал только транзакции с одной базой данных, то есть другие операции переключения источника данных не допускаются во всей цепочке вызовов, и при их наличии будет сообщено об ошибке. Потому что после того, как транзакция открыта, менеджер транзакций Spring гарантирует, что весь поток в рамках транзакции впоследствии получит одно и то же соединение. Если вы хотите поддерживать транзакции, вам нужно интегрировать Seata для выполнения распределенных задач. Но интеграция сиденья — это более тяжеловесно.

В новой версии добавлена ​​аннотация @DSTransactional для разрешения локальных транзакций. Недостатком является то, что он находится вне механизма транзакций Spring и не можетСмешанное использование. Это отдельный механизм обработки транзакций, который не имеет никакого отношения к Spring, посмотрим, как он это делает.

Различают распределенные транзакции и локальные транзакции. Локальная транзакция: относится к одной службе, ниже есть несколько баз данных, и подойдет атрибут ACID нашей серии транзакций операций с базой данных. Распределенные вещи: относится к нескольким службам, и интерфейс каждой службы может соответствовать 1+ библиотекам.В настоящее время гарантия находится между этими службами, поэтому сложность реализации будет больше, чем у локальных транзакций, поэтому сита более тяжеловесна

@Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnProperty(prefix = DynamicDataSourceProperties.PREFIX, name = "seata", havingValue = "false", matchIfMissing = true)
@Bean
public Advisor dynamicTransactionAdvisor() {
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    pointcut.setExpression("@annotation(com.baomidou.dynamic.datasource.annotation.DSTransactional)");
    return new DefaultPointcutAdvisor(pointcut, new DynamicTransactionAdvisor());
}

Во-первых, он внес некоторые изменения в источник данных:

public Connection getConnection() throws SQLException {
    String xid = TransactionContext.getXID();
    // 当前线程 LOCAL_XID 为空,说明不处于事务中
    if (StringUtils.isEmpty(xid)) {
        // 返回 不带事务的原始connection
        return determineDataSource().getConnection();
    } else {
        // 处于事务中,则获取一个该数据源的 代理connection
        String ds = DynamicDataSourceContextHolder.peek();
        ConnectionProxy connection = ConnectionFactory.getConnection(ds);
        // 该线程已经有了,就直接获取,没有则创建
        return connection == null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;
    }
}

При каждом getConnection класс TransactionContext используется для определения того, находится ли sql в транзакции, если нет, то используется исходное соединение, и если да, то возвращается прокси-соединение.

Затем в pointcut:

public Object invoke(MethodInvocation methodInvocation) throws Throwable {
    if (!StringUtils.isEmpty(TransactionContext.getXID())) {
        //注解了@DSTransaction的 有xid 直接执行
        return methodInvocation.proceed();
    }
    // 注解了@DSTransaction的 还没有xid的 加上xid
    boolean state = true;
    Object o;
    String xid = UUID.randomUUID().toString();
    TransactionContext.bind(xid);
    try {
        o = methodInvocation.proceed();
    } catch (Exception e) {
        state = false;
        throw e;
    } finally {
        // 执行失败,通知所有进行回滚
        ConnectionFactory.notify(state);
        TransactionContext.remove();
    }
    return o;
}

Если аннотация выполняетсяDSTransactionalМетод аннотации, но TransactionContext воспринимает, что состояние не находится в транзакции в это время, затем генерируется xid, который затем привязывается к TransactionContext, чтобы пометить текущий поток в транзакции. То есть логика помечается как транзакционная, а все прокси-соединения получаются позже.

Если во время выполнения метода возникает исключение, все текущие прокси-соединения потока откатываются.ConnectionFactory.notify(state);.

На официальном сайте говорится, что в настоящее время это временная версия, и рекомендуется протестировать ее локально перед использованием в Интернете. На самом деле я не чувствую себя хорошо в этой реализации, и логика кажется сложной. Я полностью храню карту в ThreadLocal, которая может хранить все необходимые соединения. Тогда это прокси-соединение не кажется необходимым.

Почему транзакция Spring не работает?

Когда транзакция Spring AOP, менеджер транзакций принудительно привязывается к Connection. Когда он запускает новую транзакцию, он получает экземпляр соединения из пула соединений и связывает транзакцию и соединение друг с другом.

После этого в транзакции будет использоваться только это соединение, и в это время это соединение будет использоваться только в одной транзакции. Следовательно, независимо от того, сколько раз БД будет работать в этой транзакции, она будет только экземпляром соединения, пока транзакция не будет зафиксирована или откатана. Когда транзакция фиксируется или откатывается, связь между транзакцией и соединением будет разорвана, и в то же время соединение будет возвращено в пул.

Суммировать

Реализация динамического источника данных проста: АОП + аннотация + стек ThreadLocal для решения.

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