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

задняя часть база данных Spring API

В больших приложениях настройка баз данных master-slave и использование разделения чтения и записи является распространенным шаблоном проектирования. В приложениях Spring для достижения разделения чтения-записи лучше всего не вносить изменения в существующий код, а поддерживать его прозрачно внизу.

Spring имеет встроенныйAbstractRoutingDataSource, он может настроить несколько источников данных на карту, а затем вернуть разные источники данных в соответствии с разными ключами. потому чтоAbstractRoutingDataSourceЭто также интерфейс DataSource, поэтому приложение может сначала установить ключ, а код для доступа к базе данных можно получить изAbstractRoutingDataSourceПолучить соответствующий реальный источник данных для доступа к указанной базе данных. Его структура выглядит следующим образом:

   ┌───────────────────────────┐
   │        controller         │
   │  set routing-key = "xxx"  │
   └───────────────────────────┘
                 │
                 ▼
   ┌───────────────────────────┐
   │        logic code         │
   └───────────────────────────┘
                 │
                 ▼
   ┌───────────────────────────┐
   │    routing datasource     │
   └───────────────────────────┘
                 │
       ┌─────────┴─────────┐
       │                   │
       ▼                   ▼
┌─────────────┐     ┌─────────────┐
│ read-write  │     │  read-only  │
│ datasource  │     │ datasource  │
└─────────────┘     └─────────────┘
       │                   │
       ▼                   ▼
┌─────────────┐     ┌─────────────┐
│             │     │             │
│  Master DB  │     │  Slave DB   │
│             │     │             │
└─────────────┘     └─────────────┘

Шаг 1. Настройте несколько источников данных

Сначала мы настраиваем два источника данных в SpringBoot, где второй источник данныхro-datasource:

spring:
  datasource:
    jdbc-url: jdbc:mysql://localhost/test
    username: rw
    password: rw_password
    driver-class-name: com.mysql.jdbc.Driver
    hikari:
      pool-name: HikariCP
      auto-commit: false
      ...
  ro-datasource:
    jdbc-url: jdbc:mysql://localhost/test
    username: ro
    password: ro_password
    driver-class-name: com.mysql.jdbc.Driver
    hikari:
      pool-name: HikariCP
      auto-commit: false
      ...

В среде разработки нет необходимости настраивать базу данных master-slave. Вам нужно только установить двух пользователей для базы данных, одинrwимеют права на чтение и запись,roСуществует только разрешение SELECT, которое имитирует разделение чтения и записи базы данных master-slave в производственной среде.

В конфигурации Scraphboot мы инициализируем два источника данных:

@SpringBootApplication
public class MySpringBootApplication {
    /**
     * Master data source.
     */
    @Bean("masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource")
    DataSource masterDataSource() {
       logger.info("create master datasource...");
        return DataSourceBuilder.create().build();
    }

    /**
     * Slave (read only) data source.
     */
    @Bean("slaveDataSource")
    @ConfigurationProperties(prefix = "spring.ro-datasource")
    DataSource slaveDataSource() {
        logger.info("create slave datasource...");
        return DataSourceBuilder.create().build();
    }

    ...
}

Шаг 2: Напишите RoutingDataSource

Затем мы используем встроенный в Spring RoutingDataSource для проксирования двух реальных источников данных в качестве источника динамических данных:

public class RoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return "masterDataSource";
    }
}

к этомуRoutingDataSource, который нужно настроить в SpringBoot и установить в качестве основного источника данных:

@SpringBootApplication
public class MySpringBootApplication {
    @Bean
    @Primary
    DataSource primaryDataSource(
            @Autowired @Qualifier("masterDataSource") DataSource masterDataSource,
            @Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource
    ) {
        logger.info("create routing datasource...");
        Map<Object, Object> map = new HashMap<>();
        map.put("masterDataSource", masterDataSource);
        map.put("slaveDataSource", slaveDataSource);
        RoutingDataSource routing = new RoutingDataSource();
        routing.setTargetDataSources(map);
        routing.setDefaultTargetDataSource(masterDataSource);
        return routing;
    }
    ...
}

