想象你是一座城市中心图书馆的馆长。五年前,馆里只有一万册藏书,几位管理员就能轻松应对所有读者的借阅需求。可是今天,藏书已突破五百万册,目录卡片堆满了整面墙,每天来借书的读者排成长龙,更麻烦的是——新书还在以每天几千册的速度源源不断运进来。 一开始你加书架、加管理员,还能勉强撑住。但渐渐地发现:书架加得再多,总有空间浪费,有的区域却拥挤到无法转身;管理员招得再多,查询书目的队伍依然一眼望不到头。真正的解法不是继续在原馆里修修补补,而是像现代图书馆那样做两件事:第一,把藏书拆分到多个专业分馆——这就是“分库分表”;第二,把热门书的副本分散到借阅区,让读者不必每次都挤进藏书库——这就是“读写分离”。理解了这两个动作,你就抓住了高并发数据库架构优化的核心智慧。

岗位关联

掌握分库分表与读写分离,是数据库成为业务瓶颈时最有力的架构解药。它能帮助你在高并发场景下做出正确的数据架构决策,也是高级开发与架构师面试的必考项。

目录
  1. 可视化演示
  2. 数据库瓶颈分析
  3. 分库分表策略
  4. 数据路由策略
  5. 读写分离架构
  6. 主从延迟处理
  7. 数据一致性保障
  8. 架构演进实践
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示水平分片数据分布、读写分离流量路由及三阶段路由流程,支持步进切换与分片数交互。

本节配有交互动效(进入视口后自动加载,约 3KB)。

水平分片数据分布 按分片键Hash值路由到对应分片 每个分片独立存储部分数据 shard = hash(key) % N 数据均匀分布到N个分片实现水平扩展

数据库瓶颈分析

想象你掌管的图书馆刚开业时只有五万册藏书,两个书架、四个柜台就够用了。如今藏书涨到五百万册,书架挨着书架,过道挤得人侧身走,目录柜排到天花板,新书入库时管理员甚至找不到空位。单座图书馆的容量、通道、人力都有天花板,数据库单实例也一样。

数据库的“天花板”主要体现在三个维度。第一是存储容量:磁盘空间再大也有上限,而且单表超过千万级后查询性能急剧劣化;第二是连接数:数据库连接池默认只有一两百个连接,并发高峰时请求在连接池外排队,TPS 被死死卡住;第三是磁盘 IO:所有读写在单块盘上排队,IO 打满后 SQL 延迟肉眼可见地上升。这三个维度就是单库单表的局限。

单库单表在业务初期非常美好——事务简单、SQL 直观、运维省心。但当订单表过亿、QPS 达到数千时,慢查询变多、锁冲突加剧、备份时间以小时计算。此时再优秀的索引、再多的缓存也难以挽回,因为瓶颈已经是实例本身的物理上限。数据库的最大 QPS 可以用一个粗略公式估算:

图书馆承载不下时,馆长的第一反应不该是继续加书架,而是规划分馆。数据库也一样——在 SQL 优化、加缓存都做完之后,“拆”,往往是通往更高并发的那扇门。

Pmax≈C×1000TavgP_{max} \approx \dfrac{C \times 1000}{T_{avg}}
其中 C 是数据库连接数上限,T_avg 是平均每个查询的耗时(毫秒)。它描述了单个数据库实例在理想情况下每秒最多能处理的请求数。
  1. C(连接数上限)数据库最多能同时服务的连接数量,相当于图书馆同时开放的服务窗口数。
    C=200C = 200
  2. T_avg(平均查询耗时)每个请求占用服务窗口的时间,单位是毫秒,相当于每个读者办理借书手续花费的时间。
    Tavg=50 msT_{avg} = 50 \text{ ms}
  3. 1000 换算系数把毫秒换算成秒,得出每秒最多能完成多少个请求。
    Pmax=200×100050=4000 QPSP_{max} = \dfrac{200 \times 1000}{50} = 4000 \text{ QPS}

