本文目录导读:
读写分离是一种常见的数据库优化架构,旨在将查询(读)操作和写入(更新/插入/删除)操作分开到不同的数据库服务器上,以提升系统吞吐量和响应速度。
通常由一主多从的架构实现:主库(Master)负责写,从库(Slave)负责读,数据从主库实时或准实时同步到从库。
下面是实现读写分离的完整步骤、技术方案和注意事项。
核心逻辑:如何判断“读”和“写”?
在代码层面,你需要将 SQL 请求动态路由:
- 写操作(INSERT、UPDATE、DELETE、DDL):强制路由到主库。
- 读操作(SELECT):默认路由到从库(可以在从库之间做负载均衡)。
使用中间件(推荐,对应用透明)
这是目前最主流、最成熟的做法,应用层不需要修改代码,只需连接中间件,由中间件负责转发请求。
- 代表产品:
- ShardingSphere-Proxy:Apache 顶级项目,功能强大,不仅支持读写分离,还支持分库分表。
- MyCat:老牌中间件,配置相对简单。
- ProxySQL:性能极高,专为 MySQL 设计,支持在线修改配置。
- 工作模式:
- 应用层配置数据源指向 Proxy(
jdbc:mysql://proxy_ip:3306/db)。 - Proxy 内部配置主库和从库的真实地址及同步关系。
- Proxy 解析 SQL 语句,判断是读还是写,然后路由到对应的真实数据库。
- 应用层配置数据源指向 Proxy(
- 优点:对开发人员零侵入,迁移成本低,运维统一管理。
- 缺点:增加了一层网络跳转,会引入少量延迟;中间件本身需要高可用部署。
配置示例(ShardingSphere-Proxy):
# config-sharding.yaml
rules:
- !READWRITE_SPLITTING
dataSources:
readwrite_ds:
writeDataSourceName: write_ds
readDataSourceNames:
- read_ds_0
- read_ds_1
loadBalancerName: round_robin
loadBalancers:
round_robin:
type: ROUND_ROBIN
在应用层代码中实现(灵活,但侵入性强)
通过访问数据库的数据源(DataSource)层,手动或通过框架注解切换数据源。
- 核心思路:创建两个(或多个)数据库连接池,然后通过拦截器或注解动态选择使用哪个连接池。
- 实现工具:
- Spring 中的
AbstractRoutingDataSource:最常用的 Java 实现方式。 - TDDL:淘宝开源,Java 框架。
- Spring 中的
- 实现步骤(以 Java Spring + MyBatis 为例):
- 配置多数据源:配置
masterDataSource和slaveDataSource。 - 编写路由类:继承
AbstractRoutingDataSource,实现determineCurrentLookupKey()方法,该方法的返回值决定了当前线程使用哪个数据源。 - 使用 AOP 或注解标记:
- 强制写:默认走主库(
@Master或直接使用Transactional注解时强制走主库,因为事务中的一致性要求)。 - 强制读:使用
@ReadOnly或@Slave注解标记 Service/Controller 方法,AOP 在方法执行前将线程上下文(ThreadLocal)设置为“从库”。 - 自动判断:也可以在 DAO 层拦截,解析 SQL 前缀(
select/update/delete)来自动判断。
- 强制写:默认走主库(
- 配置多数据源:配置
简单伪代码(路由类):
public class ReadWriteRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
// 从 ThreadLocal 中获取当前操作类型:'master' 或 'slave'
return DbContextHolder.getDbType();
}
}
- 优点:灵活,可以针对不同接口做精细控制(用户详情必须读主库保证实时)。
- 缺点:与框架深度绑定,代码侵入性强,如果有多套系统都需要改。
ORM 框架或数据库驱动自带功能
少数数据库或驱动支持原生读写分离。
- MySQL Connector/J:MySQL JDBC 驱动支持
replicationConnection,通过jdbc:mysql:replication://master,slave1,slave2/db的连接方式,让驱动自动分发读写。- 问题:功能较弱,故障转移、负载均衡策略不够完善,现在单独使用较少,多与中间件配合。
- PHP 的
MySQLi或PDO:需要手动封装,不推荐。
必须解决的四个核心问题
架构设计得再好,不考虑下面这些问题也会掉坑:
主从延迟(最头疼的问题)
从库数据同步存在毫秒到秒级别的延迟,如果刚写入一条数据,立刻去从库查询,可能会查不到。
- 解决方案:
- 强制读主:对实时性要求极高的业务(如:刚创建的订单、支付结果、用户注册),查询时强制路由到主库。
- 延迟容忍:对不关键的数据(如:历史文章、日志、排行榜),允许短时间的延迟。
- 中间件策略:某些中间件(如 ShardingSphere)支持
hint或基于 SQL 的强制路由。 - 监控报警:实时监控
Seconds_Behind_Master,超过阈值自动告警或切流量。
事务一致性
在一个事务中,如果既有读又有写,那么读操作必须也在主库上执行,否则,事务内的查询可能看不到未提交的修改。
- 方案:事务内的所有请求都走主库,在 Spring 中,只要
@Transactional生效,数据源通常会被绑定为写库。
连接池与故障转移
- 当某个从库宕机时,应用不应该报错。
- 方案:使用成熟的连接池(HikariCP、Druid)并配置健康检查;使用中间件(如 ProxySQL)自带健康检测和自动剔除/恢复。
数据不均衡
如果业务是极端写多读少,或者读压力极大,读写分离后还需要考虑:
- 写库压力:主库可能仍是瓶颈,需要分库分表。
- 读库压力:如果从库也扛不住,需要增加从库数量,或引入缓存(Redis)来兜底。
推荐路线
- 小规模 / 入门项目:使用 Spring 的
AbstractRoutingDataSource+ AOP 注解,简单快速实现。 - 中大规模 / 生产环境:特别推荐 ShardingSphere-Proxy 或 ProxySQL。
原因:解耦应用、配置灵活、支持平滑扩容、自带负载均衡和故障转移、方便未来做分库分表。
- 云原生 / 微服务:很多时候云数据库(如 AWS Aurora、阿里云 RDS)本身已经内置了读写分离和负载均衡,可以直接连接代理地址,无需自己搭建中间件。
一句话建议:能上中间件就别在代码里写死,能用云服务就别自己搭建。