SpringBoot3 实战:3 步实现读写分离,从原理到落地避坑全解析

B站影视 欧美电影 2025-09-14 01:15 1

摘要:作为一名互联网开发工程师,你是否遇到过这样的场景:线上项目用户量激增后,数据库查询压力越来越大,即使加了索引,高峰期仍会出现慢查询告警;想做数据库扩展,却又担心代码改动量太大,影响现有业务稳定性?

作为一名互联网开发工程师,你是否遇到过这样的场景:线上项目用户量激增后,数据库查询压力越来越大,即使加了索引,高峰期仍会出现慢查询告警;想做数据库扩展,却又担心代码改动量太大,影响现有业务稳定性?

其实,解决这类问题最经典且低成本的方案之一,就是读写分离。尤其在 SpringBoot3 成为主流开发框架的当下,实现读写分离早已不是复杂的技术难题。今天这篇文章,我会从原理拆解到实战落地,带你用 3 步完成 SpringBoot3 项目的读写分离改造,最后再分享 5 个生产环境避坑要点,让你看完就能直接复用。

在开始写代码前,我们得先明确读写分离的核心逻辑 —— 毕竟技术选型不能只看 “别人都在用”,得知道它到底能解决什么问题。

1.1 读写分离的核心原理

大多数互联网项目的业务场景都符合 “读多写少” 的特点:比如用户查看商品详情、获取个人订单列表、浏览新闻资讯等操作,都是 “读” 操作;而用户下单、修改密码、提交表单等操作,才是 “写” 操作。根据统计,很多项目的读写比例甚至能达到 8:2,甚至 9:1。

如果所有读写操作都集中在一台数据库服务器上,“读” 操作的高频查询就会占用大量数据库连接和 CPU 资源,进而导致 “写” 操作(如订单创建)响应变慢,严重时甚至会出现数据插入超时。

读写分离的解决方案很直接:

主库(Master):专门处理 “写” 操作(INSERT、UPDATE、DELETE),保证数据的实时性和一致性;从库(Slave):专门处理 “读” 操作(SELECT),分担主库的查询压力;数据同步:通过数据库自身的复制机制(如 MySQL 的主从复制),将主库的新增 / 修改数据同步到从库,确保从库数据与主库一致。

这样一来,“读” 和 “写” 的压力被拆分到不同服务器,既能缓解主库负担,又能通过增加从库数量进一步提升 “读” 性能,是应对高并发查询的 “性价比之王” 方案。

1.2 SpringBoot3 实现读写分离的关键:动态数据源

传统项目中,如果要区分主从库,可能需要手动在代码中判断 “当前是读操作还是写操作”,再切换对应的数据库连接 —— 这种方式不仅代码冗余,还容易出错,后期维护成本极高。

SpringBoot3 通过动态数据源(Dynamic DataSource) 机制,完美解决了这个问题:它能根据当前执行的 SQL 类型(读 / 写),自动切换到对应的数据源(从库 / 主库),开发者无需在代码中手动切换,实现 “无感知” 的读写分离。

核心逻辑是:

配置主库和从库的数据源信息;定义 “数据源路由规则”:写操作路由到主库,读操作路由到从库;通过 AOP(面向切面编程)拦截 SQL 执行,根据规则自动切换数据源。

接下来,我们就用 3 步实现这个过程,全程基于 SpringBoot3.2 和 mysql8.0,确保代码可直接复用。

在开始前,先确认你的开发环境:JDK17(SpringBoot3 最低要求)、Maven3.6+、MySQL8.0(需提前配置好主从复制,文末附简易配置教程)。

第一步:引入核心依赖

首先在pom.xml中引入 3 个关键依赖:

SpringBoot Starter Web(基础 Web 支持);SpringBoot Starter jdbc(数据源基础支持);Dynamic Datasource(阿里开源的动态数据源框架,轻量且稳定,比原生 Spring 动态数据源更好用);MySQL 驱动(适配 MySQL8.0)。org.springframework.bootspring-boot-starter-webcom.baomidoudynamic-datasource-spring-boot-starter3.6.1com.mysqlruntimecom.baomidoumybatis-plus-boot-starter3.5.5

这里为什么选择阿里的dynamic-datasource?因为它已经封装好了数据源切换、负载均衡(多从库场景)等核心功能,开发者只需简单配置就能使用,避免重复造轮子。

第二步:配置主从数据源和路由规则

2.1 配置 application.yml(核心)