分库分表策略

图书馆的破局方案很朴素——建专业分馆:人文馆、科学馆、历史馆各自独立成楼,这叫“垂直分库”。如果科学馆的藏书又塞满了,就按出版年份再分成三个房间,这叫“水平分库”。如果某类书在房间内还嫌乱,就细分书架、分开登记,这叫“分表”。

分库分表本质上也是两类拆分法。垂直分库按业务拆分:把用户、订单、商品分别放到不同的数据库实例;水平分库按某个列的值把数据散列到多个数据库。垂直分表是把一张宽表按列拆成多张窄表(例如把大字段拆到扩展表);水平分表是把一张超过千万级的大表按某个键拆成多张小表。

落地时一般先做垂直拆分(按业务域),再做水平拆分(针对数据量最大的表)。分表数量要基于数据增长预估:当前容量、年增长率、规划年限。分表键的选择至关重要,通常选用户 ID、订单 ID 等访问最频繁的列;如果选错键,后续按其他维度的查询就会跨表扫全量,性能断崖式下跌。

图书馆有了分馆和细分书架后,每个房间的压力都变得均匀。数据库分库分表后也如此——每个分片容量可控、并发压力分散。但拆完会带来新问题:读者怎么知道书在哪个房间?这就需要检索台了。

Ntable=⌈D0×(1+g)ySmax⌉N_{table} = \left\lceil \dfrac{D_0 \times (1 + g)^y}{S_{max}} \right\rceil
这是估算分表数量的公式:把当前数据量按年增长率放大到规划年限,再除以单表容量上限,向上取整。
  1. D_0(当前数据量)今天表里的总行数,例如 1 亿行订单。
    D0=108D_0 = 10^8
  2. g(年增长率)数据每年增长的倍数,假设每年翻一倍,则 g = 1.0。
    g=1.0g = 1.0
  3. y(规划年限)要为未来 3 年预留空间。
    y=3y = 3
  4. S_max(单表容量上限)单表超过 5000 万行后性能开始下滑,取上限值。
    Smax=5×107S_{max} = 5 \times 10^7
  5. 向上取整结果是小数时向上取整,得到最终分表数。
    Ntable=⌈108×(1+1)35×107⌉=16N_{table} = \left\lceil \dfrac{10^8 \times (1 + 1)^3}{5 \times 10^7} \right\rceil = 16

数据路由策略

图书馆建好多个分馆后,在大门设了总检索台。读者一来,检索员输入书名,三秒钟告诉对方“这本书在 C 馆 3 号书架”。这个检索台的决定规则,就是数据路由。

分库分表后,应用写入或查询每一条数据,都必须根据主键算出它落在哪个库哪张表。最常见的三种路由方式是哈希取模、范围路由和一致性哈希。哈希取模是 hash(分片键) % 分片数,实现简单、分布均匀,缺点是扩容时取模基数变化,几乎全部数据要迁移;范围路由按主键区间分片(如 ID 1~1000 在库 0),天然支持范围查询,但可能产生热点分片;一致性哈希把哈希值空间组织成环,数据顺时针找最近的节点,新增节点只影响邻近数据,迁移量大幅减少。生产中常用中间件(如 ShardingSphere、MyCat)封装路由逻辑,并维护一份路由表。

检索台有了规则,读者不会跑错分馆。但新问题又来了:热门书所有人都想看,总馆藏书库只备一本母本,借阅压力依然巨大。于是图书馆在每个分馆划出借阅区,把副本直接放在那里供人翻阅——读写分离的萌芽出现了。

