Java案例怎么实现主从数据源?

wen java案例 73

本文目录导读:

Java案例怎么实现主从数据源?

  1. 核心实现方案
  2. 高级扩展方案
  3. 注意事项

在Java中实现主从数据源(读写分离)有多种方式,这里我给你一个最常用、最实用的完整方案,使用Spring Boot + MyBatis + 动态数据源(基于AbstractRoutingDataSource)。

核心实现方案

Maven依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.2.18</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
    </dependency>
</dependencies>

数据库配置(application.yml)

spring:
  datasource:
    # 主库(写)
    master:
      url: jdbc:mysql://localhost:3306/db_master?useUnicode=true&characterEncoding=utf-8
      username: root
      password: root123
      driver-class-name: com.mysql.cj.jdbc.Driver
    # 从库(读) - 可以有多个
    slave:
      - url: jdbc:mysql://localhost:3307/db_slave1?useUnicode=true&characterEncoding=utf-8
        username: root
        password: root123
        driver-class-name: com.mysql.cj.jdbc.Driver
      - url: jdbc:mysql://localhost:3308/db_slave2?useUnicode=true&characterEncoding=utf-8
        username: root
        password: root123
        driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    druid:
      initial-size: 5
      min-idle: 5
      max-active: 20
      max-wait: 60000

定义数据源枚举

public enum DataSourceType {
    MASTER("master"),
    SLAVE("slave");
    private String name;
    DataSourceType(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

实现动态数据源核心类

/**
 * 动态数据源上下文持有者
 */
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();
    }
}
/**
 * 动态数据源 - 继承AbstractRoutingDataSource
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}
/**
 * 数据源配置类
 */
@Configuration
public class DataSourceConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
    @Bean
    @Primary
    public DynamicDataSource dynamicDataSource() {
        // 这里简化处理,实际应该从配置读取多个从库
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.MASTER.getName(), masterDataSource());
        targetDataSources.put(DataSourceType.SLAVE.getName(), slaveDataSource());
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
        dynamicDataSource.setTargetDataSources(targetDataSources);
        return dynamicDataSource;
    }
}

注解定义(用于标记读写操作)

/**
 * 读操作注解 - 走从库
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
    boolean value() default true;
}
/**
 * 写操作注解 - 走主库
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MasterOnly {
    boolean value() default true;
}

AOP切面实现数据源自动切换

@Aspect
@Component
@Order(1) // 优先级高,先于事务执行
public class DataSourceAspect {
    private static final Logger log = LoggerFactory.getLogger(DataSourceAspect.class);
    /**
     * 对service层方法进行拦截(这里简化为拦截所有方法)
     */
    @Pointcut("execution(* com.example..service.*.*(..))")
    public void servicePointcut() {}
    @Before("servicePointcut()")
    public void before(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        // 1. 检查方法上的@MasterOnly注解
        MasterOnly masterOnly = method.getAnnotation(MasterOnly.class);
        if (masterOnly != null) {
            DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.getName());
            log.debug("Set DataSource to MASTER for method: {}", method.getName());
            return;
        }
        // 2. 检查方法上的@ReadOnly注解
        ReadOnly readOnly = method.getAnnotation(ReadOnly.class);
        if (readOnly != null) {
            // 从从库中随机选择一个(负载均衡)
            String slaveKey = selectSlave();
            DynamicDataSourceContextHolder.setDataSourceType(slaveKey);
            log.debug("Set DataSource to SLAVE for method: {}", method.getName());
            return;
        }
        // 3. 默认走主库
        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.getName());
    }
    @After("servicePointcut()")
    public void after(JoinPoint joinPoint) {
        DynamicDataSourceContextHolder.clearDataSourceType();
        log.debug("Cleared DataSource context for method: {}", 
                  joinPoint.getSignature().getName());
    }
    /**
     * 简单的从库负载均衡 - 轮询
     */
    private String selectSlave() {
        // 可以在这里实现随机、轮询、最少连接等算法
        // 这里简单返回SLAVE,如果有多个从库可以扩展
        return DataSourceType.SLAVE.getName();
    }
}

事务支持(重要)

/**
 * 事务管理器配置 - 使用动态数据源
 */
@Configuration
public class TransactionConfig {
    @Bean
    public PlatformTransactionManager transactionManager(DynamicDataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
    /**
     * 支持在事务内切换数据源
     */
    @Bean
    public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
        return new TransactionTemplate(transactionManager);
    }
}

业务层使用示例

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    /**
     * 写操作 - 自动走主库
     */
    @Transactional
    @MasterOnly
    public void createUser(User user) {
        userMapper.insert(user);
    }
    /**
     * 读操作 - 自动走从库
     */
    @ReadOnly
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }
    /**
     * 读写混合操作
     * 注意:事务内读操作需要特别注意,默认走主库
     */
    @Transactional
    @MasterOnly
    public User updateAndGetUser(User user) {
        userMapper.updateById(user);
        // 这里虽然是读,但在事务内,需要走主库
        return userMapper.selectById(user.getId());
    }
    /**
     * 复杂查询 - 走从库
     */
    @ReadOnly
    public List<User> getUserList(String name) {
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.like(User::getName, name);
        return userMapper.selectList(wrapper);
    }
}

高级扩展方案

多从库负载均衡

/**
 * 动态数据源负载均衡器
 */
@Component
public class DynamicDataSourceBalancer {
    // 模拟多个从库
    private static final List<String> SLAVE_KEYS = Arrays.asList(
        "slave1", "slave2", "slave3"
    );
    private AtomicInteger counter = new AtomicInteger(0);
    public String getSlaveKey() {
        // 轮询
        int index = counter.getAndIncrement() % SLAVE_KEYS.size();
        if (counter.get() > Integer.MAX_VALUE - 10000) {
            counter.set(0);
        }
        return SLAVE_KEYS.get(index);
    }
}

读写分离数据源配置优化

@Configuration
public class MultiDataSourceConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slaves")
    public List<DataSource> slaveDataSources() {
        // 从配置读取多个从库
        return new ArrayList<>();
    }
    @Bean
    @Primary
    public DataSource routingDataSource() {
        DynamicDataSource routingDataSource = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource());
        // 注册多个从库
        List<DataSource> slaves = slaveDataSources();
        for (int i = 0; i < slaves.size(); i++) {
            targetDataSources.put("slave" + (i + 1), slaves.get(i));
        }
        routingDataSource.setDefaultTargetDataSource(masterDataSource());
        routingDataSource.setTargetDataSources(targetDataSources);
        return routingDataSource;
    }
}

注意事项

  1. 事务内一致性:在事务内读操作建议走主库,避免读到脏数据
  2. 数据同步延迟:主从复制有延迟,实时性要求高的读操作走主库
  3. 连接池配置:主从库使用独立的连接池配置
  4. 监控告警:监控主从同步状态和延迟时间
  5. 异常处理:从库不可用自动切换到主库

这个方案是最常用的注解驱动 + AOP拦截方式,易于理解、便于维护,适合大部分业务场景。

抱歉,评论功能暂时关闭!