在src/main/resources/application.yml中,配置主库(master)、从库(slave)的连接信息,以及动态数据源的规则:

spring: # 动态数据源配置 datasource: dynamic: # 1. 全局默认数据源(未指定时默认用主库,防止读操作路由失败时无数据源可用) primary: master # 2. 数据源列表(master=主库,slave=从库,多从库可配置slave1、slave2...) datasources: # 主库(写操作) master: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.1.100:3306/test_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: 123456 # 从库(读操作) slave: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://192.168.1.101:3306/test_db?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true username: root password: 123456 # 3. 数据源切换规则(AOP拦截SQL,根据SQL类型自动切换) strategy: # 读操作路由到从库(SELECT语句,除了SELECT LAST_INSERT_ID这类特殊SQL) read: - SELECT # 写操作路由到主库(INSERT/UPDATE/DELETE/DDL语句) write: - INSERT - UPDATE - DELETE - CREATE - ALTER - DROP # MySQL配置(可选,优化连接池) jdbc: template: query-timeout: 3000 # 查询超时时间 datasource: hikari: maximum-pool-size: 10 # 最大连接数 minimum-idle: 5 # 最小空闲连接数 idle-timeout: 300000 # 连接空闲超时时间(5分钟)

关键配置说明

primary: master:默认数据源设为主库,防止某些特殊场景(如从库宕机)下读操作无数据源可用;多从库场景:如果有多个从库(如 slave1、slave2),只需在datasources下新增配置,框架会自动实现从库负载均衡(默认轮询策略);strategy:核心路由规则,通过 SQL 关键字判断操作类型,无需手动写 AOP 拦截逻辑 —— 这是dynamic-datasource框架的核心优势。

2.2 启动类添加注解(关键)

在 SpringBoot 启动类上添加@EnableDynamicDatasource注解,开启动态数据源功能:

import com.baomidou.dynamic.datasource.annotation.EnableDynamicDatasource;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication@EnableDynamicDatasource // 开启动态数据源public class SpringBootReadWriteSplitApplication { public static void main(String args) { SpringApplication.run(SpringBootReadWriteSplitApplication.class, args); }}

这一步很容易被忽略 —— 如果不添加该注解,动态数据源配置不会生效,所有操作都会默认使用主库。

第三步:编写代码验证读写分离

配置完成后,我们通过一个简单的 “用户管理” 案例,验证读写分离是否生效。

3.1 数据库表结构(MySQL)

先在主库创建user表,主从复制会自动同步到从库:

CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '用户名', `age` int DEFAULT NULL COMMENT '年龄', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

3.2 编写 Service 层代码(核心验证)

我们编写一个 Service,包含 “新增用户”(写操作,应走主库)和 “查询用户列表”(读操作,应走从库)两个方法,通过日志打印当前使用的数据源:

