如何在Spring Boot + MyBatis-Plus中动态切换数据库?

新手上路,请多包涵

springboot + mybatisplus项目中,怎么实现动态切换数据源,要求是不能在yml文件中将数据库连接写死,根据业务需求,服务器中每过一个月会自动生成一个数据库用来存储数据,在这个库中每天会按照日期去生成一张表,我怎么能动态的切换到我想要查询的数据库??有大佬知道不

试过使用aop切面去实现功能,自定义类去继承AbstractRoutingDataSource,但是没有用

阅读 275
avatarAI BotBETA
**核心解决方案:动态数据源路由 + 表名处理器**

1. 实现动态数据源路由(关键代码):

public class DynamicDataSource extends AbstractRoutingDataSource {

private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
private final Map<Object, Object> dynamicTargetDataSources = new ConcurrentHashMap<>();

@Override
protected Object determineCurrentLookupKey() {
    return CONTEXT.get();
}

public void addDataSource(String key, DataSource dataSource) {
    dynamicTargetDataSources.put(key, dataSource);
    super.setTargetDataSources(dynamicTargetDataSources);
    super.afterPropertiesSet(); // 必须触发刷新
}

public static void setDataSourceKey(String key) {
    CONTEXT.set(key);
}

public static void clearDataSourceKey() {
    CONTEXT.remove();
}

}


2. 配置主数据源(至少需要配置一个默认数据源):

@Configuration
public class DataSourceConfig {

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

@Bean
public DynamicDataSource dynamicDataSource(DataSource masterDataSource) {
    DynamicDataSource dynamicDataSource = new DynamicDataSource();
    dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
    dynamicDataSource.setTargetDataSources(new HashMap<>());
    return dynamicDataSource;
}

}


3. AOP切面实现动态切换:

@Aspect
@Component
public class DataSourceAspect {

@Around("@annotation(ds)")
public Object around(ProceedingJoinPoint point, TargetDataSource ds) throws Throwable {
    String dbName = generateDbName(); // 根据业务生成数据库名
    String key = ds.value() + "_" + dbName;
    
    if (!dynamicDataSourceExists(key)) {
        createNewDataSource(key); // 动态创建新数据源
    }
    
    DynamicDataSource.setDataSourceKey(key);
    try {
        return point.proceed();
    } finally {
        DynamicDataSource.clearDataSourceKey();
    }
}

private void createNewDataSource(String key) {
    DataSource newDataSource = DataSourceBuilder.create()
        .driverClassName("com.mysql.cj.jdbc.Driver")
        .url("jdbc:mysql://ip:port/" + key)
        .username("root")
        .password("123456")
        .build();
    dynamicDataSource.addDataSource(key, newDataSource);
}

}


4. 动态表名处理(MyBatis-Plus配置):

public class DynamicTableNameHandler implements TableNameHandler {

@Override
public String dynamicTableName(String sql, String tableName) {
    return tableName + "_" + LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
}

}

@Configuration
public class MybatisPlusConfig {

@Bean
public ConfigurationCustomizer configurationCustomizer() {
    return configuration -> {
        DynamicTableNameHandler dynamicTableNameHandler = new DynamicTableNameHandler();
        configuration.setSqlParser(new JsqlParserCollection());
        configuration.getGlobalConfig().getTableName()
            .setTableNameHandler((sql, tableName) -> {
                if (tableName.startsWith("log_")) {
                    return dynamicTableNameHandler.dynamicTableName(sql, tableName);
                }
                return tableName;
            });
    };
}

}


**关键点说明:**
1. 数据源动态注册:通过`addDataSource`方法实时注册新数据源到Spring容器
2. 连接池管理:建议使用HikariCP并在创建时配置连接池参数
3. 多级缓存:使用LRU策略缓存最近使用的数据源,避免频繁创建
4. 事务管理:需要在切面中处理事务边界,建议使用`@Transactional(propagation = Propagation.REQUIRES_NEW)`
5. 元数据管理:建议维护一个系统表记录所有生成的数据库和表信息

