李锋镝的博客

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

使用AbstractRoutingDataSource动态切换数据源

2025年3月15日 247点热度 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/archives/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
取消回复

人间有味是清欢。

最新 热点 随机
最新 热点 随机
SpringBoot框架自动配置之spring.factories和AutoConfiguration.imports 应用型负载均衡(ALB)和网络型负载均衡(NLB)区别 什么是Helm? TransmittableThreadLocal介绍与使用 ReentrantLock深度解析 RedisTemplate和Redisson的区别
玩博客的人是不是越来越少了?准备入手个亚太的ECS,友友们有什么建议吗?什么是Helm?2024年11月1号 农历十月初一别再背线程池的七大参数了,现在面试官都这么问URL地址末尾加不加“/”有什么区别
Java数据类型判断工具类DataTypeUtil Java 灵魂拷问 13 个为什么,你都会哪些? Java 为什么有这么多 “O”? 办理居住证困难重重啊! Java之五种遍历Map集合的方式 如何查看JVM参数
标签聚合
分布式 多线程 日常 JAVA ElasticSearch IDEA MySQL Redis 设计模式 JVM SpringBoot 架构 SQL 文学 docker Spring 面试 数据库 教程 K8s
友情链接
  • i架构
  • 临窗旋墨
  • 博友圈
  • 博客录
  • 博客星球
  • 哥斯拉
  • 志文工作室
  • 搬砖日记
  • 旋律的博客
  • 旧时繁华
  • 林羽凡
  • 知向前端
  • 蜗牛工作室
  • 集博栈
  • 韩小韩博客
  • 風の声音

COPYRIGHT © 2025 lifengdi.com. ALL RIGHTS RESERVED.

Theme Kratos Made By Dylan

津ICP备2024022503号-3