一、什么是 MySQL 的主从复制
MySQL 的主从复制(Master-Slave Replication)是一种将数据从一个主数据库服务器(主库)复制到一个或多个从数据库服务器(从库)的技术。主库负责所有的数据写操作,从库则通过读取主库的二进制日志来同步主库的数据变化。主从复制主要用于实现数据的备份、负载分担和高可用性。
二、具体流程
1. 主库记录写操作
当主库执行写操作(如 INSERT
、UPDATE
或 DELETE
)时,这些操作会被记录到二进制日志(binlog)中。二进制日志保存了所有数据更改的历史记录,这些记录将成为从库同步数据的来源。
-
步骤:主库将所有写操作(数据变更)记录到二进制日志中。
2. 从库连接主库并读取二进制日志
从库通过一个称为IO 线程的进程与主库建立连接,开始读取主库的二进制日志。主库会将日志内容发送给从库的 IO 线程。
-
步骤:从库的 IO 线程连接主库,持续接收主库二进制日志的最新内容。
3. 从库将二进制日志写入中继日志
从库的 IO 线程将接收到的主库二进制日志复制到从库的**中继日志(relay log)**中。中继日志在从库本地保存了一份主库数据变更的副本,供从库执行使用。
-
步骤:从库的 IO 线程将接收的日志写入中继日志,为数据变更做好准备。
4. 从库应用中继日志,执行数据同步
从库的另一个线程(称为SQL 线程)负责读取中继日志中的内容,并逐条在从库上执行,以实现数据的同步。每当主库有新的数据变更,从库都会从中继日志中获取并执行这些变更,从而保持与主库的数据一致。
-
步骤:从库的 SQL 线程从中继日志中读取并执行记录的操作,逐步更新从库数据。
5. 持续复制和同步
整个主从复制过程是一个持续的流程,只要主库有新的数据变更,从库就会自动获取并执行对应的更改,从而保持与主库数据的一致性。主从复制会持续保持同步,以确保从库能够实时或接近实时地反映主库的最新数据。
-
步骤:主库的所有写操作都会记录到日志中,实时同步到从库;从库持续读取并执行日志内容,更新自身数据。
三、作用和好处
-
读写分离,提高性能
- 减轻主库压力:将大量的读操作分散到从库,主库主要负责写操作,大大降低了主库的负载压力。
- 提升并发处理能力:增加从库的数量可以提升系统的读并发能力,处理更多用户的并发请求,提升系统整体性能。
-
高可用性和容错性
- 数据冗余与容灾:主从复制提供了数据的实时备份,从库可以在主库发生故障时快速接管服务,提高系统的可用性。
- 故障切换:当主库出现故障时,可以临时将从库升级为主库,保障服务持续运行。
-
负载均衡
- 分担查询压力:多个从库可以共同承担读取请求,通过负载均衡算法(如轮询、随机等)分配请求,提升系统的稳定性。
- 避免单点瓶颈:读操作分散到不同的从库上执行,避免了单一数据库因访问量过大而成为系统瓶颈。
-
扩展性
- 水平扩展:根据业务增长,灵活增加从库数量,满足性能需求,而无需对主库进行大幅度改造。
- 弹性扩展:可以根据实际流量,增加或减少从库,做到灵活应对负载变化。
-
数据备份和安全性
- 数据保护:从库可以用于数据备份和容灾,防止主库故障导致的数据丢失。
- 快速恢复:在数据意外丢失或损坏时,可以通过从库恢复主库的数据。
四、在实际应用中的场景与示例
1. 电商平台
在电商平台中,用户的浏览和查询操作会产生大量的读请求,而订单创建、支付等操作会产生写请求。通过主从复制的读写分离:
- 查询库存、商品详情等高频率的读操作可以由从库承担,减轻主库压力。
- 用户下单、支付等写操作由主库负责,确保数据的完整性和一致性。
2. 社交媒体或内容网站
在社交媒体应用中,大量的内容浏览和搜索会产生大量的读操作,而发布、点赞等则是写操作。通过主从复制,平台可以:
- 使用从库来承担浏览、查询等操作,确保高并发下的快速响应。
- 将写操作指向主库,确保用户发布或点赞的实时更新。
五、在 Spring Boot 项目中集成 MySQL 主从复制
在 Spring Boot 项目中集成 MySQL 的主从复制数据同步,指的是将 Spring Boot 应用程序连接到配置有主从复制的 MySQL 数据库系统上,完成主从数据库的配置管理,实现自动的读写分离。具体来说,就是通过多数据源配置,让 Spring Boot 自动识别是写请求还是读请求,并将写请求发送到主库,读请求发送到从库。
集成的关键要素:
- 多数据源配置:在 Spring Boot 中配置主数据库和从数据库的连接。
- 动态数据源路由:在应用层面实现数据源的动态选择,读操作使用从库,写操作使用主库。
- 读写分离注解或切面:利用自定义注解或 AOP 切面控制数据源的切换。
1.配置 MySQL 的主从复制环境
配置 MySQL 的主从复制环境是实现 MySQL 主从复制数据同步的第一步。主要步骤包括设置主库(Master)和从库(Slave),并验证主从复制的成功。
1.1 设置主库(Master)
配置主库是主从复制的第一步。主库负责记录数据变更并将其传递给从库。
1.1.1 修改主库的配置文件
在主库的 MySQL 配置文件中(通常位于 /etc/my.cnf
或 /etc/mysql/my.cnf
),需要启用二进制日志并为主库设置一个唯一的 server-id
。在 [mysqld]
部分添加如下配置:
[mysqld]
server-id=1 # 主库的唯一 ID
log-bin=mysql-bin # 启用二进制日志,主从复制依赖于此
binlog-do-db=your_database # 需要同步的数据库名称,多个数据库可添加多行
- server-id:用于标识每个 MySQL 实例的唯一标识符,主库的
server-id
一般设置为 1。 - log-bin:启用二进制日志,这是主从复制的基础。
- binlog-do-db:指定需要同步的数据库(多个数据库可以多行设置)。
注意:如果需要同步多个数据库,可以多次添加 binlog-do-db
行,例如:
binlog-do-db=database1
binlog-do-db=database2
1.1.2 重启 MySQL 服务
修改配置文件后,需要重启 MySQL 以使配置生效:
# Linux 系统
sudo systemctl restart mysqld
# Windows 系统
net stop mysql
net start mysql
1.1.3 创建用于复制的用户
在主库中创建一个用于复制的用户,并授予 REPLICATION SLAVE
权限。这个用户用于从库连接主库并进行数据同步。
在主库的 MySQL 命令行中执行以下命令:
CREATE USER 'replica_user'@'%' IDENTIFIED BY 'password';
GRANT REPLICATION SLAVE ON *.* TO 'replica_user'@'%';
FLUSH PRIVILEGES;
- replica_user:用于复制的用户名,可以自定义。
- password:复制用户的密码,注意使用强密码。
- %:允许所有远程 IP 访问。如果只允许特定从库连接,可以用从库的 IP 地址代替
%
。
1.1.4 获取主库的二进制日志信息
为了让从库知道从何处开始复制数据,主库需要提供当前的二进制日志位置。可以通过以下命令查看:
SHOW MASTER STATUS;
命令执行后,会输出如下内容:
+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000001 | 154 | your_database| |
+------------------+----------+--------------+------------------+
- File:当前的二进制日志文件名。
- Position:二进制日志的偏移量,从该位置开始读取数据。
建议:在生产环境中,执行
SHOW MASTER STATUS;
之前,可以先执行FLUSH TABLES WITH READ LOCK;
来锁定表,防止数据写入导致的数据不一致。获取信息后再执行UNLOCK TABLES;
解锁。
1.2 设置从库(Slave)
配置从库是主从复制的第二步。配置从库连接到主库并开始同步数据。
1.2.1 修改从库的配置文件
在从库的 MySQL 配置文件中(通常是 /etc/my.cnf
或 /etc/mysql/my.cnf
),设置一个唯一的 server-id
并配置中继日志和只读模式。 在 [mysqld]
部分添加以下配置:
[mysqld]
server-id=2 # 从库的唯一 ID,不能与主库或其他从库相同
relay-log=relay-log # 设置中继日志文件名前缀
read-only=1 # 设置只读模式,防止误操作
- server-id:从库的唯一标识符,每个从库的
server-id
也需要是唯一的,通常从 2 开始。 - relay-log:指定中继日志的前缀,用于存储从主库同步的数据。
- read-only:开启只读模式,防止意外写入。此模式下,拥有
SUPER
权限的用户仍可以写入数据。
1.2.2 重启 MySQL 服务
修改配置文件后,重启从库的 MySQL 服务以应用配置:
# Linux 系统
sudo systemctl restart mysqld
# Windows 系统
net stop mysql
net start mysql
1.2.3 配置从库连接到主库
在从库的 MySQL 中,配置主库信息以便开始复制。需要用到在主库上记录的 File
和 Position
值。
CHANGE MASTER TO
MASTER_HOST='主库的 IP 地址',
MASTER_USER='replica_user',
MASTER_PASSWORD='password',
MASTER_LOG_FILE='mysql-bin.000001', -- 主库的 File 值
MASTER_LOG_POS=154; -- 主库的 Position 值
- MASTER_HOST:主库的 IP 地址。
- MASTER_USER:在主库上创建的用于复制的用户(如
replica_user
)。 - MASTER_PASSWORD:复制用户的密码。
- MASTER_LOG_FILE:主库的二进制日志文件名。
- MASTER_LOG_POS:二进制日志的位置偏移量。
1.2.4 启动复制进程
配置完成后,启动从库的复制进程以开始从主库复制数据:
START SLAVE;
1.2.5 查看从库的复制状态
运行以下命令检查从库的状态,确认从库已成功连接到主库并开始复制数据:
SHOW SLAVE STATUS\G
- Slave_IO_Running:应为
Yes
,表示从库的 IO 线程正在读取主库的日志。 - Slave_SQL_Running:应为
Yes
,表示从库的 SQL 线程正在执行主库传递的日志。 - Last_IO_Error 和 Last_SQL_Error:如果存在错误信息,会在此处显示。
如果
Slave_IO_Running
或Slave_SQL_Running
为No
,请查看Last_IO_Error
或Last_SQL_Error
中的错误消息,并根据错误提示排查问题。
1.3 验证主从复制是否成功
在配置完成后,验证主从复制的成功性。确保主库的写操作能够被从库正确同步。
1.3.1 在主库上创建测试数据
在主库上选择用于复制的数据库,并创建一个测试表插入数据:
USE your_database;
CREATE TABLE test_table (
id INT PRIMARY KEY,
name VARCHAR(50)
);
INSERT INTO test_table (id, name) VALUES (1, 'Test Data');
1.3.2 在从库上检查数据同步情况
在从库上选择相同的数据库,查询 test_table
表,确认是否同步了主库的数据:
USE your_database;
SELECT * FROM test_table;
- 如果可以看到
Test Data
,则表示主从复制配置成功并且工作正常。
1.3.3 验证实时同步效果
在主库上继续插入或更新数据,再次在从库上查询,验证数据是否能够及时同步。主从复制一般情况下会立即同步,延迟较小。
1.4 故障排查
在配置主从复制的过程中,可能会遇到以下常见问题:
1 从库无法连接到主库
- 检查网络连接:确保主库的防火墙允许从库的 IP 地址访问 MySQL 的 3306 端口。
2 权限问题
- 用户权限:确保在主库上创建的用户拥有
REPLICATION SLAVE
权限。
3 主从版本兼容性问题
- 版本兼容性:确保主库和从库的 MySQL 版本相互兼容,推荐使用相同的版本。
4 日志位置不正确
- 文件和位置错误:如果配置的
File
和Position
不正确,可以重新设置主从复制位置。
2.在 Spring Boot 项目中配置多数据源
配置多数据源是实现主从复制的重要步骤。通过多数据源配置,Spring Boot 应用可以自动区分并选择主库或从库,从而实现读写分离。这一步的具体操作包括添加依赖、配置数据源信息,以及配置数据源路由以实现自动选择主库或从库。以下是详细的分步讲解。
2.1 添加必要的依赖
在使用 Spring Data JPA 和 MySQL 多数据源时,需要添加以下依赖项:
- MySQL 驱动:支持连接 MySQL 数据库。
- Spring Data JPA:提供 JPA 支持,简化数据库操作。
- HikariCP 连接池:Spring Boot 默认的连接池,适合高并发环境且支持多数据源配置。
在项目的 pom.xml
中添加以下依赖:
<dependencies>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- HikariCP 连接池(Spring Boot 默认连接池) -->
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
</dependencies>
2.2 在配置文件中定义主库和从库的数据源信息
接下来,需要在 application.properties
中定义主库和从库的连接信息。主库通常用于写操作,从库用于读操作。
application.properties
文件内容
# 主库配置
spring.datasource.master.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.master.url=jdbc:mysql://主库IP地址:3306/your_database?useSSL=false&characterEncoding=utf8
spring.datasource.master.username=主库用户名
spring.datasource.master.password=主库密码
# 从库配置
spring.datasource.slave.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.slave.url=jdbc:mysql://从库IP地址:3306/your_database?useSSL=false&characterEncoding=utf8
spring.datasource.slave.username=从库用户名
spring.datasource.slave.password=从库密码
说明:
- spring.datasource.master:主库连接信息,用于写操作。
- spring.datasource.slave:从库连接信息,用于读操作。
- driver-class-name:MySQL 驱动类名。
- url:数据库连接 URL,其中包含数据库的 IP 地址和数据库名称。
- username 和 password:数据库的用户名和密码。
2.3 创建数据源配置类,配置主从数据源
在项目中添加一个配置类,用于定义主库和从库数据源,并通过一个动态数据源实现自动选择主库或从库。
2.3.1 配置主库和从库的数据源
首先,我们需要在配置类中定义两个数据源,即主库和从库。主库主要用于写操作,从库用于读操作。
package com.example.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import org.springframework.boot.jdbc.DataSourceBuilder;
@Configuration
public class DataSourceConfig {
// 定义主库数据源
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
// 定义从库数据源
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
}
说明:
-
@ConfigurationProperties:这个注解将
application.properties
中spring.datasource.master
和spring.datasource.slave
配置的信息绑定到masterDataSource
和slaveDataSource
上。这样,Spring Boot 会自动读取配置文件中的主从库信息,并将它们分别注入到两个数据源对象中。 -
DataSourceBuilder:这是 Spring 提供的一个工具类,通过它可以根据配置文件构建
DataSource
对象。DataSource
是与数据库连接的核心组件,主要用于管理数据库连接、连接池等信息。
2.3.2 动态数据源路由
为了实现主从分离,我们需要根据请求自动选择主库或从库的数据源。AbstractRoutingDataSource
是 Spring 提供的一个抽象类,可以根据用户定义的路由规则动态选择数据源。
创建 DynamicDataSource
类:该类继承自 AbstractRoutingDataSource
,通过设置键值映射来实现数据源选择。
package com.example.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();
// 设置当前线程的数据源(主库或从库)
public static void setDataSource(String dataSourceKey) {
contextHolder.set(dataSourceKey);
}
// 清除当前线程的数据源
public static void clearDataSource() {
contextHolder.remove();
}
// 确定当前数据源(由 AbstractRoutingDataSource 调用)
@Override
protected Object determineCurrentLookupKey() {
return contextHolder.get();
}
}
代码详解:
-
contextHolder:使用
ThreadLocal
存储当前线程的数据源标识(master
或slave
)。ThreadLocal
确保每个线程拥有独立的变量副本,不会干扰其他线程。 -
setDataSource:用于设置当前线程的数据源,通过传入
master
或slave
来指定主库或从库。 -
clearDataSource:用于清除当前线程的数据源,避免数据源信息在线程之间混淆。
-
determineCurrentLookupKey:这是
AbstractRoutingDataSource
提供的方法,它会在每次数据库操作时调用。根据contextHolder
中的数据源标识,选择主库或从库。
2.3.3 将主从数据源注入到动态数据源
我们需要将主库和从库的数据源注入到动态数据源中,并根据不同的业务需求自动选择。
@Bean(name = "dynamicDataSource")
public DataSource dynamicDataSource(
@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put("master", masterDataSource);
targetDataSources.put("slave", slaveDataSource);
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(masterDataSource); // 设置默认的数据源
dynamicDataSource.setTargetDataSources(targetDataSources); // 将主库和从库的数据源加入路由
return dynamicDataSource;
}
代码详解:
-
@Qualifier:指定
masterDataSource
和slaveDataSource
,通过依赖注入的方式将主库和从库的数据源传入dynamicDataSource
。 -
targetDataSources:将
master
和slave
作为键,分别映射到masterDataSource
和slaveDataSource
。DynamicDataSource
会通过determineCurrentLookupKey
方法自动选择对应的数据源。 -
setDefaultTargetDataSource:设置默认的数据源。在没有指定数据源的情况下,系统会使用默认数据源(一般为主库)。
2.3.4 配置 EntityManagerFactory
使用动态数据源
对于使用 JPA 的项目,EntityManagerFactory
是 JPA 的核心组件之一。它负责管理实体管理器,处理 JPA 的持久化操作。这里需要将 EntityManagerFactory
配置为使用动态数据源,以实现主从分离。
@Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
@Qualifier("dynamicDataSource") DataSource dynamicDataSource) {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
em.setDataSource(dynamicDataSource); // 使用动态数据源
em.setPackagesToScan("com.example.entity"); // 实体类包名
em.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); // 设置 JPA 供应商为 Hibernate
return em;
}
代码详解:
-
setDataSource:设置 JPA 的数据源为
dynamicDataSource
,这样 JPA 会根据动态数据源的路由选择合适的数据源。 -
setPackagesToScan:指定 JPA 实体类所在的包路径,JPA 会扫描该包中的实体类,进行数据库操作的映射。
- HibernateJpaVendorAdapter:设置 JPA 的供应商适配器。这里我们使用 Hibernate 作为 JPA 的实现,它提供了 Hibernate 特有的优化和配置支持。
通过配置 EntityManagerFactory
使用 dynamicDataSource
,我们可以让 JPA 在操作数据库时自动根据业务需要切换到主库或从库,从而实现主从分离和读写分离。
2.3.5 配置事务管理器
Spring Data JPA 默认需要一个事务管理器来管理事务。我们需要为动态数据源配置一个 JpaTransactionManager
,以确保事务操作可以正确地应用于当前选择的数据源(主库或从库)。
@Bean(name = "transactionManager")
public JpaTransactionManager transactionManager(
@Qualifier("entityManagerFactory") LocalContainerEntityManagerFactoryBean entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory.getObject());
}
代码详解:
-
JpaTransactionManager:Spring 提供的 JPA 事务管理器,它会自动处理
EntityManager
的创建和关闭,并确保在事务中对数据库操作的 ACID 特性(原子性、一致性、隔离性、持久性)。 -
entityManagerFactory.getObject():将
entityManagerFactory
(配置了动态数据源的实体管理器工厂)传递给JpaTransactionManager
。这样,事务管理器会根据动态数据源路由来管理事务,确保事务一致性。
3.实现数据源的动态切换
为了实现数据库的主从切换,使得 Spring Boot 项目可以根据操作类型自动选择主库或从库,我们需要实现数据源的动态切换。实现动态切换的关键步骤包括:定义自定义注解、创建 AOP 切面,在方法执行时动态地决定使用哪个数据源。
3.1 创建 @DataSource
注解(用于标识使用哪个数据源)
首先,我们创建一个自定义注解 @DataSource
,用于标识在特定方法或类上指定的数据源类型(如主库 master
或从库 slave
)。有了这个注解之后,我们可以在代码中灵活地指定哪些操作使用主库,哪些操作使用从库。
package com.example.annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD, ElementType.TYPE}) // 可以用在方法或类上
@Retention(RetentionPolicy.RUNTIME) // 在运行时保留,便于通过反射获取
@Documented
public @interface DataSource {
String value() default "master"; // 默认使用主库
}
注解参数说明:
-
@Target:定义注解的使用位置,
ElementType.METHOD
表示可以作用于方法,ElementType.TYPE
表示可以作用于类。 -
@Retention:指定注解的保留策略为
RUNTIME
,即该注解会保留到运行时,并且可以通过反射获取,这样切面类可以在运行时识别注解。 -
value:注解的属性,用来指定数据源类型,默认为
"master"
,表示主库。我们可以在使用注解时,通过设置@DataSource("slave")
来指定从库。
3.2 创建 DataSourceAspect
切面类(根据注解动态切换数据源)
AOP(面向切面编程)可以在方法调用前后动态地切入代码逻辑。在这里,我们编写一个 AOP 切面,用于在方法调用之前,根据 @DataSource
注解的值设置数据源。在方法调用之后,清除数据源的标识,以确保不会影响后续操作。
package com.example.aspect;
import com.example.annotation.DataSource;
import com.example.config.DynamicDataSource;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class DataSourceAspect {
// 定义切点:匹配带有 @DataSource 注解的方法或类
@Pointcut("@annotation(com.example.annotation.DataSource) || @within(com.example.annotation.DataSource)")
public void dataSourcePointCut() {
}
// 在方法执行前,根据注解的值切换数据源
@Before("dataSourcePointCut()")
public void before(JoinPoint point) {
String dataSource = "master"; // 默认使用主库
Class<?> targetClass = point.getTarget().getClass();
MethodSignature signature = (MethodSignature) point.getSignature();
// 获取方法上的 @DataSource 注解
DataSource ds = signature.getMethod().getAnnotation(DataSource.class);
if (ds != null) {
dataSource = ds.value();
} else if (targetClass.isAnnotationPresent(DataSource.class)) {
// 如果方法上没有注解,则读取类上的 @DataSource 注解
ds = targetClass.getAnnotation(DataSource.class);
if (ds != null) {
dataSource = ds.value();
}
}
// 设置当前线程的数据源标识
DynamicDataSource.setDataSource(dataSource);
}
// 在方法执行后,清除数据源标识
@After("dataSourcePointCut()")
public void after(JoinPoint point) {
DynamicDataSource.clearDataSource();
}
}
切面类代码详解:
-
@Pointcut:定义切点
dataSourcePointCut
,用于匹配所有带有@DataSource
注解的方法或类。这意味着我们可以在方法上、类上使用@DataSource
来控制数据源选择。 -
@Before:在目标方法执行之前触发
before
方法。-
MethodSignature:通过
MethodSignature
可以获取方法的注解。 -
dataSource:默认值为
"master"
(主库)。首先检查方法上的@DataSource
注解,如果没有找到,再检查类上的@DataSource
注解。 -
DynamicDataSource.setDataSource(dataSource):根据注解值设置当前线程使用的数据源(主库或从库)。这使得后续的数据库操作将根据注解指定的数据源执行。
-
-
@After:在目标方法执行后触发
after
方法,用于清除当前线程的数据源标识,以避免线程复用时对其他请求产生干扰。DynamicDataSource.clearDataSource()
方法会移除当前线程的数据源标识。
4.在业务代码中使用
4.1 使用 @DataSource
注解
在业务代码中,可以在需要使用主库或从库的业务方法上添加 @DataSource
注解。默认情况下,@DataSource
注解的 value
属性是 "master"
,表示主库。我们可以在查询类方法上标记 @DataSource("slave")
以使用从库,从而实现读写分离。
4.1.1.在服务层使用 @DataSource
注解
假设我们有一个 UserService
服务类,该类提供了查询用户列表和保存用户的功能。我们可以通过 @DataSource
注解指定使用的数据库。
import com.example.annotation.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
// 使用从库(slave)进行读取操作
@DataSource("slave")
public List<User> getUsers() {
// 数据库查询操作,这里会通过切面选择从库
return userRepository.findAll();
}
// 使用主库(master)进行写入操作
@DataSource("master")
public void saveUser(User user) {
// 数据库写入操作,这里会通过切面选择主库
userRepository.save(user);
}
}
示例说明:
-
getUsers 方法:由于标注了
@DataSource("slave")
注解,因此在执行getUsers
方法时会选择从库作为当前数据源,从而使查询操作通过从库完成,减轻主库压力。 -
saveUser 方法:标注了
@DataSource("master")
注解,因此在执行saveUser
方法时会选择主库作为当前数据源,从而保证数据写入操作在主库上进行,确保数据的一致性。
4.1.2.在 DAO 层(数据持久层)使用 @DataSource
注解
假设我们将数据源控制进一步细化到 DAO 层。例如,在 UserRepository
中的查询和保存方法上分别指定数据源。
import com.example.annotation.DataSource;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 使用从库(slave)进行读取操作
@DataSource("slave")
List<User> findByName(String name);
// 使用主库(master)进行写入操作
@DataSource("master")
User save(User user);
}
示例说明:
-
findByName 方法:该方法标注了
@DataSource("slave")
,因此在调用findByName
时,数据查询操作将通过从库完成。 -
save 方法:该方法标注了
@DataSource("master")
,确保数据写入操作始终通过主库进行,保证了数据的完整性。
注意:将 @DataSource
注解放在服务层和 DAO 层都会生效。实际应用中可以根据业务逻辑的复杂程度来决定在哪一层实现数据源的切换。
4.2 确保读操作走从库,写操作走主库
通过在业务方法中使用 @DataSource
注解,可以实现以下的逻辑:
1.读操作走从库:在查询数据的方法上添加 @DataSource("slave")
,使这些方法通过从库执行。
- 从库通常用于处理读操作。这样可以分担主库的负载,提升系统的读取性能。
- 因为从库的数据来自主库的复制,存在一定延迟,所以一般不用于强一致性要求的场景,而适合对实时性要求较低的查询场景。
2.写操作走主库:在插入、更新、删除数据的方法上添加 @DataSource("master")
,确保这些操作通过主库执行。
- 主库是数据的源头,负责处理写入、更新等操作,以确保数据的准确性和一致性。
- 这样可以避免因为复制延迟导致的数据不一致问题。
示例:在 ProductService
中实现读写分离
import com.example.annotation.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
// 使用从库读取产品列表
@DataSource("slave")
public List<Product> getAllProducts() {
// 从库执行查询
return productRepository.findAll();
}
// 使用主库保存产品信息
@DataSource("master")
public void addProduct(Product product) {
// 主库执行插入操作
productRepository.save(product);
}
// 更新产品信息,使用主库
@DataSource("master")
public void updateProduct(Product product) {
// 主库执行更新操作
productRepository.save(product);
}
// 删除产品信息,使用主库
@DataSource("master")
public void deleteProductById(Long productId) {
// 主库执行删除操作
productRepository.deleteById(productId);
}
}
代码解释:
-
getAllProducts() 方法:
- 使用
@DataSource("slave")
注解,从库将执行该方法中的查询操作。 - 适用于读取类操作,减轻主库压力。
- 使用
-
addProduct()、updateProduct() 和 deleteProductById() 方法:
- 这些方法使用
@DataSource("master")
注解,因此会走主库。 - 适用于写操作(新增、修改、删除),确保数据的强一致性和实时性。
- 这些方法使用
4.3 注意事项
在实现读写分离时,以下几点需要注意:
-
主从数据一致性:从库的数据源于主库的复制,所以存在一定的延迟。对于不允许数据延迟的操作,建议强制使用主库。比如订单支付或库存更新等操作,通常对实时性要求较高,应直接走主库。
-
事务管理:在事务中进行读操作,可能会导致数据源切换失效,因为在事务中默认会使用主库。这种情况下,可以使用
@Transactional(readOnly = true)
标记方法为只读事务,让 Spring 在事务中仍使用从库。 -
线程安全:因为数据源切换是基于
ThreadLocal
实现的,所以多线程环境下可以安全地设置和切换数据源标识。然而,如果有异步操作,可能会导致数据源信息传递失效,需要特别注意。
5.测试和验证
完成 Spring Boot 项目中多数据源的配置之后,需要测试项目是否能够正确地实现读写分离。主要步骤包括:检查 MySQL 主库和从库的服务状态、启动项目、编写并运行测试类,以及验证主从数据库的数据同步情况。
5.1 检查 MySQL 主库和从库的服务状态
要确保 Spring Boot 项目能够连接到主从数据库,首先需要确认 MySQL 主库和从库的服务状态。如果主库和从库未启动,Spring Boot 应用将无法连接数据库。
5.1.1 使用命令行检查 MySQL 服务状态
在主库和从库的服务器上,可以使用以下命令检查 MySQL 服务状态:
# 检查 MySQL 服务是否正在运行
sudo systemctl status mysql
如果显示 active (running)
,则表示 MySQL 服务正在运行。
5.1.2 使用命令行启动 MySQL 服务
如果 MySQL 服务未运行,可以使用以下命令启动:
# 启动 MySQL 服务
sudo systemctl start mysql
启动服务后,可以再次使用 status
命令检查服务状态,确保 MySQL 服务已启动。
注意:如果主库和从库在不同的服务器上,您需要分别在主库服务器和从库服务器上执行上述命令。
5.2 使用 IntelliJ IDEA 启动 Spring Boot 项目
在确认 MySQL 主库和从库均已启动后,可以启动 Spring Boot 项目。
5.2.1 在 IntelliJ IDEA 中启动 Spring Boot 项目
- 打开项目:在 IntelliJ IDEA 中打开您的 Spring Boot 项目。
- 定位主类:在项目的
src/main/java
目录中,找到主类(通常是带有@SpringBootApplication
注解的类,比如YourApplication
类)。 - 运行项目:右键点击主类文件,选择“Run ‘YourApplication’”(运行
YourApplication
)选项。IDEA 将会在控制台显示启动日志。
5.2.2 检查控制台输出
项目启动后,检查 IDEA 控制台的日志输出,确保没有报错信息。如果看到类似以下内容,则说明项目启动成功:
INFO 12345 --- [main] com.example.YourApplication : Started YourApplication in 3.456 seconds (JVM running for 4.123)
5.3 编写测试类,测试读写操作是否按照预期走对应的数据源
在项目启动后,我们可以编写测试类,通过测试 Spring Boot 项目中是否实现了主从分离(读操作走从库、写操作走主库)。
5.3.1 创建测试类
在项目的 src/test/java
目录下,创建一个新的测试类,例如 UserServiceTest
,用于测试服务类中的读写方法。以下是测试类的示例代码:
import com.example.service.UserService;
import com.example.model.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
// 测试读操作
@Test
public void testReadOperation() {
// 调用读取用户的方法
System.out.println("Executing read operation (should use slave database)...");
userService.getUsers();
}
// 测试写操作
@Test
public void testWriteOperation() {
// 创建一个新的用户
User user = new User();
user.setName("Test User");
user.setEmail("testuser@example.com");
// 调用保存用户的方法
System.out.println("Executing write operation (should use master database)...");
userService.saveUser(user);
}
}
代码说明
- @SpringBootTest:该注解用于在测试类中启动 Spring Boot 上下文。它会自动加载配置文件和应用上下文,确保测试类中可以注入依赖。
- @Autowired:注入
UserService
,以便在测试方法中调用getUsers
和saveUser
方法。 - @Test:每个测试方法都使用
@Test
注解,表明这是一个测试用例,测试框架会自动运行带有@Test
的方法。
测试类的测试方法
-
testReadOperation:测试从库的读取操作。
- 该方法会调用
userService.getUsers()
,这应该会使用从库来执行查询操作。我们可以在日志中确认是否成功走到了从库。
- 该方法会调用
-
testWriteOperation:测试主库的写入操作。
- 该方法会创建一个新用户并调用
userService.saveUser(user)
,应该会使用主库来执行插入操作。可以在日志中查看是否成功使用主库。
- 该方法会创建一个新用户并调用
5.3.3 在 IntelliJ IDEA 中运行测试
在 IntelliJ IDEA 中可以通过以下步骤运行测试类:
- 运行单个测试方法:在
testReadOperation
或testWriteOperation
方法上右键点击,选择“Run ‘testReadOperation’”或“Run ‘testWriteOperation’”来运行特定测试。 - 运行整个测试类:在
UserServiceTest
类名上右键点击,选择“Run ‘UserServiceTest’”,以运行所有测试方法。
5.3.4 检查测试输出
在控制台中查看输出信息。如果您在 DynamicDataSource
类中添加了日志信息,可以看到类似以下的日志:
Switching to data source: slave
Executing read operation (should use slave database)...
Switching to data source: master
Executing write operation (should use master database)...
- 读操作日志:如果
testReadOperation
的日志显示Switching to data source: slave
,说明读操作成功走了从库。 - 写操作日志:如果
testWriteOperation
的日志显示Switching to data source: master
,说明写操作成功走了主库。
5.4 检查主从数据库的数据同步
在验证读写操作走了正确的数据源后,还需要检查主从数据库的数据同步情况,以确认主库的数据更改是否成功同步到从库。
5.4.1 执行写入操作,检查数据同步
-
运行写入测试方法:通过
testWriteOperation
或直接调用服务中的写入方法,向主库插入新数据。 -
在主库中检查数据:连接到主库,执行以下查询,确认数据是否成功插入:
SELECT * FROM your_database.users WHERE name = 'Test User';
-
在从库中检查数据同步:稍等片刻后,连接到从库,执行相同的查询,检查数据是否已同步:
SELECT * FROM your_database.users WHERE name = 'Test User';
如果从库中也能查询到这条记录,则表明主库的数据成功同步到了从库。
5.4.2 执行更新操作,检查数据同步
-
运行更新操作:在主库中更新记录的某个字段,例如通过
userService.updateUser(user)
方法更新email
字段(假设该方法存在),或直接在主库执行更新 SQL 语句:UPDATE your_database.users SET email = 'updateduser@example.com' WHERE name = 'Test User';
-
在从库中检查数据同步:稍等片刻后,在从库执行相同的查询,确认
email
字段是否已更新为'updateduser@example.com'
。
5.4.3 执行删除操作,检查数据同步
-
运行删除操作:通过
userService.deleteUserById(userId)
方法(假设存在)或直接在主库执行删除语句:DELETE FROM your_database.users WHERE name = 'Test User';
-
在从库中检查数据同步:在从库中执行以下查询,确认数据是否已同步删除:
SELECT * FROM your_database.users WHERE name = 'Test User';
如果查询结果为空,则表明删除操作已成功同步到从库。