Теперь RoutingDataSource настроен, но выбор маршрутизации жестко запрограммирован, то есть возвращается навсегда"masterDataSource",

Теперь наступает вопрос: как хранить динамически выбранный ключ и где установить ключ?

В модели многопоточности Servlet наиболее целесообразно использовать ThreadLocal для хранения ключей, поэтому мы пишем RoutingDataSourceContext для установки и динамического хранения ключей:

public class RoutingDataSourceContext implements AutoCloseable {

    // holds data source key in thread local:
    static final ThreadLocal<String> threadLocalDataSourceKey = new ThreadLocal<>();

    public static String getDataSourceRoutingKey() {
        String key = threadLocalDataSourceKey.get();
        return key == null ? "masterDataSource" : key;
    }

    public RoutingDataSourceContext(String key) {
        threadLocalDataSourceKey.set(key);
    }

    public void close() {
        threadLocalDataSourceKey.remove();
    }
}

Затем измените RoutingDataSource, и код для получения ключа выглядит следующим образом:

public class RoutingDataSource extends AbstractRoutingDataSource {
    protected Object determineCurrentLookupKey() {
        return RoutingDataSourceContext.getDataSourceRoutingKey();
    }
}

Таким образом, где-то, например, внутри метода контроллера, ключ источника данных может быть установлен динамически:

@Controller
public class MyController {
    @Get("/")
    public String index() {
        String key = "slaveDataSource";
        try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(key)) {
            // TODO:
            return "html... www.liaoxuefeng.com";
        }
    }
}

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

Этот метод выполним, но если вам нужно прочитать из базы данных, вам нужно добавить большой абзацtry (RoutingDataSourceContext ctx = ...) {}код, который очень неудобен в использовании. Есть ли способ упростить это?

имеют!

Давайте подумаем об этом, декларативное управление транзакциями, предоставляемое Spring, нуждается только в одном@Transactional()Аннотация, размещенная на методе Java, этот метод автоматически имеет транзакцию.

Мы также можем написать аналогичный@RoutingWith("slaveDataSource")Аннотация размещается на методе контроллера, и внутри этого метода автоматически выбирается соответствующий источник данных. Код должен выглядеть так:

@Controller
public class MyController {
    @Get("/")
    @RoutingWith("slaveDataSource")
    public String index() {
        return "html... www.liaoxuefeng.com";
    }
}

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

Чтобы написать меньше кода в приложении, мы должны сделать немного больше низкоуровневой работы: мы должны использовать механизм, аналогичный Spring, для реализации декларативных транзакций, то есть использовать АОП для реализации динамического переключения источников данных.

Также очень просто реализовать эту функцию, напишитеRoutingAspect, используя AspectJ для реализацииAroundПерехват:

@Aspect
@Component
public class RoutingAspect {
    @Around("@annotation(routingWith)")
    public Object routingWithDataSource(ProceedingJoinPoint joinPoint, RoutingWith routingWith) throws Throwable {
        String key = routingWith.value();
        try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(key)) {
            return joinPoint.proceed();
        }
    }
}

Обратите внимание на второй параметр методаRoutingWithэкземпляр аннотации, переданный Spring, мы используем аннотацию в соответствии сvalue()Получите настроенный ключ. Вам нужно добавить зависимость Maven перед компиляцией:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Пока мы реализовали функцию динамического выбора источников данных с аннотациями. Последний шаг рефакторинга — замена строковых констант, разбросанных повсюду."masterDataSource"а также"slaveDataSource".

ограничения на использование

Из-за ограничений модели многопоточности сервлета источник динамических данных не может быть установлен в запросе, а затем изменен, т. е.@RoutingWithНе может быть вложенным. также,@RoutingWithа также@TransactionalПри микшировании установите приоритет АОП.

Код в этой статье нуждается в поддержке SpringBoot, JDK 1.8 скомпилирован и открыт.-parametersпараметры компиляции.