前言
在企业级开发中,多数据源是一种常见的技术方案。在面对复杂的业务场景时,通常会对数据库进行横向和纵向的拆分。横向拆分如读写分离,通过主从复制的方式减轻主库的读压力;纵向拆分则是按模块拆分数据库,提升单库性能。 在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实现数据源的动态上下文管理。
- ThreadLocal是一个线程安全的工具,确保每个线程都有独立的上下文变量副本。
- 这里的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的方法,动态地设置当前线程使用的数据库连接(主库或从库)。
- 定义切入点
- @Pointcut注解定义了切入点,即标记了哪些方法需要动态切换数据源。
- masterPointCut():匹配所有使用@Master注解的方法。
- slavePointCut():匹配所有使用@Slave注解的方法。
- 动态数据源切换逻辑
- @Around注解包裹方法执行,通过拦截方法调用,动态调整数据源。
- 在方法执行前,设置当前线程的数据源为主库或从库。
- 在方法执行完成后,无论是否发生异常,都清除线程中的数据源标识,防止后续线程调用时使用错误的数据源。
- 线程安全性
- 通过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尝试创建数据源。
- 我们的多数据源是通过手动配置,然后动态路由实现的。
- 如果不排除上述数据源的自动配置功能可能会出现问题
- SpringBoot尝试创建默认数据源,而我们已经手动配置了动态数据源,可能导致容器中存在多个数据源的bean。如果没有明确指定使用哪个数据源,会抛出异常。
- 如果再配置文件中没有完整的数据源配置(spring.datasource.url)供自动配置使用,会因为缺少参数抛出异常。
- 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容器的依赖注入能力,将动态路由数据源注册为全局数据源,使其对业务代码透明。
四、适用场景
-
读写分离:主库负责写操作,从库负责读操作,参考本案例。
-
多租户系统:根据租户信息动态切换不同的数据源。
-
数据分片:根据业务逻辑选择特定的数据库。