Java 主从数据库
SpringBoot主从数据库的配置和动态数据源切换原理 - 图1

第一步:配置多数据源

首先,在 SpringBoot 中配置两个数据源,其中第二个数据源是ro-datasource

  1. spring:
  2. datasource:
  3. jdbc-url: jdbc:mysql://localhost/test
  4. username: rw
  5. password: rw_password
  6. driver-class-name: com.mysql.jdbc.Driver
  7. hikari:
  8. pool-name: HikariCP
  9. auto-commit: false
  10. ...
  11. ro-datasource:
  12. jdbc-url: jdbc:mysql://localhost/test
  13. username: ro
  14. password: ro_password
  15. driver-class-name: com.mysql.jdbc.Driver
  16. hikari:
  17. pool-name: HikariCP
  18. auto-commit: false
  19. ...

在开发环境下,没有必要配置主从数据库。只需要给数据库设置两个用户,一个rw具有读写权限,一个ro只有 SELECT 权限,这样就模拟了生产环境下对主从数据库的读写分离。
在 SpringBoot 的配置代码中,初始化两个数据源:

  1. @SpringBootApplication
  2. public class MySpringBootApplication {
  3. /**
  4. * Master data source.
  5. */
  6. @Bean("masterDataSource")
  7. @ConfigurationProperties(prefix = "spring.datasource")
  8. DataSource masterDataSource() {
  9. logger.info("create master datasource...");
  10. return DataSourceBuilder.create().build();
  11. }
  12. /**
  13. * Slave (read only) data source.
  14. */
  15. @Bean("slaveDataSource")
  16. @ConfigurationProperties(prefix = "spring.ro-datasource")
  17. DataSource slaveDataSource() {
  18. logger.info("create slave datasource...");
  19. return DataSourceBuilder.create().build();
  20. }
  21. ...
  22. }

第二步:编写 RoutingDataSource

然后,用 Spring 内置的 RoutingDataSource,把两个真实的数据源代理为一个动态数据源:

  1. public class RoutingDataSource extends AbstractRoutingDataSource {
  2. @Override
  3. protected Object determineCurrentLookupKey() {
  4. return "masterDataSource";
  5. }
  6. }
  7. 对这个RoutingDataSource,需要在 SpringBoot 中配置好并设置为主数据源:
  8. @SpringBootApplication
  9. public class MySpringBootApplication {
  10. @Bean
  11. @Primary
  12. DataSource primaryDataSource(
  13. @Autowired @Qualifier("masterDataSource") DataSource masterDataSource,
  14. @Autowired @Qualifier("slaveDataSource") DataSource slaveDataSource
  15. ) {
  16. logger.info("create routing datasource...");
  17. Map<Object, Object> map = new HashMap<>();
  18. map.put("masterDataSource", masterDataSource);
  19. map.put("slaveDataSource", slaveDataSource);
  20. RoutingDataSource routing = new RoutingDataSource();
  21. routing.setTargetDataSources(map);
  22. routing.setDefaultTargetDataSource(masterDataSource);
  23. return routing;
  24. }
  25. ...
  26. }

现在,RoutingDataSource 配置好了,但是,路由的选择是写死的,即永远返回"masterDataSource"
现在问题来了:如何存储动态选择的 key 以及在哪设置 key?
在 Servlet 的线程模型中,使用 ThreadLocal 存储 key 最合适,因此,编写一个 RoutingDataSourceContext,来设置并动态存储 key:

  1. public class RoutingDataSourceContext implements AutoCloseable {
  2. // holds data source key in thread local:
  3. static final ThreadLocal<String> threadLocalDataSourceKey = new ThreadLocal<>();
  4. public static String getDataSourceRoutingKey() {
  5. String key = threadLocalDataSourceKey.get();
  6. return key == null ? "masterDataSource" : key;
  7. }
  8. public RoutingDataSourceContext(String key) {
  9. threadLocalDataSourceKey.set(key);
  10. }
  11. public void close() {
  12. threadLocalDataSourceKey.remove();
  13. }
  14. }

然后,修改 RoutingDataSource,获取 key 的代码如下:

  1. public class RoutingDataSource extends AbstractRoutingDataSource {
  2. protected Object determineCurrentLookupKey() {
  3. return RoutingDataSourceContext.getDataSourceRoutingKey();
  4. }
  5. }

这样,在某个地方,例如一个 Controller 的方法内部,就可以动态设置 DataSource 的 Key:

  1. @Controller
  2. public class MyController {
  3. @Get("/")
  4. public String index() {
  5. String key = "slaveDataSource";
  6. try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(key)) {
  7. // TODO:
  8. return "html... www.liaoxuefeng.com";
  9. }
  10. }
  11. }

到此为止,已经成功实现了数据库的动态路由访问。
这个方法是可行的,但是,需要读从数据库的地方,就需要加上一大段try (RoutingDataSourceContext ctx = ...) {}代码,使用起来十分不便。有没有方法可以简化呢?
有!
仔细想想,Spring 提供的声明式事务管理,就只需要一个@Transactional()注解,放在某个 Java 方法上,这个方法就自动具有了事务。
也可以编写一个类似的@RoutingWith("slaveDataSource")注解,放到某个 Controller 的方法上,这个方法内部就自动选择了对应的数据源。代码看起来应该像这样:

  1. @Controller
  2. public class MyController {
  3. @Get("/")
  4. @RoutingWith("slaveDataSource")
  5. public String index() {
  6. return "html... www.liaoxuefeng.com";
  7. }
  8. }

这样,完全不修改应用程序的逻辑,只在必要的地方加上注解,自动实现动态数据源切换,这个方法是最简单的。
想要在应用程序中少写代码,就得多做一点底层工作:必须使用类似 Spring 实现声明式事务的机制,即用 AOP 实现动态数据源切换。
实现这个功能也非常简单,编写一个RoutingAspect,利用 AspectJ 实现一个Around拦截:

  1. @Aspect
  2. @Component
  3. public class RoutingAspect {
  4. @Around("@annotation(routingWith)")
  5. public Object routingWithDataSource(ProceedingJoinPoint joinPoint, RoutingWith routingWith) throws Throwable {
  6. String key = routingWith.value();
  7. try (RoutingDataSourceContext ctx = new RoutingDataSourceContext(key)) {
  8. return joinPoint.proceed();
  9. }
  10. }
  11. }

注意方法的第二个参数RoutingWith是 Spring 传入的注解实例,根据注解的value()获取配置的 key。编译前需要添加一个 Maven 依赖:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-aop</artifactId>
  4. </dependency>

到此为止,就实现了用注解动态选择数据源的功能。最后一步重构是用字符串常量替换散落在各处的"masterDataSource""slaveDataSource"

使用限制

受 Servlet 线程模型的局限,动态数据源不能在一个请求内设定后再修改,也就是@RoutingWith不能嵌套。此外,@RoutingWith@Transactional混用时,要设定 AOP 的优先级。
本文代码需要 SpringBoot 支持,JDK 1.8 编译并打开-parameters编译参数。