随笔 - 144  文章 - 0  评论 - 2  阅读 - 91857

Spring Boot + MyBatis 如何优雅的实现数据库读写分离?

1. 配置主从数据源

application.yml中配置主库和从库信息:

复制代码
spring:
  datasource:
    master:
      jdbc-url: jdbc:mysql://master-host:3306/db
      username: user
      password: pass
      driver-class-name: com.mysql.cj.jdbc.Driver
    slaves:
      slave1:
        jdbc-url: jdbc:mysql://slave1-host:3306/db
        username: user
        password: pass
        driver-class-name: com.mysql.cj.jdbc.Driver
      slave2:
        jdbc-url: jdbc:mysql://slave2-host:3306/db
        username: user
        password: pass
        driver-class-name: com.mysql.cj.jdbc.Driver
复制代码

2. 创建动态数据源路由

继承AbstractRoutingDataSource实现动态路由:

复制代码
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    private final List<Object> slaveKeys = new ArrayList<>();
    private final AtomicInteger counter = new AtomicInteger(0);

    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();
        this.slaveKeys.addAll(getResolvedDataSources().keySet().stream()
                .filter(key -> key.toString().startsWith("slave"))
                .collect(Collectors.toList()));
    }

    @Override
    protected Object determineCurrentLookupKey() {
        // 存在上下文指定数据源则优先使用
        String key = DynamicDataSourceContextHolder.getDataSourceKey();
        if (key != null) return key;

        // 事务内根据读写决定
        if (TransactionSynchronizationManager.isActualTransactionActive()) {
            return TransactionSynchronizationManager.isCurrentTransactionReadOnly() 
                    ? getSlaveKey() : "master";
        }

        // 默认主库
        return "master";
    }

    private Object getSlaveKey() {
        if (slaveKeys.isEmpty()) return "master";
        int index = counter.incrementAndGet() % slaveKeys.size();
        if (counter.get() > 9999) counter.set(0);
        return slaveKeys.get(index);
    }
}
复制代码

3. 配置数据源Bean

复制代码
@Configuration
public class DataSourceConfig {
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

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

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

    @Bean
    public DataSource dynamicDataSource(DataSource masterDataSource, DataSource slave1DataSource, DataSource slave2DataSource) {
        DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
        Map<Object, Object> dataSources = new HashMap<>();
        dataSources.put("master", masterDataSource);
        dataSources.put("slave1", slave1DataSource);
        dataSources.put("slave2", slave2DataSource);
        dynamicDataSource.setTargetDataSources(dataSources);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
        return dynamicDataSource;
    }

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dynamicDataSource) {
        return new DataSourceTransactionManager(dynamicDataSource);
    }
}
复制代码

4. 数据源上下文管理

使用ThreadLocal保存当前线程的数据源选择:

复制代码
public class DynamicDataSourceContextHolder {
    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();

    public static void setDataSourceKey(String key) {
        CONTEXT.set(key);
    }

    public static String getDataSourceKey() {
        return CONTEXT.get();
    }

    public static void clearDataSourceKey() {
        CONTEXT.remove();
    }
}
复制代码

5. 自定义注解与AOP切面

定义@ReadOnly注解:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
}

创建切面处理非事务读操作:

复制代码
@Aspect
@Component
public class ReadOnlyDataSourceAspect {
    @Around("@annotation(readOnly)")
    public Object proceed(ProceedingJoinPoint joinPoint, ReadOnly readOnly) throws Throwable {
        try {
            DynamicDataSourceContextHolder.setDataSourceKey("slave");
            return joinPoint.proceed();
        } finally {
            DynamicDataSourceContextHolder.clearDataSourceKey();
        }
    }
}
复制代码

6. 事务中的读写分离

  • 写操作:使用默认的@Transactional,自动路由到主库。

  • 读操作:使用@Transactional(readOnly = true),自动路由到从库。

7. 清理数据源上下文

添加过滤器确保请求结束后清理上下文:

复制代码
@Component
public class DataSourceCleanupFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 
            throws ServletException, IOException {
        try {
            filterChain.doFilter(request, response);
        } finally {
            DynamicDataSourceContextHolder.clearDataSourceKey();
        }
    }
}
复制代码

总结

通过上述步骤,实现了以下功能:

  1. 动态路由:根据事务属性和注解自动选择主从库。

  2. 负载均衡:多个从库间轮询分配读请求。

  3. 事务安全:确保事务内数据源一致性。

  4. 灵活控制:通过注解显式指定数据源。

 

posted on   IT-QI  阅读(20)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

点击右上角即可分享
微信分享提示