shard_id=HASH(shard_key) mod Nshard\_id = HASH(shard\_key) \bmod N
这是哈希取模路由的核心公式:对分片键做哈希得到一个整数,再对分片数取余,余数就是目标分库编号。
  1. HASH(shard_key)对分片键做一次均匀的哈希散列,输出一个大整数。不同输入对应不同输出,保证数据散得开。
    HASH("user_1001")=83419249HASH(\text{"user\_1001"}) = 83419249
  2. N(分片数)当前一共规划了几个分库,例如 4 个。
    N=4N = 4
  3. 取模运算用哈希值除以分片数取余数,余数就是数据的归属分库编号,范围在 0 到 N-1 之间。
    83419249 mod 4=1⇒数据落在库 183419249 \bmod 4 = 1 \Rightarrow \text{数据落在库 1}

读写分离架构

图书馆发现,大部分读者来是为了“查一查这段内容”“翻一翻目录”,未必真要借走书。于是管理处在各分馆设置开放借阅区,把母本复制成多份副本摆在那里;母本锁进藏书库,只有真正需要修改时才动。这就是“读写分离”。

数据库的读写分离架构完全对应:主库(Master)负责处理写请求——插入、更新、删除;从库(Slave)负责读请求——查询。主库通过 binlog 把数据变更以日志形式传输给从库,从库重放日志完成同步。应用层通过数据源路由(如 Spring 的 AbstractRoutingDataSource、ShardingSphere 读写分离功能)把写请求强制发到主库,读请求按负载均衡策略分发到多个从库。连接池也按读写分离,避免混用导致主库连接被读耗尽。

读写分离最大的价值是读容量横向扩展:从库不够用,加一台只读实例即可,对现有代码几乎透明。整体吞吐可以近似用下面的公式描述:

借阅区分流了绝大多数查询压力,藏书库专心管登记。但新书入馆后,从母本复制到借阅区需要时间——在副本上架前,读者在借阅区找不到新书,这就是主从同步延迟。

Ttotal≈Twrite+Nslave×TreadT_{total} \approx T_{write} + N_{slave} \times T_{read}
系统总吞吐约等于主库写吞吐,加上所有从库读吞吐的总和。从库越多,读能力越强。
  1. T_write(写吞吐)主库每秒最多处理的写请求数,例如 2000。
    Twrite=2000T_{write} = 2000
  2. T_read(单从库读吞吐)一个从库每秒能处理的读请求数,例如 5000。
    Tread=5000T_{read} = 5000
  3. N_slave(从库数量)架构中挂载的从库实例数,例如 3 台。
    Nslave=3N_{slave} = 3
  4. 总吞吐计算写吞吐加上所有从库读吞吐之和。
    Ttotal≈2000+3×5000=17000 QPST_{total} \approx 2000 + 3 \times 5000 = 17000 \text{ QPS}

主从延迟处理

今天图书馆新入库一百本《AI 时代》,母本在总馆完成登记,但借阅区副本还在打印室装订,要半小时后才能上架。一位读者跑去借阅区找,管理员查系统显示“有库存”,书架上却没有——这就是延迟造成的“幽灵库存”。

主从延迟在技术上是必然的。MySQL 主库写 binlog、传输到从库、从库 SQL 线程重放,三步都有耗时。高峰期主库写入量大、网络带宽有限,延迟会从几十毫秒飙升到几秒。延迟期间从库读到旧数据,会造成业务事故,例如用户下单后立刻查订单显示“不存在”。常见应对思路有四类:强制读主——对一致性敏感的业务(订单详情、支付结果)加路由标识,强制走主库;延迟补偿——应用对“刚写完”的数据加一个短时缓存标记;二次读兜底——从库查不到再到主库查一次;业务容忍——前端提示“支付成功后稍等几秒再刷新”。

图书馆的解法不是取消副本,而是让“刚入库”的新书先在总馆柜台借出(强制读主),并告诉读者副本上架的大致时间。当藏书登记和借阅流通的账目不一致时,如何保证最终正确?这就轮到数据一致性登场了。

