李锋镝的博客

  • 首页
  • 时间轴
  • 留言
  • 插件
  • 左邻右舍
  • 关于我
    • 关于我
    • 另一个网站
    • 我的导航站
  • 赞助
Destiny
自是人生长恨水长东
  1. 首页
  2. 原创
  3. 正文

使用AbstractRoutingDataSource动态切换数据源

2025年3月15日 283点热度 0人点赞 0条评论

AbstractRoutingDataSource 是 Spring 框架中的一个抽象类,它位于 org.springframework.jdbc.datasource.lookup 包下,主要用于实现动态数据源的切换。以下将从其作用、原理、使用步骤和示例代码等方面进行详细介绍。

介绍

作用

在一些复杂的业务场景中,可能需要在不同的数据源之间进行动态切换,例如读写分离(读操作使用从库,写操作使用主库)、多租户系统(不同租户使用不同的数据库)等。AbstractRoutingDataSource 提供了一个简单而灵活的机制来实现这种动态数据源切换的功能。

原理

AbstractRoutingDataSource 实现了 javax.sql.DataSource 接口,它内部维护了一个目标数据源的映射关系(通常是一个 Map),并通过一个抽象方法 determineCurrentLookupKey() 来决定当前使用哪个数据源。当调用 getConnection() 方法获取数据库连接时,AbstractRoutingDataSource 会调用 determineCurrentLookupKey() 方法获取当前的数据源键,然后根据这个键从映射关系中查找对应的数据源,并从该数据源获取连接。

使用步骤

  1. 创建多个数据源:配置多个不同的数据源,例如主库数据源和从库数据源。
  2. 继承 AbstractRoutingDataSource:创建一个自定义的数据源路由类,继承 AbstractRoutingDataSource,并实现 determineCurrentLookupKey() 方法。
  3. 配置数据源映射:将多个数据源配置到自定义的数据源路由类中。
  4. 动态切换数据源:在业务代码中通过某种方式设置当前要使用的数据源键。

示例代码

1. 配置多个数据源

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

    @Bean(name = "masterDataSource")
    public DataSource masterDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/master_db");
        dataSource.setUsername("root");
        dataSource.setPassword("password");
        return dataSource;
    }

    @Bean(name = "slaveDataSource")
    public DataSource slaveDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/slave_db");
        dataSource.setUsername("root");
        dataSource.setPassword("password");
        return dataSource;
    }
}

2. 继承 AbstractRoutingDataSource

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

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

3. 创建数据源上下文持有者

public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
    }

    public static String getDataSource() {
        return contextHolder.get();
    }

    public static void clearDataSource() {
        contextHolder.remove();
    }
}

4. 配置数据源映射

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;

@Configuration
public class DynamicDataSourceConfig {

    @Bean
    public AbstractRoutingDataSource routingDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource);
        targetDataSources.put("slave", slaveDataSource);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        return dynamicDataSource;
    }
}

5. 动态切换数据源

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void readData() {
        DataSourceContextHolder.setDataSource("slave");
        try {
            // 执行读操作
            jdbcTemplate.queryForList("SELECT * FROM users");
        } finally {
            DataSourceContextHolder.clearDataSource();
        }
    }

    public void writeData() {
        DataSourceContextHolder.setDataSource("master");
        try {
            // 执行写操作
            jdbcTemplate.update("INSERT INTO users (name, age) VALUES (?, ?)", "John", 25);
        } finally {
            DataSourceContextHolder.clearDataSource();
        }
    }
}

在业务代码中,只需要调用 DataSourceContextHolder.setDataSource() 方法设置当前要使用的数据源键,就可以切换到相应的数据源进行数据库操作。

使用

在使用 AbstractRoutingDataSource 实现动态数据源切换时,除了显式调用切换方法外,还可以通过一些隐式的方式来实现数据源切换。

1. 使用 AOP 结合注解

可以通过自定义注解和 AOP(面向切面编程)来实现隐式的数据源切换。当方法上添加了特定注解时,在方法执行前自动切换数据源,方法执行后再恢复默认数据源。

