SpringBoot项目里用dynamic-datasource搞定读写分离,这个MyBatis拦截器写法真香
SpringBoot项目中基于dynamic-datasource的智能读写分离实战当数据库访问压力逐渐增大时读写分离架构成为提升系统性能的常见方案。传统做法往往需要在每个Mapper方法上手动添加DS注解来指定数据源这不仅增加了开发工作量也让代码显得臃肿。本文将介绍如何利用dynamic-datasource组件结合MyBatis拦截器实现一种更优雅、更自动化的读写分离方案。1. 环境准备与基础配置在开始实现智能读写分离之前我们需要先完成基础环境的搭建。dynamic-datasource作为一个轻量级的多数据源管理组件能够很好地与SpringBoot集成。首先在pom.xml中添加依赖dependency groupIdcom.baomidou/groupId artifactIddynamic-datasource-spring-boot-starter/artifactId version3.5.1/version /dependency接下来配置application.yml文件这里我们配置一个主库和两个从库spring: datasource: dynamic: primary: master strict: false datasource: master: url: jdbc:mysql://localhost:3306/master_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver slave_1: url: jdbc:mysql://localhost:3306/slave1_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver slave_2: url: jdbc:mysql://localhost:3306/slave2_db username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver提示在实际生产环境中建议将数据库密码等敏感信息进行加密处理dynamic-datasource支持使用ENC()函数包裹加密后的密码。2. 传统注解方式的读写分离实现在引入智能拦截器方案前我们先了解传统的基于注解的实现方式。这种方式需要在每个Mapper方法上明确指定使用哪个数据源。Mapper public interface UserMapper { DS(master) int insert(User user); DS(slave_1) User selectById(Long id); DS(slave_2) ListUser selectAll(); }这种方式的优缺点很明显优点实现简单直观可以精确控制每个方法使用的数据源适合简单的读写分离场景缺点代码冗余每个方法都需要添加注解维护成本高当数据源变更时需要修改大量代码容易出错可能忘记添加注解导致使用默认数据源3. 基于MyBatis拦截器的智能路由方案为了克服注解方式的缺点我们可以利用MyBatis的拦截器机制根据SQL类型自动选择合适的数据源。这种方案的核心思想是拦截所有SQL执行如果是查询操作则路由到从库如果是写操作则路由到主库。3.1 拦截器实现首先创建拦截器类MasterSlaveAutoRoutingPluginIntercepts({ Signature(type Executor.class, method query, args {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 Primary public class MasterSlaveAutoRoutingPlugin implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { Object[] args invocation.getArgs(); MappedStatement ms (MappedStatement) args[0]; try { // 根据SQL类型自动选择数据源 if (ms.getSqlCommandType() SqlCommandType.SELECT) { DynamicDataSourceContextHolder.push(slave); } else { DynamicDataSourceContextHolder.push(master); } return invocation.proceed(); } finally { DynamicDataSourceContextHolder.clear(); } } Override public Object plugin(Object target) { return Plugin.wrap(target, this); } Override public void setProperties(Properties properties) { } }3.2 从库负载均衡策略在实际生产环境中我们通常会有多个从库来分担读压力。dynamic-datasource支持通过简单的命名规则实现从库的负载均衡。修改application.yml配置spring: datasource: dynamic: primary: master datasource: master: url: jdbc:mysql://localhost:3306/master_db username: root password: 123456 slave_1: url: jdbc:mysql://localhost:3306/slave1_db username: root password: 123456 slave_2: url: jdbc:mysql://localhost:3306/slave2_db username: root password: 123456然后在拦截器中修改数据源选择逻辑// 随机选择一个从库 private String selectSlave() { ListString slaves Arrays.asList(slave_1, slave_2); return slaves.get(new Random().nextInt(slaves.size())); } // 在intercept方法中使用 DynamicDataSourceContextHolder.push(selectSlave());3.3 特殊SQL处理有些特殊情况需要特别注意事务中的读操作在事务中的读操作也应该使用主库以保证数据一致性强制使用主库的查询某些对实时性要求高的查询可能需要强制走主库对于这些情况我们可以通过自定义注解来实现Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) Documented public interface UseMaster { } // 在拦截器中增加判断 if (method.isAnnotationPresent(UseMaster.class) || TransactionSynchronizationManager.isActualTransactionActive()) { DynamicDataSourceContextHolder.push(master); } else if (ms.getSqlCommandType() SqlCommandType.SELECT) { DynamicDataSourceContextHolder.push(selectSlave()); } else { DynamicDataSourceContextHolder.push(master); }4. 性能优化与生产实践在实际生产环境中使用这套方案时还需要考虑一些性能优化和稳定性问题。4.1 连接池配置合理的连接池配置对性能影响很大。以HikariCP为例spring: datasource: dynamic: datasource: master: hikari: maximum-pool-size: 20 minimum-idle: 10 idle-timeout: 60000 max-lifetime: 1800000 connection-timeout: 30000不同数据源的连接池配置建议配置项主库建议值从库建议值说明maximum-pool-size较小(20-30)较大(30-50)从库通常承担更多查询minimum-idle5-1010-15保持适量空闲连接idle-timeout较短(1分钟)较长(5分钟)从库连接可以保持更久4.2 监控与故障转移在生产环境中我们需要考虑从库故障时的自动降级策略。可以在拦截器中增加健康检查private boolean isSlaveHealthy(String slaveName) { // 实现健康检查逻辑 // 可以缓存检查结果避免每次检查 return true; } // 在选择从库时 String selectedSlave selectSlave(); if (!isSlaveHealthy(selectedSlave)) { selectedSlave master; // 降级到主库 }4.3 与MyBatis-Plus的集成如果项目中使用了MyBatis-Plus拦截器需要确保与MP的分页插件等兼容。可以通过调整拦截器顺序来解决Bean Order(0) // 确保先于MP的分页插件执行 public MasterSlaveAutoRoutingPlugin masterSlaveAutoRoutingPlugin() { return new MasterSlaveAutoRoutingPlugin(); }5. 方案对比与选型建议让我们对比一下几种常见的读写分离实现方案方案实现复杂度代码侵入性维护成本适用场景手动DS注解低高高简单应用少量MapperMyBatis拦截器中低低中大型项目需要自动化中间件(如ShardingSphere)高最低最低超大型分布式系统选择建议对于小型项目或读写分离规则简单的场景可以直接使用DS注解对于中型项目推荐使用本文的拦截器方案对于大型分布式系统可以考虑专业的数据库中间件在实际项目中我们团队采用了拦截器方案后Mapper层的代码量减少了约40%同时由于自动化路由数据源使用错误的情况也大幅减少。特别是在快速迭代的项目中开发者不再需要关心数据源切换的问题可以更专注于业务逻辑的实现。