开发者

SpringBoot实现数据库读写分离的3种方法小结

目录
  • 一、数据库读写分离概述
  • 二、方案一:基于AbstractRoutingDataSource实现动态数据源
    • 2.1 实现原理
    • 2.2 具体实现步骤
    • 2.3 优缺点分析
  • 三、方案二:基于ShardingSphere-JDBC实现读写分离
    • 3.1 实现原理
    • 3.2 具体实现步骤
    • 3.3 优缺点分析
  • 四、方案三:基于MyBATis插件实现读写分离
    • 4.1 实现原理
    • 4.2 具体实现步骤
    • 4.3 优缺点分析
  • 五、三种方案对比与选型指南
    • 5.1 功能对比
    • 5.2 选型建议
  • 六、实施读写分离的最佳实践
    • 6.1 数据一致性处理
    • 6.2 事务管理
    • 6.4 监控与性能优化
  • 七、总结

    一、数据库读写分离概述

    在大型应用系统中,随着访问量的增加,数据库常常成为系统的性能瓶颈。为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式。它将数据库读操作和写操作分别路由到不同的数据库实例,通常是将写操作指向主库(Master),读操作指向从库(Slave)。

    读写分离的主要优势:

    • 分散数据库访问压力,提高系统的整体吞吐量
    • 提升读操作的性能和并发量
    • 增强系统的可用性和容错能力

    在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三种主实现方案。

    二、方案一:基于AbstractRoutingDataSource实现动态数据源

    这种方案是基于Spring提供的AbstractRoutingDataSource抽象类,通过重写其中的determineCurrentLookupKey()方法来实现数据源的动态切换。

    2.1 实现原理

    AbstractRoutingDataSource的核心原理是在执行数据库操作时,根据一定的策略(通常基于当前操作的上下文)动态地选择实际的数据源。通过在业务层或AOP拦截器中设置上下文标识,让系统自动判断是读操作还是写操作,从而选择对应的数据源。

    2.2 具体实现步骤

    第一步:定义数据源枚举和上下文持有器

    // 数据源类型枚举
    public enum DataSourceType {
        MASTER, // 主库,用于写操作
        SLAVE   // 从库,用于读操作
    }
    
    // 数据源上下文持有器
    public class DataSourceContextHolder {
        private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();
        
        public static void setDataSourceType(DataSourceType dataSourceType) {
            contextHolder.set(dataSourceType);
        }
        
        public static DataSourceType getDataSourceType() {
            return contextHolder.get() == null ? DataSourceType.MASTER : contextHolder.get();
        }
        
        public static void clearDataSourceType() {
            contextHolder.remove();
        }
    }
    

    第二步:实现动态数据源

    public class DynamicDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            return DataSourceContextHolder.getDataSourceType();
        }
    }
    

    第三步:配置数据源

    @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() {
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            
            Map<Object, Object> dataSourceMap = new HashMap<>(2);
            dataSourceMap.put(DataSourceType.MASTER, masterDataSource());
            dataSourceMap.put(DataSourceType.SLAVE, slaveDataSource());
            
            // 设置默认数据源为主库
            dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
            dynamicDataSource.setTargetDataSources(dataSourceMap);
            
            return dynamicDataSource;
        }
        
        @Bean
        public SqlSessionFactory sqlSessionFactory() throws Exception {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dynamicDataSource());
            
            // 设置MyBatis配置
            // ...
            
            return sqlSessionFactoryBean.getObject();
        }
    }
    

    第四步:实现AOP拦截器,根据方法匹配规则自动切换数据源

    @ASPect
    @Component
    public class DataSourceAspect {
        
        // 匹配所有以select、query、get、find开头的方法为读操作
        @Pointcut("execution(* com.example.service.impl.*.*(..))")
        public void servicePointcut() {}
        
        @Before("servicePointcut()")
        public void switchDataSource(JoinPoint point) {
            // 获取方法名
            String methodName = point.getSignature().getName();
            
            // 根据方法名判断是读操作还是写操作
            if (methodName.startsWith("select") || 
                methodName.startsWith("query") || 
                methodName.startsWith("get") || 
                methodName.startsWith("find")) {
                // 读操作使用从库
                DataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE);
            } else {
                // 写操作使用主库
                DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
            }
        }
        
        @After("servicePointcut()")
        public void restoreDataSource() {
            // 清除数据源配置
            DataSourceContextHolder.clearDataSourceType();
        }
    }
    

    第五步:配置文件application.yml

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

    第六步:使用注解方式灵活控制数据源(可选增强)

    // 定义自定义注解
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface DataSource {
        DataSourceType value() default DataSourceType.MASTER;
    }
    
    // 修改AOP拦截器,优先使用注解指定的数据源
    @Aspect
    @Component
    public class DataSourceAspect {
        
        @Pointcut("@annotation(com.example.annotation.DataSource)")
        public void dataSourcePointcut编程客栈() {}
        
        @Before("dataSourcePointcut()")
        public void switchDataSource(JoinPoint point) {
            MethodSignature signature = (MethodSignature) point.getSignature();
            Method method = signature.getMethod();
            
            DataSource dataSource = method.getAnnotation(DataSource.class);
            if (dataSource != null) {
                DataSourceContextHolder.setDataSourceType(dataSource.value());
            }
        }
        
        @After("dataSourcePointcut()")
        public void restoreDataSource() {
            DataSourceContextHolder.clearDataSourceType();
        }
    }
    
    // 在Service方法上使用
    @Service
    public class UserServiceImpl implements UserService {
        
        @Override
        @DataSource(DataSourceType.SLAVE)
        public List<User> findAllUsers() {
            return userMapper.selectAll();
        }
        
        @Override
        @DataSource(DataSourceType.MASTER)
        public void createUser(User user) {
            userMapper.insert(user);
        }
    }
    

    2.3 优缺点分析

    优点:

    • 实现简单,不依赖第三方组件
    • 侵入性小,对业务代码影响较小
    • 灵活性高,可以根据业务需求灵活切换数据源
    • 支持多数据源扩展,不限于主从两个库

    缺点:

    • 需要手动指定或通过约定规则判断读写操作

    适用场景:

    • 中小型项目,读写请求分离明确
    • 对中间件依赖要求低的场景
    • 临时性能优化,快速实现读写分离

    三、方案二:基于ShardingSphere-JDBC实现读写分离

    ShardingSphere-JDBC是Apache ShardingSphere项目下的一个子项目,它通过客户端分片的方式,为应用提供了透明化的读写分离和分库分表等功能。

    3.1 实现原理

    ShardingSphere-JDBC通过拦截JDBC驱动,重写SQL解析与执行流程来实现读写分离。它能够根据SQL语义自动判断读写操作,并将读操作负载均衡地分发到多个从库。

    3.2 具体实现步骤

    第一步:添加依赖

    <dependency>
        <groupId>org.apache.shardingsphere</groupId>
        <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
        <version>5.2.1</version>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    

    第二步:配置文件application.yml

    spring:
      shardingsphere:
        mode:
          type: Memory
        datasource:
          names: master,slave1,slave2
          master:
            type: com.zaxxer.hikari.HikariDataSource
            driver-class-name: com.mysql.cj.jdbc.Driver
            jdbc-url: jdbc:mysql://master-db:3306/test?useSSL=false
            username: root
            password: root
          slave1:
            type: com.zaxxer.hikari.HikariDataSource
            driver-class-name: com.mysql.cj.jdbc.Driver
            jdbc-url: jdbc:mysql://slave1-db:3306/test?useSSL=false
            username: root
            password: root
          slave2:
            type: com.zaxxer.hikari.HikariDataSource
            driver-class-name: com.mysql.cj.jdbc.Driver
            jdbc-url: jdbc:mysql://slave2-db:3306/test?useSSL=false
            username: root
            password: root
        rules:
          readwrite-splitting:
            data-sources:
              readwrite_ds:
                type: Static
                props:
                  write-data-source-name: master
                  read-data-source-names: slave1,slave2
                load-balancer-name: round_robin
            load-balancers:
              round_robin:
                type: ROUND_ROBIN
        props:
          sql-show: true # 开启SQL显示,方便调试
    

    第三步:创建数据源配置类

    @Configuration
    public class DataSourceConfig {
        
        // 无需额外配置,ShardingSphere-JDBC会自动创建并注册DataSource
        
        @Bean
        @ConfigurationProperties(prefix = "mybatis")
        public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dataSource);
            return sqlSessionFactoryBean;
        }
    }
    

    第四步:强制主库查询的注解(可选)

    在某些场景下,即使是查询操作也需要从主库读取最新数据,ShardingSphere提供了hint机制来实现这一需求。

    // 定义主库查询注解
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface MasterRoute {
    }
    
    // 创建AOP切面拦截器
    @Aspect
    @Component
    public class MasterRouteAspect {
        
        @Around("@annotation(com.example.annotation.MasterRoute)")
        public Object aroundMasterRoute(ProceedingJoinPoint joinPoint) throws Throwable {
            try {
                HintManager.getInstance().setWriteRouteOnly();
                return joinPoint.proceed();
            } finally {
                HintManager.clear();
            }
        }
    }
    
    // 在需要主库查询的方法上使用注解
    @Service
    public class OrderServiceImpl implements OrderService {
        
        @Autowired
        private OrderMapper orderMapper;
        
        @Override
        @MasterRoute
        public Order getLatestOrder(Long userId) {
            // 这里的查询会路由到主库
            return orderMapper.findLatestByUserId(userId);
        }
    }
    

    3.3 优缺点分析

    优点:

    • 自动识别SQL类型,无需手动指定读写数据源
    • 支持多从库负载均衡
    • 提供丰富的负载均衡算法(轮询、随机、权重等)
    • 完整的分库分表能力,可无缝扩展
    • 对应用透明,业务代码无需修改

    缺点:

    • 引入额外的依赖和学习成本
    • 配置相对复杂
    • 性能有轻微损耗(SQL解析和路由)

    适用场景:

    • 中大型项目,有明确的读写分离需求
    • 需要负载均衡到多从库的场景
    • 未来可能需要分库分表的系统

    四、方案三:基于MyBatis插件实现读写分离

    MyBatis提供了强大的插件机制,允许在SQL执行的不同阶段进行拦截和处理。通过自定义插件,可以实现基于SQL解析的读写分离功能。

    4.1 实现原理

    MyBatis允许拦截执行器的queryupdate方法,通过拦截这些方法,可以在SQL执行前动态切换数据源。这种方式的核心是编写一个拦截器,分析即将执行的SQL语句类型(SELECT/INSERT/UPDATE/DELETE),然后根据SQL类型切换到相应的数据源。

    4.2 具体实现步骤

    第一步:定义数据源和上下文(与方案一类似)

    public enum DataSourceType {
        MASTER, SLAVE
    }
    
    public class DataSourceContextHolder {
        private static final ThreadLocal<DataSourceType> contextHolder = new ThreadLocal<>();
        
        public static void setDataSourceType(DataSourceType dataSourceType) {
            contextHolder.set(dataSourceType);
        }
        
        public static DataSourceType getDataSourceType() {
            return contextHolder.get() == null ? DataSourceType.MASTER : contextHolder.get();
        }
        
        public static void clearDataSourceType() {
            contextHolder.remove();
        }
    }
    
    public class DynamicDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            return DataSourceContextHolder.getDataSourceType();
        }
    }
    

    第二步:实现MyBatis拦截器

    @Intercepts({
        @Signature(type = Executor.class, method = "query", pythonargs = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
    })
    @Component
    public class ReadWriteSplittingInterceptor implements Interceptor {
        
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) argshttp://www.devze.com[0];
            
            try {
                // 判断是否为事务
                boolean isTransactional = TransactionSynchronizationManager.isActualTransactionActive();
                
                // 如果是事务,则使用主库
                if (isTransactional) {
                    DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
                    return invocation.proceed();
                }
                
                // 根据SQL类型选择数据源
                if (ms.getSqlCommandType() == SqlCommandType.SELECT) {
                    // 读操作使用从库
                    DataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE);
                } else {
                    // 写操作使用主库
                    DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
                }
                
                return invocation.proceed();
            } finally {
                // 清除数据源配置
                DataSourceContextHolder.clearDataSourceType();
            }
        }
        
        @Override
        public Object plugin(Object target) {
            if (target instanceof Executor) {
                return Plugin.wrap(target, this);
            }
            return target;
        }
        
        @Override
        public void setProperties(Properties properties) {
            // 可以从配置文件加载属性
        }
    }
    

    第三步:配置数据源和MyBatis插件

    @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() {
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            
            Map<Object, Object> dataSourceMap = new HashMap<>(2);
            dataSourceMap.put(DataSourceType.MASTER, masterDataSource());
            dataSourceMap.put(DataSourceType.SLAVE, slaveDataSource());
            
            dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
            dynamicDataSource.setTargetDataSources(dataSourceMap);
            
            return dynamicDataSource;
        }
        
        @Bean
        public SqlSessionFactory sqlSessionFactory(@Autowired ReadWriteSplittingInterceptor interceptor) throws Exception {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dynamicDataSource());
            
            // 添加MyBatis插件
            sqlSessionFactoryBean.setPlugins(new Interceptor[]{interceptor});
            
            // 其他MyBatis配置
            // ...
            
            return sqlSessionFactoryBean.getObject();
        }
    }
    

    第四步:强制主库查询注解(可选)

    @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() {
            DynamicDataSource dynamicDataSource = new DynamicDataSource();
            
            Map<Object, Object> dataSourceMap = new HashMap<>(2);
            dataSourceMap.put(DataSourceType.MASTER, masterDataSource());
            dataSourceMap.put(DataSourceType.SLAVE, slaveDataSource());
            
            dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
            dynamicDataSource.setTargetDataSources(dataSourceMap);
            
            return dynamicDataSource;
        }
        
        @Bean
        public SqlSessionFactory sqlSessionFactory(@Autowired ReadWriteSplittingInterceptor interceptor) throws Exception {
            SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
            sqlSessionFactoryBean.setDataSource(dynamicDataSource());
            
            // 添加MyBatis插件
            sqlSessionFactoryBean.setPlugins(new Interceptor[]{interceptor});
            
            // 其他MyBatis配置
            // ...
            
            return sqlSessionFactoryBean.getObject();
        }
    }
    

    4.3 优缺点分析

    优点:

    • 自动识别SQL类型,无需手动指定数据源
    • 可灵活扩展,支持复杂的路由规则
    • 基于MyBatis原生插件机制,无需引入额外的中间件

    缺点:

    • 仅适用于使用MyBatis的项目
    • 需要理解MyBatis插件机制
    • 没有内置的负载均衡能力,需要额外开发
    • 可能与其他MyBatis插件产生冲突
    • 事务管理较为复杂

    适用场景:

    • 纯MyBatis项目
    • 定制化需求较多的场景
    • 对第三方中间件有限制的项目
    • 需要对读写分离有更精细控制的场景

    五、三种方案对比与选型指南

    5.1 功能对比

    功能特性方案一:AbstractRoutingDataSource方案二:ShardingSphere-JDBC方案三:MyBatis插件
    自动识别SQL类型❌ 需要手动或通过规则指定✅ 自动识别✅ 自动识别
    多从库负载均衡❌ 需要自行实现✅ 内置多种算法❌ 需要自行实现
    与分库分表集成❌ 不支持✅ 原生支持❌ 需要额外开发
    开发复杂度⭐⭐ 中等⭐ 较低⭐⭐⭐ 较高
    配置复杂度⭐ 较低⭐⭐⭐ 较高⭐⭐ 中等

    5.2 选型建议

    选择方案一(AbstractRoutingDataSource)的情况:

    • 项目规模较小,读写分离规则简单明确
    • 对第三方依赖敏感,希望减少依赖
    • 团队对Spring原生机制较为熟悉
    • 系统处于早期阶段,可能频繁变动

    选择方案二(ShardingSphere-JDBC)的情况:

    • 中大型项目,有复杂的数据库访问需求
    • 需要多从库负载均衡能力
    • 未来可能需要分库分表
    • 希望尽量减少代码侵入
    • 对开发效率要求较高

    选择方案三(MyBatis插件)的情况:

    • 项目完全基于MyBatis架构
    • 团队对MyBatis插件机制较为熟悉
    • 有特定的定制化需求
    • 希望对SQL路由有更细粒度的控制
    • 对框架依赖有严格限制

    六、实施读写分离的最佳实践

    6.1 数据一致性处理

    从库数据同步存在延迟,这可能导致读取到过期数据的问题。处理方法:

    • 提供强制主库查询的选项:对于需要最新数据的查询,提供从主库读取的机制
    • 会话一致性:同一会话内的读写操作使用相同的数据源
    • 延迟检测:定期检测主从同步延迟,当延迟超过阈值时暂停从库查询
    // 实现延迟检测的示例
    @Component
    @Slf4j
    public class ReplicationLagMonitor {
        
        @Autowired
        private JdbcTemplate masterJdbcTemplate;
        
        @Autowired
        private JdbcTemplate slaveJdbcTemplate;
        
        private AtomicBoolean slaveTooLagged = new AtomicBoolean(false);
        
        @Scheduled(fixedRate = 5000) // 每5秒检查一次
        public void checkReplicationLag() {
            try {
                // 在主库写入标记
                String mark = UUID.randomUUID().toString();
                masterJdbcTemplate.update("INSERT INTO replication_marker(marker, create_time) VALUES(?, NOW())", mark);
                
                // 等待一定时间,给从库同步的机会
                Thread.sleep(1000);
                
                // 从从库查询该标记
                Integer count = slaveJdbcTemplate.queryForObject(
                    "SELECT COUNT(*) FROM replication_marker WHERE marker = ?", Integer.class, mark);
                
                // 判断同步延迟
                boolean lagged = (count == null || count == 0);
                slaveTooLagged.set(lagged);
     
                if (lagged) {
                    log.warn("Slave replication lag detected, routing read operations to master");
                } else {
                    log.info("Slave replication is in sync");
                }
            } catch (Exception e) {
                log.error("Failed to check replication lag", e);
                slaveTooLagged.set(true); // 发生异常时,保守地认为从库延迟过大
            } finally{
                // 删除标记数据
                masterJdbcTempjavascriptlate.update("DELETE FROM replication_marker WHERE marker = ?", mark);
            }
        }
        
        public boolean isSlaveTooLagged() {
            return slaveTooLagged.get();
        }
    }
    

    6.2 事务管理

    读写分离环境下的事务处理需要特别注意:

    • 事务内操作都走主库:确保事务一致性
    • 避免长事务:长事务会长时间锁定主库资源
    • 区分只读事务:对于只读事务,可以考虑路由到从库

    6.4 监控与性能优化

    • 监控读写比例:了解系统的读写比例,优化资源分配
    • 慢查询监控:监控各数据源的慢查询
    • 连接池优化:根据实际负载调整连接池参数
    # HikariCP连接池配置示例
    spring:
      datasource:
        master:
          # 主库偏向写操作,连接池可以适当小一些
          maximum-pool-size: 20
          minimum-idle: 5
        slave:
          # 从库偏向读操作,连接池可以适当大一些
          maximum-pool-size: 50
          minimum-idle: 10
    

    七、总结

    在实施读写分离时,需要特别注意数据一致性、事务管理和故障处理等方面的问题。

    通过合理的架构设计和细致的实现,读写分离可以有效提升系统的读写性能和可扩展性,为应用系统的高可用和高性能提供有力支持。

    无论选择哪种方案,请记住读写分离是一种架构模式,而非解决所有性能问题的万能药。在实施前应充分评估系统的实际需求和潜在风险,确保收益大于成本。

    到此这篇关于SpringBoot实现数据库读写分离的3种方法小结的文章就介绍到这了,更多相关SpringBoot数据库读写分离内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