import com.baomidou.dynamic.datasource.annotation.DS;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import org.springframework.stereotype.Service;import com.example.demo.mapper.UserMapper;import com.example.demo.entity.User;@Servicepublic class UserServiceImpl extends ServiceImplimplements UserService { /** * 新增用户(写操作,默认走主库) * 注:@DS注解可手动指定数据源,优先级高于全局规则;不写则按全局规则自动切换 */ @Override public boolean addUser(User user) { // 打印当前使用的数据源(方便验证) System.out.println("当前数据源:" + com.baomidou.dynamic.datasource.DynamicDataSourceContextHolder.peek); return save(user); } /** * 查询用户列表(读操作,默认走从库) */ @Override public ListgetUserList { System.out.println("当前数据源:" + com.baomidou.dynamic.datasource.DynamicDataSourceContextHolder.peek); return list(new QueryWrapper); }}

3.3 编写 Controller 层代码(测试接口)

import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import com.example.demo.entity.User;import com.example.demo.service.UserService;import java.util.List;@RestController@RequestMapping("/user")public class UserController { @Autowired private UserService userService; // 新增用户(POST请求,写操作) @PostMapping("/add") public String addUser(@RequestBody User user) { boolean result = userService.addUser(user); return result ? "新增成功" : "新增失败"; } // 查询用户列表(GET请求,读操作) @GetMapping("/list") public ListgetUserList { return userService.getUserList; }}

3.4 验证读写分离效果

启动项目,用 Postman 调用POST /user/add接口,新增一个用户:

控制台会打印:当前数据源:master(说明写操作走了主库);查看主库user表,会新增一条数据;等待 1-2 秒(主从复制延迟),查看从库user表,数据会同步过来。

调用GET /user/list接口,查询用户列表:

控制台会打印:当前数据源:slave(说明读操作走了从库);即使停掉从库,框架会自动切换回主库(因为primary: master),不会导致查询失败。

到这里,SpringBoot3 的读写分离就已经实现了 —— 整个过程没有复杂的代码编写,核心是通过dynamic-datasource框架简化了数据源切换逻辑,开发者只需关注业务代码即可。

很多开发者在测试环境实现读写分离后,上线时会遇到各种问题 —— 比如数据不一致、从库延迟导致查询不到新数据等。下面这 5 个避坑要点,是我在多个生产项目中总结的经验,一定要牢记:

1. 主从复制延迟问题:如何避免 “刚写就查不到”?

问题场景:用户刚提交订单(写主库),立即跳转到订单列表页(读从库),但此时主从复制还没完成,导致用户看不到刚创建的订单 —— 这是读写分离最常见的问题。

解决方案

关键业务手动指定主库:对于 “写后立即读” 的场景(如订单创建后查详情),用@DS("master")注解强制读主库,示例:

@DS("master") // 强制读主库,解决主从延迟问题@Overridepublic User getUserById(Long id) { return getById(id);}

优化主从复制配置:MySQL 主从复制默认是 “异步复制”,可改为 “半同步复制”(需要额外配置),减少复制延迟到毫秒级;

业务妥协:非核心业务(如商品列表)可接受 1-2 秒延迟,无需特殊处理。

从库宕机:如何自动切换到其他数据源?

问题场景:如果从库服务器故障,读操作会报错 “数据源不可用”,影响用户体验。

解决方案

配置多从库:在application.yml中配置多个从库(slave1、slave2),框架会自动实现负载均衡和故障转移 —— 当 slave1 宕机时,会自动切换到 slave2;配置默认数据源:primary: master的配置一定要加,确保所有从库都宕机时,读操作会自动切换到主库,避免服务不可用。

3. 特殊 SQL 的路由问题:避免 “读操作走主库”

问题场景:有些 SQL 虽然是SELECT语句,但实际需要操作主库(如SELECT LAST_INSERT_ID、SELECT @@identity),如果被路由到从库,会导致数据查询错误。

在application.yml的strategy.read中排除特殊 SQL:strategy: read: - SELECT - !SELECT LAST_INSERT_ID # 排除该SQL,使其走主库 - !SELECT @@identity手动用@DS("master")指定数据源:对于特殊 SQL,直接在方法上标注@DS("master"),强制走主库。

问题场景:在一个事务中,如果既有写操作(走主库),又有读操作(按规则走从库),会导致 “同一事务内操作不同数据源”,出现数据不一致(因为事务只对主库生效,从库的读操作不受事务控制)。

解决方案

事务内所有操作强制走主库:在事务方法上添加@DS("master"),确保事务内的所有读写操作都走主库,示例:@Transactional // 事务注解@DS("master") // 事务内强制走主库,避免数据源切换@Overridepublic void updateAndQuery(User user) { // 写操作(更新用户) updateById(user); // 读操作(查询用户,此时也走主库) User dbUser = getById(user.getId);}避免在事务内做非必要的读操作:尽量将 “读操作” 移出事务,减少主库压力。

连接池配置优化:避免数据库连接耗尽

问题场景:读写分离后,主库和从库各有一个连接池,如果连接池配置不合理(如最大连接数太小),高并发时会出现 “连接耗尽” 错误。

解决方案

合理配置连接池参数:根据服务器 CPU 核心数(如 4 核 8G 服务器)和数据库性能,建议主库maximum-pool-size设为 10-20(写操作频率低但需保证实时性),从库设为 20-30(读操作高频,需更多连接支撑)。同时配置connection-timeout(连接超时时间,建议 3000ms),避免连接等待过久:

spring: datasource: dynamic: datasources: master: hikari: maximum-pool-size: 15 connection-timeout: 3000 idle-timeout: 300000 slave: hikari: maximum-pool-size: 25 connection-timeout: 3000 idle-timeout: 300000

监控连接池状态:通过 SpringBoot Actuator 监控主从库的连接

management: endpoints: web: exposure: include: hikarihealth,metrics # 暴露Hikari连接池健康状态和指标

启动项目后,访问http://localhost:8080/actuator/hikarihealth,可实时查看主从库连接池的 “活跃连接数”“空闲连接数”,当活跃连接接近最大连接数时,及时扩容服务器或调整连接池参数。

避免长连接占用:禁止在代码中手动持有数据库连接(如不关闭 ResultSet、PreparedStatement),确保 ORM 框架(如 MyBatis-Plus)自动管理连接生命周期,使用完后及时归还连接池。

要实现读写分离,必须先配置 MySQL 主从复制(主库数据同步到从库),以下是基于 Linux 系统的快速配置步骤,新手也能轻松操作:

步骤 1:修改 MySQL 配置文件my.cnf

# 编辑主库配置文件vim /etc/my.cnf

添加以下内容(开启二进制日志,指定主库 ID):

[mysqld]# 开启二进制日志(主从复制依赖二进制日志)log-bin=mysql-bin# 主库唯一ID(1-255,不能与从库重复)server-id=1# 只同步test_db数据库(按需配置,不配置则同步所有数据库)binlog-do-db=test_db# 忽略系统数据库同步binlog-ignore-db=mysqlbinlog-ignore-db=information_schema

步骤 2:重启 MySQL 服务

systemctl restart mysqld

步骤 3:创建主从复制账号并授权

登录 MySQL 主库,执行 SQL:

-- 创建用于复制的账号(用户名:repl,密码:123456)CREATE USER 'repl'@'%' IDENTIFIED BY '123456';-- 授予复制权限(只能从从库IP访问,%表示所有IP,生产环境建议指定从库IP)GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';-- 刷新权限FLUSH PRIVILEGES;-- 查看主库二进制日志状态(记录File和Position值,从库配置需要)SHOW MASTER STATUS;

执行结果示例(需记录File=mysql-bin.000001和Position=156):

FilePositionBinlog_Do_DBBinlog_Ignore_DBmysql-bin.000001156test_dbmysql,information_schemavim /etc/my.cnf

添加以下内容(指定从库 ID,关闭二进制日志(可选)):

[mysqld]# 从库唯一ID(不能与主库重复,如2)server-id=2# 关闭从库二进制日志(若从库不作为其他从库的主库,可关闭)skip-log-bin# 只同步test_db数据库(与主库保持一致)replicate-do-db=test_db

步骤 3:配置从库连接主库

登录 MySQL 从库,执行 SQL(替换主库 IP、File 和 Position 值):

-- 停止从库复制进程(首次配置可忽略)STOP SLAVE;-- 配置主库信息CHANGE MASTER TOMASTER_HOST='192.168.1.100', -- 主库IPMASTER_USER='repl', -- 主库创建的复制账号MASTER_PASSWORD='123456', -- 复制账号密码MASTER_LOG_FILE='mysql-bin.000001', -- 主库SHOW MASTER STATUS中的File值MASTER_LOG_POS=156; -- 主库SHOW MASTER STATUS中的Position值-- 启动从库复制进程START SLAVE;-- 查看从库复制状态(关键看Slave_IO_Running和Slave_SQL_Running是否为Yes)SHOW SLAVE STATUS\G;

若执行结果中出现以下两行,说明主从复制配置成功:

Slave_IO_Running: YesSlave_SQL_Running: Yes

验证主从复制

在主库test_db的user表中插入一条数据,1-2 秒后查看从库相同表,若数据同步成功,主从复制配置完成。

1. 哪些项目适合用读写分离?

读多写少场景:如电商商品详情页、新闻资讯平台、博客系统(读写比例 > 7:3);高并发查询场景:日均 PV 超 10 万,数据库查询耗时超过 100ms 的项目;数据安全性要求高的场景:主库故障时,从库可作为备用库,减少数据丢失风险。

不适用场景:写操作密集的项目(如秒杀订单创建、实时统计系统),这类项目更适合分库分表或使用分布式数据库(如 ShardingSphere)。

2. 后续优化方向

从库读写分离升级:若从库查询压力仍大,可增加从库数量(如 3 个从库),框架自动实现轮询负载均衡;读写分离与分库分表结合:当单库数据量超 1000 万时,在读写分离基础上增加分库分表(按用户 ID 哈希分库),进一步提升性能;引入缓存减少数据库访问:在从库前增加 Redis 缓存(如缓存商品列表、用户信息),热点数据直接从缓存读取,减少从库查询压力。

最后,留给大家一个思考题:如果项目中同时使用了事务和读写分离,当事务回滚时,从库已经同步的数据该如何处理?欢迎在评论区分享你的解决方案,也可以提出文中遇到的问题,我会逐一解答~

来源:从程序员到架构师

相关推荐