专业的JAVA编程教程与资源

网站首页 > java教程 正文

面试官, 你Java项目是如何实现读写分离的?Java代码实战

temp10 2025-03-12 20:38:21 java教程 8 ℃ 0 评论

面试官, 你Java项目是如何实现读写分离的?Java代码实战

首先,我需要回忆一下读写分离的基本概念和应用场景。读写分离主要是为了提升数据库的扩展性,通过将读操作和写操作分发到不同的数据库节点,减轻主库的压力,提高系统的整体性能。

面试官, 你Java项目是如何实现读写分离的?Java代码实战

接下来,我应该考虑常见的实现方式。通常有中间件如MyCat、ShardingSphere,或者框架本身的支持,比如Spring的AbstractRoutingDataSource。用户可能更倾向于具体的代码实现,所以可能需要用Spring的动态数据源切换来举例。

然后,我得想具体的步骤。首先配置主从数据源,然后通过AOP或者注解在运行时决定使用哪个数据源。这里的关键是AbstractRoutingDataSource,它允许程序动态选择数据源。需要定义一个上下文持有当前的数据源标识,比如用ThreadLocal来保证线程安全。

然后,用户可能需要具体的代码示例,比如如何配置主从数据源,如何实现动态路由,以及如何通过注解切换数据源。可能还需要提到事务的问题,因为在写操作后立即读的话,如果主从同步有延迟,可能需要强制读主库,这时候可以用注解处理。

另外,需要注意事务管理,避免在切换数据源时出现问题。比如,如果在事务中切换数据源,可能会导致不一致,所以通常建议在事务方法外部进行数据源切换,或者使用一些高级的事务管理策略。

可能还需要提到测试和验证的部分,确保读写确实分发到了不同的数据源,比如通过日志观察SQL执行的情况,或者使用不同的数据库实例进行验证。

最后,要总结一下这种方案的优缺点,比如优点是实现相对简单,对业务代码侵入性小,但缺点是需要处理主从同步延迟,以及动态数据源切换可能带来的事务问题。还可以提到其他方案,比如使用中间件,但可能配置更复杂。

在思考过程中,还需要考虑用户可能的背景,可能是中级Java开发者,正在准备面试,所以需要回答既要有理论也要有实践代码,结构清晰,关键点明确。同时,要避免过于复杂的配置,保持示例简洁,突出核心逻辑。

在Java项目中实现读写分离通常可以通过 动态数据源路由 + AOP切面 的方式来实现。以下是一个基于Spring Boot的实战方案示例:


一、核心实现思路

  1. 配置主从数据源:配置两个数据源(主库写、从库读)
  2. 动态数据源路由:通过AbstractRoutingDataSource实现动态切换
  3. AOP切面:通过自定义注解标记方法使用读库或写库
  4. ThreadLocal存储上下文:保存当前线程的数据源标识

二、完整代码实现

1. 数据源配置(application.yml)

spring:
  datasource:
    master:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://master-host:3306/db?useSSL=false
      username: root
      password: master-password
    slave:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://slave-host:3306/db?useSSL=false
      username: root
      password: slave-password

2. 数据源上下文管理器

public class DataSourceContextHolder {
    private static final ThreadLocal contextHolder = new ThreadLocal<>();

    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
    }

    public static String getDataSource() {
        return contextHolder.get();
    }

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

3. 动态数据源路由

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

4. 数据源配置类

@Configuration
public class DataSourceConfig {

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

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public DataSource dynamicDataSource() {
        Map targetDataSources = new HashMap<>();
        targetDataSources.put("master", masterDataSource());
        targetDataSources.put("slave", slaveDataSource());

        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
        
        return dynamicDataSource;
    }
}

5. 自定义注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataSource {
    String value() default "master";
}

6. AOP切面实现

@Aspect
@Component
public class DataSourceAspect {

    @Before("@annotation(dataSource)")
    public void beforeSwitchDataSource(JoinPoint point, DataSource dataSource) {
        String value = dataSource.value();
        if ("slave".equalsIgnoreCase(value)) { // 实际生产环境可能有多个从库
            // 可以在此处实现负载均衡逻辑
            DataSourceContextHolder.setDataSource("slave");
        } else {
            DataSourceContextHolder.setDataSource("master");
        }
    }

    @After("@annotation(dataSource)")
    public void afterSwitchDataSource(DataSource dataSource) {
        DataSourceContextHolder.clear();
    }
}

7. 使用示例

@Service
public class UserService {

    // 写操作默认走主库
    public void createUser(User user) {
        // insert操作...
    }

    // 读操作指定走从库
    @DataSource("slave")
    public User getUserById(Long id) {
        // select操作...
        return userRepository.findById(id);
    }
}

三、关键点说明

  1. 主从同步延迟处理
  2. 对于写后立即读的场景,可以通过@DataSource("master")强制读主库
  3. 或使用数据库中间件保证数据一致性
  4. 事务处理
  5. 事务方法中切换数据源需要在事务开启前完成
  6. 建议在Service层进行数据源切换
  7. 从库负载均衡
  8. 可以扩展DataSourceAspect实现随机/轮询选择从库
  9. 监控报警
  10. 建议增加数据源健康检查
  11. 监控主从延迟

四、其他实现方案对比

方案

优点

缺点

本文方案

代码级控制,灵活

需要自行处理事务边界

ShardingSphere-JDBC

功能完善,支持分布式事务

需要学习额外API

MyCAT

无需改代码

需要维护中间件,增加运维成本

实际选择时需根据团队技术栈和业务复杂度权衡,中小型项目使用动态数据源方案即可满足需求。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表