Skip to content
清晨的一缕阳光
返回

Spring Boot 动态多数据源配置

前言

在企业应用中,经常需要使用多个数据源,如读写分离、分库分表等场景。Spring Boot 支持多数据源配置,通过 AbstractRoutingDataSource 可以实现动态数据源切换。

快速开始

1. 添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
    <version>4.2.0</version>
</dependency>

2. 配置多数据源

spring:
  datasource:
    dynamic:
      primary: master
      strict: false
      datasource:
        master:
          url: jdbc:mysql://master:3306/demo
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave_1:
          url: jdbc:mysql://slave1:3306/demo
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver
        slave_2:
          url: jdbc:mysql://slave2:3306/demo
          username: root
          password: 123456
          driver-class-name: com.mysql.cj.jdbc.Driver

3. 使用多数据源

@Service
@RequiredArgsConstructor
public class UserService {
    
    private final UserRepository userRepository;
    
    /**
     * 默认使用主库
     */
    public User create(User user) {
        return userRepository.save(user);
    }
    
    /**
     * 使用从库
     */
    @DS("slave_1")
    public User findById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
    
    /**
     * 动态切换
     */
    public List<User> findAll() {
        // 使用从库查询
        DynamicDataSourceContextHolder.push("slave_1");
        try {
            return userRepository.findAll();
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }
}

自定义多数据源

1. 配置数据源

@Configuration
public class DataSourceConfig {
    
