高性能系统设计之数据库设计
在高性能系统中,往往数据库的读写性能非常重要,但是数据库的性能瓶颈往往和很多东西有关,
- 数据量有关系,数据量会影响B+ 数的高度
- 索引建立的好不好,比如一些高频数据库查询
- 数据迁移,往往之前的表设计的不好,需要把表的数据迁移到新的表上面
- 分库分表,分库分表通常分为垂直拆分和水平拆分
B+ 树的基本原理
MySQL 底层的树使用的是 B+ 树,理解这种数据结构对我们优化数据库非常重要,首先 B+ 树是存储在磁盘中的,如果树的高度比较高,就会导致磁盘的 IO 是非常高的,其次B+ 树的非叶子节点不存放数据,非叶子节点存储的都是索引,叶子节点存储的才是数据,如果索引不是聚簇索引那么还得回表找一下主键,然后从主键找到对应的数据。
如果一个叶子数量超过一个根节点可以存储的指针数量,那么B+ 树就会扩容一层 一个索引的大小条目是主键8 个字节 + 指针 6 个字节,大概是 14 个字节, 加上一些额外开销我们估算为20个字节。
15270 / 20 ≈ 763 条索引记录
那也就是意味着是可以有 763 个叶子节点节点的,因为一个指针指向一个叶子节点
[ 根节点 ] ← 1 个 page,只存索引 + 指针
/ | ... \
[页1] [页2] ... [页763] ← 763 个 page,存真实数据行
根据上面的估算一下那大概就有12208 行数据
763 × 16 ≈ 12,208 行
上面这个计算的是 2 层的数据,那如果是 三层就会是 763 * 763 这样的
B+ 树的高度是多少层,通过主键查找一行数据最坏就需要多少次磁盘 I/O 查找路径是这样的:
- 读根节点(第 1 次 I/O)
- 根据根节点里的指针,读对应的中间层节点(第 2 次 I/O)
- 根据中间节点的指针,读对应的叶子节点(数据页)(第 3 次 I/O)
数据量预估与设计:
MySQL 单表建议控制在 500万~1000万行 以内, 之所以要选择这个数据来完成主要是为了:
- 防止把 buffer pool 给刷爆炸了,毕竟这个咱们内存还是很大的
- 运维维护起来方便, 毕竟一个破表搞500 万行也是醉了
分库分表
分库分表的原则:
- 垂直拆分: 比如说按照用户类型来拆分,比如普通用户和管理员用户
- 水平拆分: 比如说按照用户 id 来拆分,比如每个用户 id 都放到一个库中
老系统表垂直拆分
- 实体数据(适合缓存):像“用户账号表”,通过一个固定的ID就能找到。这类数据最稳定,最适合放缓存里随用随取。
- 辅助数据(冷门备用):像“用户家庭住址”,平时不用,关键时刻才找。放缓存里占地方,放数据库就行。
- 历史记录(不建议缓存):像“谁访问过你”的记录,又乱又一直变。把“来访记录”和“好友状态”硬塞一起的例子,就像把垃圾和新鲜食物混放,还要求垃圾和食物保持一样新鲜,纯属给自己找麻烦,得彻底拆开。
- 关系数据(有状态的数据):比如“好友关系”,经常变,但也需要快。要单独用合适的容器(数据结构)管理。
但是一般的场景下是不需要分库分表的,
- 你的机器性能足够, 不需要分库分表
- 你的表访问量其实不是很大, 不需要分库分表
分库分表前也可以考虑 分区表+从库
- 分区表: 比如说按照用户 id 来分区,比如每个用户 id 都放到一个分区中
- 从库: 比如说每个分区都有一个从库,从库的读取性能会更好
真正需要分库分表的时候, 才需要考虑分库分表的策略。
分库分表的常见手段
- SDK: 提供一个 SDK, 内置了常见的分库分表的手段: 缺点就是维护成本高
- 数据代理, 常见的数据代理 Apache ShardingSphere, MyCat
- sidcar: 在应用中添加一个 sidecar 容器, 用于处理分库分表的逻辑
- 手搓: 手动实现分库分表的逻辑, 但是需要自己维护, 维护成本高
主键设计:
在分库分表中,其实最难的其实是主键的设计, 为什么要采用雪花算法的设计原则,因为在分布式环境下,我们的数据是分布的,你要可以保证每个节点的全局唯一且有一定业务含义的分布式ID。
-
全局唯一+性能: 生成的64 位整数, 比 uuid 快
-
分布式,没有中性化: 无中心化:每个服务节点独立生成ID,不需要网络请求或锁竞争,完全本地生成,性能极高(单机可达百万级/秒)。 通过“机器ID”区分节点:只要保证每个节点的workerId(工作机器ID)不重复,就能保证全局唯一,无需集中协调。
-
时间有序 vs. 业务可理解
单纯的随机ID虽然也唯一,但完全丢失了时序信息。在很多业务场景中,需要按时间范围查询数据(如“查询最近10分钟的订单”)。如果ID不含时间,这类查询就必须扫描全表或依赖额外的时间索引。
雪花算法的解决方式:
-
其核心构成是:时间戳(41位) + 机器ID(10位) + 序列号(12位)。
-
高位是时间戳,可以直接从ID中解析出生成时间。这使得ID天然趋势递增且包含时间。
-
因此,可以直接用ID进行时间排序或按时间范围分片(如按ID取模来路由),而无需额外维护时间字段索引。
-
灵活性 vs. 扩展性 实际生产环境中,数据量会持续增长,可能需要调整分片数或重新划分分片。如果主键设计时包含了分片信息,后面就会非常被动。 雪花算法的解决方式: 其64位结构允许自定义位分配。例如,可以将10位机器ID拆分为 5位数据中心ID + 5位机器ID,或者将其中几位设计为业务分片键。 这意味着可以通过ID中的特定比特位,直接确定该行数据属于哪个分片,实现路由计算无查表、无哈希,极大提升分片路由效率,也为未来的扩容预留了空间。
一个分库分表 sharding 库设计:
type ShardingDB struct {
DBPrefix string
TablePrefix string
dbNumber int
tableNumber int
}