springboot jpa druid 动态切换数据源记录

场景

  • 大型应用中的读写分离;
  • 相同业务逻辑,但是对应多个数据库;

    几张非常形象的图片奉上

  1. 单一数据库场景
  1. 多数据库场景
  1. 动态切换

实现方案

  • 步骤
  1. 核心是实现Spring 内置的 AbstractRoutingDataSource 抽象类,实现determineCurrentLookupKey() 方法;
  2. 实例化出多个datasource,并将这些datasource加入到该抽象类中的targetDataSources。
  3. 在操作数据前,dao层会先调用 AbstractRoutingDataSource 抽象类getConnection(),其中的调用的是determineCurrentLookupKey(),获取到对应的datasource;
  4. 借助ThreadLocal保存数据源信息,在整个业务流程中方便获取;
  • 难点
    不同连接池实例化事务管理、和生成类似数据库的sessionFactory方法有所不同,可能会费些时间。

代码实现

环境介绍

  • springboot版本: 1.5.6.RELEASE
  • 连接池:alibaba druid

部分代码

  • 配置文件, 密码就暴漏吧,哈哈

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    spring.datasource.druid.test1.url=jdbc:mysql://localhost:3306/multi_test1?characterEncoding=utf8&characterSetResults=utf8&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT
    spring.datasource.druid.test1.username=root
    spring.datasource.druid.test1.password=123456

    spring.datasource.druid.test2.url=jdbc:mysql://localhost:3306/multi_test2?characterEncoding=utf8&characterSetResults=utf8&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT
    spring.datasource.druid.test2.username=root
    spring.datasource.druid.test2.password=123456

    spring.datasource.type = com.alibaba.druid.pool.DruidDataSource

    spring.jpa.show-sql=true
    spring.jpa.hibernate.naming.physical-strategy=org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
  • 实现AbstractRoutingDataSource 抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class DynamicDataSourceRouter extends AbstractRoutingDataSource {

public static final ThreadLocal<String> dbStore = new ThreadLocal<>();

@Override
protected Object determineCurrentLookupKey() {
String dbBean = dbStore.get();
if(StringUtils.isBlank(dbBean)){
dbBean = SourceName.TEST1.dbName;
} else {

}
return dbBean;
}
enum SourceName{
TEST1("test1"),TEST2("test2");

public String dbName;

private SourceName(String name){
this.dbName = name;
}
}
}
  • 实例化出多个数据源,加入到DynamicDataSourceRouter父类的setTargetDataSources,并设置默认值
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
@Configuration
@Slf4j
public class MultiDataConfig {

@Value("${spring.datasource.type}")
private Class<? extends DataSource> dataSourceType;


@Bean("test1")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.druid.test1")
public DataSource test1(){
log.info("构建test1数据源");
DataSource source = DataSourceBuilder.create().type(dataSourceType).build();
return source;
}

@Bean("test2")
@ConfigurationProperties(prefix = "spring.datasource.druid.test2")
public DataSource test2(){
log.info("构建test2--- 数据源");
DataSource source = DataSourceBuilder.create().type(dataSourceType).build();
return source;
}

@Bean("routingDataSource")
public AbstractRoutingDataSource routingDataSource(@Qualifier("test1") DataSource test1,@Qualifier("test2") DataSource test2){
DynamicDataSourceRouter assistant = new DynamicDataSourceRouter();
Map<Object, Object> dataSources = new HashMap<>();
dataSources.put(DynamicDataSourceRouter.SourceName.TEST1.dbName,test1);
dataSources.put(DynamicDataSourceRouter.SourceName.TEST2.dbName,test2);
assistant.setDefaultTargetDataSource( test2 );
assistant.setTargetDataSources(dataSources);
return assistant;
}
}
  • 手动对 Jpa 的 EntityManager 进行初始化和配置
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
 */
@Configuration
@EnableConfigurationProperties(JpaProperties.class)
@EnableJpaRepositories(value = "cn.rzwl.multidata.dao")
public class JpaEntityManager {

@Autowired
private JpaProperties jpaProperties;

@Resource(name = "routingDataSource")
private DataSource routingDataSource;

@Bean(name = "entityManagerFactoryBean")
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(EntityManagerFactoryBuilder builder) {
// 不明白为什么这里获取不到 application.yml 里的配置
Map<String, String> properties = jpaProperties.getProperties();
//要设置这个属性,实现 CamelCase -> UnderScore 的转换
properties.put("hibernate.physical_naming_strategy",
"org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");
return builder
.dataSource(routingDataSource)//关键:注入routingDataSource
.properties(properties)
.packages("cn.rzwl.multidata.entity") //TODO 改成自己的实体类包
.persistenceUnit("myPersistenceUnit")
.build();
}

@Primary
@Bean(name = "entityManagerFactory")
public EntityManagerFactory entityManagerFactory(EntityManagerFactoryBuilder builder) {
return this.entityManagerFactoryBean(builder).getObject();
}

@Primary
@Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder) {
return new JpaTransactionManager(entityManagerFactory(builder));
}
}
  • 为方便测试,用filter验证
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
@WebFilter(urlPatterns = "/*")
@Slf4j
public class MyFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("初始化自定义过滤器");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String site = request.getParameter("source");
if( DynamicDataSourceRouter.SourceName.TEST1.dbName.equals( site ) ){
DynamicDataSourceRouter.dbStore.set( DynamicDataSourceRouter.SourceName.TEST1.dbName );
} else {
DynamicDataSourceRouter.dbStore.set( DynamicDataSourceRouter.SourceName.TEST2.dbName );
}
log.info("进入业务流程");
chain.doFilter( request,response );
log.info("完成处理");
System.out.println("后续操作");
}

@Override
public void destroy() {

}
}

源码地址:

点击可看

小结:

  • 一个神奇的AbstractRoutingDataSource类,使数据库切换操作举重若轻,优雅得体,告别劳力式编程。
打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2015-2020 AChampion
  • Powered by Hexo Theme Ayer
  • PV: UV:

开玩笑的~不用打赏