    @Bean
    @ConfigurationProperties("spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }
    
    @Bean
    @ConfigurationProperties("spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }
}

2. 创建路由数据源

@Configuration
public class DataSourceConfig {
    
    @Bean
    public DataSource routingDataSource(
        @Qualifier("masterDataSource") DataSource master,
        @Qualifier("slaveDataSource") DataSource slave
    ) {
        AbstractRoutingDataSource routingDataSource = new AbstractRoutingDataSource() {
            @Override
            protected Object determineCurrentLookupKey() {
                return DataSourceContextHolder.get();
            }
        };
        
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", master);
        targetDataSources.put("slave", slave);
        
        routingDataSource.setTargetDataSources(targetDataSources);
        routingDataSource.setDefaultTargetDataSource(master);
        
        return routingDataSource;
    }
}

3. 创建数据源上下文

public class DataSourceContextHolder {
    
    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
    
    public static void set(String key) {
        CONTEXT.set(key);
    }
    
    public static String get() {
        return CONTEXT.get();
    }
    
    public static void clear() {
        CONTEXT.remove();
    }
}

4. 创建切面

@Aspect
@Component
@Order(1)
public class DataSourceAspect {
    
    @Around("@annotation(ds)")
    public Object around(ProceedingJoinPoint pjp, DS ds) throws Throwable {
        String key = ds.value();
        DataSourceContextHolder.set(key);
        
        try {
            return pjp.proceed();
        } finally {
            DataSourceContextHolder.clear();
        }
    }
}

5. 创建注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DS {
    String value() default "master";
}

读写分离

1. 配置主从数据源

spring:
  datasource:
    dynamic:
      primary: master
      datasource:
        master:
          url: jdbc:mysql://master:3306/demo
          username: root
          password: 123456
        slave:
          url: jdbc:mysql://slave:3306/demo
          username: root
          password: 123456

2. 使用读写分离

@Service
@RequiredArgsConstructor
public class UserService {
    
    private final UserRepository userRepository;
    
    /**
     * 写操作 - 主库
     */
    @DS("master")
    public User create(User user) {
        return userRepository.save(user);
    }
    
    /**
     * 读操作 - 从库
     */
    @DS("slave")
    public User findById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
    
    /**
     * 自动读写分离
     */
    public List<User> findAll() {
        // 根据方法名自动选择数据源
        return userRepository.findAll();
    }
}

3. 自动读写分离切面

@Aspect
@Component
@Order(1)
public class ReadWriteAspect {
    
    @Around("execution(* com.example.demo.service..*.find*(..)) || " +
            "execution(* com.example.demo.service..*.get*(..)) || " +
            "execution(* com.example.demo.service..*.query*(..)) || " +
            "execution(* com.example.demo.service..*.select*(..))")
    public Object readAround(ProceedingJoinPoint pjp) throws Throwable {
        DataSourceContextHolder.set("slave");
        try {
            return pjp.proceed();
        } finally {
            DataSourceContextHolder.clear();
        }
    }
    
    @Around("execution(* com.example.demo.service..*.save*(..)) || " +
            "execution(* com.example.demo.service..*.insert*(..)) || " +
            "execution(* com.example.demo.service..*.update*(..)) || " +
            "execution(* com.example.demo.service..*.delete*(..))")
    public Object writeAround(ProceedingJoinPoint pjp) throws Throwable {
        DataSourceContextHolder.set("master");
        try {
            return pjp.proceed();
        } finally {
            DataSourceContextHolder.clear();
        }
    }
}

分库分表

1. 配置多个数据库

spring:
  datasource:
    dynamic:
      datasource:
        db_0:
          url: jdbc:mysql://db0:3306/demo
          username: root
          password: 123456
        db_1:
          url: jdbc:mysql://db1:3306/demo
          username: root
          password: 123456
        db_2:
          url: jdbc:mysql://db2:3306/demo
          username: root
          password: 123456

2. 动态路由

@Service
public class OrderService {
    
    /**
     * 根据用户 ID 路由到对应数据库
     */
    public Order findById(Long userId, Long orderId) {
        // 计算数据库索引
        int dbIndex = Math.abs(userId.hashCode() % 3);
        String dsKey = "db_" + dbIndex;
        
        DynamicDataSourceContextHolder.push(dsKey);
        try {
            return orderRepository.findById(orderId).orElse(null);
        } finally {
            DynamicDataSourceContextHolder.poll();
        }
    }
}

3. 使用 ShardingSphere

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core</artifactId>
    <version>5.4.0</version>
</dependency>
spring:
  shardingsphere:
    datasource:
      names: ds0,ds1
      ds0:
        url: jdbc:mysql://ds0:3306/demo
        username: root
        password: 123456
      ds1:
        url: jdbc:mysql://ds1:3306/demo
        username: root
        password: 123456
    
    rules:
      sharding:
        tables:
          order:
            actual-data-nodes: ds$->{0..1}.order$->{0..1}
            table-strategy:
              standard:
                sharding-column: order_id
                sharding-algorithm-name: order-table-algorithm
        
        sharding-algorithms:
          order-table-algorithm:
            type: CLASS_BASED
            props:
              strategy: STANDARD
              algorithm-class: com.example.demo.sharding.OrderTableShardingAlgorithm

事务管理

1. 单数据源事务

@Service
public class UserService {
    
    @Transactional
    @DS("master")
    public User create(User user) {
        return userRepository.save(user);
    }
}

2. 多数据源事务

@Service
public class OrderService {
    
    /**
     * 多数据源事务 - 需要 JTA
     */
    @Transactional
    public void createOrder(Order order) {
        // 订单库
        orderRepository.save(order);
        
        // 用户库
        userService.updatePoints(order.getUserId(), order.getPoints());
    }
}

3. 配置 JTA

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
spring:
  jta:
    enabled: true
    atomikos:
      datasource:
        max-pool-size: 10
        min-pool-size: 1
        max-lifetime: 60000
        borrow-connection-timeout: 30000

最佳实践

1. 数据源隔离

// ✅ 推荐 - 明确指定数据源
@Service
public class UserService {
    
    @DS("master")
    public User create(User user) {
        return userRepository.save(user);
    }
    
    @DS("slave")
    public User findById(Long id) {
        return userRepository.findById(id).orElse(null);
    }
}

// ❌ 不推荐 - 依赖自动切换
@Service
public class UserService {
    
    public User create(User user) {
        // 不知道使用哪个数据源
        return userRepository.save(user);
    }
}

2. 避免跨库 Join

// ✅ 推荐 - 应用层组装
@Service
public class OrderService {
    
    public OrderDetailVO getDetail(Long orderId) {
        Order order = orderRepository.findById(orderId).orElse(null);
        User user = userRepository.findById(order.getUserId()).orElse(null);
        
        OrderDetailVO vo = new OrderDetailVO();
        vo.setOrder(order);
        vo.setUser(user);
        return vo;
    }
}

// ❌ 不推荐 - 跨库 Join
@Query("SELECT o, u FROM Order o JOIN User u ON o.userId = u.id")
OrderDetailVO getDetail(Long orderId);

3. 数据源监控

@Configuration
public class DataSourceMonitorConfig {
    
    @Bean
    public DataSource dataSource(DataSource routingDataSource) {
        return new ProxyDataSource(routingDataSource) {
            @Override
            public Connection getConnection() throws SQLException {
                Connection connection = super.getConnection();
                DataSourceContextHolder.get();
                log.info("获取数据源连接:{}", DataSourceContextHolder.get());
                return connection;
            }
        };
    }
}

总结

多数据源要点:

多数据源是处理大数据量、高并发场景的重要手段。


分享这篇文章到:

上一篇文章
Java 注解体系详解
下一篇文章
Nacos 配置中心