半步多 玄玉的博客

数据库水平拆分和垂直拆分


水平拆分和垂直拆分都能降低单库(表)大小,以达到提升性能的目的,具体区别如下

水平拆分

以某字段为依据(如uid),按照一定规则(如取模),将一个库(表)上的数据拆分到多个库(表)上,其特点是:

  • 每个库(表)的结构都一样
  • 每个库(表)的数据都不一样,没有交集
  • 所有库(表)的并集是全量数据

垂直拆分

针对一个属性较多,一行数据较大的表,将不同的属性拆分到不同的表中,其特点是:

  • 每个库(表)的结构都不一样
  • 一般来说,每个库(表)的属性至少有一列交集,一般是主键
  • 所有库(表)的并集是全量数据

垂直拆分的依据

当一个表属性很多时,如何来进行垂直拆分呢?

如果没有特殊情况,拆分依据主要有几点:

  1. 将长度较短,访问频率较高的属性尽量放在一个表里,这个表暂且称为主表
  2. 将字段较长,访问频率较低的属性尽量放在一个表里,这个表暂且称为扩展表
  3. 如果 1 和 2 都满足,还可以考虑第三点:经常一起访问的属性,也可以放在一个表里

优先考虑 1 和 2,第 3 点不是必须

另,如果实在属性过多,主表和扩展表都可以有多个(一般来说只有一个主表)

为何要将字段短,访问频率高的属性放到一个表内?为何这么垂直拆分可以提升性能?这是因为:

  1. 数据库有自己的内存 buffer,会将磁盘上的数据 load 到内存 buffer 里(暂且理解为进程内缓存吧)
  2. 内存 buffer 缓存数据是以 row 为单位的
  3. 在内存有限的情况下,在数据库内存 buffer 里缓存短 row,就能缓存更多的数据
  4. 在数据库内存 buffer 里缓存访问频率高的 row,就能提升缓存命中率,减少磁盘的访问,吞吐量会大大增加

垂直拆分查询时注意

一般来说,数据量并发量比较大时,数据库的上层都会有一个服务层

若要同时访问主表和扩展表中的属性时,服务层不要 join 来连表访问,而应该分两次进行查询

这是因为,大数据高并发互联网场景下,一般来说,吞吐量和扩展性是主要矛盾:

  1. join 更损耗数据库性能
  2. join 会让 base 表和 ext 表耦合在一起(必须在一个数据库实例上)
    不利于数据量大时拆分到不同的数据库实例上(机器上),毕竟减少数据量,提升性能才是垂直拆分的初衷

水平拆分查询时注意

举例:用 uid 分库,uname 上的查询怎么办?

描述:根据 uid 查询,可直接路由到具体的分库。若根据 uname 查询,由于不知道落在哪个分库,往往要扫描所有库

解决方案一:索引表(思路就是通过 uname 能查询到 uid)

建立一个索引表记录 uname 和 uid 的映射关系。用 uname 来访问时,先通过索引表查询到 uid,再定位相应的库

索引表属性较少,可以容纳非常多数据,一般不需要分库。如果数据量过大,可以通过 uname 来分库

潜在不足就是多了一次数据库查询,性能下降一倍

解决方案二:缓存映射(访问索引表性能较低,把映射关系放在缓存里性能更佳)

uname 查询先到 cache 中查询 uid,再根据 uid 定位数据库

若 cache miss,则扫全库获取 uname 对应的 uid,放入 cache

uname 到 uid 的映射关系不会变化,映射关系一旦放入缓存,不会更改,无需淘汰,缓存命中率超高

如果数据量过大,可以通过 name 进行 cache 水平切分

潜在不足就是多了一次 cache 查询

解决方案三:uname 生成 uid(不进行远程查询,由 uname 直接得到uid)

用户注册时,设计函数 uname 生成 uid,按 uid 分库插入数据。用 uname 访问时,先通过函数计算 uid,由 uid 路由到库

潜在不足就是该函数设计需要非常讲究技巧,有 uid 生成冲突风险

解决方案四:uname 基因融入 uid(不能用 uname 生成uid,可以从 uname 抽取“基因”,融入 uid 中)

假设 uid 共 64 bit,那就让前 61 bit 全局唯一,剩下 3 bit 由 uname 通过函数生成(这里 61 和 3 只是举例)


相关文章

Content