Ldelay=BbacklogRsync+LnetworkL_{delay} = \dfrac{B_{backlog}}{R_{sync}} + L_{network}
主从延迟的估算公式:延迟时间约等于主库积压的日志量除以同步速率,再加上固定的网络传播延迟。
  1. B_backlog(积压日志量)主库 binlog 因网络或 IO 问题还没传完的数据量,例如 20 MB。
    Bbacklog=20 MBB_{backlog} = 20 \text{ MB}
  2. R_sync(同步速率)主从之间每秒传输多少日志,例如 50 MB/s。
    Rsync=50 MB/sR_{sync} = 50 \text{ MB/s}
  3. L_network(网络延迟)数据包在网络上传输固定的耗时,例如 5 ms。
    Lnetwork=5 msL_{network} = 5 \text{ ms}
  4. 总延迟计算积压量除以速率(换算成毫秒)再加网络延迟。
    Ldelay=2050×1000+5=405 msL_{delay} = \dfrac{20}{50} \times 1000 + 5 = 405 \text{ ms}

数据一致性保障

总馆的登记系统显示某本在馆,借阅区副本却还没上架;另一个分馆的书已借出,总馆系统仍显示在馆。图书馆每天闭馆后做一次盘点,把登记册与实际馆藏对照修正,保证第二天开馆时账实相符。这种“最终对得上”的机制就是最终一致性。

分库分表之后,单库的 ACID 事务被拆散了:一个订单要写订单库、用户库、库存库,事务不再由一个数据库搞定。业界常用方案包括:分布式事务——2PC 两阶段提交、TCC 补偿事务、SAGA 长事务;本地消息表 + 消息队列实现最终一致;同时,分库后自增主键必然冲突,必须用全局唯一 ID,最经典的是雪花算法(Snowflake)。它把 64 位 ID 拆成:1 位符号 + 41 位毫秒时间戳 + 10 位机器 ID + 12 位序列号,每毫秒每台机器可生成 4096 个不重复 ID。此外,跨库 JOIN 已不可行,常用做法是业务层多次查询再内存聚合,或做宽表冗余。

图书馆通过每日盘点让账实最终一致,分库系统通过分布式事务、消息补偿和全局唯一 ID 实现最终一致。架构落地的最后一步,也是最容易被忽视的一步:怎样把一个运行中的图书馆改造成分馆体系而不闭馆?

ID=(Tstamp−Tbase)×222+Mid×212+SseqID = (T_{stamp} - T_{base}) \times 2^{22} + M_{id} \times 2^{12} + S_{seq}
这是雪花算法生成全局唯一 ID 的核心公式:时间戳部分、机器 ID、序列号三段拼接成一个整数。
  1. 时间戳差值当前毫秒时间戳减去基准时间,得到一个单调递增的相对时间值,再左移 22 位腾出空间。
    (Tstamp−Tbase)×222(T_{stamp} - T_{base}) \times 2^{22}
  2. 机器 ID(10 位)每台机器分配一个唯一编号(0~1023),左移 12 位放入中间段。
    Mid×212M_{id} \times 2^{12}
  3. 序列号(12 位)同一毫秒内从 0 递增到 4095,满了就等待下一毫秒。
    Sseq∈[0,4095]S_{seq} \in [0, 4095]
  4. 三段相加时间戳段加机器段加序列段,拼成一个全局唯一的 64 位整数。
    ID=时间戳段+机器段+序列段ID = \text{时间戳段} + \text{机器段} + \text{序列段}

架构演进实践

图书馆改造最大的难题不是设计图纸,而是不能闭馆。工程队采用“边营业边改造”的方案:先给所有图书贴上新编号,每天夜里搬一批到新分馆,白天新旧目录并行查询;搬完一批切换一批。这个过程中的双写、校验、逐步切流,就是架构演进的工程智慧。

