在具体的实践过程中,为了更快、更好地设计出的架构,除了掌握一些架构基础知识外,还需要掌握业界已经成熟的各种架构模式。在大部分情况下,我们做架构设计主要都是基于已有的成熟模式,结合业务和团队的具体情况,进行一定的优化或者调整;即使少部分情况我们需要进行较大的创新,前提也是需要对已有的各种架构模式和技术非常熟悉。
一.读写分离
“读写分离”,其本质是将访问压力分散到集群中的多个节点,但是没有分散存储压力;其实现方式可以是一主一从,也可以是一主多从,其中,数据库主机负责读写操作,从机只负责读操作。读写分离的逻辑实现并不是很复杂,但有两个细节,会引入复杂度,分别是:主从复制延迟(网络问题)和分配机制(数据读取问题)。
主从和主备经常被提起,但是这两个概念并不等同:
“主从”:其中的“从”可以理解为“仆从”,仆从是要帮主人干活的,“从机”是需要提供读数据的功能的。
“主备”:其中的“备机”一般被认为仅仅提供备份功能,不提供访问功能。
1.主从复制延迟
读写分离主从机制的实现,意味着需要进行数据备份(不同主机间),那么就会存在数据复制延迟的行为(网络传输)。以 MySQL 为例,主从复制延迟可能达到 1 秒、1分钟、甚至更久。如果数据写入主机后,然后去从机读取数据,因为复制延迟,导致读取的数据为空,那么,就可能会影响到业务。
解决主从复制延迟有几种常见的方法:
业务绑定:写操作后的读操作指定发给数据库主服务器
需要和对应的业务进行强关联
二次读取:读从机失败后再读一次主机
业务分类:关键业务读写操作全部指向主机,非关键业务采用读写分离
2.分配机制
分配机制主要指的是将读写操作区分开来,然后访问不同的数据库服务器,其实就是如何访问读写分离的数据库集群。其使用方式一般有两种:程序代码封装和中间件封装。
这里并没有介绍这两种方案的具体实现细节,旨在概括总的方向。
(1)程序代码封装
程序代码封装指在代码中抽象一个数据访问层,实现读写操作分离和数据库服务器连接的管理。例如,基于 Hibernate 进行简单封装,就可以实现读写分离。其基本结构表如下:
该方式实现起来简单,但是每个编程语言都需要自己来实现,没有办法通用,并且,在发生故障情况下,如果发生主从切换,那么可能需要所有系统都修改配置并重启。
(2)中间件封装
中间件封装指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。其基本结构如下:
其特点如下:
支持多种编程语言。
支持完整的 SQL 语法和数据库服务器的协议。
数据库中间件自己不执行真正的读写操作,但所有的数据库操作请求都要经过中间件,中间件的性能要求也很高。
数据库主从切换对业务服务器无感知。
开源数据库中间件方案:
MySQL 官方提供了 MySQL Proxy(没有正式 GA)
MySQL 官方推荐 MySQL Router
奇虎 360 公司开源的数据库中间件 Atlas
二.分表分库
“读写分离”,的本质只是将访问压力分散到集群中的多个节点,但是没有分散存储压力;而“分库分表”,既可以分散访问压力,又可以分散存储压力。看着如此强大,却是在引入了复杂度和放弃一定功能的基础上完成的。
1.业务分库
业务分库指的是按照业务模块将数据分散到不同的数据库服务器。这样,每个业务模块都有自己的数据库,而不是所有的模块共用一个数据库,这样,也就降低了数据库的压力,提升了数据库的性能。
在软件架构中,是没有完美的方案的,解决一个问题的同时,有可能会引入新的问题。拿业务分库来说,会引入以下一些问题:
join 问题
原本在一个数据库中的数据,现在分散到了多个数据库上,那么,表之间的 join 就无法使用(不同数据库的表之间,是没办法 join 的)。
事物问题
MySQL 提供了事物的特性,在同一个数据库中,支持很好。但是数据分库后,就是不同系统间的交互了,也就是引入了分布式事物的问题。虽然数据库厂商提供了一些分布式事务的解决方案(例如,MySQL 的 XA),但性能实在太低,与高性能存储的目标是相违背的。
分库后就无法使用数据库事务了,那么就需要业务程序自己来模拟实现事务的功能。
成本问题
原来只需要一台服务器就能处理,现在需要多台,成本会增加。这些成本对对于小公司初创业务来说,会表现的更加的严重。
2.分表
分库只是将不同的业务模块,做了拆分,是一种垂直方向拆分的手段。但是,当表的数据超过的千万的时候,就需要考虑分表(水平分表)了,其依据一般是表的大小,而不是表行数据的多少。
单表数据拆分有两种方式:垂直分表和水平分表。示意图如下:
单表的拆分,并不强制要求切分后的多表必须分散在不同的数据库中,器原因在于,单表切分为多表后,新的表即使在同一个数据库服务器中,也可能带来可观的性能提升,如果性能能够满足业务要求,是可以不拆分到多台数据库服务器的。
分表是可以降低存储压力和性能提升的,但是,和分库一样,依旧会引入复杂度。
(1)垂直分表
垂直分表主要是对表中的字段进行拆分,适合将表中某些不常用且占了大量空间的列拆分出去。其复杂度的体现为:操作表的数量会增加。
(2)水平分表
水平分表适合表行数特别大的表,相对于垂直分表,会引入更多的复杂性,如下所示:
路由
数据在进行水平分表时,某条数据具体属于哪个切分后的子表,需要增加路由算法进行计算,这样,在查找的时候,才能更加快速的定位数据。常见的路由算法有:范围路由、Hash 路由、配置路由。
(1)范围路由:将某一段范围的值,路由到同一张表中。其优点是数据能够进行平滑的扩充;其缺点是数据可能分布不均匀。
(2)Hash 路由:计算某些列的 hash 值,将相同 hash 值的数据,路由到一张表中。其优点是表的数据分布较均匀;其缺点是数据扩充会比较麻烦。可借鉴一致性哈希的方式来优化。
(3)配置路由:使用一张路由配置表,来记录数据和分片表的对应关系。其优点是设计简单,扩充表数据也比较容易;其缺点是会增加一次SQL查询,且路由表本身过大的时候,也会降低性能。
join 操作
水平分表后,数据分散在多个表中,如果需要与其他表进行 join 查询,需要在业务代码或者数据库中间件中进行多次 join 查询,然后将结果合并。
count() 操作
这里的count() 不再是一张表的简单统计,常见方式有两种:count() 相加、记录数表
(1)count() 相加:分别对每张表做 count(),然后再汇总返回给客户端。其优点是实现简单;其缺点是性能较差。
(2)记录数表:使用一张记录表来专门记录总条数。其优点是性能较好;其缺点是要额外的维护记录数表。
order by 操作
水平分表后,数据分散到多个子表中,排序操作无法在数据库中完成,只能由业务代码或者数据库中间件分别查询每个子表中的数据,然后汇总进行排序。
3.实现
不管是分库还是分表,其实现方式都和读写分离类似,可分为“程序代码封装”和“中间件封装”,只是,其实现的复杂度相比读写分离会更高。读写分离实现时只要识别 SQL 操作是读操作还是写操作,而分库分表的实现除了要判断操作类型外,还要判断 SQL 中具体需要操作的表、操作函数(例如 count 函数)、order by、group by 操作等。