В больших приложениях настройка баз данных 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
параметры компиляции.