读写分离
一、读写分离架构概述
- 目标: 分摊主库压力,提升读取性能。
- 基本结构: 一主多从 。
- 两种实现方式:
- 3.1 客户端直连负载均衡:
- 机制: 客户端(连接层)直接选择后端数据库进行查询。
- 优点:
- 查询性能稍好(少一层转发)。
- 整体架构简单,排查问题方便。
- 缺点:
- 客户端需了解后端部署细节。
- 主备切换、库迁移时,客户端需调整连接信息。
- 缓解: 通常配合服务发现组件 (如 Zookeeper) 管理后端信息,业务端尽量解耦。
- 3.2 中间代理层 (Proxy):
- 机制: 客户端只连接 Proxy,Proxy 根据请求类型和上下文决定路由。
- 优点:
- 对客户端友好,无需关注后端细节。
- 连接维护、后端信息维护由 Proxy 完成。
- 缺点:
- 对后端维护团队要求更高。
- Proxy 自身也需要高可用架构。
- 整体架构相对复杂。
- 趋势: 目前更倾向于带 Proxy 的架构。
- 3.1 客户端直连负载均衡:
二、读写分离的核心问题:过期读 (Stale Read)
- 定义: 由于主从存在延迟,客户端执行完更新后立即查询从库,可能读到更新前的旧数据。
- 根源: 主从延迟无法100%避免。
- 目标: 客户端希望查询从库的数据结果与查询主库的结果一致。
三、处理过期读的方案
-
方案一:强制走主库方案 (Classification of Queries)
- 机制: 将查询请求分类:
- 必须最新结果的请求: 强制发往主库 (如:卖家发布商品后立即查看商品状态)。
- 可接受旧数据的请求: 发往从库 (如:买家浏览商品列表)。
- 优点: 最常用,简单有效。
- 缺点:
- 若所有查询都不能过期读 (如金融业务),则需放弃读写分离,牺牲扩展性。
- 机制: 将查询请求分类:
-
方案二:Sleep 方案 (Client-side Delay)
- 机制: 主库更新后,读从库前先
sleep
一段时间 (如SELECT SLEEP(1);
)。 - 假设: 大多数情况下主备延迟在
sleep
时间内。 - 改进版 (前端体验优化):
- 以卖家发布商品为例:商品发布后,前端Ajax直接用客户端输入内容在页面上模拟“新商品”显示,而非立即查询数据库。
- 用户刷新页面时(通常已过一段时间),才真正查询从库。
- 优点: 在特定场景 (如Ajax异步更新) 下能一定程度上解决问题,对用户体验影响较小。
- 缺点 (不精确):
- 若0.5秒可同步,仍等待1秒 (资源浪费)。
- 若延迟超过1秒,仍会过期读。
- 评价: 看起来“不靠谱”,但有其适用场景。
- 机制: 主库更新后,读从库前先
-
方案三:判断主备无延迟方案 (Synchronization Check)
- 目标: 确保查询前备库已同步完成。
- 3.1 方法一:判断
seconds_behind_master
SHOW SLAVE STATUS;
查看seconds_behind_master
是否为0。- 缺点: 单位是秒,精度可能不够。
- 3.2 方法二:对比位点 (Log Position)
- 比较
Master_Log_File
与Relay_Master_Log_File
。 - 比较
Read_Master_Log_Pos
与Exec_Master_Log_Pos
。 - 两组值完全相同,则表示接收到的日志已同步完成。
- 比较
- 3.3 方法三:对比 GTID 集合
Auto_Position=1
(GTID模式)。- 比较
Retrieved_Gtid_Set
(备库收到的GTID集合) 与Executed_Gtid_Set
(备库已执行的GTID集合)。 - 两者相同,则表示备库接收到的日志已同步完成。
- 优点: 比
sleep
方案准确。 - 缺点 (仍不够精确):
- 场景: 主库已执行完事务trx3并回复客户端,但trx3的binlog尚未传到备库。
- 此时,上述判断可能认为“无延迟”(因为备库已收到的都执行完了),但查询仍读不到trx3。
- 根本原因: 判断的是“备库收到的日志都执行完了”,未考虑“主库已提交但尚未发送到备库的日志”。
-
方案四:配合 Semi-Sync 方案 (Semi-Synchronous Replication)
- Semi-Sync 机制:
- 事务提交时,主库将binlog发给从库。
- 从库收到binlog后,给主库发送ack。
- 主库收到ack后,才给客户端返回“事务完成”确认。
- 效果: 保证所有已向客户端确认的事务,其binlog至少已送达一个从库。
- 解决普通异步复制的数据丢失问题: 若主库掉电前binlog未发出,semi-sync可避免此情况。
- 结合位点/GTID判断:
- 一主一备: 可以确保避免过期读。
- 一主多从:
- 主库只需等到一个从库ack即可。
- 若查询落到响应ack的从库,能读到最新数据。
- 若查询落到其他未ack的从库,仍可能过期读。
- 潜在问题 (同方案三):
- 业务更新高峰期,位点/GTID更新快,等值判断可能持续不成立,导致从库长时间无法响应查询。
- 过度等待: 客户端只需保证其自身发起的更新已在从库执行,无需等待主备完全同步。
- Semi-Sync 机制:
-
方案五:等主库位点方案 (Wait for Master Position)
- 命令:
SELECT master_pos_wait(file, pos[, timeout]);
(在从库执行)file
,pos
: 主库上的文件名和位置。timeout
: 可选,最多等待N秒。- 返回值:
>=0
(正整数M): 从开始到应用完指定位点,执行了M个事务。NULL
: 同步线程异常。-1
: 超时。0
: 执行命令时已过指定位点。
- 流程:
- 客户端:事务trx1更新完成后,立即在主库执行
SHOW MASTER STATUS;
获取当前File和Position。 - 客户端:选定一个从库执行查询。
- 从库:执行
SELECT master_pos_wait(File, Position, 1);
(假设等待1秒)。 - 从库:若返回值
>=0
,则在此从库执行查询。 - 否则 (超时或错误):退化到主库执行查询 (需做好限流)。
- 客户端:事务trx1更新完成后,立即在主库执行
- 优点:
- 解决了方案三、四中“过度等待”和一主多从场景下部分从库过期读的问题。
- 更精确地等待到特定事务同步完成。
- 缺点:
- 若所有从库都严重延迟,查询压力可能全部压向主库。
- 命令:
-
方案六:等 GTID 方案 (Wait for GTID)
- 前提: 数据库开启GTID模式。
- 命令:
SELECT wait_for_executed_gtid_set(gtid_set, timeout);
(在从库执行)gtid_set
: 要等待的GTID集合。timeout
: 最多等待N秒。- 返回值:
0
: 等到指定GTID集合在本库执行完毕。1
: 超时。
- 获取事务GTID:
- MySQL 5.7.6+:设置参数
session_track_gtids=OWN_GTID
。 - 客户端API:通过
mysql_session_track_get_first
从事务返回包中解析GTID。
- MySQL 5.7.6+:设置参数
- 流程:
- 客户端:事务trx1更新完成后,从返回包直接获取其GTID (记为
gtid1
)。 - 客户端:选定一个从库执行查询。
- 从库:执行
SELECT wait_for_executed_gtid_set(gtid1, 1);
(假设等待1秒)。 - 从库:若返回值
0
,则在此从库执行查询。 - 否则 (超时):退化到主库执行查询 (需做好限流)。
- 客户端:事务trx1更新完成后,从返回包直接获取其GTID (记为
- 优点 (相比等位点方案):
- 减少一次到主库查询
SHOW MASTER STATUS
的开销。
- 减少一次到主库查询
- 缺点 (同等位点方案):
- 若所有从库都严重延迟,查询压力可能全部压向主库。
四、方案总结与实践建议
- 混合使用: 实际应用中,可组合多种方案。
- 请求分类: 先区分哪些请求可接受过期读,哪些不能。
- 精确等待: 对不能接受过期读的,再采用等GTID或等位点方案。
- 权衡: 过期读本质上由“一写多读”导致。解决过期读往往需要在读性能、写性能、数据一致性之间做权衡。
- 限流策略: 当等待超时或从库普遍延迟导致查询退化到主库时,必须有相应的限流策略,防止主库被打垮。
五、延伸思考:文末问题预告
问题: 采用等GTID方案,对主库大表做DDL,可能出现什么情况?如何避免?
* 思考方向 (DDL对GTID等待的影响):
* 大表DDL执行时间长,会产生一个GTID。
* 在DDL完成前,后续的查询如果需要等待这个DDL的GTID,会发生什么?
* wait_for_executed_gtid_set
是否会长时间阻塞?
* 如何优雅地处理这种情况,既保证DDL的执行,又不严重影响依赖GTID等待的读请求?(例如,是否可以分阶段进行,或者有特殊的DDL工具支持)
六、上期问题回顾:GTID模式下,新从库所需binlog已在主库删除的处理
- 方案总结:
- 允许主从不一致:
- 主库获取
gtid_purged
(已删除的GTID集合)。 - 从库
RESET MASTER;
,然后SET GLOBAL gtid_purged = '主库的gtid_purged值';
。 START SLAVE;
(会丢失部分数据)。
- 主库获取
- 要求主从数据一致 (推荐):
- 重新搭建从库: 从主库全量备份恢复。
- 从其他保留全量binlog的从库同步: 先接到这个从库,追上后再切回主库。
- 从binlog备份恢复: 先在从库应用缺失的binlog备份,再
START SLAVE;
。
- 允许主从不一致: