WEBKT

Spring Boot整合Druid实现多数据源与读写分离:动态配置与深度监控实践

230 0 0 0

随着业务的快速发展,单数据源往往难以支撑日益增长的并发请求和数据吞吐量。数据库的读写分离和多数据源管理成为了高并发、大数据量场景下不可或缺的架构优化手段。然而,如何优雅、灵活地实现这些功能,并确保系统稳定性和可观测性,是许多开发者面临的挑战。

本文将聚焦于如何利用阿里巴巴开源的Druid连接池,在Spring Boot项目中实现一套健壮的多数据源管理和读写分离方案,并提供动态配置与详细监控的能力。

1. 为什么选择Druid连接池?

在众多Java连接池中,Druid以其强大的监控功能、丰富的配置选项、SQL防火墙以及对各种数据库方言的良好支持脱颖而出。它不仅能提供高性能的连接管理,还能在生产环境中提供宝贵的数据洞察,帮助我们快速定位问题。

核心优势:

  • 性能卓越: 高效的连接管理策略,减少连接创建和销毁的开销。
  • 功能强大: 内置SQL监控、慢SQL记录、SQL防火墙等功能。
  • 动态配置: 支持运行时修改连接池参数,无需重启应用。
  • 易于集成: 与Spring Boot等主流框架无缝集成。

2. 项目准备

首先,确保你的Spring Boot项目引入了必要的依赖。
pom.xml中添加:

<dependencies>
    <!-- Spring Boot Web Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Druid Spring Boot Starter -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.2.8</version> <!-- 根据实际情况选择最新版本 -->
    </dependency>
    <!-- MySQL Connector (或其他数据库驱动) -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- MyBatis (或JPA等ORM框架) -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.2.0</version>
    </dependency>
    <!-- AOP用于实现动态数据源切换 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
</dependencies>

3. 配置多数据源

假设我们有两个数据源:一个主库(master)用于写操作,一个从库(slave)用于读操作。

application.yml中配置:

spring:
  datasource:
    # 禁用Spring Boot的自动配置,我们手动管理
    type: com.alibaba.druid.pool.DruidDataSource
    dynamic: # 自定义动态数据源配置
      primary: master # 默认数据源
      strict: false   # 是否严格模式,当找不到数据源时报错
      datasource:
        master:
          url: jdbc:mysql://localhost:3306/db_master?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
          username: root
          password: your_master_password
          driver-class-name: com.mysql.cj.jdbc.Driver
          # Druid连接池其他配置
          initial-size: 5
          min-idle: 5
          max-active: 20
          max-wait: 60000
          time-between-eviction-runs-millis: 60000
          min-evictable-idle-time-millis: 300000
          validation-query: SELECT 1
          test-while-idle: true
          test-on-borrow: false
          test-on-return: false
          pool-prepared-statements: true
          max-pool-prepared-statement-per-connection-size: 20
          filters: stat,wall,log4j2 # 开启监控和SQL防火墙
          connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

        slave:
          url: jdbc:mysql://localhost:3306/db_slave?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
          username: root
          password: your_slave_password
          driver-class-name: com.mysql.cj.jdbc.Driver
          initial-size: 3
          min-idle: 3
          max-active: 10
          # 其他与master类似或根据从库负载调整的配置
          filters: stat,wall,log4j2
          connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000

4. 实现动态数据源切换

为了在运行时动态选择数据源,我们需要一个自定义的AbstractRoutingDataSource实现类和一套切换机制。

4.1 定义数据源上下文

public class DynamicDataSourceContextHolder {
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    public static void setDataSourceType(String dataSourceType) {
        CONTEXT_HOLDER.set(dataSourceType);
    }

    public static String getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }

    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}

4.2 自定义动态数据源

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.Map;

public class DynamicDataSource extends AbstractRoutingDataSource {

    public DynamicDataSource(javax.sql.DataSource defaultDataSource, Map<Object, Object> targetDataSources) {
        // 设置默认数据源
        super.setDefaultTargetDataSource(defaultDataSource);
        // 设置所有目标数据源
        super.setTargetDataSources(targetDataSources);
        // 调用 afterPropertiesSet 方法初始化数据源
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}

4.3 配置动态数据源Bean

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

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

@Configuration
public class DataSourceConfig {

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