分库分表的落地同样要平滑迁移。经典流程:先做双写(新库老库同时写入),再同步历史数据,用对账任务校验两边数据一致,然后把读请求按比例逐步切到新库,最后下线老库。整个过程业务无感知。扩容是绕不开的难题:从 4 个分片扩到 8 个,哈希取模的 N 变了,几乎一半数据要重新映射。可选方案有停服迁移(数据量小时可用)、双写迁移(不停机),以及一致性哈希——让扩容只影响邻近 1/N 数据。另一个实用技巧是“一次性到位”:现在只需要 8 个分片,但逻辑上直接建 64 个槽位,扩容时只需搬动数据到新槽位,避免全量重算。监控同样关键,主从延迟、慢 SQL、分片倾斜、连接池水位必须钉在告警大盘上。

图书馆的渐进式改造最终让读者几乎无感地走进了新分馆体系。分库分表与读写分离的架构优化同样是一场精细工程,它需要平衡成本、一致性与运维复杂度。理解了这套从拆到路由、从读写分离到演进落地的完整链路,你就能在数据库成为瓶颈时,像城市规划师一样从容地画出新蓝图。

Mmigrate=(1−NoldNnew)×DtotalM_{migrate} = \left(1 - \dfrac{N_{old}}{N_{new}}\right) \times D_{total}
扩容迁移量估算公式:扩容后大约有多大比例的数据需要迁移,乘以总数据量得到迁移规模。
  1. N_old / N_new(扩建前后分片数)例如从 4 个分片扩到 8 个。
    NoldNnew=48=0.5\dfrac{N_{old}}{N_{new}} = \dfrac{4}{8} = 0.5
  2. 1 减旧新比取模基数变化后,约一半数据会落入不同分片,这个差值就是迁移比例。
    1−0.5=0.51 - 0.5 = 0.5
  3. D_total(总数据量)当前全部数据量,例如 100 GB。
    Dtotal=100 GBD_{total} = 100 \text{ GB}
  4. 迁移量计算迁移比例乘以总数据量,得到需要迁移的数据规模。
    Mmigrate=0.5×100=50 GBM_{migrate} = 0.5 \times 100 = 50 \text{ GB}

AI 应用场景

推荐系统用户行为日志存储

阶段:数据采集与存储

解决的问题:AI 推荐系统每天要接收上亿条用户点击、曝光行为日志。传统单库 MySQL 写入几千万行后性能直线下降,离线特征查询也极慢。按 user_id 哈希分库分表后,写入并发提升一个数量级;配合读写分离,离线训练数据的读取从从库拉取,完全不影响在线日志写入。

具体案例:某电商推荐团队把用户行为表按 user_id 分成 128 张表,并挂载 4 个只读从库。日志写入 TPS 从 3000 提升到 3 万以上,特征抽取任务的 SQL 查询全部走从库,再也没有出现慢查询阻塞主库写入的情况。

金融交易系统的实时风控特征查询

阶段:模型特征在线存储与读取

解决的问题:金融风控模型需要秒级查询近半年的交易流水来生成实时特征。单库无法承载亿级流水,按 account_id 分库后,特征查询命中单个分片;配合读写分离,风控在线查询走只读副本,保障实时性也保护主库写入稳定性。

具体案例:某支付公司把交易流水表按账号哈希分到 64 个库,加上 8 个只读从属库,交易特征查询从平均 800ms 降到 30ms,AI 风控模型的特征获取提速超过 26 倍,大促期间未再出现数据库瓶颈导致的拒单。

AIGC 产品的会话记录存储

阶段:应用数据持久化

解决的问题:AIGC 应用的用户会话历史、生成记录量级增长快,且读多写少(用户回顾历史会话多,写入一次对话多)。把会话表按 user_id 分库分表,同时做主从分离,用户读取历史会话走从库,写入走主库,显著降低单库压力。

具体案例:某 AI 写作工具上线三个月积累了 2 亿条会话记录,采用 32 库 × 4 从库架构后,历史会话查询 P99 延迟稳定在 120ms 以内,同时支撑了 10 万 QPS 的峰值读取流量。

Python 演示

