动态切换数据源的原理为使用 AbstractRoutingDataSource,根据数据源的名字查找数据源:
- 在 Spring IoC 容器中创建一个 AbstractRoutingDataSource 实现的对象 routingDataSource (Spring 默认没有使用此数据源)
- 创建一个数据源的 Map dataSourceMap,key 为 DataSource 的名字,value 为 DataSource 的对象
- 调用
routingDataSource.setTargetDataSources(dataSourceMap)
设置可供使用的数据源
- 在需要使用数据源的时候,Spring JDBC 会调用
routingDataSource.determineTargetDataSource()
获取数据源,而要返回哪个数据源,由 routingDataSource.determineCurrentLookupKey()
返回的数据源的名字决定
下面我们从最简单的方式实现数据源切换,然后一步一步的深入优化基于 AbstractRoutingDataSource 的数据源。
一、暴力实现
把下面的代码放到项目中,就实现了数据源的动态切换,会自动的轮流使用数据源一 master 和数据源二 slave。
配置数据源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| package com.xtuer.config;
import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource; import java.util.HashMap; import java.util.Map;
@Configuration public class DataSourceConfig { public DataSource masterDataSource() { return DataSourceBuilder.create() .username("root") .password("root") .url("jdbc:mysql://127.0.0.1:3306/test-1?useUnicode=true&characterEncoding=UTF-8&useSSL=false") .driverClassName("com.mysql.jdbc.Driver") .build(); }
public DataSource slaveDataSource() { return DataSourceBuilder.create() .username("root") .password("root") .url("jdbc:mysql://127.0.0.1:3306/test-2?useUnicode=true&characterEncoding=UTF-8&useSSL=false") .driverClassName("com.mysql.jdbc.Driver") .build(); }
@Bean public DataSource routingDataSource() { DataSource masterDataSource = this.masterDataSource(); DataSource slaveDataSource = this.slaveDataSource();
Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("master", masterDataSource); targetDataSources.put("slave", slaveDataSource);
RoutingDataSource rds = new RoutingDataSource(); rds.setTargetDataSources(targetDataSources);
rds.setDefaultTargetDataSource(masterDataSource);
return rds; } }
|
动态数据源
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| package com.xtuer.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class RoutingDataSource extends AbstractRoutingDataSource { private int count = 0;
@Override protected Object determineCurrentLookupKey() { String dataSourceName = count % 2 == 0 ? "master" : "slave"; System.out.println("获取数据源: " + dataSourceName); count++;
return dataSourceName; } }
|
验证多数据源
使用动态切换数据源的目的,大多数目标都是为了实现读写分离,不过配置 MySQL 的读写分离有点花时间,为了验证数据源的切换是否生效,可以采用下面的简单办法:
- 创建 2 个数据库: test-1, test-2
- 在它们里面分别创建结构完全一样的表 foo,只有 2 列: id 和 name
- test-1 中: id 为 1,name 为 test-1-name
- test-1 中: id 为 1,name 为 test-2-name
- 使用 MyBatis 的 Mapper 查询 id 为 1 的记录,如果看到轮流的输出 test-1-name 和 test-2-name,说明数据源切换生效了
数据库脚本:
1 2 3 4 5 6 7 8 9 10 11
| CREATE TABLE foo ( id INT(11) NOT NULL, name VARCHAR(64) NOT NULL, PRIMARY KEY (id) )
# 数据库 test-1 中执行 INSERT INTO foo (id, name) VALUES (1, 'test-1-name');
# 数据库 test-2 中执行 INSERT INTO foo (id, name) VALUES (1, 'test-2-name');
|
疑惑
在不少其他人的文章中,上面的 masterDataSource 和 slaveDataSource 会使用 @Bean
的方式自动创建,然后注入 routingDataSource()
但是我们没这么做,有以下几个原因:
- 在 Spring IoC 容器中,如果同时存在多个 DataSource 对象,有时候有可能会出错,例如引入分布式唯一 ID 生成器 uid-generator-spring-boot-starter,可能是由于 Mapper 存在于不同的包中导致的
- 不同环境中数据库的配置可能不同,读取数据源的配置动态生成数据源比较合理 (这里为了方便说明原理,忽略这种做法),而不是用函数固定写死,例如:
- 开发环境: 只有一个
- 测试环境: 一主一从
- 线上环境: 一主多从,甚至使用 MySQL 的黑洞引擎实现多主多从
- 方便创建数据库连接池: 在 Spring Boot 2.0 中,每个 DataSource 都会自动的为其创建一个连接池,但是使用的是默认配置,application.yml 中 HikariCP 的配置不会生效
到此我们已经知道了怎么实现动态的切换数据源,接下来的内容只是为了把事情做的更好,大家可以先看标题,然后思考一下如果是自己来做,应该怎么实现呢?
二、使用连接池
修改 masterDataSource() 和 slaveDataSource() 的代码,返回 HikariDataSource 即可使用数据库连接池 HikariCP,使用其他数据库连接池如 Druid 在此修改即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public DataSource masterDataSource() { HikariDataSource ds = new HikariDataSource(); ds.setUsername("root"); ds.setPassword("root"); ds.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/test-1?useUnicode=true&characterEncoding=UTF-8&useSSL=false"); ds.setDriverClassName("com.mysql.jdbc.Driver"); ds.setMaximumPoolSize(3);
return ds; }
public DataSource slaveDataSource() { HikariDataSource ds = new HikariDataSource(); ds.setUsername("root"); ds.setPassword("root"); ds.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/test-2?useUnicode=true&characterEncoding=UTF-8&useSSL=false"); ds.setDriverClassName("com.mysql.jdbc.Driver"); ds.setMaximumPoolSize(5);
return ds; }
|
连接池的配置都写在代码里不太好,可以把它们放到 application.yml 中:
1 2 3 4 5 6
| spring.datasource.hikari.minimumIdle : 5 spring.datasource.hikari.maximumPoolSize : 5 spring.datasource.hikari.idleTimeout : 30000 spring.datasource.hikari.maxLifetime : 2000000 spring.datasource.hikari.connectionTimeout: 30000
|
在 DataSourceConfig 中读取这些配置到 HikariConfig,用于创建 HikariDataSource:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| package com.xtuer.config;
import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource; import java.util.HashMap; import java.util.Map;
@Configuration public class DataSourceConfig { @Bean @ConfigurationProperties("spring.datasource.hikari") public HikariConfig hikariConfig() { return new HikariConfig(); }
public DataSource masterDataSource(HikariConfig hikariConfig) { hikariConfig.setUsername("root"); hikariConfig.setPassword("root"); hikariConfig.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/test-1?useUnicode=true&characterEncoding=UTF-8&useSSL=false"); HikariDataSource ds = new HikariDataSource(hikariConfig);
return ds; }
public DataSource slaveDataSource(HikariConfig hikariConfig) { hikariConfig.setUsername("root"); hikariConfig.setPassword("root"); hikariConfig.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/test-2?useUnicode=true&characterEncoding=UTF-8&useSSL=false"); HikariDataSource ds = new HikariDataSource(hikariConfig);
return ds; }
@Bean public DataSource routingDataSource(HikariConfig hikariConfig) { DataSource masterDataSource = this.masterDataSource(hikariConfig); DataSource slaveDataSource = this.slaveDataSource(hikariConfig);
Map<Object, Object> targetDataSources = new HashMap<>(); targetDataSources.put("master", masterDataSource); targetDataSources.put("slave", slaveDataSource);
RoutingDataSource rds = new RoutingDataSource(); rds.setTargetDataSources(targetDataSources);
rds.setDefaultTargetDataSource(masterDataSource);
return rds; } }
|
由于 username, password, url 因数据库而异,应该使用其他配置,而不能放在连接池的统一配置中,如:
1 2 3 4 5 6 7 8
| spring.datasource.master: username: root password: root url: jdbc:mysql://127.0.0.1:3306/test-1?useUnicode=true&characterEncoding=UTF-8&useSSL=false
spring.datasource.slaves: - { username: root, password: root, url: jdbc:mysql://192.168.0.90:3306/test-2?useUnicode=true&characterEncoding=UTF-8&useSSL=false } - { username: root, password: root, url: jdbc:mysql://192.168.0.91:3306/test-2?useUnicode=true&characterEncoding=UTF-8&useSSL=false }
|
读取数据库的配置和连接池的配置,替换代码中 routingDataSource()
创建数据源的部分,就可以根据不同的环境生成不同的动态数据源。
三、MyBatis 数据源配置
在 Spring Boot 2.0 中不需要配置下面的代码也会自动的使用我们注入的数据源 routingDataSource,Spring MVC 4 等没有试过,如果有需要时就给加上吧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Configuration @EnableTransactionManagement public class MyBatisConfig { @Resource(name = "routingDataSource") private DataSource dataSource;
@Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource); sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/*.xml"));
return sqlSessionFactoryBean.getObject(); }
@Bean public PlatformTransactionManager platformTransactionManager() { return new DataSourceTransactionManager(dataSource); } }
|
四、读写分离
读写分离的本质也是动态切换数据源,使用 Spring AoP 的 Before Advice 进行数据源切换,一般实现方式有:
- 在方法上使用注解明确的指定要使用写的数据源还是读的数据源
- 按照一定的规则命名读或写的方法,AoP 中根据方法名确定要使用读还是写的数据源,自动切换。方法的命名规则如:
- 读:
- findUserByName
- countUsers
- selectUser
- getUser
- 写:
- saveUser
- insertUser
- createUser
- updateUser
- upsertUser
- deleteUser
- 推荐参考 JPA 的命名规则
- 为了解决线程安全问题,可以使用 ThreadLocal 存储当前线程使用的数据源名称: Before Advice 的函数中确定数据源的名字,接下来 Spring 在
determineCurrentLookUpKey()
中使用此数据源名字
- 读库的负载均衡,可以使用简单的轮询方式使用读数据源
读写分离的实现主要就是 Spring AoP 的内容了,可以搜索其他人的文章进行学习,例如:
这里我们介绍通过切换数据源的方式实现读写分离,主要目的是介绍实现原理,此外还可以使用专业的分布式数据库中间件 MyCat,Apache ShardingSphere 等来实现。