以下是 通过接口动态切换数据源 的完整实现流程,基于 dynamic-datasource-spring-boot-starter 4.2.0,确保切换后所有接口使用新数据源。


1. 添加依赖

确保 pom.xml 包含以下依赖:

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
    <version>4.2.0</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.2.8</version>
</dependency>

2. 配置数据源

application.yml 中配置多数据源:

spring:
  datasource:
    dynamic:
      primary: master   # 默认数据源
      strict: false     # 是否严格匹配数据源
      datasource:
        master:
          url: jdbc:mysql://192.168.0.4:3306/qy2?useSSL=false&serverTimezone=Asia/Shanghai
          username: mysql
          password: mysql
          driver-class-name: com.mysql.cj.jdbc.Driver
          type: com.alibaba.druid.pool.DruidDataSource
        slave1:
          url: jdbc:mysql://127.0.0.1:3306/qy2?useSSL=false&serverTimezone=Asia/Shanghai
          username: root
          password: root
          driver-class-name: com.mysql.cj.jdbc.Driver
          type: com.alibaba.druid.pool.DruidDataSource

3. 实现全局数据源拦截器

在请求前根据 Session 中的标识切换数据源:

@Component
@Slf4j
public class GlobalDataSourceInterceptor implements HandlerInterceptor {

    @Resource
    private DynamicDataSourceProperties dataSourceProperties;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        HttpSession session = request.getSession(false);
        log.warn("当前 Session ID: " + (session != null ? session.getId() : "null"));
        if (session != null) {
            String ds = (String) session.getAttribute("current_ds");
            log.warn("从 Session 中读取的数据源: {}",ds);
            Set<String> dataSourceNames = dataSourceProperties.getDatasource().keySet();
            if (ds != null && dataSourceNames.contains(ds)) {
                DynamicDataSourceContextHolder.push(ds);
                log.warn("切换数据源: {}",ds);
            }else {
                DynamicDataSourceContextHolder.push("master");
                log.warn("切换到默认数据源: master");
            }
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        DynamicDataSourceContextHolder.clear();
    }
}

注册拦截器并设置最高优先级:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private DataSourceInterceptor dataSourceInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(dataSourceInterceptor)
                .order(Ordered.HIGHEST_PRECEDENCE); // 确保最先执行
    }
}

4. 编写切换接口

通过接口将数据源名称保存到 Session 中:

@RestController
@Api(tags = "数据源管理")
public class DataSourceController {

    @Autowired
    private DynamicDataSourceProperties dataSourceProperties;

    @PostMapping("/switch")
    @ApiOperation("切换数据源")
    public ResponseEntity<String> switchDataSource(
            @RequestParam @ApiParam("数据源名称(如 master, slave1)") String ds,
            HttpSession session
    ) {
        // 校验数据源是否存在
        Set<String> validDataSources = dataSourceProperties.getDatasource().keySet();
        if (!validDataSources.contains(ds)) {
            return ResponseEntity.badRequest().body("无效数据源: " + ds);
        }

        // 保存到 Session
        session.setAttribute("current_ds", ds);
        return ResponseEntity.ok("全局数据源已切换至: " + ds);
    }

    @GetMapping("/current")
    @ApiOperation("查询当前数据源")
    public ResponseEntity<String> getCurrentDataSource(HttpSession session) {
        String currentDs = (String) session.getAttribute("current_ds");
        if (currentDs == null) {
            return ResponseEntity.ok("当前使用默认数据源: " + dataSourceProperties.getPrimary());
        }
        return ResponseEntity.ok("当前数据源: " + currentDs);
    }
}

5. 事务处理建议

场景 1:无事务方法

直接使用拦截器切换数据源即可。

场景 2:需要事务的方法

在事务外层手动切换数据源:

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public List<User> getUsersFromSlave() {
        // 手动切换数据源
        DynamicDataSourceContextHolder.push("slave1");
        return transactionalGetUsers();
    }

    @Transactional
    public List<User> transactionalGetUsers() {
        return userMapper.selectList(null);
    }
}

6. 验证流程

步骤 1:切换数据源
POST http://localhost:8080/switch?ds=slave1
响应:全局数据源已切换至: slave1
步骤 2:查询当前数据源
GET http://localhost:8080/current
响应:当前数据源: slave1
步骤 3:执行业务查询
GET http://localhost:8080/api/users

观察控制台日志:

[数据源拦截器] 已切换至数据源: slave1

7. 常见问题排查

问题 1:切换无效
  • 检查点
    1. 确认拦截器日志是否打印切换成功。
    2. 检查 Session ID 是否一致(切换和查询是否同一会话)。
    3. 检查业务方法是否开启了事务(导致数据源提前绑定)。
问题 2:数据源不存在
  • 检查点
    1. 确保 application.yml 中数据源名称与代码中一致。
    2. 校验切换接口中的 validDataSources.contains(ds) 逻辑。
问题 3:连接失败
  • 检查点
    1. 直接使用 MySQL 客户端连接 slave1 的 URL、用户名、密码。
    2. 检查防火墙或网络权限。

8. 最终效果

  • 动态切换:通过 /switch 接口切换后,所有后续请求自动使用新数据源。
  • 会话隔离:不同用户的 Session 独立维护数据源状态。
  • 事务安全:通过外层手动切换避免事务绑定问题。

通过以上步骤,可以实现通过接口动态切换全局数据源。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