python99 行
# -*- coding: utf-8 -*-"""模拟分库分表与读写分离的核心机制"""import hashlibimport threadingimport timeimport random  class Shard:    """分库节点基类"""    def __init__(self, name):        self.name = name        self.store = {}        self.lock = threading.Lock()     def write(self, key, value):        with self.lock:            self.store[key] = value     def read(self, key):        with self.lock:            return self.store.get(key)  class PrimaryShard(Shard):    """主分库: 写后异步广播到从库"""    def __init__(self, name):        super().__init__(name)        self.replicas = []     def write(self, key, value):        super().write(key, value)        for replica in self.replicas:            t = threading.Thread(target=self._sync, args=(replica, key, value))            t.start()     def _sync(self, replica, key, value):        time.sleep(0.3)  # 模拟主从同步延迟        replica.write(key, value)  class ReplicaShard(Shard):    """从分库: 只读"""    pass  def route_key(key, shard_count):    """哈希取模路由"""    digest = hashlib.md5(key.encode("utf-8")).hexdigest()    return int(digest, 16) % shard_count  class Router:    """读写分离路由器"""    def __init__(self, shards):        self.shards = shards     def write(self, key, value):        idx = route_key(key, len(self.shards))        if not isinstance(self.shards[idx], PrimaryShard):            raise RuntimeError("路由到非主节点")        self.shards[idx].write(key, value)        return idx     def read(self, key):        idx = route_key(key, len(self.shards))        primary = self.shards[idx]        if primary.replicas and random.random() < 0.7:            replica = random.choice(primary.replicas)            return replica.read(key)        return primary.read(key)  def main():    shards = []    for i in range(2):        primary = PrimaryShard("主库%d" % i)        for j in range(2):            primary.replicas.append(ReplicaShard("从库%d_%d" % (i, j)))        shards.append(primary)     router = Router(shards)    keys = ["user_%d" % i for i in range(8)]    print("===== 模拟分库分表路由 =====")    for k in keys:        idx = router.write(k, {"value": 1})        print("写入 %s -> 分片%d" % (k, idx))     print("===== 模拟主从同步延迟 =====")    val = router.read("user_0")    if val is None:        print("读 user_0 -> 从库尚未同步, 读到 None")    time.sleep(0.5)    val = router.read("user_0")    print("延迟后再读 user_0 ->", val)  if __name__ == "__main__":    main()
目的:用 Python 模拟对 8 个用户数据的哈希路由写入、读写分离读取,并演示主从延迟导致从库暂时读不到新数据的现象。
  1. digest = hashlib.md5(key.encode("utf-8")).hexdigest()对分片键做哈希得到十六进制摘要,作为均匀散列的基础。
  2. int(digest, 16) % shard_count取模得到分片编号,对应正文中的 hash(key) % N 路由公式。
  3. class PrimaryShard(Shard)主分库继承基类并扩展复制能力,对应架构中“主库负责写并同步到从库”。
  4. time.sleep(0.3)刻意模拟 300ms 主从同步延迟,让“写后立即读从库可能读不到”的问题真实发生。
  5. if primary.replicas and random.random() < 0.7模拟读写分离:70% 的读请求走从库,30% 读主库兜底。

常见误区

误区分库分表只是把数据拆开了,我还能像以前一样在一个事务里更新多个库的表,出了错一起回滚。

正解分库分表后,跨库事务不再是单库的强 ACID。比如订单写订单库、用户写用户库,一个事务跨两个数据库实例,单库事务机制管不到另一个库。必须引入分布式事务(TCC、SAGA、本地消息表),或者从设计上尽量避免跨库事务。就像图书馆拆成两个分馆后,在 A 馆借书、B 馆还书,总馆没法用一个登记本同时锁住两边,只能靠事后对账(最终一致性)。

为何常见:单库事务用得太顺手,潜意识觉得“事务是数据库自带的”,忽略了分库后数据库已经变成多个了。

