54. 数据库读写分离了解吗?
答:
读写分离是 MySQL 高可用和高并发架构中非常常见的一种方案。 它的核心思想是:
把“写请求”和“读请求”分发到不同的数据库节点上执行。
通常情况下:
- 主库(Master):负责写,也通常可以读
- 从库(Slave / Replica):主要负责读
这样就能把大量读流量从主库分摊出去,提高整体吞吐能力。
1)为什么需要读写分离?
在很多业务系统中,数据库的压力主要来自于读请求,而不是写请求。 比如:
- 电商商品详情页
- 用户信息查询
- 文章列表页
- 后台报表查询
这些场景通常“读多写少”。
如果所有请求都打到一台主库上,会带来问题:
- 主库压力过大
- 读写互相抢资源
- 响应时间变慢
- 可扩展性差
所以就引入读写分离:
- 写操作依然只走主库
- 读操作分发到一个或多个从库
2)读写分离的基本架构
常见架构是:
- 一主一从
- 一主多从
例如:
应用服务 | ----------------- | | 写请求 读请求 | | 主库 从库1 / 从库2 / 从库3主库通过主从复制把数据同步到从库。 这样应用可以:
- 写主库
- 读从库
3)读写分离的好处
(1)分担主库压力
尤其是查询量很大的系统,效果明显。
(2)提升整体吞吐量
多个从库可以并行承担大量读请求。
(3)提升可扩展性
当读压力继续增大时,可以增加更多从库。
(4)一定程度提升可用性
从库除了读,还可以在主库故障时用于切换或恢复。
4)读写分离适合什么场景?
适合:
- 读多写少
- 查询压力大
- 允许一定程度主从延迟
- 高并发读场景
例如:
- 内容平台
- 电商商品系统
- 社交信息流
- 后台查询报表
5)读写分离会带来什么问题?
它并不是没有代价,最典型的问题是:
主从延迟
主库写完之后,从库不是绝对实时同步。 如果刚写完马上去从库查,有可能查不到最新数据。
例如:
- 刚注册账号
- 紧接着查询账号信息
- 如果查从库,可能因为延迟而查不到
这就是“读写分离一致性问题”。
6)一句话总结
读写分离的核心,就是主库负责写,从库负责读,用空间和架构复杂度换更高的读并发能力。
55. 那读写分离的分配怎么实现呢?
答:
读写分离的关键不只是“有主从库”,更重要的是:
应用层怎么把写请求发到主库,把读请求发到从库。
常见实现方式有两种:
- 程序代码层实现
- 中间件层实现
1)程序代码层实现
这种方式是指在业务代码中自己判断:
- 当前 SQL 是读还是写
- 应该连接主库还是从库
例如:
insert/update/delete走主库select走从库
通常会在以下位置封装:
- DAO 层
- ORM 层
- 数据源路由层
- 连接池扩展层
优点
- 灵活
- 容易针对具体业务做特殊处理
- 不一定需要额外中间件
缺点
- 侵入业务代码
- 容易写错
- 维护成本高
- 复杂场景不好统一管理
2)中间件层实现
这种方式是引入数据库中间件,由中间件负责解析 SQL 并进行路由。
对应用来说,只连接一个中间件地址即可。 中间件内部再判断:
- 读请求发到从库
- 写请求发到主库
常见中间件包括:
- MyCat
- ShardingSphere
- Atlas(较老)
- ProxySQL
优点
- 对业务代码侵入小
- 路由规则统一管理
- 扩展性强
- 更适合大型系统
缺点
- 增加一层基础设施
- 运维复杂度上升
- 中间件本身也可能成为瓶颈或故障点
3)实际如何判断读写?
最简单的规则是:
SELECT走从库INSERT / UPDATE / DELETE / DDL走主库
但真正线上系统没这么简单,因为还要考虑:
(1)事务中的查询
如果一个事务里先写后读,这里的读通常也必须走主库,否则会有一致性问题。
(2)强一致性场景
例如:
- 注册后立即登录
- 下单后立即查订单
- 更新资料后立即查看详情
这些读请求常常也要走主库。
(3)从库延迟
如果从库延迟过大,需要临时切回主库。
4)常见工程化方案
方案一:框架自动路由
例如 Spring 中通过 AOP 或动态数据源,在读方法上打注解:
@ReadOnly自动走从库。
方案二:事务强制主库
只要在事务中,所有操作都走主库。
方案三:关键接口强制主库
对一致性要求高的接口,直接指定走主库。
5)一句话总结
读写分离的分配,本质上是“SQL 路由”问题,常见做法是代码层路由或中间件路由。
56. 主从复制原理了解吗?
答:
主从复制是 MySQL 高可用、读写分离的基础。 它的核心作用是:
把主库上的数据变更同步到从库。
这样从库就可以和主库保持大体一致,用于:
- 读写分离
- 数据备份
- 故障切换
- 灾备恢复
1)主从复制的核心流程
主从复制大致流程可以概括为:
- 主库写数据
- 主库记录 binlog
- 从库拉取 binlog
- 从库重放 binlog
- 从库数据同步更新
2)详细过程
第一步:主库执行写操作
比如主库执行了一条:
UPDATE orders SET status = 2 WHERE id = 1001;这条更新成功后,主库会把这次变更记录到 binlog 中。
第二步:主库把 binlog 提供给从库
当从库连接到主库时,主库会创建一个 dump 线程(也叫 binlog dump thread)。
这个线程负责:
- 读取主库 binlog
- 把 binlog 内容发送给从库
第三步:从库 IO 线程接收 binlog
从库上会有一个 IO 线程,它负责:
- 连接主库
- 接收主库推送的 binlog
- 把接收到的内容写入本地 relay log(中继日志)
所以你可以理解为:
- 主库 binlog 是原始变更日志
- 从库 relay log 是中转日志
第四步:从库 SQL 线程重放 relay log
从库还有一个 SQL 线程,负责:
- 读取 relay log
- 解析里面的事件
- 在从库上重新执行这些操作
这样从库就把主库上的变更“重演”了一遍,最终达到数据同步。
3)一句话串起来
主从复制可以概括为:
主库写 binlog -> 从库 IO 线程拉 binlog 写 relay log -> 从库 SQL 线程重放 relay log
4)为什么要 relay log?
因为从库不能一边接收主库日志一边直接执行,否则耦合太强。 引入 relay log 后,可以把“接收”和“执行”分离:
- IO 线程负责拉取
- SQL 线程负责执行
这样架构更清晰,也便于恢复和排错。
5)主从复制默认是同步的吗?
不是。 MySQL 常见主从复制默认是:
异步复制
也就是说:
- 主库事务提交成功
- 不需要等待从库一定执行完
这也是为什么会存在主从延迟。
后来也有:
- 半同步复制
- 组复制
- 更强一致性的集群方案
但经典主从复制大多数是异步。
6)主从复制的意义
(1)支持读写分离
从库承担读请求。
(2)支持高可用切换
主库挂了可以提升从库。
(3)支持备份和恢复
可以基于 binlog 和从库做恢复。
(4)支持灾备
异地从库可以用于容灾。
7)一句话总结
主从复制的本质,是主库把 binlog 传给从库,从库通过 relay log 重放这些变更,从而实现数据同步。
57. 主从同步延迟怎么处理?
答:
主从延迟是读写分离架构中最常见、最现实的问题之一。 它指的是:
主库上的数据已经更新成功,但从库还没来得及同步到最新状态。
这会导致:
- 主库查得到
- 从库查不到
- 或从库查到旧数据
对于一些强一致业务场景,这个问题非常关键。
1)为什么会发生主从延迟?
主从延迟的本质是:
主库写入速度 > 从库复制和重放速度
常见原因包括:
(1)主库写压力大
短时间大量更新、插入,binlog 积压快。
(2)从库执行能力不足
从库 CPU、IO、磁盘性能跟不上。
(3)慢 SQL / 大事务
从库在执行 relay log 中某些 SQL 时耗时很长,导致后续复制堆积。
(4)锁等待
从库回放过程中遇到锁竞争,也会拖慢复制速度。
(5)网络问题
主从之间网络波动、延迟大,也会影响同步。
2)延迟会带来什么问题?
最典型的问题就是:
写后读不一致
例如:
- 用户注册成功(写主库)
- 立即去查询用户信息(读从库)
- 从库还没同步到,结果查不到
这类问题在:
- 注册登录
- 下单查单
- 更新资料立即查看
- 支付状态查询
中都很常见。
3)怎么处理主从延迟?
常见方案有以下几种。
方案一:写后读走主库
对于刚刚发生写操作后立刻要读的场景,直接让读请求走主库。
例如:
- 注册后登录
- 下单后查订单
- 修改昵称后立刻刷新个人页
这是最常见、最稳妥的办法。
优点
- 强一致
- 实现简单
缺点
- 对主库压力更大
- 需要业务知道哪些读必须走主库
方案二:关键业务读写都走主库
对于强一致性要求高的业务,直接不做读写分离。
例如:
- 账户余额
- 支付状态
- 库存扣减
- 交易订单状态
这些业务宁可牺牲一点读扩展性,也要保证正确性。
方案三:读从库失败后回源主库
也叫“二次读取”。
思路是:
- 先查从库
- 如果查不到或版本不对
- 再回主库重查一次
优点
- 对业务层侵入相对小
- 大部分普通请求仍走从库
缺点
- 实现复杂
- 高峰期可能放大主库压力
- 需要对异常场景做细致处理
方案四:监控延迟并动态摘除落后从库
如果某个从库延迟太严重,可以临时不让它承担读流量。
常见做法:
- 监控 seconds_behind_master
- 超过阈值自动摘除
- 恢复后再重新加入
适合中间件或数据库代理层来做。
方案五:优化复制链路本身
从根本上减少延迟,包括:
(1)避免大事务
大事务会让从库重放时间很长。
(2)拆分慢 SQL
减少从库执行压力。
(3)提升从库配置
增加 CPU、磁盘、内存性能。
(4)使用并行复制
MySQL 新版本支持一定程度的并行复制,可提升从库 apply 速度。
方案六:使用更强一致性的复制方案
如果业务对一致性要求很高,可以考虑:
- 半同步复制
- MGR(MySQL Group Replication)
- Galera Cluster
- 分布式数据库
不过复杂度和成本也会显著提升。
4)实际系统中的常见策略
实际项目中常常是组合使用:
- 普通查询走从库
- 关键查询走主库
- 写后短时间内读主库
- 监控从库延迟
- 延迟过高时自动切回主库
5)一句话总结
主从延迟无法彻底消除,常见处理思路是“关键读走主库 + 监控延迟 + 优化复制性能”。
58. 你们一般是怎么分库的呢?
答:
分库就是把原来放在一个数据库实例里的数据,拆分到多个数据库实例中。 它通常是为了解决:
- 单库容量瓶颈
- 单库性能瓶颈
- 单机资源瓶颈
- 高并发访问压力
分库一般分为两大类:
- 垂直分库
- 水平分库
1)垂直分库
垂直分库是指:
按照业务模块,把不同的表拆到不同的数据库中。
例如一个电商系统原来所有表都在一个库里:
- 用户表
- 商品表
- 订单表
- 支付表
- 物流表
后来可以按业务拆成:
- 用户库
- 商品库
- 订单库
- 支付库
垂直分库的特点
优点
- 业务边界清晰
- 降低单库表数量
- 降低库级别资源竞争
- 更利于按业务独立扩展
缺点
- 跨库 join 变复杂
- 分布式事务问题出现
- 业务开发复杂度增加
2)水平分库
水平分库是指:
同一张表的数据,按某个规则拆到多个库中。
例如用户表原来所有用户都在一个库里,后来按用户 ID 哈希分到多个库:
- user_db_0
- user_db_1
- user_db_2
- user_db_3
每个库中都有一张用户表,但存的是不同范围/不同路由规则的数据。
水平分库的特点
优点
- 可以突破单库容量和性能瓶颈
- 更适合海量数据场景
缺点
- 路由复杂
- 跨库查询复杂
- 分布式事务更麻烦
- 运维和扩容成本更高
3)什么时候用垂直分库?什么时候用水平分库?
适合垂直分库的场景
- 系统业务模块很多
- 不同业务之间相对独立
- 单库里表太多
- 不同业务读写压力差异大
适合水平分库的场景
- 某一张表数据量特别大
- 单表 / 单库已经扛不住
- 单业务已经达到海量数据规模
通常真实系统里,往往是先:
- 垂直分库 再在某些热点业务里:
- 水平分库
4)实际项目中怎么做?
很多系统一开始单库就够用。 随着业务增长,常见演进路径是:
- 单库单表
- 垂直分库
- 热点表水平分库分表
- 中间件或自研路由
也就是说,分库不是一上来就做,而是随着规模增长逐步演进。
5)一句话总结
分库一般分垂直分库和水平分库:垂直分库按业务拆,水平分库按数据拆。
59. 那你们是怎么分表的?
答:
分表和分库类似,但粒度更细。 它是把原来的一张大表拆成多张小表,以解决:
- 单表数据量过大
- 索引膨胀
- 查询变慢
- 写入性能下降
分表一般也分为两类:
- 水平分表
- 垂直分表
1)水平分表
水平分表是指:
表结构完全相同,但每张表只存一部分数据。
例如原来只有一张订单表:
orders现在拆成:
orders_0orders_1orders_2orders_3
每张表结构一样,只是存的数据不同。
水平分表常见规则
- 按 ID 取模
- 按时间范围
- 按用户 ID 哈希
- 按业务路由表
适用场景
- 单表数据量太大
- 查询和写入压力集中在一张表
- 想降低单表索引高度和维护成本
2)垂直分表
垂直分表是指:
按照字段维度,把一张宽表拆成多张表。
通常会拆成:
- 主表(核心字段)
- 扩展表(不常用字段、大字段)
例如用户表:
原始表:
- id
- name
- phone
- avatar
- profile
- intro
- settings_json
可以拆成:
主表
- id
- name
- phone
扩展表
- user_id
- avatar
- profile
- intro
- settings_json
适用场景
- 表字段很多,行很宽
- 大字段影响热点查询性能
- 部分字段访问频率很低
3)分表和分库的区别
- 分表:还是在同一个数据库实例中拆表
- 分库:拆到不同数据库实例
实际项目里,常常会先分表,再在必要时进一步分库。
4)一句话总结
分表分为水平分表和垂直分表:水平分表按数据拆,垂直分表按字段拆。
60. 水平分表有哪几种路由方式?
答:
水平分表之后,最关键的问题就是:
一条数据到底应该落到哪一张表?
这个过程就叫路由。
常见的水平分表路由方式主要有三种:
- 范围路由
- Hash 路由
- 配置路由
1)范围路由
范围路由是指:
按照某个有序字段的取值范围,把数据分配到不同表中。
例如按时间分表:
orders_202401orders_202402orders_202403
或者按 ID 范围分表:
user_0:1 ~ 100万user_1:100万 ~ 200万
优点
- 规则直观
- 容易理解
- 对按时间 / 范围查询很友好
- 扩容时可以平滑增加新表
缺点
- 数据可能分布不均
- 热点可能集中在最新一张表
- 某些范围可能特别大或特别小
2)Hash 路由
Hash 路由是指:
根据某个字段做哈希或取模运算,把数据分配到不同表中。
例如按用户 ID:
user_id % 4决定落到:
orders_0orders_1orders_2orders_3
优点
- 分布通常比较均匀
- 热点相对容易打散
- 实现简单
缺点
- 不适合范围查询
- 扩容麻烦,可能要重分布数据
- 路由规则一旦定死,后续调整成本高
3)配置路由
配置路由是指:
单独维护一张路由表,记录某条业务数据应该去哪张分表。
例如有一张:
order_router里面存:
order_idtable_id
查询时先查路由表,再定位真实表。
优点
- 灵活性最高
- 可动态调整
- 方便迁移数据和扩容
缺点
- 多一次查询
- 路由表本身也会变大
- 维护复杂度高
4)怎么选择?
范围路由适合
- 按时间查询多
- 数据天然有时间属性
- 适合日志、订单、流水类业务
Hash 路由适合
- 想尽量均匀分布数据
- 等值查询多
- 对范围查询要求不高
配置路由适合
- 规则复杂
- 迁移灵活性要求高
- 需要手工控制分布策略
5)一句话总结
水平分表常见路由方式有范围路由、Hash 路由和配置路由,分别在可读性、均匀性和灵活性上各有优劣。
61. 不停机扩容怎么实现?
答:
不停机扩容是分库分表演进中的一个经典难题。 它的核心目标是:
在业务不停服的情况下,把原有库表的数据平滑迁移到新的库表结构。
这个过程本质上是:
- 数据迁移
- 双写同步
- 切流量
- 下线旧库
如果处理不好,可能会带来:
- 数据不一致
- 漏写
- 读写异常
- 线上故障
所以面试里通常会考你是否理解一个标准迁移流程。
1)第一阶段:新库准备 + 双写旧库和新库
第一步:创建新库新表
先把新的目标结构搭好,例如:
- 新分表
- 新分库
- 新索引结构
第二步:上线双写逻辑
此时应用层对写请求同时写:
- 旧库
- 新库
也就是说从某个时刻开始,增量数据同时进入两边。
这一步的作用
避免在历史数据迁移期间,新产生的数据只进入旧库,导致新库追不上。
2)第二阶段:迁移历史数据
因为新库刚开始是空的,所以需要把旧库里的历史数据搬过去。
常见做法:
- 分批迁移
- 按主键范围迁移
- 按时间批量迁移
注意点
- 不能一次性全量大事务搬,容易影响线上
- 要限速
- 要可重试
- 要保证幂等
3)第三阶段:数据校验和补偿
历史数据迁移完成后,还要做校验,确保:
- 旧库和新库数量一致
- 关键字段一致
- 没有漏数据、脏数据
常见做法:
- count 校验
- checksum 校验
- 分片比对
- 差异补偿任务
如果发现不一致,就补数据。
4)第四阶段:读流量切到新库
当历史数据迁移完成,且双写也稳定、新旧数据比对无误后,就可以开始:
把读请求切到新库
注意此时通常还是保持双写:
- 写:旧库 + 新库
- 读:新库
这样即使发现问题,也还有旧库做兜底。
5)第五阶段:停止旧库写入并下线
确认新库运行稳定后,再进入最后阶段:
- 停止旧库写入
- 只写新库
- 观察一段时间
- 确认旧库没有请求后,逐步下线旧库
6)为什么双写是关键?
因为不停机扩容最大的问题不是“历史数据搬迁”,而是:
迁移期间业务还在不断产生新数据
如果没有双写:
- 搬历史数据时旧库还在继续写
- 等你搬完,旧库又新增了一批
- 永远追不齐
所以通常需要:
- 全量迁移
- 增量双写
- 校验补偿
- 切换流量
7)这种方案的风险点
(1)双写失败
一边成功一边失败,容易导致不一致。
(2)顺序问题
旧库和新库写入顺序不同,可能引发覆盖问题。
(3)幂等问题
补偿任务重复执行可能造成脏数据。
(4)切流量时瞬间不一致
读切换窗口如果控制不好,可能读到旧数据。
所以实际中通常会配合:
- MQ
- binlog 增量同步
- 幂等控制
- 校验程序
- 灰度切换
8)一句话总结
不停机扩容的核心流程是:新库建好 -> 双写 -> 历史迁移 -> 数据校验 -> 读切新库 -> 下线旧库。
62. 常用的分库分表中间件有哪些?
答:
分库分表中间件的作用,是帮我们解决:
- 数据路由
- SQL 改写
- 分片管理
- 结果聚合
- 读写分离(部分支持)
常见中间件主要有:
- ShardingSphere(Sharding-JDBC / Sharding-Proxy)
- Mycat
这是面试里最常见的答案。
1)ShardingSphere
ShardingSphere 是目前比较主流的开源数据库中间件生态,原来叫:
- Sharding-JDBC
- Sharding-Proxy
现在归入 Apache ShardingSphere。
(1)Sharding-JDBC
它是一个 客户端增强型框架。 直接嵌入到应用里,以 jar 包方式存在。
特点
- 对业务透明度较高
- 性能损耗相对低
- 适合 Java 生态
- 不需要额外部署代理层
缺点
- 侵入应用
- 强依赖语言生态
- 多语言系统不方便统一
(2)Sharding-Proxy
它是一个独立部署的代理服务。
应用连接它,就像连接数据库一样。 它负责路由 SQL 到后端实际分片库表。
特点
- 对应用更透明
- 支持多语言
- 易于集中治理
缺点
- 多一层代理
- 运维复杂度更高
2)Mycat
Mycat 是比较早期、比较知名的数据库中间件。 它工作在代理层,应用连接 Mycat,由 Mycat 转发到后端 MySQL。
支持能力
- 分库分表
- 读写分离
- SQL 路由
- 结果聚合
优点
- 较早普及,社区影响力大
- 对业务透明
缺点
- 某些版本稳定性、维护性和兼容性曾被吐槽
- 在现代新项目里,很多团队更偏向 ShardingSphere
3)怎么选?
偏 Java 应用内嵌
通常更倾向:
- Sharding-JDBC
偏统一代理、多语言接入
通常会考虑:
- Sharding-Proxy
- Mycat
新项目
现在更多团队优先考虑:
- ShardingSphere 生态
4)一句话总结
常见分库分表中间件主要有 ShardingSphere 和 Mycat,其中现代项目更常用 ShardingSphere。
63. 你觉得分库分表会带来什么问题?
答:
分库分表能解决单库单表的性能和容量瓶颈,但它不是没有代价。 它会把原本数据库帮你处理的很多事情,转移到:
- 中间件
- 应用层
- 运维层
所以分库分表带来的问题非常多,这也是为什么它属于“最后手段”。
常见问题可以从以下几个方面来回答。
1)分布式事务问题
单库时,本地事务很好处理。 一旦跨库操作,就可能涉及多个数据库实例。
例如:
- 订单库写订单
- 库存库扣库存
- 支付库记账
这时一个本地事务就不够了,会变成分布式事务问题。
带来的挑战
- 一致性难保证
- 实现复杂
- 性能下降
常见解决方案:
- 最终一致性
- 可靠消息
- TCC / SAGA
- Seata 等框架
2)跨库 Join 问题
在单库里可以直接:
SELECT ...FROM order oJOIN user u ON o.user_id = u.id;但分库后,如果:
order在订单库user在用户库
数据库本身就不能直接高效 join 了。
解决方式
- 业务层拼装
- 冗余字段
- 数据异构到 ES / 宽表
- 中间件做有限聚合(通常性能有限)
3)跨节点分页、排序、聚合问题
分表后,一个查询可能要访问多张表。 例如:
ORDER BY create_time DESC LIMIT 20或者:
COUNT(*)GROUP BYSUM()这些操作在分片环境下会变复杂:
- 每个分片先局部查询
- 再在中间层做汇总
- 再排序 / 分页 / 聚合
问题
- 实现复杂
- 性能开销大
- 深分页尤其麻烦
4)全局唯一 ID 问题
单库单表时可以直接依赖数据库自增主键。 分库分表后,如果每个分片都自增,就会出现主键冲突。
所以必须引入全局唯一 ID 方案,例如:
- 雪花算法(Snowflake)
- 号段模式
- UUID(但不推荐做主键)
- Redis / ZooKeeper 发号
5)扩容和数据迁移问题
分库分表之后,如果原来的分片数不够用了,再扩容会很麻烦。
例如 Hash 分片:
- 原来 4 张表
- 现在变成 8 张表
路由规则变了,很多数据要重分布。 这就涉及:
- 数据迁移
- 双写
- 校验
- 切换流量
也就是上一题提到的“不停机扩容”难题。
6)路由规则复杂度问题
分片后,应用必须知道:
- 这条数据在哪个库
- 在哪个表
- 用哪个字段路由
如果路由字段传错、缺失,可能会:
- 查不到数据
- 扫描所有分片
- 性能急剧下降
所以路由设计本身也是系统复杂度来源。
7)运维和排障复杂度上升
单库时问题排查相对直接。 分库分表后:
- 数据分散在很多节点
- 慢 SQL 可能发生在某一个分片
- 某个分片热点更严重
- 主从、路由、中间件都可能出问题
这会导致:
- 排障更复杂
- 运维成本更高
- 监控体系要求更高
8)数据热点问题
即使做了分库分表,如果路由规则不合理,也可能出现热点。
例如:
- 按用户 ID 哈希还比较均匀
- 按时间范围分表时,最新表可能特别热
一旦热点集中到单分片,分片就失去了意义。
9)开发复杂度上升
分库分表后,应用开发需要考虑更多事情:
- 路由字段设计
- 分页跨分片
- 聚合跨分片
- 事务一致性
- 扩容迁移
- SQL 兼容限制
原来一个简单 SQL 解决的问题,可能现在要多次查询 + 代码聚合。
10)一句话总结
分库分表能解决容量和性能瓶颈,但会带来事务、跨库查询、聚合分页、全局 ID、扩容迁移和运维复杂度等一系列问题。