Яма разделения чтения-записи базы данных, вы уже в яме

Архитектура
Яма разделения чтения-записи базы данных, вы уже в яме

Разделение чтения и записи базы данных

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

содержание

  • задний план
  • Запас знаний
  • проблемное явление
  • решение
  • краткое изложение проблемы

задний план

SpringbootинтегрированныйmybatisиспользоватьHikariCPсоединятьMySQL, чтобы выполнить разделение чтения и записи.

Запас знаний

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

Когда мы настраиваем источник динамических данных, мы наследуем классAbstractRoutingDataSource, Давайте посмотрим, что делает этот класс.

package com.fxb.doraemon.human;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.jdbc.datasource.lookup.DataSourceLookup;
import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;


/**
 * DataSource的实现:根据一个指定的key,调用各种的目标的数据源。 之后,通常根据一些线程的事务上下文来指定数据源。
 */
public abstract class AbstractRoutingDataSourceTest extends AbstractDataSource implements InitializingBean {

    /**
     * 目标数据源
     */
    @Nullable
    private Map<Object, Object> targetDataSources;

    /**
     * 默认的数据源
     */
    @Nullable
    private Object defaultTargetDataSource;

    /**
     * ??
     */
    private boolean lenientFallback = true;

    /**
     * 数据源查找??
     */
    private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup();

    @Nullable
    private Map<Object, DataSource> resolvedDataSources;

    /**
     * 被处理之后的数据源??
     */
    @Nullable
    private DataSource resolvedDefaultDataSource;


    /**
     * 根据指定查找的key获取目标数据源的映射,
     * 映射的值可以是一个DataSource的实例,也可以是String。如果是String的话,会通过setDataSourceLookup方法进行解析。
     * 指定的key可以是任意类型,这个类仅仅实现了通用的查找过程,具体的key标识将由resolveSpecifiedLookupKey(Object)方法和
     * determineCurrentLookupKey()进行解析。
     * 也就是说:
     * #1.如果我们自己定义了Map中的value是字符串的话,就需要重写setDataSourceLookup这个方法进行返回正确的数据源。
     * #2.如果我们自己定义了Map中的key的话,我们就需要重写resolveSpecifiedLookupKey方法和determineCurrentLookupKey()。
     */
    public void setTargetDataSources(Map<Object, Object> targetDataSources) {
        this.targetDataSources = targetDataSources;
    }

    /**
     * 指定默认的数据源(如果存在的话)
     * 如果我们自己定义了Map中的value是字符串的话,就需要重写setDataSourceLookup这个方法进行返回正确的数据源。
     * 如果根据指定的key在targetDataSources中找不到Datasource的时候,就用使用这个默认的数据源。
     */
    public void setDefaultTargetDataSource(Object defaultTargetDataSource) {
        this.defaultTargetDataSource = defaultTargetDataSource;
    }

    /**
     * 如果找不到指定的Datsource的时候,可以通知指定lenientFallback来确定是否使用默认数据源
     * true: 找不到就会使用默认数据源
     * false: 仅在key为null的时候进行回退。即当key为null的时候才使用默认的数据源,否则就会抛出IllegalStateException异常
     */
    public void setLenientFallback(boolean lenientFallback) {
        this.lenientFallback = lenientFallback;
    }

    /**
     * 解析setTargetDataSource中数据源名称是dataSource的情况,默认值是JndiDataSourceLookup。
     */
    public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup) {
        this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup());
    }


    @Override
    public void afterPropertiesSet() {
        if (this.targetDataSources == null) {
            throw new IllegalArgumentException("Property 'targetDataSources' is required");
        }
        this.resolvedDataSources = new HashMap<>(this.targetDataSources.size());
        this.targetDataSources.forEach((key, value) -> {
            Object lookupKey = resolveSpecifiedLookupKey(key);
            DataSource dataSource = resolveSpecifiedDataSource(value);
            this.resolvedDataSources.put(lookupKey, dataSource);
        });
        if (this.defaultTargetDataSource != null) {
            this.resolvedDefaultDataSource = resolveSpecifiedDataSource(this.defaultTargetDataSource);
        }
    }

    /**
     * 解析指定的key,对应着setTargetDataSources#2的情况。
     * 默认是直接返回
     */
    protected Object resolveSpecifiedLookupKey(Object lookupKey) {
        return lookupKey;
    }

    /**
     * 将指定的数据源对象解析为DataSource实例
     * 默认是通过:setDataSourceLookup进行解析。
     * 如果是字符串的话,可以用通过setDataSourceLookup设置自定义的dataSourceLookup。
     */
    protected DataSource resolveSpecifiedDataSource(Object dataSource) throws IllegalArgumentException {
        if (dataSource instanceof DataSource) {
            return (DataSource) dataSource;
        } else if (dataSource instanceof String) {
            return this.dataSourceLookup.getDataSource((String) dataSource);
        } else {
            throw new IllegalArgumentException(
                    "Illegal data source value - only [javax.sql.DataSource] and String supported: " + dataSource);
        }
    }


    @Override
    public Connection getConnection() throws SQLException {
        return determineTargetDataSource().getConnection();
    }

    @Override
    public Connection getConnection(String username, String password) throws SQLException {
        return determineTargetDataSource().getConnection(username, password);
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T unwrap(Class<T> iface) throws SQLException {
        if (iface.isInstance(this)) {
            return (T) this;
        }
        return determineTargetDataSource().unwrap(iface);
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return (iface.isInstance(this) || determineTargetDataSource().isWrapperFor(iface));
    }

    /**
     * 检索当前目标数据源。
     * 调用determineCurrentLookupKey获取key,在targetDataSources中进行查找,是否要会回退,使用默认数据源。
     * 如果找不到数据源就抛出IllegalStateException异常。
     */
    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = determineCurrentLookupKey();
        DataSource dataSource = this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }
        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        }
        return dataSource;
    }

    /**
     * 确定当前查找建。
     * 通常会实现这个方法类检测线程绑定的事务上下文.
     * 返回的key需要与targetDatasources这个Map中的key的类型进行匹配,由resolveSpecifiedLookupKey进行解析。
     */
    @Nullable
    protected abstract Object determineCurrentLookupKey();
}

