第70篇
极客时间《从0开始学架构》课程笔记。
关系数据库目前还是各种业务系统中关键和核心的存储系统,在很多场景下高性能的设计最核心的部分就是关系数据库的设计。因此高性能架构模式的重点依然是高性能数据库集群的设计。
高性能数据库集群有两种方式:
1、读写分离,其本质是将访问压力分散到集群中的多个节点,但是没有分散存储压力。
2、分库分表,既可以分散访问压力,又可以分散存储压力。
一、读写分离
读写分离的基本原理是将数据库读写操作分散到不同的节点上。
读写分离的基本实现:
- 数据库服务器搭建主从集群,一主一从、一主多从都可以。
- 数据库主机负责读写操作,从机只负责读操作。
- 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
- 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。
从以上4点可以看出,第3、4两步操作是实现重点,因此也带来两个问题:
问题1:主从复制延迟
一般主从复制延迟可能达到 1 秒或更长,如果业务服务器将数据写入到数据库主服务器后立刻(1 秒内)进行读取,此时读操作访问的是从机,主机还没有将数据复制过来,到从机读取数据是读不到最新数据的,业务上就可能出现问题。
常见解决方案
- 写操作后的读操作指定发给数据库主服务器
- 读从机失败后再读一次主机
- 关键业务读写操作全部指向主机,非关键业务采用读写分离
问题2:分配机制
将读写操作区分开,然后访问不同的数据库服务器,一般有两种方式:程序代码封装和中间件封装。
1、 程序代码封装
程序代码封装指在代码中抽象一个数据访问层(所以有的文章也称这种方式为“中间层封装”),实现读写操作分离和数据库服务器连接的管理。
特点:
- 实现简单,而且可以根据业务做较多定制化的功能。
- 每个编程语言都需要自己实现一次,无法通用,如果一个业务包含多个编程语言写的多个子系统,则重复开发的工作量比较大。
- 故障情况下,如果主从发生切换,则可能需要所有系统都修改配置并重启。
2、中间件封装
中间件封装指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。中间件对业务服务器提供 SQL 兼容的协议,业务服务器无须自己进行读写分离。对于业务服务器来说,访问中间件和访问数据库没有区别,事实上在业务服务器看来,中间件就是一个数据库服务器。
特点:
- 能够支持多种编程语言,因为数据库中间件对业务服务器提供的是标准 SQL 接口。
- 要支持完整的 SQL 语法和数据库服务器的协议,实现比较复杂,细节特别多,很容易出现 bug。
- 数据库中间件自己不执行真正的读写操作,但所有的数据库操作请求都要经过中间件,中间件的性能要求也很高。
- 数据库主从切换对业务服务器无感知,数据库中间件可以探测数据库服务器的主从状态。
建议:一般情况下采用程序语言封装的方式,或者使用成熟的开源数据库中间件。如MySQL Router、Atlas。
二、分库分表
业务分库
业务分库指的是按照业务模块将数据分散到不同的数据库服务器。
问题
业务分库能够分散存储和访问压力,但同时也带来了新的问题。
- join 操作问题
业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用 SQL 的 join 查询。 - 事务问题
原本在同一个数据库中不同的表可以在同一个事务中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改。虽然数据库厂商提供了一些分布式事务的解决方案(例如,MySQL 的 XA),但性能实在太低,与高性能存储的目标是相违背的。 - 成本问题
业务分库同时也带来了成本的代价,本来 1 台服务器搞定的事情,现在要 3 台,如果考虑备份,那就是 2 台变成了 6 台。
方案
因为用户规模不同,对于小公司初创业务,不建议一开始就这样拆分。对于业界成熟的大公司来说,最好在业务开始设计时就考虑业务分库。
分表
将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。
因此需要分表,单表数据拆分有两种方式:垂直分表和水平分表
分表说明:
- 实际架构设计过程中不局限切分的次数,可以切一次、两次,也可以切很多次。
- 单表进行切分后,是否要将切分后的多个表分散在不同的数据库服务器中,可以根据实际的切分效果来确定,并不强制要求单表切分为多表后一定要分散到不同数据库中。
分表能够有效地分散存储压力和带来性能提升,但和分库一样,也会引入各种复杂性。
垂直分表
垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。
垂直分表引入的复杂性主要体现在表操作的数量要增加。
水平分表
水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000 万就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。
水平分表会引入更多的复杂性,主要表现在下面4方面:路由、join操作、count()操作、order by 操作。
- 1、路由
水平分表后,某条数据具体属于哪个切分后的子表,需要增加路由算法进行计算,这个算法会引入一定的复杂性。
常见的路由算法有3种:范围路由、Hash路由、配置路由,优缺点对比表如下:
路由算法 | 定义 | 设计复杂点 | 优点 | 缺点 |
---|---|---|---|---|
范围路由 | 选取有序的数据列(例如,整形、时间戳等)作为路由的条件,不同分段分散到不同的数据库表中 | 分段大小的选取。分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题 | 可以随着数据的增加平滑地扩充新的表 | 分布不均匀 |
Hash路由 | 选取某个列(或者某几个列组合也可以)的值进行 Hash 运算,然后根据 Hash 结果分散到不同的数据库表中 | 初始表数量的选取。表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题 | 表分布比较均匀 | 扩充新的表很麻烦,所有数据都要重新分布 |
配置路由 | 增加路由表,用一张独立的表来记录路由信息 | 无 | 设计简单,容易扩充。在扩充表的时候,只需要迁移指定的数据,然后修改路由表就可以了 | 必须多查询一次,会影响整体性能;而且路由表本身如果太大(例如,几亿条数据),性能同样可能成为瓶颈 |
2、join操作
水平分表后,数据分散在多个表中,如果需要与其他表进行 join 查询,需要在业务代码或者数据库中间件中进行多次 join 查询,然后将结果合并。3、count()操作
水平分表后,虽然物理上数据分散到多个表中,但某些业务逻辑上还是会将这些表当作一个表来处理。
常见处理方式对比如下:
处理方式 | 操作方法 | 优点 | 缺点 |
---|---|---|---|
count()相加 | 在业务代码或者数据库中间件中对每个表进行 count() 操作,然后将结果相加 | 实现简单 | 性能比较低 |
记录数表 | 新建一张表,表名为“记录数表”,包含 table_name、row_count 两个字段,每次插入或者删除子表数据成功后,都更新“记录数表” | 性能要大大优于 count() 相加的方式 | 复杂度增加不少,对子表的操作要同步操作“记录数表”,增加了数据库的写压力,并且会出现数据不一致问题 |
- 4、order by 操作
水平分表后,数据分散到多个子表中,排序操作无法在数据库中完成,只能由业务代码或者数据库中间件分别查询每个子表中的数据,然后汇总进行排序。
分库分表的实现方法
分库分表具体的实现方式也是“程序代码封装”和“中间件封装”,但实现会更复杂。读写分离实现时只要识别 SQL 操作是读操作还是写操作,通过简单的判断 SELECT、UPDATE、INSERT、DELETE 几个关键字就可以做到,而分库分表的实现除了要判断操作类型外,还要判断 SQL 中具体需要操作的表、操作函数(例如 count 函数)、order by、group by 操作等,然后再根据不同的操作进行不同的处理。