项目背景:
在工作中,我们使用Flyway对数据库进行版本管理,每个微服务都有各自的数据库。最近我们需要对三个微服务合并成一个服务,且保留数据库不合并,有以下改动要求:
- 在不影响功能正常使用的情况下对代码结构尽量改动小
- 尽可能短的开发周期
- “分久必合,合久必分”,方便后面对服务的拆分
改动思路
- 三个工程包合并成一个大包,可以通过具体的子包名区分以前的三个服务
- Flyway脚本分开管理,单独作用在三个文件夹对应以前三个工程
- 引入多数据源(Dynamic Datasource),根据包名和文件名对应以前三个工程独立数据源
现有三个微服务Master、DB1和DB2,整合成一个demo工程示例,三个微服务通过包区分
代码分包如下:
依赖配置:
这里只展示部分主要的多数据源依赖和Flyway的依赖,详细的配置可查看文末下载demo资源。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- 多数据源依赖包 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!-- flyway依赖包 -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>5.2.1</version>
</dependency>
多数据源配置方案:
application.yml配置多数据源,spring.dynamic.datasource下对数据源进行命名配置连接信息
spring:
datasource:
dynamic:
primary: master #主数据源
datasource:
master:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/master?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimumIdle: 10
maximumPoolSize: 50
db1:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/db1?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimumIdle: 10
maximumPoolSize: 50
db2:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/db2?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=CONVERT_TO_NULL&useSSL=false&serverTimezone=GMT%2B8
username: root
password: 123456
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimumIdle: 10
maximumPoolSize: 50
对于三个工程来说,每个工程自己的数据源配置是独立的,自己包和xml也是独立的,互不影响。
注入HikariDatasource属性工具类
DataSourceConfigUtil
import com.zaxxer.hikari.HikariConfig;
import org.apache.commons.lang.StringUtils;
import org.springframework.core.env.Environment;
public class DataSourceConfigUtil {
public static HikariConfig setDataSourceEnvConfig(String prefix1, String prefix, Environment env) {
HikariConfig config = new HikariConfig();
String driver = env.getProperty(prefix1 + "." + "driver-class-name");
String dataSourceUrl = env.getProperty(prefix1 + "." + "url");
String user = env.getProperty(prefix1 + "." + "username");
String password = env.getProperty(prefix1 + "." + "password");
String minimumIdle = env.getProperty(prefix + "." + "minimumIdle");
String maximumPoolSize = env.getProperty(prefix + "." + "maximumPoolSize");
String autoCommit = env.getProperty(prefix + "." + "autoCommit");
String idleTimeout = env.getProperty(prefix + "." + "idleTimeout");
String poolName = env.getProperty(prefix + "." + "poolName");
String maxLifetime = env.getProperty(prefix + "." + "maxLifetime");
String connectionTimeout = env.getProperty(prefix + "." + "connectionTimeout");
String dataSourceClassName = env.getProperty(prefix + "." + "type");
if (StringUtils.isNotBlank(dataSourceUrl)) {
config.setJdbcUrl(dataSourceUrl);
}
if (StringUtils.isNotBlank(user)) {
config.setUsername(user);
}
if (StringUtils.isNotBlank(password)) {
config.setPassword(password);
}
if (StringUtils.isNotBlank(driver)) {
config.setDriverClassName(driver);
}
if (StringUtils.isNotBlank(minimumIdle)) {
config.setMinimumIdle(Integer.parseInt(minimumIdle));
}
if (StringUtils.isNotBlank(maximumPoolSize)) {
config.setMaximumPoolSize(Integer.parseInt(maximumPoolSize));
}
if (StringUtils.isNotBlank(autoCommit)) {
config.setAutoCommit(Boolean.parseBoolean(autoCommit));
}
if (StringUtils.isNotBlank(idleTimeout)) {
config.setIdleTimeout(Integer.parseInt(idleTimeout));
}
if (StringUtils.isNotBlank(poolName)) {
config.setPoolName(poolName);
}
if (StringUtils.isNotBlank(maxLifetime)) {
config.setMaxLifetime(Integer.parseInt(maxLifetime));
}
if (StringUtils.isNotBlank(connectionTimeout)) {
config.setConnectionTimeout(Integer.parseInt(connectionTimeout));
}
return config;
}
}
Master工程的数据源配置:
MasterDatasourceConfig配置
@Component
@MapperScan(basePackages = "com.example.flywaydemo.master.dao",sqlSessionFactoryRef = "db1SqlSessionFactory")
public class MasterDatasourceConfig {
private static final String MAPPER_LOCATION = "classpath:/mybatis/mapper/master/*Mapper.xml";
@Bean("master")
// @ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.master")
public DataSource getMasterDataSource(Environment env){
HikariConfig hikariConfig = DataSourceConfigUtil.setDataSourceEnvConfig("spring.datasource.dynamic.datasource.master", "spring.datasource.dynamic.datasource.master.hikari", env);
return new HikariDataSource(hikariConfig);
}
@Bean("masterSqlSessionFactory")
public SqlSessionFactory masterSqlSessionFactory(@Qualifier("master") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATION));
return bean.getObject();
}
@Bean("masterSqlSessionTemplate")
public SqlSessionTemplate masterSqlSessionTemplate(@Qualifier("masterSqlSessionFactory") SqlSessionFactory sqlSessionFactory){
return new SqlSessionTemplate(sqlSessionFactory);
}
}
DB1工程的数据源配置:
Db1DatasourceConfig配置
@Configuration
@MapperScan(basePackages = "com.example.flywaydemo.db1.dao",sqlSessionFactoryRef = "db1SqlSessionFactory")
public class Db1DatasourceConfig {
private static final String MAPPER_LOCATION = "classpath:/mybatis/mapper/db1/*.xml";
@Bean("db1")
// @ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.db1")
public DataSource getDb1DataSource(Environment env){
HikariConfig hikariConfig = DataSourceConfigUtil.setDataSourceEnvConfig("spring.datasource.dynamic.datasource.db1", "spring.datasource.dynamic.datasource.db1.hikari", env);
return new HikariDataSource(hikariConfig);
}
@Bean("db1SqlSessionFactory")
public SqlSessionFactory db1SqlSessionFactory(@Qualifier("db1") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATION));
return bean.getObject();
}
@Bean("db1SqlSessionTemplate")
public SqlSessionTemplate db1SqlSessionTemplate(@Qualifier("db1SqlSessionFactory") SqlSessionFactory sqlSessionFactory){
return new SqlSessionTemplate(sqlSessionFactory);
}
}
DB2工程的数据源配置:
Db2DatasourceConfig配置
@Configuration
@MapperScan(basePackages = "com.example.flywaydemo.db2.dao", sqlSessionFactoryRef = "db2SqlSessionFactory")
public class Db2DatasourceConfig {
private static final String MAPPER_LOCATION = "classpath:/mybatis/mapper/db2/*.xml";
@Bean("db2")
// @ConfigurationProperties(prefix = "spring.datasource.dynamic.datasource.db2")
public DataSource getDb2DataSource(Environment env) {
HikariConfig hikariConfig = DataSourceConfigUtil.setDataSourceEnvConfig("spring.datasource.dynamic.datasource.db2", "spring.datasource.dynamic.datasource.db2.hikari", env);
return new HikariDataSource(hikariConfig);
}
@Bean("db2SqlSessionFactory")
public SqlSessionFactory db2SqlSessionFactory(@Qualifier("db2") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(MAPPER_LOCATION));
return bean.getObject();
}
@Bean("db2SqlSessionTemplate")
public SqlSessionTemplate db2SqlSessionTemplate(@Qualifier("db2SqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
Flyway配置
application.yml关闭自启动校验,flyway.enabled=false
这里我们通过java代码flyway.migrate()进行脚本迁移,延后通过spring IOC注入的时候执行该步骤
flyway:
enabled: false
baseline-on-migrate: true
locations: /db/migration
FlywayConfig配置,这里通过构造器注入所有数据源dataSources
通过key => dbName, value => locations 循环执行脚本迁移检查
@Component
public class FlywayConfig {
private final Map<String, DataSource> dataSources;
@Value("${spring.flyway.locations}")
private String SQL_LOCATION;
@Value("${spring.flyway.baseline-on-migrate}")
private boolean BASELINE_ON_MIGRATE;
public FlywayConfig(Map<String, DataSource> dataSources) {
this.dataSources = dataSources;
}
@Bean
@PostConstruct
public void migrateOrder() {
List<String> locations = Arrays.stream(SQL_LOCATION.split(",")).collect(Collectors.toList());
// 根据数据源分别去构建属于这个数据源的locations的路径,用这个map保存,key:数据源名称。value:迁移脚本所在的路径
Map<String, List<String>> flywayLocals = new HashMap<>();
locations.forEach(location -> dataSources.forEach((k, v) -> {
String s = location + "/" + k;
List<String> flyways = flywayLocals.getOrDefault(k, new ArrayList<>());
flyways.add(s);
flywayLocals.put(k, flyways);
}));
//真正的触发迁移执行在这个循环里
flywayLocals.forEach((k, v) -> {
Flyway flyway = Flyway.configure()
.dataSource(dataSources.get(k))
.locations(v.toArray(new String[0]))
.baselineOnMigrate(BASELINE_ON_MIGRATE)
.load();
flyway.migrate();
});
}
}
运行结果