MySQL读写分离是一种数据库架构优化方案,通过将读操作和写操作分离到不同的数据库节点,以提高系统性能和可用性。其核心思想是:写操作(INSERT、UPDATE、DELETE)由主库(Master)处理,读操作(SELECT)由从库(Slave)处理,利用主从复制机制保证数据一致性。这种架构能够有效分担单库压力,提升系统的并发处理能力。
一、读写分离的基本概念
1. 读写分离的目的
- 减轻主库压力:将读操作分散到从库,主库专注处理写操作
- 提高查询性能:多个从库可同时处理读请求,提升查询吞吐量
- 增强系统可用性:主库故障时,可快速切换到从库提供读服务
- 灵活扩展:可根据读负载动态增加从库数量
2. 读写分离的前提
- 主从复制:必须先搭建好主从复制环境,确保从库数据与主库一致
- 应用适配:应用程序或中间件需能区分读写操作并路由到相应节点
- 数据一致性容忍:需接受一定程度的数据延迟(主从复制延迟)
3. 读写分离的挑战
- 数据一致性:主从复制存在延迟,可能导致读不到最新数据
- 故障转移:主库或从库故障时,需自动切换以保证服务可用
- 事务处理:跨库事务和写后立即读场景处理复杂
- 负载均衡:需合理分配读请求,避免部分从库负载过高
二、读写分离的架构
1. 一主一从架构
最简单的读写分离架构,一个主库对应一个从库:
- 主库:处理所有写操作和部分关键读操作
- 从库:处理大部分读操作
优点:结构简单,易于维护
缺点:读性能提升有限,从库故障后无备用节点
2. 一主多从架构
一个主库对应多个从库,是最常用的读写分离架构:
- 主库:处理所有写操作
- 多个从库:共同分担读操作,可根据负载动态增减
优点:读性能可线性扩展,可用性高
缺点:主库仍可能成为瓶颈,需合理分配读请求
3. 级联复制架构
主库→从库→从库的级联结构,减轻主库的复制压力:
- 主库:处理写操作,仅向一个从库(中间层)复制数据
- 中间层从库:既作为主库的从库,又作为其他从库的主库
- 下层从库:处理读操作,从中间层从库复制数据
优点:减少主库的IO压力,适合从库数量较多的场景
缺点:数据延迟可能增加,架构较复杂
4. 双主架构
两个主库互为主从,可同时处理读写操作:
- 主库A和主库B互相同步数据
- 可将写操作分配到两个主库,读操作分配到各自的从库
优点:写性能提升,避免单主瓶颈
缺点:需处理数据冲突,实现复杂
三、读写分离的实现方式
1. 应用程序层实现
在应用程序中直接实现读写分离逻辑,根据SQL类型选择连接主库或从库。
(1)实现方式
- 配置多个数据源:主库数据源和从库数据源
- 编写路由逻辑:根据操作类型(读/写)选择相应的数据源
- 结合ORM框架:如MyBatis可通过插件实现,Spring可通过AOP实现
(2)Spring + MyBatis示例
// 1. 配置多数据源
@Configuration
public class DataSourceConfig {
// 主库数据源
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DruidDataSourceBuilder.create().build();
}
// 从库数据源
@Bean
@ConfigurationProperties("spring.datasource.slave")
public DataSource slaveDataSource() {
return DruidDataSourceBuilder.create().build();
}
// 动态数据源
@Bean
public DataSource routingDataSource() {
Map dataSources = new HashMap<>();
dataSources.put("master", masterDataSource());
dataSources.put("slave", slaveDataSource());
DynamicDataSource routingDataSource = new DynamicDataSource();
routingDataSource.setTargetDataSources(dataSources);
routingDataSource.setDefaultTargetDataSource(masterDataSource());
return routingDataSource;
}
}
// 2. 动态数据源路由
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceKey();
}
}
// 3. 数据源上下文
public class DataSourceContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal<>();
public static void setDataSourceKey(String key) {
contextHolder.set(key);
}
public static String getDataSourceKey() {
return contextHolder.get();
}
public static void clearDataSourceKey() {
contextHolder.remove();
}
}
// 4. AOP实现读写路由
@Aspect
@Component
public class DataSourceAspect {
// 读操作切入点
@Pointcut("execution(* com.example.dao..*.select*(..)) || " +
"execution(* com.example.dao..*.get*(..)) || " +
"execution(* com.example.dao..*.query*(..))")
public void readPointcut() {}
// 写操作切入点
@Pointcut("execution(* com.example.dao..*.insert*(..)) || " +
"execution(* com.example.dao..*.update*(..)) || " +
"execution(* com.example.dao..*.delete*(..))")
public void writePointcut() {}
@Before("readPointcut()")
public void setReadDataSource() {
DataSourceContextHolder.setDataSourceKey("slave");
}
@Before("writePointcut()")
public void setWriteDataSource() {
DataSourceContextHolder.setDataSourceKey("master");
}
@After("readPointcut() || writePointcut()")
public void clearDataSource() {
DataSourceContextHolder.clearDataSourceKey();
}
}
(3)优缺点
优点:
- 实现灵活,可根据业务需求定制路由规则
- 无中间件开销,性能较好
- 开发和调试简单
缺点:
- 读写分离逻辑与业务代码耦合
- 从库数量变化时需修改配置和代码
- 难以实现复杂的负载均衡和故障转移
- 跨团队开发时难以统一管理
2. 中间件实现
通过独立的中间件实现读写分离,应用程序只需连接中间件,无需关心后端数据库拓扑。
(1)主流中间件
① MySQL Router(官方):
- 轻量级代理,由MySQL官方提供
- 支持读写分离、故障转移和负载均衡
- 配置简单,与MySQL兼容性好
② ShardingSphere-JDBC:
- 基于JDBC的客户端中间件
- 支持读写分离、分库分表、分布式事务等
- 功能丰富,扩展性强
③ ProxySQL:
- 高性能的MySQL代理服务器
- 支持读写分离、连接池、查询缓存等
- 适合大规模部署,有完善的监控功能
④ MyCat:
- 开源的分布式数据库中间件
- 支持读写分离、分库分表、全局序列号等
- 配置灵活,文档丰富
(2)MySQL Router配置示例
# 配置文件:mysqlrouter.conf [DEFAULT] logging_folder = /var/log/mysqlrouter runtime_folder = /var/run/mysqlrouter data_folder = /var/lib/mysqlrouter [logger] level = INFO # 主从集群配置 [cluster.default] router_id = 1 master_addresses = 192.168.1.100:3306 # 主库地址 slave_addresses = 192.168.1.101:3306,192.168.1.102:3306 # 从库地址 mode = read-write # 读写分离模式 balancing = round_robin # 负载均衡策略:轮询 # 读写端口配置 [rw-split:default] bind_address = 0.0.0.0 bind_port = 6446 # 读写端口,自动路由读写操作 read_only_port = 6447 # 只读端口,强制路由到从库 # 账户配置 [metadata_cache:default] user = router_user password = router_password ttl = 300
应用程序连接方式:
// 连接MySQL Router的读写端口 jdbc:mysql://192.168.1.105:6446/dbname // 或连接只读端口(强制读操作) jdbc:mysql://192.168.1.105:6447/dbname
(3)优缺点
优点:
- 读写分离逻辑与业务代码解耦
- 支持复杂的负载均衡策略
- 便于统一管理和动态调整
- 支持自动故障转移
缺点:
- 引入中间件增加系统复杂度
- 存在一定的性能开销(代理模式)
- 学习和配置成本较高
- 中间件本身可能成为瓶颈或单点故障
四、读写分离的关键问题
1. 数据一致性问题
由于主从复制存在延迟,写操作后立即读可能读取不到最新数据,解决方案包括:
① 强制读主库:
对一致性要求高的场景,写操作后强制从主库读取
// 伪代码:写后读主库
@Transactional
public void createOrder(Order order) {
// 写操作(自动路由到主库)
orderDAO.insert(order);
// 强制读主库
DataSourceContextHolder.setDataSourceKey("master");
Order newOrder = orderDAO.selectById(order.getId());
DataSourceContextHolder.clearDataSourceKey();
return newOrder;
}
② 延迟读取:
写操作后等待一段时间再读,适用于对实时性要求不高的场景
// 伪代码:延迟读取
public Order getOrderAfterCreate(Long orderId) {
// 等待主从复制完成(不精确,不推荐在生产环境使用)
try {
Thread.sleep(500); // 等待500ms
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return orderDAO.selectById(orderId); // 从从库读取
}
③ 监控复制延迟:
通过监控Seconds_Behind_Master值,动态选择从库
// 伪代码:根据延迟选择从库
public DataSource selectSlaveDataSource() {
List slaves = slaveMonitor.getSlaveInfos();
// 选择延迟最小的从库
return slaves.stream()
.filter(slave -> slave.getSecondsBehindMaster() < 1) // 延迟小于1秒
.min(Comparator.comparingInt(SlaveInfo::getSecondsBehindMaster))
.map(SlaveInfo::getDataSource)
.orElse(getMasterDataSource()); // 无合适从库则读主库
}
④ 采用半同步复制:
确保主库事务提交前,至少有一个从库已接收日志,减少数据丢失风险
2. 负载均衡策略
多个从库时,需合理分配读请求,常用策略:
① 轮询(Round Robin):
依次将请求分配给每个从库,简单公平,适用于各从库性能相近的场景
② 权重(Weighted):
为性能不同的从库分配不同权重,性能好的从库处理更多请求
# MyCat配置示例
③ 最少连接数(Least Connections):
将请求分配给当前连接数最少的从库,动态适应负载变化
④ 哈希(Hash):
根据用户ID或其他关键字哈希,确保同一用户的请求路由到同一从库,利用缓存
⑤ 性能感知(Performance-aware):
根据从库的实时负载(CPU、内存、IO)动态分配请求,需监控支持
3. 故障转移机制
主库或从库故障时,需自动切换以保证服务可用性:
① 从库故障转移:
- 中间件定期检测从库健康状态
- 发现故障从库后,自动将请求路由到其他正常从库
- 从库恢复后,自动重新加入集群
② 主库故障转移:
- 检测到主库故障后,从从库中选举新主库(通常选择数据最新的从库)
- 其他从库重新指向新主库
- 应用程序写请求切换到新主库
- 常用工具:MHA(Master High Availability)、Orchestrator等
4. 事务处理
读写分离环境下的事务处理需特别注意:
① 同一事务中的读写操作应路由到同一节点(通常是主库)
② 避免在事务中混用读写操作,尽量将读操作放在事务外
③ 分布式事务需使用专门的解决方案(如2PC、TCC等)
// 推荐做法:事务中只包含写操作
@Transactional
public Long createOrder(Order order) {
// 只包含写操作
orderDAO.insert(order);
orderItemDAO.batchInsert(order.getItems());
return order.getId();
}
// 读操作在事务外执行
public OrderDetailVO getOrderDetail(Long orderId) {
Order order = orderDAO.selectById(orderId); // 读从库
List items = orderItemDAO.selectByOrderId(orderId); // 读从库
// 组装结果
return new OrderDetailVO(order, items);
}
五、读写分离的最佳实践
1. 架构设计
- 从库数量根据读负载确定,通常3-5个从库可满足大部分场景
- 主库和从库硬件配置不同:主库侧重写入性能(高IOPS),从库侧重读取性能(大内存)
- 重要业务可采用双主架构,避免单主故障导致写服务不可用
- 跨机房部署时,尽量将应用与数据库部署在同一机房,减少网络延迟
2. 数据一致性保障
- 明确业务对数据一致性的要求,选择合适的解决方案
- 对核心业务(如支付、订单创建)采用读主库策略
- 非核心业务(如商品列表、历史记录)可接受一定延迟,使用从库
- 启用半同步复制,减少数据丢失风险
- 监控主从复制延迟,设置阈值告警(如延迟超过5秒)
3. 应用开发
- 避免使用SELECT ... FOR UPDATE等会在从库执行失败的语句
- 写操作后需立即读取的场景,明确指定读主库
- 避免长事务,减少主库锁竞争和复制延迟
- 分页查询和统计查询尽量在从库执行
- 合理使用缓存,减少数据库读压力
4. 监控与运维
- 监控主从复制状态:Slave_IO_Running、Slave_SQL_Running、Seconds_Behind_Master
- 监控各节点的性能指标:CPU、内存、磁盘IO、连接数、QPS等
- 监控读写分离中间件的状态和性能
- 定期进行故障演练,测试故障转移机制
- 从库定期重建,避免长期运行导致的数据不一致
5. 性能优化
- 从库启用查询缓存(注意MySQL 8.0已移除查询缓存)
- 从库可适当调整参数优化读性能(如增大innodb_buffer_pool_size)
- 主库和从库使用不同的索引策略:主库优化写性能,从库优化读性能
- 大表查询优先在从库执行,并做好查询优化
- 避免在从库执行耗时的操作,防止影响复制进程
6. 安全考虑
- 从库设置为只读(read_only=1),防止误写入
- 复制用户只授予必要的权限(REPLICATION SLAVE)
- 数据库连接密码加密存储,避免明文配置
- 定期备份主库和从库数据,确保可恢复性
六、读写分离与分库分表的结合
在大规模系统中,读写分离通常与分库分表结合使用,形成更强大的分布式数据库架构:
1. 先进行分库分表,按业务拆分数据
2. 每个分库再配置主从复制,实现读写分离
3. 通过中间件(如ShardingSphere)统一管理,提供透明访问
这种架构的优势:
- 同时解决数据量过大和并发过高的问题
- 可根据不同业务模块的特点,定制读写策略
- 系统扩展性更好,可独立扩展某个业务模块的数据库
实现示例:
// ShardingSphere同时配置分库分表和读写分离
spring:
shardingsphere:
datasource:
names: master0, slave0, master1, slave1
# 主库0配置
master0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/master0
username: root
password: 123456
# 从库0配置
slave0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3307/slave0
username: root
password: 123456
# 主库1配置
master1:
# ... 类似配置
# 从库1配置
slave1:
# ... 类似配置
rules:
# 读写分离配置
readwrite-splitting:
data-sources:
ds0:
type: Static
props:
write-data-source-name: master0
read-data-source-names: slave0
load-balancer-name: round_robin
ds1:
type: Static
props:
write-data-source-name: master1
read-data-source-names: slave1
load-balancer-name: round_robin
# 分库分表配置
sharding:
tables:
t_order:
actual-data-nodes: ds${0..1}.t_order${0..3}
# ... 分库分表规则配置
# ... 分片算法配置
props:
sql-show: true
981

被折叠的 条评论
为什么被折叠?



