SpringBoot多环境开发

108 阅读7分钟

前言

在企业级开发中,多数据源是一种常见的技术方案。在面对复杂的业务场景时,通常会对数据库进行横向和纵向的拆分。横向拆分如读写分离,通过主从复制的方式减轻主库的读压力;纵向拆分则是按模块拆分数据库,提升单库性能。 在Spring Boot项目中,怎么实现多数据源支持?一起通过案例解析,探索下数据源的应用。

一、案例

1.1配置文件

  • 配置两个druid数据源,作为主从库
  • "master"、"slave"自定义,主要是在配置类中进行区分,以注入不同的数据源属性
 spring:
   datasource:
     druid:
       master:
         url: jdbc:mysql://*.*.*.*:3306/snail_db
         username: lazysnail
         password: ******
         driver-class-name: com.mysql.cj.jdbc.Driver
       slave:
         url: jdbc:mysql://*.*.*.*:3306/snail_db_slave
         username: lazysnail
         password: ******
         driver-class-name: com.mysql.cj.jdbc.Driver

1.2动态数据源配置

  • 定义动态数据源DynamicRoutingDataSource,所有的数据库操作会通过此动态数据源Bean路由到正确的目标数据源。
  • 通过@ConfigurationProperties将以spring.datasource.druid.master为前缀的配置绑定到DruidDataSource属性,定义主数据源 DataSource的bean
  • 通过@ConfigurationProperties将以spring.datasource.druid.slave为前缀的配置绑定到DruidDataSource属性,定义从数据源DataSource的bean
  • 定义MyBatis的SqlSessionFactory,负责创建和管理与数据库交互的SqlSession实例。
  • 定义MyBatis的SqlSessionTemplate,SqlSessionTemplate是Mapper层与数据库交互的桥梁。
 package com.lazy.snail.config;
 ​
 import com.alibaba.druid.pool.DruidDataSource;
 import com.lazy.snail.datasource.DynamicRoutingDataSource;
 import org.apache.ibatis.session.SqlSessionFactory;
 import org.mybatis.spring.SqlSessionFactoryBean;
 import org.mybatis.spring.SqlSessionTemplate;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
 ​
 import javax.sql.DataSource;
 import java.util.HashMap;
 import java.util.Map;
 ​
 /**
  * @ClassName DynamicDataSourceConfig
  * @Description TODO
  * @Author lazysnail
  * @Date 2024/11/18 14:24
  * @Version 1.0
  */
 @Configuration
 public class DynamicDataSourceConfig {
 ​
     @Bean
     public DataSource dynamicDataSource(
             @Qualifier("masterDataSource") DataSource masterDataSource,
             @Qualifier("slaveDataSource") DataSource slaveDataSource) {
         Map<Object, Object> targetDataSources = new HashMap<>();
         targetDataSources.put("master", masterDataSource);
         targetDataSources.put("slave", slaveDataSource);
 ​
         // 默认使用主数据源
         DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
         dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
         dynamicDataSource.setTargetDataSources(targetDataSources);
         return dynamicDataSource;
     }
 ​
     @Bean(name = "masterDataSource")
     @ConfigurationProperties(prefix = "spring.datasource.druid.master")
     public DataSource masterDataSource() {
         return new DruidDataSource();
     }
 ​
     @Bean(name = "slaveDataSource")
     @ConfigurationProperties(prefix = "spring.datasource.druid.slave")
     public DataSource slaveDataSource() {
         return new DruidDataSource();
     }
 ​
     @Bean
     public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dynamicDataSource) throws Exception {
         SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
         factoryBean.setDataSource(dynamicDataSource);
         //factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")); // 根据实际情况调整路径
         return factoryBean.getObject();
     }
 ​
     @Bean
     public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
         return new SqlSessionTemplate(sqlSessionFactory);
     }
 }

