# 数据库如何实现高性能?

# 读写分离和分库分表
在数据库的高并发和海量数据场景下,读写分离和分库分表是两种非常经典的架构优化手段。简单来说,它们一个是解决“并发压力大”的问题,一个是解决“数据量太大”的问题
# 读写分离
# 什么是读写分离
在大多数互联网应用中,读(Select)的压力远大于写(Insert/Update/Delete)(通常是 8:2 甚至 9:1)。读写分离的核心思想就是把“写操作”和“读操作”分给不同的数据库服务器来处理。
主库(Master): 只负责处理写操作,以及少量的实时读操作。
从库(Slave): 只负责处理读操作,可以有多个。
# 核心原理:主从复制
简单来说,就搞一个主库,挂多个从库,然后我们就单单只是写主库,然后从库读取binlog进行重放,这样主库和从库数据就一样

总的来说,MySQL复制有三个步骤
- 主库把所有的写操作记录到 Binlog(二进制日志) 中
- 从库的 I/O 线程去读取主库的 Binlog,并写入到自己的 Relay Log(中继日志) 中
- 从库的 SQL 线程重放 Relay Log 中的操作,从而与主库保持数据一致
# 带来的挑战:主从延迟
主从延迟:数据同步需要时间。如果主库刚写完,用户立马去从库读,可能会读到旧数据
解决方案如下:
1. 强制路由主库(最常用)
对于某些对实时性要求极高的场景,不走从库,直接查主库
2. 关键路径判断
利用 Redis 配合做一个“延迟标记”。
- 某个用户执行了写操作(比如发帖),在 Redis 中记录一个 Key:user_lock:userId = 1,过期时间设置为 2 秒(预估的主从延迟最大时间)。
- 当该用户接下来读数据时,先看 Redis 里有没有这个 user_lock 标记。
- 如果有,说明他刚写过数据,为了防止主从延迟,强制路由到主库。
- 2 秒后,Key 过期消失,该用户的读请求恢复路由到从库
# 分库分表
# 1. 什么时候进行分库分表?
不要过早优化。在决定分库分表之前,先确认是否已经尝试过以下低成本手段
- 代码及SQL优化:检查是否有慢查询,索引是否建立合理
- 提高硬件配置:提升CPU、内存、换SSD固态硬盘
- 读写分离/主从架构:如果读并发高,可以通过增加从库解决
- 缓存(Redis等):拦截高并发的读请求
- 历史数据归档:将1年前或不常用的数据迁移到冷备库
行业经验指标:单表数据量超过 500万行 或 数据容量超过 2GB,且经过上述优化后性能仍无法满足要求时,再考虑分库分表
# 2. 拆分策略:垂直拆分 vs 水平拆分
分库分表主要有两种维度,通常是结合使用

# 垂直拆分(按业务/字段拆)
垂直分库: 按照业务模块,把一个大数据库拆分成多个业务库。例如把一个电商大库拆分为用户库,商品库,订单库
垂直分表:把一张表里不常用的字段、或者大字段(如 text、blob)拆分到另一张辅助表中,大表变小表
# 水平拆分(按数据行拆)
水平拆分是结构不变,只拆分数据行,也就是我们常说的 Sharding
水平分表: 单表数据量太大(如超过 2000 万行),把这一张表的数据拆分到多张结构完全一样的表中。例子: t_order 拆分为 t_order_0、t_order_1、t_order_2
水平分库:如果不仅数据量大,高并发写入也把单台服务器的 CPU/内存/IO 撑爆了,就把这些表再分散到不同的物理数据库服务器上
# 3. 水平拆分的核心:Sharding Key(分片键)与路由算法
选择合适的 分片键(Sharding Key) 是分库分表最关键的一步,它决定了数据的分布规则
# 常见的分片算法
| 策略 | 举例 | 优点 | 缺点 |
|---|---|---|---|
| 范围分片 | 按 user_id 范围 1~10000 表1,10001~20000 表2 | 顺序扩展方便 | 容易热点不均 |
| 哈希取模 | user_id % 4 | 数据均匀 | 扩容时需大量迁移 |
| 一致性哈希 | 虚拟节点环 | 扩容影响小 | 实现复杂 |
| 日期分片 | 按月分表 order_202401 | 便于归档 | 近期热点 |
# 经典业务场景下的分片键选择
以“用户”为中心的业务(如社交、博客):
通常以 user_id 作为分片键。这样同一个用户的所有数据都在同一个库表里,查询方便
# 4. 分库分表带来的副作用
# 分库后出现的问题
跨库join问题
不同库之间的表无法直接进行 JOIN 操作 解决办法:
- 字段冗余:把经常需要JOIN的字段直接冗余到表里(反规范化设计)
- 全局表(广播表):字典表、配置表等数据量小但常被关联的表,在每个数据库都保存一份,修改时同步更新
- 应用层组装:先查出A表数据,拿着ID去代码里拼装查询B表数据
跨库事务问题
原本一个事务可以搞定的操作,现在跨了多个数据库
解决办法:尽量通过合理的设计规避分布式事务(让相关高频操作在同一个库)。如果无法规避,采用 Seata 等分布式事务框架,或者使用 MQ 柔性事务(最终一致性)
# 分表后出现的问题
分布式全局唯一id问题
单表时可以使用数据库自增ID,分库分表后各表自增会导致ID冲突
解决办法:使用全局唯一 ID 生成器(如雪花算法 Snowflake)
跨分片分页、排序、函数问题
如果查询条件没有带上 Sharding Key(如:查询全表按时间排序前10条),中间件需要去所有分片把数据查出来,在内存中重新排序分页
解决办法:针对这类复杂的多维查询、大分页、数据统计,不要走关系型数据库。应该通过数据异构,将数据同步到 Elasticsearch、ClickHouse中查询
# 5. 分库分表中间件选择
实现层面上,无论是读写分离还是分库分表,核心要解决的问题都是“路由”——即把一条 SQL 语句,在正确的时间发送到正确的数据库(和表)上
| 实现方式 | 核心原理 | 代表产品 | 优点 | 缺点 |
|---|---|---|---|---|
| 客户端模式 | 在应用内部以Jar包形式存在,侵入应用但性能更高 | ShardingSphere-JDBC | 1. 性能好,无网络代理损耗 2. 部署简单,与应用一体 | 1. 与特定语言(如Java)绑定 2. 配置分散在各应用,升级维护成本高 |
| 代理模式 | 作为独立服务部署在应用和数据库之间,对应用透明 | Mycat, ShardingSphere-Proxy | 1. 对应用完全透明,支持异构语言 2. 便于集中管理和运维(如监控、升级) | 1. 多一跳网络延迟,有性能损耗 2. 代理本身需考虑高可用,避免单点故障 |
客户端模式

代理模式