Итак, каков процесс выполнения этого класса?

Когда программа хочет получить соединение с базой данных, она вызовет этоAbstractRoutingDataSourceизgetConnection(), конечно, на самом деле, поDataSource.getConnection()сделал звонок.

проблемное явление

Проблема в коде ниже

protected DataSource determineTargetDataSource() {
    Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
    Object lookupKey = determineCurrentLookupKey();
    DataSource dataSource = this.resolvedDataSources.get(lookupKey);
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
        dataSource = this.resolvedDefaultDataSource;
    }
    if (dataSource == null) {
        throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    }
    return dataSource;
}

Далее я продемонстрирую это тщательно. Когда я выполняю операцию сохранения, вызовитеdetermineCurrentLookupKey()не попал когдаlookupKey.Приводит к тому, что источник данных по умолчанию используется при получении источника данных (просто источник данных по умолчаниюMaster), и мы используемMasterИсточник данных (совпадение). Как показано ниже

Если я изменю источник данных по умолчанию наSlave1Шерстяная ткань? -> Хотя то, что мы печатаем в журнале,master, но на самом деле мы используемslave1

На самом деле эту проблему можно увидеть и при просмотре лога.

При сохранении пользователей используйтеmaster, распечатанный журнал: Используемый источник данных:master.Но!Выше есть еще два предложения

HikariPool-1 - Starting...
HikariPool-1 - Start completed.

При запросе пользователей используется источник данных slave 2. Ниже напечатано:

HikariPool-2 - Starting...
HikariPool-2 - Start completed.

Ну тут явление описано.В чем причина?

Я узнал позже, что я был в бизнес-кодеServiceслоистыйsaveдобавлено в методTransactionalаннотация. Другие методы не добавляются. Я добавил аннотацию к классу и обнаружил, что возникает та же проблема. Вот вопрос: добавить ли метод запросаTransactionalА аннотации?

Мое понимание:@1: если метод запроса включает в себя несколько операций запроса к базе данных, рекомендуется добавлять аннотации.@Transactional(readOnly=true)

@2: Зачем вы добавляете аннотации?TransactionalЭта проблема еще не изучена. Можно предположить, что транзакция открывается до входа в метод, но наше разделение чтения-записи — это источник данных, определяемый после входа в метод прокси. Таким образом, это приводит к звонку заранееdetermineTargetDataSourceметод, и когда мы используем плетение АОП, источник данных был определен и больше не будет вызыватьсяdetermineTargetDataSourceметод.

@3: Новый вопрос: почему на картинке выше первые два отпечатка?HikariPool-2 - Starting... HikariPool-2 - Start completed., последующий запрос не напечатал это предложение?

решение

Решение первое

Измените положение переплетения:

@Pointcut("@annotation(com.fxb.doraemon.human.annotation.Master) " +
    "|| execution(public * com.fxb.doraemon.human.service..*.save*(..)) " +
    "|| execution(public * com.fxb.doraemon.human.rest..*.insert*(..)) " +
    "|| execution(public * com.fxb.doraemon.human.rest..*.update*(..)) " +
    "|| execution(public * com.fxb.doraemon.human.rest..*.edit*(..)) " +
    "|| execution(public * com.fxb.doraemon.human.rest..*.delete*(..)) " +
    "|| execution(public * com.fxb.doraemon.human.rest..*.del*(..)) " +
    "|| execution(public * com.fxb.doraemon.human.rest..*.remove*(..)) ")
public void writePointcut() {
}

В этом решении есть проблема, то есть когда операция запроса включает в себя два запроса, когда вы хотите открыть транзакцию, вы не можетеget*,select*Назовем метод. Вам также необходимо использовать аннотацию к методу, чтобы указать используемый источник данных.

краткое изложение проблемы

Не просто смотрите на явление, но смотрите на суть. Проблема решена, давайте посмотрим на внутренний механизм работы.

дыра слева

@1: Вы хотите добавить аннотации транзакций к методу запроса? Если метод запроса включает в себя несколько операций запроса к базе данных, рекомендуется добавить аннотации.@Transactional(readOnly=true)@2: Зачем вы добавляете аннотации?TransactionalЭта проблема еще не изучена. @3: Новый вопрос: почему на картинке выше первые два отпечатка?HikariPool-2 - Starting... HikariPool-2 - Start completed., последующий запрос не напечатал это предложение?

В конце концов

Если вы хотите узнать последние новости, обратите внимание на публичный аккаунт: Fangjia Xiaobai, Добро пожаловать на обмен и совместное обучение.

期待你的关注