1.3动态路由

  • 继承自AbstractRoutingDataSource,实现 Spring 的动态数据源路由功能。
  • determineCurrentLookupKey是AbstractRoutingDataSource的核心方法,用于决定当前线程需要使用哪个数据源。
  • 通过DynamicDataSourceContextHolder获取上下文中指定的数据源标识,实现主从库切换。
 package com.lazy.snail.datasource;
 ​
 import com.lazy.snail.holder.DynamicDataSourceContextHolder;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
 ​
 /**
  * @ClassName DynamicRoutingDataSource
  * @Description TODO
  * @Author lazysnail
  * @Date 2024/11/18 14:35
  * @Version 1.0
  */
 @Slf4j
 public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
 ​
     @Override
     protected Object determineCurrentLookupKey() {
         String currentDataSource = DynamicDataSourceContextHolder.getDataSourceKey();
         log.info("Current DataSource is: {}", currentDataSource);
         return currentDataSource;
     }
 }
 ​
 ​
  • 使用ThreadLocal实现数据源的动态上下文管理。

    1. ThreadLocal是一个线程安全的工具,确保每个线程都有独立的上下文变量副本。
    2. 这里的ThreadLocal用于存储每个线程当前使用的数据源标识(如 "master"或"slave")。
  • setDataSourceKey用于设置当前线程的数据源标识,一般在切换数据源(如进入@Master或@Slave注解的方法)时调用。

  • getDataSourceKey获取当前线程的数据源标识,DynamicRoutingDataSource.determineCurrentLookupKey()方法调用这个方法,动态决定当前目标数据源。

  • clearDataSourceKey清除当前线程的数据源标识,一般在数据源切换完成后,防止上下文污染其他操作。

 package com.lazy.snail.holder;
 ​
 /**
  * @ClassName DynamicDataSourceContextHolder
  * @Description TODO
  * @Author lazysnail
  * @Date 2024/11/18 14:30
  * @Version 1.0
  */
 public class DynamicDataSourceContextHolder {
     private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
 ​
     public static void setDataSourceKey(String key) {
         CONTEXT_HOLDER.set(key);
     }
 ​
     public static String getDataSourceKey() {
         return CONTEXT_HOLDER.get();
     }
 ​
     public static void clearDataSourceKey() {
         CONTEXT_HOLDER.remove();
     }
 }

1.4主从注解

  • 用于标记主从库数据源
  • 在需要写操作(如INSERT、UPDATE、DELETE)或对数据一致性要求高的场景下,方法上标记@Master,确保这些操作始终在主库执行。
  • 在只需要读操作(如SELECT)的场景下,方法上标记@Slave,将查询请求路由到从库,减轻主库压力,提高读写分离性能。
 package com.lazy.snail.annotation;
 ​
 /**
  * @ClassName Master
  * @Description TODO
  * @Author lazysnail
  * @Date 2024/11/18 14:28
  * @Version 1.0
  */
 import java.lang.annotation.*;
 ​
 @Target({ElementType.METHOD, ElementType.TYPE})
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 public @interface Master {
 }
 ​
 package com.lazy.snail.annotation;
 ​
 import java.lang.annotation.*;
 ​
 /**
  * @ClassName Slave
  * @Description TODO
  * @Author lazysnail
  * @Date 2024/11/18 14:28
  * @Version 1.0
  */
 @Target({ElementType.METHOD, ElementType.TYPE})
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 public @interface Slave {
 }

1.5数据源切面

  • DataSourceAspect类通过拦截被标记为@Master或@Slave的方法,动态地设置当前线程使用的数据库连接(主库或从库)。

    1. 定义切入点

      • @Pointcut注解定义了切入点,即标记了哪些方法需要动态切换数据源。
      • masterPointCut():匹配所有使用@Master注解的方法。
      • slavePointCut():匹配所有使用@Slave注解的方法。
    2. 动态数据源切换逻辑

      • @Around注解包裹方法执行,通过拦截方法调用,动态调整数据源。
      • 在方法执行前,设置当前线程的数据源为主库或从库。
      • 在方法执行完成后,无论是否发生异常,都清除线程中的数据源标识,防止后续线程调用时使用错误的数据源。
    3. 线程安全性

      • 通过DynamicDataSourceContextHolder类的ThreadLocal存储数据源信息,保证每个线程的数据源选择互不干扰。
 package com.lazy.snail.aspect;
 ​
 import com.lazy.snail.holder.DynamicDataSourceContextHolder;
 import org.aspectj.lang.ProceedingJoinPoint;
 import org.aspectj.lang.annotation.Around;
 import org.aspectj.lang.annotation.Aspect;
 import org.aspectj.lang.annotation.Pointcut;
 import org.springframework.stereotype.Component;
 ​
 /**
  * @ClassName DataSourceAspect
  * @Description TODO
  * @Author lazysnail
  * @Date 2024/11/18 14:25
  * @Version 1.0
  */
 @Aspect
 @Component
 public class DataSourceAspect {
 ​
     @Pointcut("@annotation(com.lazy.snail.annotation.Master)")
     public void masterPointCut() {}
 ​
     @Pointcut("@annotation(com.lazy.snail.annotation.Slave)")
     public void slavePointCut() {}
 ​
     @Around("masterPointCut()")
     public Object useMaster(ProceedingJoinPoint joinPoint) throws Throwable {
         DynamicDataSourceContextHolder.setDataSourceKey("master");
         try {
             return joinPoint.proceed();
         } finally {
             DynamicDataSourceContextHolder.clearDataSourceKey();
         }
     }
 ​
     @Around("slavePointCut()")
     public Object useSlave(ProceedingJoinPoint joinPoint) throws Throwable {
         DynamicDataSourceContextHolder.setDataSourceKey("slave");
         try {
             return joinPoint.proceed();
         } finally {
             DynamicDataSourceContextHolder.clearDataSourceKey();
         }
     }
 }

