在高性能系统中,往往数据库的读写性能非常重要,但是数据库的性能瓶颈往往和很多东西有关,

  1. 数据量有关系,数据量会影响B+ 数的高度
  2. 索引建立的好不好,比如一些高频数据库查询
  3. 数据迁移,往往之前的表设计的不好,需要把表的数据迁移到新的表上面
  4. 分库分表,分库分表通常分为垂直拆分和水平拆分

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 万行也是醉了

分库分表

分库分表的原则:

  1. 垂直拆分: 比如说按照用户类型来拆分,比如普通用户和管理员用户
  2. 水平拆分: 比如说按照用户 id 来拆分,比如每个用户 id 都放到一个库中

老系统表垂直拆分

  • 实体数据(适合缓存):像“用户账号表”,通过一个固定的ID就能找到。这类数据最稳定,最适合放缓存里随用随取。
  • 辅助数据(冷门备用):像“用户家庭住址”,平时不用,关键时刻才找。放缓存里占地方,放数据库就行。
  • 历史记录(不建议缓存):像“谁访问过你”的记录,又乱又一直变。把“来访记录”和“好友状态”硬塞一起的例子,就像把垃圾和新鲜食物混放,还要求垃圾和食物保持一样新鲜,纯属给自己找麻烦,得彻底拆开。
  • 关系数据(有状态的数据):比如“好友关系”,经常变,但也需要快。要单独用合适的容器(数据结构)管理。

但是一般的场景下是不需要分库分表的,

  1. 你的机器性能足够, 不需要分库分表
  2. 你的表访问量其实不是很大, 不需要分库分表

分库分表前也可以考虑 分区表+从库

  1. 分区表: 比如说按照用户 id 来分区,比如每个用户 id 都放到一个分区中
  2. 从库: 比如说每个分区都有一个从库,从库的读取性能会更好

真正需要分库分表的时候, 才需要考虑分库分表的策略。

分库分表的常见手段

  1. SDK: 提供一个 SDK, 内置了常见的分库分表的手段: 缺点就是维护成本高
  2. 数据代理, 常见的数据代理 Apache ShardingSphere, MyCat
  3. sidcar: 在应用中添加一个 sidecar 容器, 用于处理分库分表的逻辑
  4. 手搓: 手动实现分库分表的逻辑, 但是需要自己维护, 维护成本高

主键设计:

在分库分表中,其实最难的其实是主键的设计, 为什么要采用雪花算法的设计原则,因为在分布式环境下,我们的数据是分布的,你要可以保证每个节点的全局唯一且有一定业务含义的分布式ID。

  1. 全局唯一+性能: 生成的64 位整数, 比 uuid 快

  2. 分布式,没有中性化: 无中心化:每个服务节点独立生成ID,不需要网络请求或锁竞争,完全本地生成,性能极高(单机可达百万级/秒)。 通过“机器ID”区分节点:只要保证每个节点的workerId(工作机器ID)不重复,就能保证全局唯一,无需集中协调。

  3. 时间有序 vs. 业务可理解

单纯的随机ID虽然也唯一,但完全丢失了时序信息。在很多业务场景中,需要按时间范围查询数据(如“查询最近10分钟的订单”)。如果ID不含时间,这类查询就必须扫描全表或依赖额外的时间索引。

雪花算法的解决方式:

  1. 其核心构成是:时间戳(41位) + 机器ID(10位) + 序列号(12位)。

  2. 高位是时间戳,可以直接从ID中解析出生成时间。这使得ID天然趋势递增且包含时间。

  3. 因此,可以直接用ID进行时间排序或按时间范围分片(如按ID取模来路由),而无需额外维护时间字段索引。

  4. 灵活性 vs. 扩展性 实际生产环境中,数据量会持续增长,可能需要调整分片数或重新划分分片。如果主键设计时包含了分片信息,后面就会非常被动。 雪花算法的解决方式: 其64位结构允许自定义位分配。例如,可以将10位机器ID拆分为 5位数据中心ID + 5位机器ID,或者将其中几位设计为业务分片键。 这意味着可以通过ID中的特定比特位,直接确定该行数据属于哪个分片,实现路由计算无查表、无哈希,极大提升分片路由效率,也为未来的扩容预留了空间。

一个分库分表 sharding 库设计:

type ShardingDB struct {
    DBPrefix string
    TablePrefix string
    dbNumber int
    tableNumber int
}