误区主库一写,从库就应该立刻同步,我刚提交的数据马上能在从库读到。

正解MySQL 主从复制默认是异步的,主库事务提交成功不等于从库已收到日志。高峰期延迟可达秒级。架构上必须有“容忍短时间不一致”的策略:一致性敏感的操作强制走主库,普通列表查询才走从库。这就像图书馆借阅区副本总比总馆晚几小时上架,这不是 Bug,而是物理现实。

为何常见:大家习惯了单库“读到的永远是刚提交的数据”,误以为集群是同一时刻的镜像。

误区只要数据库慢了就分库分表,拆得越细越快,一劳永逸。

正解分库分表带来的是容量和并发上限的提升,但代价巨大:跨库查询、分布式事务、全局 ID、数据迁移、运维复杂度都会剧增。如果数据量不到千万、QPS 只有几百,先优化索引、缓存、SQL 反而见效更快。架构师的智慧是“先做减法再做拆分”。就像图书馆只有一万本书时,先整理书架和目录,而不是急着盖六座分馆。

为何常见:很多技术文章把分库分表吹成高并发银弹,让人误以为拆了就万事大吉。

误区分库分表后,每张表还是可以用 AUTO_INCREMENT,反正各自表里流水号不重复就行。

正解多个库各自自增必然产生重复主键。比如库 1 和库 2 的第一条订单 ID 都是 1,合并查询时直接冲突。必须用全局唯一 ID:雪花算法、UUID、号段模式等。就像分馆内各自给书架编号,两座分馆的"A-001"并不是同一本书,必须在封面上加一个全局唯一的 ISBN。

为何常见:惯性思维,自增 ID 用得顺手,没意识到它只在单库语境下才唯一。

面试题

Q1. 读多写少场景下,哪种架构最能分散查询压力?

单选题 · easy

  • A.垂直分库拆分业务
  • B.增加只读从库分流← 正确答案
  • C.水平分表拆分数据
  • D.升级数据库服务器

答案:B。读写分离的核心就是让从库分担读请求,直接提升读容量。水平分表和垂直分库主要解决存储与写并发问题,而升级服务器受单机物理上限约束。就像图书馆在借阅区加副本书架,是对付“查书人多”最直接的办法。

Q2. 分库分表的核心目标是什么?

单选题 · easy

  • A.提高单条 SQL 执行速度
  • B.突破单库容量与连接上限← 正确答案
  • C.完全替代 Redis 缓存
  • D.简化 SQL 开发和运维

答案:B。分库分表解决的是容量和并发上限问题,数据一拆多份,连接与 IO 分散到多个实例。单条 SQL 提速靠索引和缓存,分库分表反而让 SQL 复杂度上升、运维更难,所以不是简化开发。就像图书馆建分馆是为了装下更多书,而不是让查一本书更快。

Q3. 现在有一张自增主键的订单表要分库,最应该选的拆分键是?

单选题 · easy

  • A.订单创建时间字段
  • B.自增订单 ID 字段
  • C.购买用户 ID 字段← 正确答案
  • D.商品唯一编码字段

答案:C。拆分键应选业务访问最频繁、分布最均匀的字段。订单查询通常按用户查(“我的订单”),按 user_id 路由能让一个用户的订单落在同一分片,避免跨库聚合。自增主键无法全局唯一,时间字段易产生热点,商品编码无法覆盖按用户查询的路径。这就像检索台按读者证号找借阅记录,比按流水号找更贴合需求。

Q4. 主从复制采用异步模式,业务上最常用的延迟应对手段是?

单选题 · mid

  • A.所有查询统一走从库
  • B.高一致操作强制走主库← 正确答案
  • C.关闭主库的 binlog 日志
  • D.让从库定期重启清理