    @Bean
    @ConfigurationProperties("spring.datasource.dynamic.datasource.slave")
    public DataSource slaveDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @Primary // 标记为主数据源,Spring会优先注入这个
    public DataSource dynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource);
        targetDataSources.put("slave", slaveDataSource);
        return new DynamicDataSource(masterDataSource, targetDataSources); // 默认使用master
    }
}

4.4 定义数据源切换注解和AOP切面

// DataSourceType.java
import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSourceType {
    String value() default "master"; // 默认是master
}
// DataSourceAspect.java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Order(1) // 确保在事务注解之前执行
@Component
public class DataSourceAspect {

    @Around("@annotation(com.example.demo.annotation.DataSourceType)") // 替换为你的注解路径
    public Object switchDataSource(ProceedingJoinPoint point) throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        DataSourceType dataSourceType = method.getAnnotation(DataSourceType.class);

        if (dataSourceType != null) {
            String ds = dataSourceType.value();
            DynamicDataSourceContextHolder.setDataSourceType(ds);
            System.out.println("Switching to dataSource: " + ds);
        }

        try {
            return point.proceed();
        } finally {
            DynamicDataSourceContextHolder.clearDataSourceType();
            System.out.println("Cleared dataSource context.");
        }
    }
}

现在,你可以在Service层的方法上使用@DataSourceType("slave")来指定读操作使用从库,@DataSourceType("master")或不加注解(默认为master)用于写操作。

import com.example.demo.annotation.DataSourceType; // 替换为你的注解路径
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

    // 注入你的Mapper或Repository
    // @Autowired
    // private UserMapper userMapper;

    @Transactional // 事务默认在master库
    public void createUser(String name) {
        // userMapper.insert(name);
        System.out.println("Creating user in master DB: " + name);
    }

    @DataSourceType("slave") // 指定从库
    public String getUserById(Long id) {
        // return userMapper.selectById(id);
        System.out.println("Querying user from slave DB with ID: " + id);
        return "User from slave DB";
    }
    
    @DataSourceType("master") // 明确指定主库
    @Transactional
    public void updateUser(Long id, String newName) {
        // userMapper.update(id, newName);
        System.out.println("Updating user in master DB with ID: " + id + ", new name: " + newName);
    }
}

5. Druid监控页面集成

Druid提供了一个强大的内置监控页面,可以实时查看连接池状态、SQL执行情况、慢SQL等。

5.1 配置Druid Servlet

import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DruidMonitorConfig {

    @Bean
    public ServletRegistrationBean<StatViewServlet> druidStatViewServlet() {
        ServletRegistrationBean<StatViewServlet> servletRegistrationBean =
                new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); // 访问路径
        // 设置白名单或黑名单
        // servletRegistrationBean.addInitParameter("allow", "127.0.0.1");
        // servletRegistrationBean.addInitParameter("deny", "192.168.1.100");
        // 设置登录名和密码
        servletRegistrationBean.addInitParameter("loginUsername", "admin");
        servletRegistrationBean.addInitParameter("loginPassword", "123456");
        // 是否可以重置数据
        servletRegistrationBean.addInitParameter("resetEnable", "true");
        return servletRegistrationBean;
    }

    @Bean
    public FilterRegistrationBean<WebStatFilter> druidWebStatFilter() {
        FilterRegistrationBean<WebStatFilter> filterRegistrationBean =
                new FilterRegistrationBean<>(new WebStatFilter());
        // 过滤所有请求
        filterRegistrationBean.addUrlPatterns("/*");
        // 排除对静态资源和Druid监控页面的过滤
        filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
        return filterRegistrationBean;
    }
}

启动应用后,访问 http://localhost:8080/druid/index.html (根据你的端口和Context Path调整) 即可进入Druid监控页面,输入配置的用户名密码即可查看详细的监控指标。

6. 总结

通过上述步骤,我们成功在Spring Boot应用中集成了Druid连接池,并实现了多数据源管理和基于AOP的读写分离。这套方案不仅具备了良好的扩展性和灵活性,还能通过Druid强大的监控功能,为系统的稳定运行提供有力保障。在实际项目中,你可以根据业务需求和数据库负载情况,进一步优化连接池参数,并考虑引入数据库中间件(如ShardingSphere、MyCAT等)来处理更复杂的分布式事务和分库分表场景。

码匠阿星 Druid数据库

评论点评