本文主要是介绍SpringBoot实现数据库读写分离的3种方法小结,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三...
一、数据库读写分离概述
在大型应用系统中,随着访问量的增加,数据库常常成为系统的性能瓶颈。为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式。它将数据库读操作和写操作分别路由到不同的数据库实例,通常是将写操作指向主库(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 dataSourcePointcutChina编程() {} @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允许拦截执行器的query
和update
方法,通过拦截这些方法,可以在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.chinasem.cn[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数据库读写分离内容请搜索编程China编程(www.chinasem.cn)以前的文章或继续浏览下面的相关文章希望大家以后多多支持China编程(www.chinasem.cn)!
这篇关于SpringBoot实现数据库读写分离的3种方法小结的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!