互联网大厂后端开发者必看!Spring Boot3 数据库读写分离轻松实现
作为互联网大厂的后端开发者,在使用 Spring Boot3 构建高并发、高性能的应用系统时,你是否正面临这样的困境?随着业务规模的不断扩张,数据库的读写压力呈指数级增长,查询响应时间越来越长,系统吞吐量逐渐下降,即使投入大量精力进行代码优化,效果却依旧不尽如人意。其实,这很可能是因为你还没有充分利用数据库读写分离这一关键技术。今天,我们就深入探讨在 Spring Boot3 中如何高效实现数据库的读写分离,彻底攻克性能瓶颈!
数据库读写分离的重要性与应用场景
在互联网大厂的复杂业务场景下,数据的读写操作存在着显著的不均衡性。以电商平台为例,用户浏览商品详情、查看历史订单等读操作的频率,往往是下单、修改订单等写操作的数十倍甚至上百倍。若将所有的读写请求都集中在单一数据库上处理,就如同让一个人同时承担繁重的文件查阅与编辑任务,不仅效率低下,还极易引发性能瓶颈。
而且,单数据库架构存在较高的单点故障风险,一旦数据库出现宕机、磁盘损坏等问题,整个系统的读写服务都将陷入瘫痪,严重影响用户体验和业务正常运转。而通过 Spring Boot3 实现数据库读写分离,将读操作分发到从数据库,写操作交由主数据库处理,不仅能有效分散主数据库的负载压力,提升系统的并发处理能力,还能增强系统的可用性和稳定性,为业务的持续发展提供坚实保障。
Spring Boot3 实现数据库读写分离的详细步骤
主从数据库连接信息配置
在application.yml配置文件中,进行主从数据库连接信息的精细化配置是实现读写分离的基础。以常见的 MySQL 数据库结合 Hikari 连接池为例,具体配置如下:
spring:
datasource:
master:
jdbc-url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=UTF - 8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: 123456
driver - class - name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimum - idle: 5
idle - timeout: 60000
maximum - pool - size: 10
auto - commit: true
pool - name: MasterHikariCP
max - lifetime: 1800000
connection - timeout: 30000
connection - test - query: SELECT 1
slave:
jdbc-url: jdbc:mysql://localhost:3306/slave_db?useUnicode=true&characterEncoding=UTF - 8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: 123456
driver - class - name: com.mysql.cj.jdbc.Driver
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimum - idle: 5
idle - timeout: 60000
maximum - pool - size: 10
auto - commit: true
pool - name: SlaveHikariCP
max - lifetime: 1800000
connection - timeout: 30000
connection - test - query: SELECT 1
在上述配置中,master节点用于配置主数据库的连接参数,涵盖数据库 URL、登录用户名、密码、驱动类等关键信息,同时对 Hikari 连接池的最小空闲连接数(minimum - idle)、空闲连接超时时间(idle - timeout)、最大连接池大小(maximum - pool - size)等参数进行设置,以优化主数据库的连接性能和资源管理。slave节点则针对从数据库进行类似配置,确保从数据库能够稳定接收和处理读请求 。这些参数可根据实际业务流量、数据库服务器性能等因素进行针对性调整,以达到最佳的性能表现。
数据源配置类创建
为了将配置文件中的数据源信息转化为可被 Spring 容器管理的数据源对象,需要创建 Java 配置类。以
MasterDataSourceConfig.java和
SlaveDataSourceConfig.java为例:
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
@Configuration
public class MasterDataSourceConfig {
@Bean(name = "masterDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return new HikariDataSource();
}
}
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class SlaveDataSourceConfig {
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return new HikariDataSource();
}
}
在上述代码中,@Configuration注解表明这是一个配置类,@Bean注解用于将方法返回的数据源对象注册到 Spring 容器中。@ConfigurationProperties注解通过指定prefix属性,将application.yml中对应前缀的数据源配置属性自动绑定到数据源对象上,实现配置信息与数据源的无缝对接。其中,@Primary注解标记的masterDataSource方法返回的主数据源,在多个数据源存在的情况下,会被 Spring 优先选用,确保写操作能够正确路由到主数据库 。
动态路由数据源构建
动态路由数据源是实现数据库读写分离的核心组件,它能够根据业务需求动态切换数据源。创建DynamicDataSource类继承自AbstractRoutingDataSource:
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import java.util.HashMap;
import java.util.Map;
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final String MASTER = "master";
private static final String SLAVE = "slave";
public DynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(MASTER, masterDataSource);
targetDataSources.put(SLAVE, slaveDataSource);
setTargetDataSources(targetDataSources);
setDefaultTargetDataSource(masterDataSource);
}
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
同时,创建DataSourceContextHolder类,利用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();
}
}
在DynamicDataSource类中,构造函数接收主数据源和从数据源,并将它们存储在targetDataSources映射中。determineCurrentLookupKey方法通过调用
DataSourceContextHolder.getDataSourceType()获取当前线程对应的数据源类型标识,以此来决定实际使用的数据源。ThreadLocal的使用确保了每个线程都能独立维护自己的数据源类型,避免了多线程环境下数据源切换的冲突问题。
动态数据源注册
在 Spring 配置类中,将动态路由数据源注册到 Spring 容器,使其能够被应用程序调用:
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean
public DynamicDataSource dynamicDataSource(
@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
return new DynamicDataSource(masterDataSource, slaveDataSource);
}
@Bean
public PlatformTransactionManager transactionManager(DynamicDataSource dynamicDataSource) {
return new DataSourceTransactionManager(dynamicDataSource);
}
}
上述代码中,dynamicDataSource方法通过注入主数据源和从数据源,创建并返回DynamicDataSource实例,完成动态数据源的注册。同时,transactionManager方法为动态数据源配置事务管理器,确保在进行数据库操作时,事务的原子性、一致性、隔离性和持久性(ACID 特性)能够得到保障,避免因数据读写操作导致的数据不一致问题。
数据库操作切面拦截
为了实现读写操作的自动路由,创建切面类
DataSourceAnnotationAspect,通过自定义注解来区分读写操作:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class DataSourceAnnotationAspect {
@Around("@annotation(com.example.annotation.SlaveDataSource)")
public Object read(ProceedingJoinPoint point) throws Throwable {
try {
DataSourceContextHolder.setDataSourceType("slave");
return point.proceed();
} finally {
DataSourceContextHolder.clearDataSourceType();
}
}
@Around("!@annotation(com.example.annotation.SlaveDataSource)")
public Object write(ProceedingJoinPoint point) throws Throwable {
try {
DataSourceContextHolder.setDataSourceType("master");
return point.proceed();
} finally {
DataSourceContextHolder.clearDataSourceType();
}
}
}
这里的自定义注解@SlaveDataSource用于标记读操作方法:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface SlaveDataSource {
}
DataSourceAnnotationAspect切面类中,@Around注解定义的通知方法会在目标方法执行前后进行拦截。当检测到方法上带有@SlaveDataSource注解时,将当前线程的数据源类型设置为slave,使该方法的数据库操作路由到从数据库;反之,若方法上没有该注解,则将数据源类型设置为master,确保写操作能够正确发送到主数据库。在操作完成后,通过
DataSourceContextHolder.clearDataSourceType()方法清除当前线程的数据源类型设置,避免对后续操作产生干扰。
排除默认数据源自动配置
为了避免 Spring Boot 默认的数据源自动配置与我们自定义的数据源配置产生冲突,需要在 Spring Boot 应用主类上添加@SpringBootApplication(exclude = {
DataSourceAutoConfiguration.class})注解:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class YourApplication {
public static void main(String[] args) {
SpringApplication.run(YourApplication.class, args);
}
}
通过排除默认的数据源自动配置,确保 Spring 容器按照我们自定义的配置方式创建和管理数据源,从而准确实现数据库的读写分离功能。
总结
通过以上详细的步骤,我们全面掌握了在 Spring Boot3 中实现数据库读写分离的技术要点。从主从数据库连接配置到动态数据源构建,再到读写操作的切面拦截,每一个环节都紧密相连,共同构建起高效的数据读写分离架构。
在实际项目开发中,建议你结合具体的业务场景和性能需求,对各项配置参数进行精细化调整和优化。同时,注意监控主从数据库的运行状态,及时处理可能出现的数据同步延迟、连接池资源耗尽等问题。
如果你在实践过程中遇到任何疑问,或者有更优秀的实现方案和优化技巧,欢迎在评论区留言分享,我们一起探讨交流,共同提升后端开发技术水平!也别忘了点赞、收藏这篇文章,方便随时查阅,还可以转发给身边的后端开发者小伙伴,让更多人受益于数据库读写分离技术的强大威力!