**典型使用场景:**

@Service
public class LogService {

@TargetDataSource("log") // 自定义注解
public List<Log> getTodayLogs() {
    // 会自动路由到log_202308数据库的log_20230801表
    return logMapper.selectList(Wrappers.emptyWrapper());
}

}

1 个回答

你要的效果:

每个月自动创建新数据库,每天自动建新表,查询时自动切换到对应月份的库和当天表。

实现步骤:

动态创建数据库和表

// 每月1号凌晨创建新数据库
@Scheduled(cron = "0 0 0 1 * ?")
void createNewDatabase(){
    String dbName = "db_" + LocalDate.now().format("yyyyMM");
    jdbcTemplate.execute("CREATE DATABASE IF NOT EXISTS " + dbName);
}

// 每天凌晨创建新表
@Scheduled(cron = "0 0 0 * * ?")
void createNewTable(){
    String tableName = "tbl_" + LocalDate.now().format("yyyyMMdd");
    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS " + tableName + "(id INT PRIMARY KEY)");
}

动态切换数据库的核心代码

// 继承Spring的数据源路由类
public class DynamicDataSource extends AbstractRoutingDataSource {
    // 用ThreadLocal保存当前要用的数据库名
    private static final ThreadLocal<String> DB_NAME = new ThreadLocal<>();

    // 动态添加新数据源
    public void addDataSource(String dbName) {
        DataSource dataSource = DataSourceBuilder.create()
                .url("jdbc:mysql://localhost:3306/"+dbName)
                .username("root")
                .password("123456")
                .build();
        
        // 把新数据源加入路由池
        Map<Object, Object> targetDataSources = new HashMap<>(getTargetDataSources());
        targetDataSources.put(dbName, dataSource);
        setTargetDataSources(targetDataSources);
        afterPropertiesSet(); // 刷新
    }

    // 实际切换数据源的方法
    @Override
    protected Object determineCurrentLookupKey() {
        return DB_NAME.get();
    }

    // 设置当前线程用哪个库
    public static void setCurrentDb(String dbName) {
        DB_NAME.set(dbName);
    }

    // 用完后清理
    public static void clear() {
        DB_NAME.remove();
    }
}

MyBatis-Plus动态表名处理

public class DynamicTableNameHandler implements TableNameHandler {
    // 保存当前表名
    private static final ThreadLocal<String> TABLE_NAME = new ThreadLocal<>();

    @Override
    public String dynamicTableName(String sql, String originalTable) {
        return TABLE_NAME.get() != null ? TABLE_NAME.get() : originalTable;
    }

    public static void setTable(String tableName) {
        TABLE_NAME.set(tableName);
    }

    public static void clear() {
        TABLE_NAME.remove();
    }
}

用AOP自动切换库和表

@Aspect
@Component
public class DataSourceAspect {
    @Around("execution(* com.example.service.*.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 生成当前要用的库名和表名
        String dbName = "db_" + LocalDate.now().format("yyyyMM");
        String tableName = "tbl_" + LocalDate.now().format("yyyyMMdd");

        try {
            // 切换数据源
            DynamicDataSource.setCurrentDb(dbName);
            // 设置动态表名
            DynamicTableNameHandler.setTable(tableName);
            
            return joinPoint.proceed();
        } finally {
            // 清理
            DynamicDataSource.clear();
            DynamicTableNameHandler.clear();
        }
    }
}

使用示例

@Service
public class OrderService {
    @Autowired
    private OrderMapper orderMapper;

    // 直接查即可,自动路由到当月的库和当天的表
    public List<Order> getTodayOrders() {
        return orderMapper.selectList(null);
    }
}

注意事项:

第一次访问新库时会自动创建数据源
需要提前确保数据库用户有创建库/表的权限
生产环境建议用连接池(如HikariCP)
事务中切换数据源会有问题,建议在事务外层切换
这个方案的核心思想:通过AOP在每次查询前,根据当前时间算出要用的库和表,然后动态设置到数据源路由和表名处理器中。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
宣传栏