想象你管理着一座巨型城市图书馆,馆内藏书 2000 万册,每天有几十万读者进出。糟糕的是,这座图书馆没有检索目录——管理员找一本书要从第一排书架逐本翻到最后一排,运气差时一整天都耗在一本书上。这就是“没有索引的数据库”:每次查询都是一次全表扫描,每一行记录都要读一遍。 后来你痛定思痛,先做了一套「分类编号检索目录」,读者先查目录、再按书架号取书,三五步就能定位目标。再后来,图书馆单层放不下了,你按读者卡尾号把藏书分流到 8 个分馆(分库分表),让总馆只负责入库登记(写),各分馆开放阅览(读)——这就是读写分离。于是一座吞吐百万读者、毫秒级响应的“城市图书馆系统”诞生了。
对 AI 从业者而言,数据库是所有 AI 应用的数据底座。掌握索引、分库分表与读写分离,就是掌握高并发系统的地基工程能力,这是后端架构师、分布式系统工程师与技术专家的必修内功。
可视化演示
展示数据库优化三模式:B+Tree索引结构(根-中间-叶子三层+叶子链表+四种索引类型对比+查询性能Before/After)、分库分表策略(垂直分库UDB/ODB/PDB/IDB+水平分表Hash/Range/Consistent/Geo四策略+数据分布柱状图)、读写分离架构(APP-PRX-MST主库写+S0-S2从库读+主从复制+读写比例80/20)。
本节配有交互动效(进入视口后自动加载,约 3KB)。
B+Tree索引结构与查询优化 B+Tree三层结构:根节点ROT→中间层N0-N2→叶子层L0-L5链表连接 BT/HS/FN/CPT四种索引类型对比,B+Tree范围查询最优90% IO = O(log N), 索引加速8x Before 120 QPS → After 950 QPS,加速7.9x
索引寻址原理
走进那座藏书 2000 万册的图书馆,入口处最显眼的是一面巨大的卡片目录柜。每张卡片写着「分类号—书架编号—书名」,你要找一本《分布式系统设计》,管理员按「计算机类 → 分布式系统 → 设计模式」三级分类,两三分钟就能定位到 B2-15-07 号书架。没有这套目录,管理员只能从第一排书架开始逐本翻阅——这正是全表扫描的真实写照。
数据库索引就是图书馆的“卡片目录柜”。最常见的 B+ 树索引,结构上像三级分类体系:根节点相当于一级分类(计算机/文学/历史),中间层节点相当于二级分类(分布式/网络/数据库),叶子节点保存数据行的主键或物理地址。查询时从根节点逐层下探,每层只需一次磁盘 I/O 就能确定走向哪个分支,而不是扫描整本书架。
B+ 树最精妙的设计是:所有数据都存放在叶子节点,非叶子节点只存“键值+指针”。这样整棵树保持“矮胖”形态,高度只有 3~4 层时就能容纳千万级数据;叶子节点之间用链表相连,范围查询(如“价格 100~200 元的商品”)可以沿链表顺序扫描,无需回溯上层。MySQL 的 InnoDB 引擎中,主键索引就是聚簇索引——表数据按照主键物理排序存放,二级索引的叶子存的是主键值,查到后还要“回表”再取一次完整行。
回到图书馆的场景:你不需要翻遍 2000 万张卡片,只需在三五层目录内逐级下钻。这正是“以空间换时间”——多放一排目录柜,换来读者几秒钟的精准定位。索引的本质,就是用额外的存储空间,换取数量级级别的查询速度提升。
- N(数据总行数)表里一共要索引多少行记录,比如 2000 万条用户数据。
- F(扇出)一个节点最多能伸出多少个子分支;MySQL 一个 16KB 页约能存 1000 个键值对。
- log_F N(取对数)把 N 不断均分成 F 份,需要分多少层。
- \lceil \cdot \rceil(向上取整)层数必须是整数,算出 2.4 层也要按 3 层算。
索引设计策略
图书馆的目录柜并非随意摆放,而是按「作者姓氏 → 书名首字母 → 出版年份」三级顺序排列的。读者查书时必须从第一级“作者姓氏”开始,跳过作者直接查书名,目录管理员就无从下手。这种“必须从最左边开始匹配”的规则,正是联合索引的最左前缀原则。
假设我们建了一个联合索引 (a, b, c),它相当于一本按 a、b、c 三个字段排序的字典。查询条件 WHERE a = 1 AND b = 2 AND c = 3 能完整走索引;而 WHERE a = 1 AND c = 2 只能用到 a 列,因为 b 列被跳过,c 列无法继续匹配;WHERE b = 1 AND c = 2 则完全用不上索引。设计联合索引时,要把等值查询频率最高的列放在最左边,把范围查询列放在右边。
覆盖索引则更进一步:如果查询所需的列全部包含在某棵索引树内,数据库就直接从索引里取数,连“回表”这一步都省了。比如索引 (user_id, name) 上执行 SELECT name FROM t WHERE user_id = 100,数据全部在索引中,无需回表。但索引并非越多越好——每多一个索引,写入时就要多维护一棵 B+ 树,相当于图书馆每本新书都要多抄一份目录卡片。
目录卡片多了,书架反而被挤占;卡片排序乱了,读者反而找不到书。索引设计的精髓就是“少而精”:用最少的索引覆盖最高频的查询路径,让每一棵 B+ 树都成为读者真正会去翻的那张卡片。
- N_rows_scanned(扫描行数)优化器预估需要扫描多少行,EXPLAIN 的 rows 列可以看到。
- C_single(单行成本)比较一行数据所需的 CPU 与内存开销,通常视为常数。
- Cost_query(总代价)两者相乘得到查询总开销。
- 索引的目标把扫描行数降到目标行附近,总代价骤降。
慢查询优化
图书馆前台放着一本《找书超时登记本》,每次查书超过 5 秒就会被记录在案,管理员每天翻看这本子,找出哪些书架、哪些查询方式浪费了读者时间。数据库里的“慢查询日志”正是这样一本登记本——它记录下所有执行时间超过阈值的 SQL,是性能优化的第一手线索。
定位到慢 SQL 后,数据库提供了“执行计划体检表”:EXPLAIN。它像图书管理员的路线图,展示这条 SQL 会走哪些表、用哪个索引、扫描多少行。重点关注三列:type 是否出现 ALL(全表扫描)、key 是否命中索引、rows 预估扫描行数是否异常大。若发现某条 SQL 扫描了 20 万行只返回 1 行,就要立刻检查索引是否建对。
SQL 改写是常见优化手段。把 SELECT * 改成只查需要的列;把 IN 子查询改写成 JOIN,更能利用索引;在 LIKE '%keyword%' 这种无法走索引的查询里,换成前缀匹配 LIKE 'keyword%' 就能命中索引;避免在索引列上做函数运算,例如 WHERE DATE(create_time) = '2024-01-01' 改成 create_time >= ... AND create_time < ... 的范围写法。
慢查询优化的本质,就是拿着登记本找到耗时最大的那个环节:如果 T_io 是 900ms,就建索引;如果 T_queue 是 800ms,就去查锁和连接池。像管理员一样,把每一毫秒都花在刀刃上。
- T_queue(排队等待)连接池和锁等待占用的时间,并发高时占比飙升。
- T_exec(执行计算)排序、分组、函数计算等 CPU 消耗。
- T_io(磁盘读写)扫描数据页的 I/O 时间,全表扫描时占大头。
- T_network(传输返回)把结果集传回客户端的时间。
分库分表策略
图书馆单层楼再也放不下 2000 万册书了。馆长决定开分馆:把藏书按读者卡尾号 0~7 分成 8 个分馆,尾号 0 的去第一分馆,尾号 1 的去第二分馆……读者拿卡一刷,就知道该往哪走。数据库的“分库分表”正是这个逻辑——当单表数据量过大、写入和查询都变慢时,把数据拆到多个库、多张表中去。
拆分有两种姿势。垂直拆分就像按楼层分大类:一层放计算机书、二层放文学书——在数据库里就是把一张列很多的宽表,按业务边界拆成“用户基础表”和“用户扩展表”,减少单行宽度、让每页容纳更多行。水平拆分则像同一类书分多个书架:把 2000 万条用户记录,按某个分片键散列到 user_0、user_1、user_2…… 多张结构完全相同的表里,每张表只有 250 万行。
分片键的选择决定一切。它必须是查询频率最高的条件,且分布足够均匀。比如按 user_id 分片,WHERE user_id = 1024 就能直接定位到对应分片——这称为“数据亲和”;如果按 id 分片却经常按 order_no 查询,就不得不广播到所有分片再聚合,反而更慢。容量规划上,业界经验是单表超过 500 万行或单表超过 10GB 时,就该认真考虑水平拆分。
分馆开起来后,每个分馆的藏书量只有原来的八分之一,找书、上架都快了好几倍。分库分表的核心就是“把大问题拆小”——用一台机器扛不住的容量,交给多台机器一起扛。
- shard_key(分片键)决定数据归属的字段,必须是高频查询条件。
- hash()(哈希函数)将任意长度的键映射为整数,让数据分布更均匀。
- mod N(取模)对分片总数取余,得到 0~(N-1) 的分片编号。
- node_id(目标分片)数据落到第 6 号分片。
中间件方案
分馆开多了,读者站在总馆大厅一脸茫然:“我这本书到底在哪个分馆?”于是馆长设了一个“总咨询台”——读者只需对咨询台报出书名,咨询台立刻告诉你该去第几馆、第几层。数据库中间件就是这样一个统一路由的咨询台,让应用无需关心数据散落在哪张表。
市面上主流方案有两类。ShardingSphere 属于“应用内路由”:它作为客户端 SDK 嵌入业务代码,SQL 经过它解析后自动路由到真实分片,Java 生态使用最广;MyCat 则是“独立代理网关”:应用先连 MyCat,再由 MyCat 转发到后端分片,对业务代码透明,支持多语言。选择时,团队以 Java 为主可优先 ShardingSphere,异构系统多则 MyCat 更灵活。
分片后最大难题是主键:多个分片各自自增会产生重复。雪花算法(Snowflake)是工业界标准解法——用一个 64 位长整型做主键,高 41 位存时间戳、中间 10 位存机器 ID、低 12 位存同一毫秒内的序列号,保证全局唯一且趋势递增。平滑迁移则讲究“双写+校验”:新老库同时写入,跑对比任务校验两边数据一致后,再把读流量逐步切到新库。
有了总咨询台,读者不需要记住每个分馆的藏书目录,应用层也不需要感知分片细节。中间件把“数据在哪里”的复杂度封装起来,让分库分表成为一件“对上层透明”的工程决策。
- 时间戳段(41 位)当前时间与起始时间 T0 的毫秒差,保证 ID 随时间递增。
- 机器段(10 位)5 位机房号 + 5 位机器号,区分不同节点。
- 序列段(12 位)同一毫秒内从 0 递增到 4095,单机每毫秒可生成 4096 个 ID。
- 位或拼接三段向左移位后按位或,合成一个 64 位整数。
读写分离
图书馆原来的设计有个大问题:读者来借书要排队登记,管理员既要办借阅手续、又要回答“这本书在哪个书架”的咨询,两条队伍堵在一起。馆长决定改造:总馆只负责入库登记和写数据,各分馆专供读者阅览查询——这就是读写分离。
实现依赖“主从复制”机制:主库(Master)负责处理 INSERT/UPDATE/DELETE,每次写入都会记录到 binlog(二进制日志);从库(Slave)通过 IO 线程拉取 binlog、由 SQL 线程回放,从而与主库保持数据同步。应用层再配一个读写路由——写请求强制走主库,读请求由负载均衡分发到多个从库,主库的压力骤减。
但复制不是瞬时的,binlog 从主库传到从库、再回放完成需要时间,称为“主从延迟”。若延迟 200ms,用户刚下单就去从库查订单,可能查不到新记录。应对方案有很多:把“关键读”强制路由到主库、从库延迟过高时自动摘除、使用半同步复制让主库等待至少一个从库确认收到 binlog 再提交。
登记和咨询分开后,借书队伍不再互相堵路。读写分离的本质是“读写分流、各司其职”——写路径保持强一致,读路径追求高吞吐,中间用一个小小的延迟窗口换来了整体系统数倍的并发能力。
- T_主库写入主库执行写入并生成 binlog 的时间。
- T_日志传输binlog 通过网络从主库传输到从库。
- T_从库回放从库执行 binlog 中 SQL 的时间。
- 延迟窗口三者叠加,通常为几十毫秒。
高可用架构
深夜,总馆突然电路起火,整个借阅登记系统瘫了。好在馆长早有预案:对面大楼里设了一个“备用总馆”,平时同步着总馆的全部数据。火灾发生后,监控系统 30 秒内自动探测到总馆不可用,立刻把备用总馆提升为新主馆,读者几乎无感知地继续借书——这就是故障转移。
高可用架构的核心是“消除单点”。双主互备(Master-Master)让两台机器互为主备,任一宕机另一台立即接管;更常见的是“一主多从 + 自动选主”,借助 ZooKeeper 或 Raft 协议从多个从库中选举出新主库,配合 VIP 漂移或配置中心实现应用层无感知切换。切换后从库自动重连新主库,继续同步数据。
备份与监控是高可用的另一条腿。备份策略通常是“全量+增量”:每周日全量备份,每天做 binlog 增量备份,既能恢复到任意时间点,又能缩短故障时数据丢失窗口。监控则盯住四个黄金指标:主从延迟、活跃连接数、慢查询数量、磁盘使用率——延迟突增往往预示着复制故障,连接数打满则是雪崩的前兆。
图书馆从火灾中毫发无损地站了起来,读者甚至不知道昨夜发生过意外。高可用架构的真正含义,是让系统故障从“事故”变成“演练”——用自动化的检测与切换,守住数据库作为数据底座的最后一道防线。
- MTBF(平均无故障时间)系统连续正常运行的平均时长。
- MTTR(平均修复时间)从故障发生到恢复服务的平均时长。
- 比值计算两者代入公式得到可用性。
- 目标逼近要达 99.99%,需把 MTTR 压到分钟级甚至秒级。
AI 应用场景
电商大促订单系统
阶段:数据存储与高并发访问
解决的问题:秒杀场景下订单表写入量激增,单库单表无法承受读写压力;商品详情页高频读取却只有一份数据。传统单库方案在 618、双11 期间必然雪崩。
具体案例:某电商平台将订单表按 user_id 水平分成 64 张表,写入压力分散到 8 个数据库节点;商品详情走读写分离,主库负责库存扣减,8 个从库承接详情页流量。大促压测时,下单链路 P99 延迟从 800ms 降到 120ms。
大模型特征存储服务
阶段:在线推理前的特征读取
解决的问题:推荐模型在线打分需在 50ms 内读取用户的上千维特征。特征表按 user_id 分片存储,避免单表过大导致查询超时;读多写少场景用读写分离把训练产出的特征异步同步到从库。
具体案例:某短视频推荐系统将用户特征表按 user_id 水平拆分为 16 片,主库接收特征更新任务,4 个从库承接每秒 10 万次的推理读取请求。特征读取 P99 稳定在 20ms 以内,支撑模型实时个性化推荐。
金融交易流水查询
阶段:合规风控与审计数据服务
解决的问题:交易流水表每天新增上亿行,监管要求按账号快速拉取流水。按 account_id 分片保证单账号数据落在同一分片,配合读写分离让审计查询走从库,不干扰在线交易写入。
具体案例:某支付平台把 3 年交易流水按 account_id 分为 32 个分片,风控引擎批量拉取用户流水进行反欺诈分析时,直接定位到对应分片并行查询,全量审计任务耗时从小时级降到分钟级。
Python 演示
import hashlibimport randomimport time class UserTable: """模拟 20 万行用户表的查找过程。""" def __init__(self, size=200000): self.size = size self.rows = list(range(size)) self.index = {} for position, user_id in enumerate(self.rows): self.index[user_id] = position def scan_lookup(self, user_id): """全表扫描:从头逐个比较,相当于没有索引。""" for position, current in enumerate(self.rows): if current == user_id: return position return -1 def index_lookup(self, user_id): """索引查找:字典相当于哈希索引,O(1) 定位。""" return self.index.get(user_id, -1) def shard_route(shard_key, shard_count): """分片路由:对分片键做稳定哈希后取模。""" digest = hashlib.md5(str(shard_key).encode("utf-8")).hexdigest() return int(digest, 16) % shard_count def bench_lookup(table, target_id, mode): start = time.perf_counter() if mode == "scan": pos = table.scan_lookup(target_id) else: pos = table.index_lookup(target_id) elapsed = (time.perf_counter() - start) * 1000 return pos, elapsed def main(): table = UserTable(200000) target = random.randint(0, table.size - 1) pos1, t1 = bench_lookup(table, target, "scan") pos2, t2 = bench_lookup(table, target, "index") print("全表扫描: 找到位置 %d, 耗时 %.2f ms" % (pos1, t1)) print("索引查找: 找到位置 %d, 耗时 %.2f ms" % (pos2, t2)) print("索引比全表扫描快 %.1f 倍" % (t1 / t2)) print("--- 分片路由演示 ---") for user_id in [1024, 65536, 199999]: node = shard_route(user_id, 8) print("用户 %d -> 分片 %d" % (user_id, node)) if __name__ == "__main__": main()
self.index = {}用 Python 字典模拟数据库索引,键是 user_id,值是行号。for position, current in enumerate(self.rows)全表扫描需要逐行比较,时间复杂度 O(n)。self.index.get(user_id, -1)索引查找直接按哈希定位,时间复杂度 O(1)。hashlib.md5(str(shard_key).encode("utf-8"))对分片键做稳定哈希,保证同一键每次路由结果一致。int(digest, 16) % shard_count取模运算得到分片编号,使数据均匀散列到各分片。time.perf_counter()高精度计时,对比两种查找方式的实际耗时差异。
常见误区
误区既然索引能加速查询,那就给所有常用字段都建上索引,反正多建几个也不吃亏。
正解索引像图书馆的目录卡片,书本身只有 2000 万册,目录卡片却有 100 套,书架空间被大量挤占。每次插入、更新、删除数据,都要同步维护每一棵 B+ 树,写性能会急剧下降。正确的做法是只给高频查询路径建索引,用 EXPLAIN 验证索引真的被用到。
为何常见:初学者只看到索引对读的加速,忽略了写放大和存储开销,以为“免费午餐”。
误区数据库查询慢,第一时间就想着分库分表,觉得这是大厂标配的终极方案。
正解绝大多数慢查询问题出在索引设计或 SQL 写法上,先做索引优化和 SQL 改写,再看是否需要缓存。分库分表是“大手术”:分片后跨片查询、分布式事务、全局 ID 都变得复杂。单表未超过 500 万行且写入压力不大时,分库分表弊大于利。
为何常见:看到技术社区里大厂分享分库分表实践,误以为这是性能优化的第一步,其实是他们最后一步。
误区主从复制是同步的,主库写入成功,从库马上就能查到这条数据。
正解主从复制要经历“主库写 binlog → 网络传输 → 从库回放”三步,通常有几十毫秒延迟,高并发下可能更长。用户刚下单就去从库查,很可能查不到。解决方法是关键读路由到主库,或用半同步复制缩短延迟窗口。
为何常见:从主库视角看写入已成功,容易忽视从库回放还需要时间,这是分布式系统“最终一致性”直觉盲区。
误区EXPLAIN 里出现全表扫描(type=ALL)就一定该骂,必须想方设法加索引消灭它。
正解如果表只有几百行数据,全表扫描只读几个数据页,比走索引还快——索引查找本身也有根节点到叶子节点的多次 I/O 开销。优化器会基于表行数和数据分布做代价估算,小表全表扫描反而是最优解。要关注的是“大表上的全表扫描”。
为何常见:教程里反复强调“避免全表扫描”,被当成绝对真理,忽略了代价模型这个前提。
误区只要把数据散到多个分片就行,选哪个字段当分片键差别不大。
正解分片键必须同时满足“高频查询条件”和“数据分布均匀”。按 user_id 分片后,所有用户维度的查询都亲和;但如果业务主要按 order_no 查,而表按 user_id 分片,每个查询都要广播到所有分片再聚合,性能比不分片还差。选错分片键是分库分表最大的返工原因。
为何常见:只从“写入能不能分散”角度考虑,忽略了查询路由的亲和性才是分片键的第一使命。
面试题
Q1. 关于 B+ 树索引,下列说法正确的是?
- A.非叶子节点也存储数据行
- B.所有数据都存在非叶子节点
- C.叶子节点链表相连可范围扫描← 正确答案
- D.B+ 树本质是哈希索引结构
答案:C。B+ 树所有数据都在叶子节点,非叶子只存键与指针,故 A、B 错;B+ 树是树形结构而非哈希,D 错。叶子链表相连让范围查询可沿链表顺序扫描,C 正确,如同图书馆卡片目录按编号串成一串,连续取书不用回目录柜。
Q2. 数据库“慢查询日志”记录什么?
- A.慢 SQL 语句的备份文件
- B.执行超时阈值的 SQL 记录← 正确答案
- C.数据库服务的错误日志
- D.所有用户的操作记录
答案:B。慢查询日志记录执行时间超过 long_query_time 阈值的 SQL 语句,是性能优化的第一手线索。它不备份 SQL、不记录错误、也不记录所有操作,B 正确。就像图书馆的“找书超时登记本”,只记那些异常慢的找书过程。
Q3. 联合索引 (a,b,c) 中,查询 WHERE a=1 AND c=2 的效果?
- A.完全无法利用该索引
- B.只能利用索引的 a 列← 正确答案
- C.索引的 a 和 c 列都用
- D.索引全部列都能使用
答案:B。最左前缀原则要求从最左边开始连续匹配。a=1 能走索引,但 c=2 前面跳过了 b 列,无法继续匹配,因此只能用 a 列。如同查字典必须从首字母开始,跳级查询是查不到的。
Q4. 分片数从 8 扩展到 16,旧数据如何?
- A.取模函数自动适应正确
- B.需要重新路由分布数据← 正确答案
- C.只把新增数据写入新片
- D.将所有数据复制到全部分片
答案:B。取模路由是 hash(key) % N,N 从 8 变 16 后,几乎所有数据的余数都变了,必须重新计算并迁移旧数据(resharding)。这是分库分表扩容时最复杂的工程问题,常见解法是双写迁移加校验。
Q5. 主从复制延迟会造成什么?
- A.主库的写入立刻失败
- B.从库可能会读到旧数据← 正确答案
- C.从库进程自动崩溃退出
- D.主库拒绝所有的读请求
答案:B。binlog 从主库传输到从库并回放需要时间,这个窗口内读从库会读到旧数据,B 正确。主从延迟不影响主库写入,也不会让从库崩溃,更不会影响主库读服务。想象新书上架有时间差,早到的读者看不到最新的书。
Q6. 覆盖索引的最大优势是什么?
- A.显著减少索引占空间
- B.查询无需回表取整行← 正确答案
- C.防止索引被意外删除
- D.索引可自动修复数据
答案:B。覆盖索引指查询所需列全部在索引树中,查完索引直接返回,省去回表取完整行的磁盘 I/O。它并不减少索引空间,也不涉及索引删除保护或数据修复。如同目录卡片上直接印着摘要,读者不必再去书架搬书。
Q7. 垂直拆分与水平拆分的区别?
- A.垂直按行拆分水平按列
- B.垂直按列分水平按行分← 正确答案
- C.两种拆分概念完全相同
- D.垂直拆分无需分片键
答案:B。垂直拆分是把一张宽表按业务拆成多张窄表(按列拆),减少单行宽度;水平拆分是把大数据量按分片键散列到多张同构表(按行拆)。两者完全不同,垂直拆分也涉及业务边界设计。
Q8. 哪些情况会导致索引失效?
- A.对索引列使用函数运算← 正确答案
- B.发生隐式类型转换时← 正确答案
- C.不满足最左前缀匹配← 正确答案
- D.使用 SELECT * 查询时
答案:ABC。对索引列用函数(如 DATE(create_time))、隐式类型转换(如 varchar 列与数字比较)、不满足最左前缀,都会让优化器放弃索引。SELECT * 不会导致索引失效,只是可能增加回表和网络传输开销。
Q9. 分库分表后要面对哪些新问题?
- A.跨分片的 JOIN 很难做← 正确答案
- B.分布式事务复杂度上升← 正确答案
- C.全局唯一 ID 需要方案← 正确答案
- D.所有 SQL 无需任何改动
答案:ABC。数据分散后,跨分片 JOIN 需要应用层聚合,分布式事务需要引入 Seata 等方案,自增主键在各分片会重复、必须用雪花算法等方式生成全局唯一 ID。SQL 必须改造以支持分片键路由,D 错误——中间件只是让改造更透明,并非零改动。
Q10. 主从高可用方案通常包含?
- A.故障检测与主从切换← 正确答案
- B.定期备份与恢复演练← 正确答案
- C.主从延迟与连接数监控← 正确答案
- D.从库禁止承接读请求
答案:ABC。高可用需要故障检测与自动选主、备份与恢复演练、延迟和连接数监控三要素。从库承接读流量正是读写分离的核心,不仅能用,还应做负载均衡,D 错误。它让主库专注写,从库分摊读,整体可用性更高。