答案:B。一致性敏感的操作(支付结果、订单详情)路由到主库读取,能完全规避延迟窗口;从库只承担可容忍旧数据的读。A 会放大延迟影响,C 会让从库彻底无法同步,D 属于拆东墙补西墙。这就像图书馆把“刚入库的新书”放在总馆柜台,急需的读者直接去总馆借。

Q5. 关于哈希取模路由 hash(key) % N,下列说法正确的有?

多选题 · mid

  • A.数据分布通常比较均匀← 正确答案
  • B.扩容时只需迁移较少数据
  • C.取模结果随 N 变化而变化← 正确答案
  • D.适合范围查询业务场景

答案:AC。哈希取模可以把数据散得比较均匀,但 N 一旦改变,几乎所有数据都要重新取模并迁移,所以 B 错;范围查询在哈希路由下通常会跨多个分片,D 错。就像图书按作者名哈希分配到分馆,均匀是均匀了,但一旦增加分馆,所有书的归位都要重新算。

Q6. 分布式事务最终一致性方案中,本地消息表的核心思路是?

单选题 · mid

  • A.用一个共享表存所有库的事务日志
  • B.本地事务写业务表和消息表← 正确答案
  • C.每个库独立提交但用全局锁串行
  • D.应用层毫秒轮询强制所有提交

答案:B。本地消息表把业务操作与消息写入放在同一个本地事务中,之后由异步任务投递消息,下游消费成功后再确认。即使下游暂时失败,消息还在表里可以重试。这是用“先记账后送达”的方式换取最终一致,而不是用全局锁或轮询强行同步。

Q7. 分库分表后,会面临哪些新问题?

多选题 · mid

  • A.跨库 JOIN 变得很困难← 正确答案
  • B.自增主键可能产生重复← 正确答案
  • C.单表索引失效问题
  • D.SQL 注入风险降低

答案:AB。分库后跨库 JOIN 需要业务层多次查询再聚合,自增主键在不同库各自递增必然重复,所以需要雪花算法等全局唯一 ID。单表索引是否失效与分库无关,SQL 注入风险也不会因为分库而降低。就像图书馆分成多个馆后,“跨馆查资料”和“编号重号”成了新难题。

Q8. 哈希取模分片从 4 片扩到 8 片,平均需要迁移多少比例数据?

单选题 · hard

  • A.大约 25% 的数据
  • B.大约 50% 的数据← 正确答案
  • C.大约 75% 的数据
  • D.几乎全部的数据

答案:B。取模从 4 变为 8,原 hash(key)%4 相同的 key,在新的 hash(key)%8 规则下约一半会落入不同分片。整体迁移比例约为 1 - (旧片数/新片数) = 50%。所以扩容前必须规划迁移方案,或使用一致性哈希降低迁移量。这就像图书馆从 4 个房间改成 8 个房间后,大约一半的书要重新挪位置。

Q9. 雪花算法中,同一毫秒同一台机器最多能生成多少不重复 ID?

单选题 · hard

  • A.约 1024 个
  • B.约 4096 个← 正确答案
  • C.约 419 万个
  • D.约 2 万亿个

答案:B。雪花算法 64 位中序列号占 12 位,同毫秒同机器序列号从 0 递增到 4095,最多 4096 个,达到上限就等待下一毫秒。时间戳和机器 ID 只是标识维度,不决定数量上限。就像图书馆每分钟最多盖 4096 个采编章,盖满就要等下一分钟。

Q10. 关于主从延迟的工程应对方案,合理的有哪些?

多选题 · hard

  • A.强制读主路由标识← 正确答案
  • B.延迟补偿二次查询← 正确答案
  • C.关闭 binlog 求零延迟
  • D.从库加短时缓存标记← 正确答案

答案:ABD。强制读主、从库查不到再查主库、加短时缓存标记都是常用的延迟规避手段。关闭 binlog 会导致从库完全失去数据来源,绝不可行。这些方案就像图书馆为“刚入库新书”提供总馆柜台、二次检索和“半小时后可见”提示,属于多管齐下的工程取舍。