目录
一、问题背景
1.1 mysql读写分离
1.2 适配多种类型数据库
1.3 多数据源
二、适配多数据源场景和问题
2.1 支持快速切换其他数据源
2.2 代码层面最小化改造
2.3 数据迁移问题
2.4 跨库事务问题
三、多数据源适配解决方案
3.1 自己造轮子
3.2 基于providerId方式
3.3 基于dynamic-datasource方式
3.3.1 dynamic-datasource介绍
3.4 自定义SDK嵌入方式
四、案例操作演示
4.1 前置准备
4.2 基于providerId适配方案
4.2.1 导入工程依赖
4.2.2 三个核心配置文件
4.2.3 providerId核心配置类
4.2.4 自定义测试接口
4.2.5 mybatis实现
4.2.6 效果演示
4.3 基于dynamic-datasource适配方案
4.3.1 导入基础依赖
4.3.2 核心配置文件
4.3.3 dao接口
4.3.4 自定义测试接口
4.3.5 接口效果测试
4.3.5 使用dynamic-datasource注意点
4.4 基于SDK的适配方案
4.4.1 公共配置类
4.4.2 发布jar
4.4.3 在其他模块引入SDK
4.4.4 模拟效果验证
五、写在文末
一、问题背景
随着业务的发展和变更,你的springboot工程中连接单一数据源或单一类型数据库的模式可能需要调整,比如下面这些场景下你可能需要适配多数据源。
1.1 mysql读写分离
比如说,你的项目一开始使用的是mysql数据库,工程中连接一个mysql数据库实例就行了,后来随着业务发展壮大,单库已无法承载较高流量的读写了,所以需要对数据库扩展,此时你的工程中可能需要进行读写分离的模式,即连接多个数据源的配置。
1.2 适配多种类型数据库
最近几年,越来越多的项目开始重视数据安全的问题,所以国产数据库在最近两年开始崛起,如果一开始你的项目使用的是mysql,后续项目为了适应市场监管要求或客户的需求,逐步引入其他数据库,此时在你的项目中需要同时兼容多类型数据库,即你的项目需要同时适配mysql,oracle,pg等。
1.3 多数据源
在你的项目中可能遇到这么一种情况,一个接口返回的数据是来自于多个不同的数据库实例,或者是同时来自于mysql和oracle数据的聚合,在这种情况下,就需要你的工程中支持同时连接多个数据源的配置,这在某些工具类的项目中是一个比较常见的场景。
二、适配多数据源场景和问题
根据上述不同的业务形态,具体到实际的业务中,又可以细分成不同的使用场景,这里结合实际经验列举下面几种常用的多数据源适配场景。
2.1 支持快速切换其他数据源
比如说你的系统原本是以mysql为底层数据存储,但是项目上需要更换为pg,或其他国产数据库,这种情况下,这就要求你的项目具备一定的底层存储动态切换能力,设想,在这个项目需要mysql,其他的项目需要PG,最快的解决方式是什么呢?没毛病,通过动态的参数配置快速切换数据源。
2.2 代码层面最小化改造
适配过程中一个绕不开的话题就是需要对现有的项目或架构进行局部的调整,这一点无法避免,当然具体实施过程中,还需要看你项目的技术架构,如果是基于springboot+mybatis这一套技术栈做的,相对来说改造量还是可以控制的。一个比较好的做法是,通过开发外部插件,或者SDK的方式,然后由微服务中各个模块统一接入即可,这在那些微服务众多的平台类型的项目中比较适用。
2.3 数据迁移问题
当一个稳定运行的生产项目需要从mysql迁移到pg或其他数据库时,技术层面支持了,也可以切换了,但是那些历史数据怎么办呢?总不能就撒手不管了吧,关于这一点是适配多数据源一个必须要考虑的问题。
2.4 跨库事务问题
当程序只操作mysql时,程序代码中可以利用mysql自身的事务机制来保证数据的安全性,现在假如你的项目需要同时连接多个数据源,比如一个接口需要同时操作mysql和pg,恐怕仅仅使用mysql的事务就不好使了,这一点也是适配多数据源中一个需要解决的问题。
三、多数据源适配解决方案
以大家熟悉的springboot+mysql+mybatis这样一个技术栈为例进行说明,也是很多微服务项目的基础框架,结合上面的谈到的一些问题,下面列举几种可以实践的解决方案。
3.1 自己造轮子
顾名思义,就是自己封装适配多数据源的组件,最常见的一种方式就是:自定义注解+AOP的方式实现动态切换多数据源(这种做法网上有不少参考资料)。多数据源适配。
举例来说,可能你的项目需要同时连接DS1和DS2两个数据源,DS1作为Master主库,DS2作为Slave从库,自己封装完组件后,在需要操作的方法块上添加对应的注解,实际在执行SQL的时候就可以将SQL语句路由到指定的数据库实例去执行。
3.2 基于providerId方式
providerId是mybatis框架在解析sql语句时一种自带的方式,简单理解就是,通过providerId可以动态的区分数据源,关于providerId做如下补充:
- databaseId属性: 如果配置了 databaseIdProvider,MyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;
- 如果带或者不带的语句都有,则不带的会被忽略。新增,修改和删除都有这个属性;
3.3 基于dynamic-datasource方式
dynamic-datasource是一个很实用的用于做多数据源动态切换的第三方组件,也是一种不错的解决方案,关于dynamic-datasource做如下几点介绍;
3.3.1 dynamic-datasource介绍
官方文档:文档地址
dynamic-datasource-spring-boot-starter是一个基于springboot的快速集成多数据源的启动器。
特征:
- 支持 数据源分组 ,适用于多种场景 纯粹多库 读写分离 一主多从 混合模式;
- 支持数据库敏感配置信息 加密 ENC();
- 支持每个数据库独立初始化表结构schema和数据库database;
- 支持无数据源启动,支持懒加载数据源(需要的时候再创建连接);
- 支持 自定义注解 ,需继承DS(3.2.0+);
- 提供并简化对Druid,HikariCp,BeeCp,Dbcp2的快速集成;
- 提供对Mybatis-Plus,Quartz,ShardingJdbc,P6sy,Jndi等组件的集成方案;
- 提供 自定义数据源来源 方案(如全从数据库加载);
- 提供项目启动后 动态增加移除数据源 方案;
- 提供Mybatis环境下的 纯读写分离 方案;
- 提供使用 spel动态参数 解析数据源方案。内置spel,session,header,支持自定义;
- 支持 多层数据源嵌套切换 。(ServiceA >>> ServiceB >>> ServiceC);
- 提供 基于seata的分布式事务方案;
- 提供 本地多数据源事务方案。 附:不能和原生spring事务混用;
几个约定
- 本框架只做 切换数据源 这件核心的事情,并不限制你的具体操作,切换了数据源可以做任何CRUD;
- 配置文件所有以下划线 _ 分割的数据源 首部 即为组的名称,相同组名称的数据源会放在一个组下;
- 切换数据源可以是组名,也可以是具体数据源名称。组名则切换时采用负载均衡算法切换;
- 默认的数据源名称为
master
,你可以通过spring.datasource.dynamic.primary
修改; - 方法上的注解优先于类上注解;
DS
支持继承抽象类上的DS
,暂不支持继承接口上的DS;
3.4 自定义SDK嵌入方式
针对那些微服务众多的平台级别的项目,为了尽可能减少各应用适配的工作量,可以考虑统一封装适配多数据源的SDK,然后各微服务引入SDK,然后进行代码层面的少许改动,这也是行业内一种相对通用的做法。比如大家在项目中升级某个中间件版本的时候,几乎是无感的。
四、案例操作演示
4.1 前置准备
为了后面的代码中演示方便,需要准备两种数据库环境,分别是mysql和postgresql,然后创建一张测试用的表tb_user,建表sql如下,注意在mysql和pg中各自创建一个相同的表;
create table tb_user(
id varchar(32) NOT NULL,
user_name varchar(32) NOT NULL,
email varchar(32) NOT NULL,
pass_word varchar(32) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
分别向mysql和pg相同的表插入一条数据
mysql中插入一条数据
insert into tb_user(id,user_name,email,pass_word) values("001","jerry","001@qq.com","123456");
pg中插入一条数据
insert into tb_user(id,user_name,email,pass_word) values("001","jerry","100@qq.com","1234567");
4.2 基于providerId适配方案
从实践来说,基于providerId是一种相对比较优雅,并且适配过程中代码改动也比较少的方式,上面简单介绍过providerId的原理,下面就直接看具体的代码实现过程吧。工程结构如下:
4.2.1 导入工程依赖
只需导入必须的依赖即可
<dependencies>
<!-- postgresSql -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<!-- 集成mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.16</version>
</dependency>
<!-- druid数据库连接池组件 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
</dependencies>
4.2.2 三个核心配置文件
需要说明的是,基于providerId这种方式的实现,是为了满足生产环境下快速的从某种数据库切换到另一种数据库,所以这里通过加载不同的外部配置文件的方式实现数据源的切换;
application.yml,公共配置
# 切换对应的环境 postgresql mysql
spring:
profiles:
active: postgresql
# mybatis配置
mybatis:
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.congge.entity
configuration:
map-underscore-to-camel-case: true
# showSql 控制台打印sql日志
logging:
level:
com:
valten:
dao: debug
system:
sql:
types: mysql,postgresql
application-mysql.yml,mysql配置
# 端口
server:
port: 8081
# 数据源配置
spring:
datasource:
hikari:
jdbc-url: jdbc:mysql://IP:3306/biz-db?&useSSL=false
driver-class-name: com.mysql.jdbc.Driver
username: root
password: 123456
application-postgresql.yml,pg配置文件
# 端口
server:
port: 8081
# 数据源配置
spring:
datasource:
hikari:
jdbc-url: jdbc:postgresql://IP:5432/biz-db
driver-class-name: org.postgresql.Driver
username: postgres
password: password
4.2.3 providerId核心配置类
该类主要做的事情如下:
- 创建数据源DataSource;
- 告诉mybatis支持哪些类型的数据库,这样在mybatis的xml文件中,就可以通过指定databaseId的方式被mybatis框架正确解析了;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.mapping.VendorDatabaseIdProvider;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.env.Environment;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.StringUtils;
import javax.sql.DataSource;
import java.util.*;
@Configuration
public class DbDataSourceConfig {
@Value("${mybatis.mapper-locations}")
private String mapperLocations;
@Autowired
private Environment environment;
static Map<String,String> sqlTypeMap = new HashMap<>();
static {
sqlTypeMap.put("Oracle", "oracle");
sqlTypeMap.put("MySQL", "mysql");
sqlTypeMap.put("PostgreSQL", "postgresql");
sqlTypeMap.put("DB2", "db2");
sqlTypeMap.put("SQL Server", "sqlserver");
}
@Primary
@Bean(name = "dataSource")
@ConfigurationProperties("spring.datasource.hikari")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource());
}
@Bean
public DatabaseIdProvider databaseIdProvider() {
DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
Properties p = new Properties();
String sqlTypeCollections = environment.getProperty("system.sql.types");
if(StringUtils.isEmpty(sqlTypeCollections)){
sqlTypeMap.forEach((key,val) ->{
p.setProperty(key, val);
});
}else {
List<String> sqlTypeList = Arrays.asList(sqlTypeCollections.split(","));
for(String sqlType : sqlTypeList){
if("mysql".equals(sqlType)){
p.setProperty("MySQL", "mysql");
}else if("oracle".equals(sqlType)){
p.setProperty("Oracle", "oracle");
}else if("postgresql".equals(sqlType)){
p.setProperty("PostgreSQL", "postgresql");
}else if("db2".equals(sqlType)){
p.setProperty("DB2", "db2");
}else if("sqlserver".equals(sqlType)){
p.setProperty("SQL Server", "sqlserver");
}
}
}
databaseIdProvider.setProperties(p);
return databaseIdProvider;
}
@Primary
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("dataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setDatabaseIdProvider(databaseIdProvider());
if(StringUtils.isEmpty(mapperLocations)){
mapperLocations = "classpath*:*.xml";
}
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
return factoryBean;
}
}
4.2.4 自定义测试接口
应该说自定义完成了providerId的配置类之后,其他地方要做的就是和之前的开发一样的方式了,即正常编码即可,这里为了便于工程的目录管理,建议在编写xml文件时创建多个目录,比如mysql的xml在一个目录,而pg在pg的目录,后续可能还有oracle的适配,为什么这么做呢?从适配经验来看,不同类型的数据库,在某些sql语法上还是存在一定的差异的,但是只需要在xml中通过databaseId即可区分开来,新增如下接口;
@RestController
public class UserController {
@Autowired
private UserService userService;
//localhost:8081/getById?id=001
@GetMapping("/getById")
public TbUser getTbUser(String id){
return userService.getTbUser(id);
}
}
4.2.5 mybatis实现
比如以mysql为例,上面最终的查询sql如下,
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.congge.dao.TbUserMapper">
<resultMap id="mysqlResultMap" type="com.congge.entity.TbUser">
<result property="id" column="id"/>
<result property="userName" column="user_name"/>
<result property="email" column="email"/>
<result property="passWord" column="pass_word"/>
</resultMap>
<select id="selectById" parameterType="java.lang.String" resultMap="mysqlResultMap" databaseId="mysql">
select
*
from
tb_user
where id = #{id}
</select>
</mapper>
如果是pg环境,sql如下
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.congge.dao.TbUserMapper">
<resultMap id="pgResultMap" type="com.congge.entity.TbUser">
<result property="id" column="id"/>
<result property="userName" column="user_name"/>
<result property="email" column="email"/>
<result property="passWord" column="pass_word"/>
</resultMap>
<select id="selectById" parameterType="java.lang.String" resultMap="pgResultMap" databaseId="postgresql">
select
*
from
tb_user
where id = #{id}
</select>
</mapper>
4.2.6 效果演示
启动工程后,先将application.yml中的下面的配置设置为pg环境
spring:
profiles:
active: postgresql
浏览器调用上述的测试接口,看到如下效果
切换配置文件为mysql,再次查询看到下面的效果;
通过上面的演示,就完成了在一个项目中基于配置的方式快速切换不同类型数据库的效果。
4.3 基于dynamic-datasource适配方案
适用场景,项目中需要同时使用多种数据源,或者混合使用多数据源的情况,工程目录结构如下
4.3.1 导入基础依赖
其他的依赖,比如mybatis,mysql等,和上面保持一致即可;
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
4.3.2 核心配置文件
spring:
datasource:
dynamic:
#默认使用的是gp数据库,对应下面gp和mysql,可视情况更改
primary: mysql
strict: false
datasource:
pg:
url: jdbc:postgresql://IP:5432/biz-db
username: postgres
password: postgres
driver-class-name: org.postgresql.Driver
type: com.alibaba.druid.pool.DruidDataSource
mysql:
url: jdbc:mysql://IP:3306/biz-db?&useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
druid:
#初始连接数
initial-size: 1
#最小连接数
min-idle: 1
#最大连接数
max-active: 100
#获取连接池超时时间
max-wait: 60000
filters: config,stat
connect-properties: druid.stat.mergeSql=true;druid.stat.logSlowSql=true;druid.stat.slowSqlMillis=500
filter:
commons-log:
enabled: true
statement-log-enabled: false
statement-log-error-enabled: true
statement-executable-sql-log-enable: true
server:
port: 8082
mybatis:
mapper-locations: classpath:mybatis/*/*.xml
type-aliases-package: com.congge.entity
configuration:
map-underscore-to-camel-case: true
4.3.3 dao接口
在使用这种方式时,由于dynamic-datasource的SDK中支持注解的方式帮助程序自动使用不同的数据源,可以简化自定义的配置类(网上也有基于dynamic-datasource通过添加配置类的方式实现),这里添加一个dao接口,里面有两个查询,通过@DS这个注解既可以区分不同的数据源,注解里面的值即配置文件中指定的那个参数值;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.congge.entity.TbUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface DbUserMapper {
@DS(value = "mysql")
TbUser queryById(@Param("id") String id);
@DS(value = "pg")
TbUser getById(@Param("id") String id);
}
4.3.4 自定义测试接口
@RestController
public class UserController {
@Autowired
private UserService userService;
//localhost:8081/getById?id=001
@GetMapping("/getById")
public TbUser getTbUser(String id){
return userService.getTbUser(id);
}
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
private DbUserMapper dbUserMapper;
@Override
public TbUser getTbUser(String id) {
return dbUserMapper.getById(id);
}
}
4.3.5 接口效果测试
启动工程后,浏览器中调用一下,看到如下效果
4.3.5 使用dynamic-datasource注意点
在实际使用这种方式进行整合适配时,需要注意下面几点
- 如果一个方法中需要同时混合操作多个数据库,建议不要放在一个事务中;
- 为了方便管理不同的dao,以及xml文件,建议进行分包处理;
4.4 基于SDK的适配方案
在类似SAAS平台这样的多服务模式下,为了减少上层应用的适配成本,可以考虑做一个通用的SDK,SDK中定义基本的一些规则,比如mybatis的xml文件扫描路径信息、上层应用支持的数据源类型等,以上述的providerId这种模式为例,在整个适配过程中,最重要的一个就是DbDataSourceConfig配置类,在该类中主要做了下面几件事:
- 初始化默认支持的数据库类型,比如oracle,mysql,postgresql等;
- 同时支持外部参数配置数据库类型,假如还有更多数据库需要支持,可以通过参数配置传入;
- 定义xml的文件扫描路径,如果外部传入了,就使用外部的,否则,就走默认的;
其实SDK的作用就是减少重复的工作,把那些公共的,带有共性的,或者说是具备系统级的配置抽取到一起,这样其他项目在引入之后就可以不用自己再单独提供配置类了,这就是SDK的目的所在。基于这个思路,我们单独做一个工程模块,将DbDataSourceConfig的核心逻辑抽取到该模块中,最后将jar包发布出去,被其他模块引用即可,模块结构如下:
4.4.1 公共配置类
在真实的项目中,SDK中要抽象的逻辑可能需要考虑更多的点,比如是否需要覆盖mybtais中的某些配置类,是否需要针对多种类型数据库中的sql语法做一下预兼容,如果客户端引入SDK之后未按照约定规范使用该如何处理等,考虑的越周全,你的SDK的健壮性和扩展性就越好。
@Configuration
public class DbDataSourceConfig {
@Value("${mybatis.mapper-locations}")
private String mapperLocations;
@Autowired
private Environment environment;
static Map<String,String> sqlTypeMap = new HashMap<>();
static {
sqlTypeMap.put("Oracle", "oracle");
sqlTypeMap.put("MySQL", "mysql");
sqlTypeMap.put("PostgreSQL", "postgresql");
sqlTypeMap.put("DB2", "db2");
sqlTypeMap.put("SQL Server", "sqlserver");
}
@Primary
@Bean(name = "dataSource")
@ConfigurationProperties("spring.datasource.hikari")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource());
}
@Bean
public DatabaseIdProvider databaseIdProvider() {
DatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
Properties p = new Properties();
String sqlTypeCollections = environment.getProperty("system.sql.types");
if(StringUtils.isEmpty(sqlTypeCollections)){
sqlTypeMap.forEach((key,val) ->{
p.setProperty(key, val);
});
}else {
List<String> sqlTypeList = Arrays.asList(sqlTypeCollections.split(","));
for(String sqlType : sqlTypeList){
if("mysql".equals(sqlType)){
p.setProperty("MySQL", "mysql");
}else if("oracle".equals(sqlType)){
p.setProperty("Oracle", "oracle");
}else if("postgresql".equals(sqlType)){
p.setProperty("PostgreSQL", "postgresql");
}else if("db2".equals(sqlType)){
p.setProperty("DB2", "db2");
}else if("sqlserver".equals(sqlType)){
p.setProperty("SQL Server", "sqlserver");
}
}
}
databaseIdProvider.setProperties(p);
return databaseIdProvider;
}
@Primary
@Bean
public SqlSessionFactoryBean sqlSessionFactoryBean(@Qualifier("dataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
factoryBean.setDatabaseIdProvider(databaseIdProvider());
if(StringUtils.isEmpty(mapperLocations)){
mapperLocations = "classpath*:*.xml";
}
factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
return factoryBean;
}
}
4.4.2 发布jar
这一步比较简单,就不过多赘述了。
4.4.3 在其他模块引入SDK
在上述的biz-diff中引入上面发布的SDK,并将本地的DbDataSourceConfig注释掉;
4.4.4 模拟效果验证
工程启动后,再次尝试调用上面的接口,在pg环境下可以得到下面的效果
再次切换mysql,调用接口看到如下效果
五、写在文末
多数据源适配是日常项目投产过程中一个很常见的业务,如何让你的项目通过最小化的改造达到具备快速切换的能力是每个开发工程师需要考虑的,其实在实践过程中方案可能很多,但是需要结合自身的情况酌情使用,以最小的代价完成改造是我们的终极目标。