重学SpringBoot3-AbstractRoutingDataSource

重学SpringBoot3-AbstractRoutingDataSource

CoderJia 12 2024-10-12

在现代的应用开发中,尤其是在 SaaS 多租户架构、读写分离、或者多数据源的场景下,通常需要动态地切换数据源。Spring Boot 3 提供的 AbstractRoutingDataSource 类是实现这一功能的核心工具之一。

本文将详细介绍 AbstractRoutingDataSource 的作用、使用场景及其实现方法。

1. 什么是 AbstractRoutingDataSource

位置:org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource

AbstractRoutingDataSource

AbstractRoutingDataSource 是 Spring 提供的一个抽象类,它是 DataSource 的子类,用于实现数据源的动态路由。动态路由意味着可以根据某些条件或上下文来决定具体使用哪个数据源。这个类的核心功能是根据用户定义的规则,动态决定在特定的操作中使用哪个数据源,而不需要每次手动切换。

这个机制特别适用于以下几种场景:

  • 多租户架构:不同的租户需要访问不同的数据库或数据源。
  • 读写分离:写操作使用主库,读操作使用从库。
  • 多数据源:根据业务逻辑动态选择使用不同的数据库。

2. AbstractRoutingDataSource 的作用

在实际应用中,AbstractRoutingDataSource 主要用于以下几个方面:

  • 根据上下文动态选择数据源:例如,在多租户系统中,可以根据当前租户的 ID 动态选择相应的数据库。
  • 简化多数据源的管理:避免硬编码多个数据源,提供更灵活的动态数据源切换能力。
  • 读写分离:通常在高并发的应用场景中,将写操作路由到主数据库,读操作路由到从数据库,从而提高系统性能和可扩展性。

3. 如何使用 AbstractRoutingDataSource

实现 AbstractRoutingDataSource 的步骤相对简单,关键在于实现其 determineCurrentLookupKey() 方法,该方法用于根据上下文条件返回数据源的 key。

3.1. 添加依赖

首先,在 pom.xml 中引入 Spring Boot 3mybatis 的相关依赖,具体参考重学SpringBoot3-整合SSM

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>3.0.3</version>
		</dependency>
		<dependency>
			<groupId>com.mysql</groupId>
			<artifactId>mysql-connector-j</artifactId>
			<scope>runtime</scope>
		</dependency>

3.2. 配置数据源

3.2. 配置文件

spring:
  datasource:
    master: # 自定义名称
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbcUrl: jdbc:mysql://localhost:3306/master_db?useSSL=false&serverTimezone=UTC
      username: master_user
      password: master_password
    slave:  # 自定义名称
      driver-class-name: com.mysql.cj.jdbc.Driver
      jdbcUrl: jdbc:mysql://localhost:3306/slave_db?useSSL=false&serverTimezone=UTC
      username: slave_user
      password: slave_password

3.3. 创建数据源配置类

首先,需要配置多个数据源,并使用 Spring 的 DataSource 配置工厂来创建这些数据源。

package com.coderjia.boot311datasource.config;

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.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @author CoderJia
 * @create 2024/10/11 下午 09:52
 * @Description
 **/
@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master") // 和配置文件对应
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public DataSource routingDataSource() {
        AbstractRoutingDataSource routingDataSource = new CustomRoutingDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource());
        targetDataSources.put("slave", slaveDataSource());

        routingDataSource.setTargetDataSources(targetDataSources);
        routingDataSource.setDefaultTargetDataSource(masterDataSource());
        return routingDataSource;
    }
}

3.4. MyBatis配置

确保 MyBatis 的 SqlSessionFactory 使用的是动态数据源。

package com.coderjia.boot311datasource.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

/**
 * @author CoderJia
 * @create 2024/10/11 下午 10:37
 * @Description
 **/
@Configuration
@MapperScan("com.coderjia.boot311datasource.dao")
public class MyBatisConfig {

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource routingDataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(routingDataSource);
        return sessionFactory.getObject();
    }
}

3.5. 创建 CustomRoutingDataSource

需要继承 AbstractRoutingDataSource,并实现 determineCurrentLookupKey() 方法。

package com.coderjia.boot311datasource.config;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * @author CoderJia
 * @create 2024/10/11 下午 09:53
 * @Description
 **/
public class CustomRoutingDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        // 获取当前数据源的key,可能是根据上下文或线程变量动态设置的
        return DataSourceContextHolder.getDataSourceKey();
    }
}

在上面的例子中,我们通过 DataSourceContextHolder 来动态获取当前数据源的 key。这个 key 决定了使用哪个数据源。

3.6. 配置 DataSourceContextHolder

为了管理数据源 key,我们可以创建一个 DataSourceContextHolder 类,用来存储当前线程的数据源标识符。

package com.coderjia.boot311datasource.config;

/**
 * @author CoderJia
 * @create 2024/10/11 下午 09:53
 * @Description
 **/
public class DataSourceContextHolder {

    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();
    }
}

3.7. 在业务逻辑中切换数据源

通过调用 DataSourceContextHoldersetDataSourceKey() 方法,可以在业务逻辑中动态切换数据源。例如,在读操作时选择从库,在写操作时选择主库。

package com.coderjia.boot311datasource.service.impl;

import com.coderjia.boot311datasource.bean.User;
import com.coderjia.boot311datasource.config.DataSourceContextHolder;
import com.coderjia.boot311datasource.dao.UserMapper;
import com.coderjia.boot311datasource.service.IUserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * @author CoderJia
 * @create 2024/3/16 下午 05:28
 * @Description
 **/
@Slf4j
@Service
public class UserServiceImpl implements IUserService {

    @Resource
    private UserMapper userMapper;
    @Override
    public User getUserById(Long id) {

        // 切换到从库
        DataSourceContextHolder.setDataSourceKey("slave");
        User user = userMapper.selectUserById(id);
        log.info("user:{}", user);

        // 切换回主库
        DataSourceContextHolder.setDataSourceKey("master");
        user = userMapper.selectUserById(id);
        log.info("user:{}", user);
        return user;
    }


}

3.8. 演示

准备两个数据库,相同给表里存放着不同的数据:

2个数据库

查询用户id=1的用户信息,手动切换数据源,相同的用户id查出两个不同的用户信息。

curl localhost:8080/users/1

切换数据源

4. AbstractRoutingDataSource 的优势

AbstractRoutingDataSource 通过动态数据源切换,提供了极大的灵活性。与手动管理多个数据源相比,它具有以下优势:

  • 灵活的路由规则:可以根据任何自定义条件选择数据源,规则完全由开发者定义。
  • 简化代码:开发者只需专注于业务逻辑,不需要显式地管理数据源切换。
  • 线程安全:使用 ThreadLocal 保证在多线程环境下每个线程使用自己的数据源,避免数据源混乱。

5. 总结

AbstractRoutingDataSource 是 Spring Boot 3 中非常强大且灵活的工具,适合多租户、读写分离和多数据源的场景。通过它,我们可以在应用中动态地选择不同的数据源,而无需手动切换,极大地提高了代码的灵活性和可维护性。

如果你正在开发需要多个数据库或者需要动态切换数据源的应用,AbstractRoutingDataSource 是一个非常值得推荐的解决方案。