读写分离分散了数据库读写操作的压力,但是没有分散存储压力,当数据库的数据量达到千万甚至上亿条的时候,单台数据库服务器的存储能力就会达到瓶颈,主要体现在以下几个方面:
- 数据量太大,读写性能会下降,即使有索引,索引也会变得很大,性能同样会下降
- 数据文件会变得很大,数据库备份和恢复需要消耗更长的时间
- 数据文件越大,极端情况下丢失数据的风险就会越高
基于上述原因,单个数据库服务器存储的数据量不能太大,需要控制在一定的范围内,为了满足业务数据存储的需求,需要将存储分散到多台数据库服务器上
常见的分散存储的方法有分库和分布两大类
- 业务分库 业务分库之的是按照业务模块将数据分散到不同的数据库服务器,虽然业务分库能够分散存储和访问的压力,但是同时也带来了新的问题,主要存在的问题如下:
- join操作问题 业务分库后,原本在同一个数据库中的表分散到不同数据库中,导致无法使用SQL中的join查询
- 事务问题 原本在同一个数据库中不同的表可以在同一个事物中修改,业务分库后,表分散到不同的数据库中,无法通过事务统一修改,虽然数据库厂商针对此问题提供了一些分布式事务解决方案(例如,MySQL的XA),但是性能实在太低,与高性功能存储的目标是相违背的
- 成本问题 业务分库同时也带来了成本的代价,本来1台服务器搞定的事情,现在需要3台,如果考虑备份,那就是2台变成了6台
基于上述原因,对于初创业务,并不建议一开始就这样拆分,主要有几个原因:
- 初创业务存在很大的不确定性,业务不一定能发展起来,业务开始的时候并没有真正的存储和访问压力,业务分库并不能为业务带来价值
- 业务分库后,表之间的join查询,数据库事务无法简单实现了发
- 业务分库后,因为不同的数据要读写不同的数据库,代码需要增加根据数据类型映射到不同数据库的逻辑,增加了工作量,而业务初创期最重要的是快速实现,快速验证,业务分库会拖慢业务节奏
- 分表 将不同的业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但是如果业务继续发展,同一个业务的单表数据也会达到单台数据库服务器的处理瓶颈,此时就需要对单表进行拆分,单表数据拆分有两种方式:垂直分表和水平分表
分表能够有效的分散存储压力和带来性能提升,但是和分库一样,也会引入各种复杂性,主要存在的问题如下:
- 垂直分表 垂直分表适合将表中某些不常用而且占了大量空间的列拆分出去,垂直分表的引入的复杂性主要体现在表操作的数量会增加,例如原来只要一次查询的就可以获取,现在要查询两次或者多次才能获得想要的数据
- 水平分表 水平分表适合表行数特别大的表,如果单表行数超过5000万就必须进行分表,这个数字可以作为参考,但是并不是绝对的标准,关键还是要看表的访问性能
水平分表相比垂直分表,会引入更多的复杂性,主要表现在以下几个方面:
- 路由 水平分表后,某条数据具体属于哪个切分后的表,需要增加路由算法进行计算,这个算法会引入一定的复杂性,常见的路由算法有如下几种:
- 范围路由 选择有序的数据列作为路由条件,不同分段分散到不同的数据库表中,以常见的用户ID为例,路由算法可以按照10000的范围大小进行分段 1-9999放到数据库1中的表,10000-19999的数据放到数据库2中的表,依次类推,范围路由算法的复杂性主要体现在分段大小的选取上,分段太小会导致切分后的子表数据量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大学在100万到200万之间,具体要根据业务选择合适的大小分段,路由算法的优点就是可以随着数据的增加可以平滑的扩充新的表,原有的数据不需要懂,范围路由的一个比较隐含的缺点就是分布不均匀
- Hahs路由算法 选择某个列(或者某几个列组合也可以)的进行Hash运算,然后根据Hash结果分散到不同的数据库表中,同样根据用户ID为例,假如一开始就规划10个数据库表,路由算法可以简单的用user_id%10的值来表示数据所属的数据库表编号,ID为985的用户放到编号为5的子表中,ID为10086的用户放到编号为6的子表中;Hash 路由算法设计的复杂点主要体现在初始表数量的选取上,表数量太多维护比较麻烦,表数据量太少又可能导致单表性能问题,而用了Hash路由后,增加表的数量非常麻烦,所有数据都要重新分布,Hash路由算法的优缺点和范围路由基本相反,Hash路由算法的优点是表分布比较均匀,缺点是扩充新的表很麻烦,所有数据需要重新分布
- 配置路由 配置路由就是路由表,用一张独立的表来记录路由信息,同样根据用户ID为例,我们新增一张user_router表,这个表包含user_id和table_id两列,根据user_id就可以查询对应的table_id,配置路由设计简单,使用起来非常灵活,尤其是在扩充表的时候,只需要迁移指定书,然后修改路由表就可以。配置路由的缺点就是必须多查询一次,会影响整体的性能;而且路由表本身如果太大,性能同样可能成为瓶颈,如果我们再次将路由表分库分表,则面临一个死循环式的路由算法选择问题
分表操作和分库操作一样,同样会存在一些问题,主要体现在如下几个方面:
- join操作 水平分表后,数据分散到多个表中,如果需要与其他表进行join 查询,需要在业务代码或者数据库中间件中进行多次join查询,然后将结果合并
- count()操作 水平分表后,虽然物理上数据分散到多个表中,但是某些业务逻辑上还是会将这些表当作一个表进行处理,例如,获取记录总数用于分页或展示,水平分表之前用一个count()就能完成的操作,在分表之后就没有那么简单了,常见的处理方式有如下两种:
- count()相加 具体做法就是在业务代码或者数据库中间件中对每个表进行count()操作,然后将结果相加,这种方式实现简单,缺点就是性能比较低
- 记录数表 具体做法就是新建一张表,例如表名为:记录数表,包含table_name,row_count两个字段,每次插入或删除子表数据成功后,都更新记录数表,这种方式获取表记录数的性能要大大优于count()相加方式,因为只需要一次简单的查询就可以获得数据,缺点是复杂度增加不少,对子表的操作要同步操作记录数表,如果一个业务逻辑遗漏了,数据就会不一致;而且针对记录数表的操作和针对子表的操作无法放在同一个事物中进行处理,异常的情况会出现操作子表成功了而操作记录数表示不,同样导致数据不一致,同时,记录数表的方式也增加了数据库的写压力,因为每次针对子表的insert 和 delete操作需要update记录数表,所以对于一些不要去记录数实时保持精确的业务,也可以通过后台定时更新记录数表,定时更新实际上就是count()相加和记录数表的结合,定时通过count()相加计算表的记录数,然后更新记录数表中的数据
3 order by 操作 水平分表后,数据分散到多个子表中,排序操作无法在数据库中完成,只能由业务代码或数据库中间件分表查询美国子表中的数据,然后汇总进行排序
|