1.6启动类修改

  • 此处去掉了两个自动配置类,DataSourceAutoConfiguration和DruidDataSourceAutoConfigure

  • SpringBoot启动时,如果存在spring-boot-starter-jdbc或spring-boot-starter-data-jpa,则会使用DataSourceAutoConfiguration自动配置数据源。

  • 如果存在druid-spring-boot-starter,则会通过DruidDataSourceAutoConfigure尝试创建数据源。

  • 我们的多数据源是通过手动配置,然后动态路由实现的。

  • 如果不排除上述数据源的自动配置功能可能会出现问题

    1. SpringBoot尝试创建默认数据源,而我们已经手动配置了动态数据源,可能导致容器中存在多个数据源的bean。如果没有明确指定使用哪个数据源,会抛出异常。
    2. 如果再配置文件中没有完整的数据源配置(spring.datasource.url)供自动配置使用,会因为缺少参数抛出异常。
    3. SpringBoot默认的数据源逻辑可能与动态数据源切换逻辑相互干扰,导致切换功能失效。
 @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, DruidDataSourceAutoConfigure.class})

1.7客户端调用

  • 通过在方法上使用@Master或者@Slave注解,指定方法使用哪个数据源
 package com.lazy.snail.mapper;
 ​
 import com.lazy.snail.annotation.Master;
 import com.lazy.snail.annotation.Slave;
 import com.lazy.snail.dimain.User;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Select;
 ​
 import java.util.List;
 ​
 @Mapper
 public interface UserMapper {
     @Master
     @Select("select * from user_info where user_id=#{id}")
     User selectById(int id);
 ​
     @Select("select * from user_info")
     @Slave
     List<User> selectAll();
 }
  • 调用结果,从控制台可以直观的看出执行selectById和selectAll方法时,数据源的切换动作。

二、核心原理

2.1动态数据源路由

  • 核心机制基于Spring提供的AbstractRoutingDataSource。
  • 动态路由的关键在于重写其determineCurrentLookupKey方法,根据上下文决定当前使用的数据源。

2.2数据源上下文隔离

  • 利用ThreadLocal维护每个线程独立的数据源标识,保证线程间的数据源设置互不干扰。

2.3动态切换的数据源映射

  • 通过配置一个路由数据源,将多个实际数据源注册到路由映射表中。
  • 默认数据源用于应对没有明确标识时的访问。

2.4方法级数据源切换

  • 利用Spring AOP实现方法级别的数据源切换:

    • 在方法调用前,通过注解动态设置线程上下文中的数据源标识。
    • 方法执行后清理上下文,避免数据源污染。

三、核心技术

3.1数据源Bean管理

  • 通过手动配置多个数据源Bean,并利用动态路由管理器将其映射到对应的标识。

3.2注解驱动

  • 自定义注解(如@Master和@Slave),结合AOP切面实现方法级别的切换逻辑。

3.3配置剥离

  • 禁用默认的自动数据源配置(如DataSourceAutoConfiguration),手动管理数据源注册与加载,增强灵活性。

3.4Spring AOP

  • 切面用于拦截注解方法,在方法执行前后动态调整线程上下文中的数据源设置。

3.5线程安全保障

  • 使用ThreadLocal实现线程级的数据源标识存储,保证多线程环境下的安全切换。

3.6IoC动态绑定

  • 利用Spring IoC容器的依赖注入能力,将动态路由数据源注册为全局数据源,使其对业务代码透明。

四、适用场景

  • 读写分离:主库负责写操作,从库负责读操作,参考本案例。
  • 多租户系统:根据租户信息动态切换不同的数据源。
  • 数据分片:根据业务逻辑选择特定的数据库。