步骤:

  • 定义数据源注解:用于标记需要切换数据源的方法。
  • 创建 AOP 切面:在方法执行前后进行数据源的切换和恢复操作。
  • 在业务方法上使用注解:标记需要切换数据源的方法。

示例代码:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 定义数据源注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceSwitch {
    String value() default "master";
}
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

// 创建 AOP 切面
@Aspect
@Component
public class DataSourceSwitchAspect {

    @Before("@annotation(com.example.demo.DataSourceSwitch)")
    public void before(JoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        DataSourceSwitch dataSourceSwitch = method.getAnnotation(DataSourceSwitch.class);
        if (dataSourceSwitch != null) {
            DataSourceContextHolder.setDataSource(dataSourceSwitch.value());
        }
    }

    @After("@annotation(com.example.demo.DataSourceSwitch)")
    public void after(JoinPoint point) {
        DataSourceContextHolder.clearDataSource();
    }
}
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @DataSourceSwitch("slave")
    public void readData() {
        // 执行读操作
        jdbcTemplate.queryForList("SELECT * FROM users");
    }

    @DataSourceSwitch("master")
    public void writeData() {
        // 执行写操作
        jdbcTemplate.update("INSERT INTO users (name, age) VALUES (?, ?)", "John", 25);
    }
}

2. 根据请求路径或请求参数切换

在 Web 应用中,可以根据请求的路径或请求参数来隐式地切换数据源。例如,不同的租户可能有不同的请求路径前缀,根据这个前缀来切换对应的数据源。

步骤:

  • 创建过滤器或拦截器:在请求处理前根据请求路径或参数切换数据源。
  • 在请求处理完成后恢复默认数据源。

示例代码(使用过滤器):

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

// 创建过滤器
@WebFilter(urlPatterns = "/*")
public class DataSourceSwitchFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String requestURI = httpRequest.getRequestURI();
        if (requestURI.startsWith("/tenant1")) {
            DataSourceContextHolder.setDataSource("tenant1DataSource");
        } else if (requestURI.startsWith("/tenant2")) {
            DataSourceContextHolder.setDataSource("tenant2DataSource");
        }
        try {
            chain.doFilter(request, response);
        } finally {
            DataSourceContextHolder.clearDataSource();
        }
    }
}

3. 根据业务逻辑自动切换

在某些业务场景中,可以根据业务逻辑的状态自动切换数据源。例如,在一个电商系统中,当订单处于不同的状态时,可能需要使用不同的数据源进行数据处理。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void processOrder(Order order) {
        if (order.getStatus().equals("PENDING")) {
            DataSourceContextHolder.setDataSource("pendingDataSource");
        } else if (order.getStatus().equals("COMPLETED")) {
            DataSourceContextHolder.setDataSource("completedDataSource");
        }
        try {
            // 处理订单业务逻辑
            jdbcTemplate.update("UPDATE orders SET status = ? WHERE id = ?", "PROCESSING", order.getId());
        } finally {
            DataSourceContextHolder.clearDataSource();
        }
    }
}

class Order {
    private Long id;
    private String status;

    // 省略 getter 和 setter 方法
}

结合Mybatis

当结合 MyBatis 与 AbstractRoutingDataSource 实现动态数据源切换时,整体思路是在 MyBatis 操作数据库前,根据业务逻辑隐式或显式地切换数据源。

1. 环境准备

首先确保项目中添加了 Spring、Spring Boot、MyBatis 和 MyBatis-Spring-Boot-Starter 的依赖。以 Maven 为例,在 pom.xml 中添加以下依赖:

<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!-- MyBatis Spring Boot Starter -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.3.1</version>
    </dependency>
    <!-- MySQL Connector -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

2. 配置多个数据源

配置多个不同的数据源,例如主库数据源和从库数据源。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {

    @Bean(name = "masterDataSource")
    public DataSource masterDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/master_db");
        dataSource.setUsername("root");
        dataSource.setPassword("password");
        return dataSource;
    }

    @Bean(name = "slaveDataSource")
    public DataSource slaveDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/slave_db");
        dataSource.setUsername("root");
        dataSource.setPassword("password");
        return dataSource;
    }
}

