在Spring Boot + MyBatis项目中实现MySQL读写分离

32 阅读4分钟

在Spring Boot和MyBatis项目中实现MySQL读写分离,主要有两种思路:一种是在应用层通过代码和配置手动控制,另一种是借助中间件自动路由

下面是几种主流的实现方案,并提供一个基于Spring抽象路由数据源的实战指南。

🎯 如何选择实现方案

你可以根据项目的复杂度、团队的技术栈和运维能力,参考下表进行选择:

场景推荐方案核心特点
快速上手、简单应用应用层手动分离实现简单,但代码有侵入性,新增节点需重启
对应用透明、生产环境中间件代理 (如 ProxySQLMySQL Router)功能强大,解耦应用与数据库,支持动态扩展
Java生态、复杂场景ShardingSphere支持读写分离、分库分表,与Java应用集成度高
云环境云服务商代理 (如 AWS RDS Proxy, 阿里云读写分离)免运维,开箱即用,与云服务深度集成

对于大多数Java开发者来说,从应用层手动分离开始学习是最直观的路径。

🔧 核心实现:应用层手动分离

这种方法的核心是利用Spring的 AbstractRoutingDataSource 来动态决定使用哪个数据源。

主要步骤

  1. 配置多个数据源
    在你的application.yml文件中,分别配置主库和从库的连接信息。

    spring:
      datasource:
        master:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://master-host:3306/db_name
          username: root
          password: master-pass
        slave:
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://slave-host:3306/db_name
          username: root
          password: slave-pass
    
  2. 创建数据源上下文持有者
    使用ThreadLocal来安全地存储和获取当前线程需要使用的数据源键值。

    public class DataSourceContextHolder {
        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();
        }
    }
    
  3. 创建动态数据源类
    继承AbstractRoutingDataSource并重写determineCurrentLookupKey方法,使其从上下文持有者中获取数据源键值。

    java

    public class DynamicDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            return DataSourceContextHolder.getDataSourceType();
        }
    }
    
  4. 使用AOP自动切换数据源
    通过切面,根据方法名或自定义注解自动设置数据源。

    @Aspect
    @Component
    public class DataSourceAspect {
        
        // 拦截所有Mapper层方法
        @Before("execution(* com.yourpackage.mapper..*.*(..))")
        public void beforeMethod(JoinPoint joinPoint) {
            String methodName = joinPoint.getSignature().getName();
            // 根据方法名判断是读还是写
            if (methodName.startsWith("get") || methodName.startsWith("select") || 
                methodName.startsWith("find") || methodName.startsWith("query")) {
                // 读方法切换到从库
                DataSourceContextHolder.setDataSourceType("slave");
            } else {
                // 写方法切换到主库
                DataSourceContextHolder.setDataSourceType("master");
            }
        }
        
        // 方法执行后清理数据源设置
        @After("execution(* com.yourpackage.mapper..*.*(..))")
        public void afterMethod() {
            DataSourceContextHolder.clearDataSourceType();
        }
    }
    
  5. 处理特殊情况:强制读主库
    在某些需要实时读取数据的场景(如刚写入后立刻查询),你可以使用自定义注解来强制方法走主库。

    java

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Master {
    }
    

    然后在切面中增加对@Master注解的判断,优先使用主库。

⚠️ 实施中的关键问题与解决方案

  • 主从延迟问题:这是读写分离架构中最常见的挑战。对于需要实时性保证的查询,可以使用上面提到的@Master注解强制走主库。
  • 事务管理:为了确保数据一致性,在写操作(如@Transactional注解的方法)中,整个事务内的所有数据库操作都应使用主库。你可以在事务开始时就强制指定数据源为主库。
  • 故障转移与监控:需要建立从库健康检查机制。当监测到从库延迟过大或宕机时,应能自动将读请求降级到主库,保证系统的可用性。

💎 总结

实现Spring Boot + MyBatis的读写分离,应用层手动方案适合快速上手和理解原理,而中间件方案则在动态扩展和运维管理上更有优势。

在学习或中小型项目中先从应用层方案入手,随着业务规模扩大,再平滑迁移到ProxySQL或ShardingSphere等更强大的中间件上。

image.png

谢谢你看我的文章,既然看到这里了,如果觉得不错,随手点个赞、转发、在看三连吧,感谢感谢。那我们,下次再见。

您的一键三连,是我更新的最大动力,谢谢

山水有相逢,来日皆可期,谢谢阅读,我们再会

我手中的金箍棒,上能通天,下能探海