3. 继承 AbstractRoutingDataSource

创建一个自定义的数据源路由类,继承 AbstractRoutingDataSource,并实现 determineCurrentLookupKey() 方法。

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

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

4. 创建数据源上下文持有者

用于在当前线程中存储和获取数据源信息。

public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
    }

    public static String getDataSource() {
        return contextHolder.get();
    }

    public static void clearDataSource() {
        contextHolder.remove();
    }
}

5. 配置数据源映射

将多个数据源配置到自定义的数据源路由类中,并配置 MyBatis 的 SqlSessionFactory 使用该动态数据源。

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

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

@Configuration
@MapperScan("com.example.demo.mapper")
public class DynamicDataSourceConfig {

    @Bean
    public AbstractRoutingDataSource routingDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource);
        targetDataSources.put("slave", slaveDataSource);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        dynamicDataSource.setTargetDataSources(targetDataSources);
        return dynamicDataSource;
    }

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

6. 定义 MyBatis Mapper 接口

创建一个简单的 MyBatis Mapper 接口,用于操作数据库。

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;
import java.util.Map;

@Mapper
public interface UserMapper {

    @Select("SELECT * FROM users")
    List<Map<String, Object>> getAllUsers();

    @Select("INSERT INTO users (name, age) VALUES (#{name}, #{age})")
    void insertUser(String name, Integer age);
}

7. 结合 AOP 实现隐式数据源切换

使用自定义注解和 AOP 来实现隐式的数据源切换。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 定义数据源注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSourceSwitch {
    String value() default "master";
}
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

// 创建 AOP 切面
@Aspect
@Component
public class DataSourceSwitchAspect {

    @Before("@annotation(com.example.demo.DataSourceSwitch)")
    public void before(JoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        DataSourceSwitch dataSourceSwitch = method.getAnnotation(DataSourceSwitch.class);
        if (dataSourceSwitch != null) {
            DataSourceContextHolder.setDataSource(dataSourceSwitch.value());
        }
    }

    @After("@annotation(com.example.demo.DataSourceSwitch)")
    public void after(JoinPoint point) {
        DataSourceContextHolder.clearDataSource();
    }
}

8. 业务服务类使用

在业务服务类中使用 UserMapper 进行数据库操作,并通过注解隐式切换数据源。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @DataSourceSwitch("slave")
    public List<Map<String, Object>> getAllUsers() {
        return userMapper.getAllUsers();
    }

    @DataSourceSwitch("master")
    public void insertUser(String name, Integer age) {
        userMapper.insertUser(name, age);
    }
}

9. 控制器调用

创建一个简单的控制器来调用业务服务类的方法。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("/users")
    public List<Map<String, Object>> getAllUsers() {
        return userService.getAllUsers();
    }

    @PostMapping("/users")
    public void insertUser(@RequestParam String name, @RequestParam Integer age) {
        userService.insertUser(name, age);
    }
}

通过以上步骤,就可以将 MyBatis 与 AbstractRoutingDataSource 结合使用,实现动态数据源的切换。

除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/article/4237

相关文章

  • 分布式服务生成唯一不重复ID(24位字符串)
  • SpringBoot常用注解
  • CompletableFuture使用详解
  • SpringBoot 中内置的 49 个常用工具类
  • SpringBoot 实现接口防刷的 5 种实现方案
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: SpringBoot 数据库
最后更新:2025年3月14日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >

文章评论

1 2 3 4 5 6 7 8 9 11 12 13 14 15 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 46 47 48 49 50 51 52 53 54 55 57 58 60 61 62 63 64 65 66 67 69 72 74 76 77 78 79 80 81 82 85 86 87 90 92 93 94 95 96 97 98 99
取消回复

COPYRIGHT © 2025 lifengdi.com. ALL RIGHTS RESERVED.

Theme Kratos Made By Dylan

津ICP备2024022503号-3