存储高性能[关系型数据库]

关系型数据库由于其ACID的特性和功能强大的SQL能力,目前仍旧是各种业务系统关键且核心的存储系统,很多场景下高性能的设计核心部分就是关系型数据库的设计,尤其是互联网时代,海量用户加上海量数据的特点,单个数据库服务器已经难以满足业务需求,必须考虑数据库集群的方式提升性能,高性能数据库集群的第一种方式是读写分离,其本质是将访问亚丽分散到集群中的多个节点,但没有分三分存储压力;第二种方式是分库分表,既可以分散访问压力,又可以分散存储压力

读写分离

在这里插入图片描述

读写分离的基本原理是将数据库读写操作分散到不同的节点上:

  • 数据库服务器搭建主从集群,一主一丛或一主多从
  • 数据库主机负责读写操作,从机之负责读操作
  • 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据
  • 业务服务器将写操作发给数据库主机,将读操作发给数据库从机

读写分离的实现逻辑并不复杂,而实际应用过程中需要应对的是复制延迟带来的复杂性,以MySQL为例,主从复制延迟可能1秒,如果大量数据同步延迟达到1分钟也很常见,这种情况就带来了一个问题业务服务器将数据写入到数据库主服务器,应用程序立刻进行读取,此时读操作访问的是从机,主机还没有将数据复制过来,到从机读取数据是读不到最新数据的;再距离注册账号的功能,注册完立即登陆是会得到类似“你还没有注册”的提示的

解决主从复制延迟

将写操作后的读操作发送给数据库主服务器

例如注册账号完成后,立即登陆时读取账号的读操作也要发给数据库主服务器,这种方式与业务强绑定,对业务的侵入和影响较大

读从机失败后再读一次主机

这也就是常说的“二次读取”,二次读取和业务无绑定,对底层访问数据库的API进行封装,实现代价比较小,缺点在于二次读取过多,仍旧会增加主机的读操作压力

关键业务读写操作全部指向主机,非关键业务采用读写分离

分库分表

读写分离分散了数据库读写操作的压力,但没有分散存储压力,数据量达到千万级以上的时候,单台数据库服务器的存储能力会成为系统的瓶颈,主要体现在如下三个方面:

  • 数据量太大,读写的性能就会下降,即使有索引,索引也会变得很大,性能同样会下降
  • 数据文件会变得很大,数据库备份和恢复需要消耗很长的时间
  • 数据文件越大,极端情况下丢失数据的风险越高
    因此,单个数据库服务器存储的数据量不能太大,需要控制在一定的范围,需要将存储分散到多台数据库服务器,也就是分库分表

业务分库

业务分库是指按照业务模块将数据分散到不同的数据库服务器
在这里插入图片描述

分库虽然分散了存储和访问压力,但同时也带来了新的问题

join操作问题

业务分库后,原本在同一个数据库中的表分散到不同的数据库中,导致无法使用SQL的join查询

事物问题

原本在同一个数据库中不同的表可以在同一个事物中修改,业务分库后,表分散到不同的数据库中,无法通过事物统一修改,虽然数据库提供了分布式事务的解决方案例如MySQL的XA,但性能比较差,与高性能存储的目标是相违背的

成本问题

业务分库同时带来了成本的代价,一台的事变成了三台的事

分表

将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,随着业务的发展同一业务的单表数据也会达到单台数据库服务器的处理瓶颈,单表拆分有两种方式垂直分表和水平分表

  • 表的垂直切分:将一个表切成两个,这两个表的记录相同但包含不同的列,例如一个表包含ID、name、age、sex列;另一个表包含ID、nickname、description列
  • 表的水平切分:将一个表切成两个,这两个表的列相同但包含不同的行数据,例如两个表都包含ID、name、age、sex、nickname,description列,但一个表包含的是ID从1到999999的行数据,另一个表包含的是ID从100000到99999999的行数据
  • 实际上架构设计过程中并不局限切分次数,一个表可以切两次,也可以多次
  • 单表切分后的表是否需要分散在不同的数据库服务器中,可以根据实际切分效果来确定,并不强制要求单表切分变成多表后一定要分散到不同的数据库中,原因在于单表切分为多表后,新的表即使在同一个数据库服务器中,也会带来可观的性能提升,如果单表拆分后单台服务器依然无法满足性能要求,可以再进行分库

垂直分表

  • 垂直分表适合将表中某些不常用且占了大量空间的列拆分出去,例如用户在筛选其他用户的时候,主要是用age和sex两个字段进行查询,而nickname和description两个字段主要用于展示,一般不会用到业务查询中使用,这个时候将nickname和description独立到另外一张表中,这样在查询age和sex时,就会带来性能提升
  • 垂直分表引入的复杂性主要体现在表操作的数量要增加,例如分表前一次查询就可以获取name、age、sex、nickname、description,现在需要查询两次,一次查询获取name、age、sex,另一次查询获取nickname、description

水平分表

水平分表适合表行数特别大的表,通常单表行数超过5000万就必须进行分表,但不绝对标准需要视情况而定做取舍,但单表行数达到千万级架构师就必须警觉,水平分表比垂直分表更复杂,主要体现在如下几个方面

路由

水平分表后,某条数据具体属于哪个切分后的字表,需要增加路由算法进行计算,常见的路由算法有

范围路由
  • 选取有序的数据列(例如,整型、时间戳等)作为路由的条件,不同分段分散到不同的数 据库表中 以最常见的用户ID为例,路由算法可以按照1000000 的范围大小进行分段,1~999999 放到A数据库的表中,1000000~1999999放到B数据库的表中 ,以此类推
  • 范围路由设计的复杂点主要体现在分段大小的选取上,分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在100 万至 2000 万之间具体需要根据业务选取合适的分段大小
  • 范围路由的优点是可以随着数据的增加平滑地扩充新的表。例如,现在的用户是100 万,如果增加到 1000万,只需要增加新的表就可以了,原有的数据不需要动
  • 范围路由的一个比较隐含的缺点是分布不均匀,假如按照 1000万来进行分表,有可能某个分段实际存储的数据只有 1000 条, 而另外一个分段实际存储的数据有900万条
Hash路由
  • 选取某个列(或者某几个列组合也可以〉的值进行 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()操作,然后将结果相加 。这种方式实现简单,缺点就是性能比较低 ,例如,水平分表后切分为20张表,则要进行20次count(*) 操作,如果串行的话, 则可能需要几秒钟才能得到结果。

记录数表
  • 具体做法是新建一张表,假如表名为"记录数表",包含table_name、row_count两个字段,每次插入或删除子表数据成功后,都更新“记录数表”
  • 这种方式获取表记录数的性能要大大优于count()相加的方式,因为只需要一次简单查询就可以获取数据
  • 缺点是复杂度增加不少,对子表的操作要同步操作“记录数表”,如果有一个业务逻辑遗漏了,数据就会不一致;且针对“记录数表”的操作和针对子表的操作无法放在同一事务中进行处理,异常的情况下会出现操作子表成功了而操作记录数表失败,同样会导致数据 不一致。
  • 此外,记录数表的方式也增加了数据库的写压力,因为每次针对子表的insert和delete操作都要update记录数表,所以对于一些不要求记录数实时保持精确的业务,也可以通过后台定时更新记录数表。定时更新实际上就是“ count()相加”和“记录数表”的结合,即定时通过count()相加计算表的记录数,然后更新记录数表中的数据。
order by 操作

水平分表后,数据分散到过个字表中,排序操作无法在数据库中完成,只能由业务代码或数据库中间件分别查询每个字表中的数据,然后汇总再排序

实现方法

读写分离需要将读写操作区分开来,然后访问不同的数据库服务器;分库分表需要根据不同的数据访问不同的数据库服务器,两者本质上都是一种分配机制,即将不同的 SQL 语句发送 到不同 的 数据库服务器,常见的分配实现方式有两种:程序代码封装和中间件封装

程序代码封装

程序代码封装指在代码中抽象一个数据访问层来实现读写分离、分库分表。例如,基于Hibernate进行简单封装,就可以实现读写分离 , 基本架构如下图所示

在这里插入图片描述

程序代码封装的方式具备如下几个特点:

  • 实现简单 , 而且可以根据业务做较多定制化的功能
  • 每个编程语言都需要自己实现一次,无法通用 ,如果一个业务包含多个编程语言写的多个子系统 ,则重复开发 的工作量比较大
  • 故障情况下,如果主从发生切换, 则可能需要所有系统都修改配置井重启 。

目前开源的实现方案中,淘宝的TDDL ( Taobao Distributed Data Layer)是比较有名的。它是一个通用数据访问层,所有功能封装在jar包中供业务代码调用。其基本原 理是一个基于集中式配置的jdbc datasource实现,具有主备、读写分离、动态数据库配置等功 能,基本架构如下图所示
在这里插入图片描述

中间件封装

中间件封装指的是独立一套系统 出来,实现读写分离和分库分表操作,中间件对业务服务器提供 SQL兼容的协议,对于业务服务器来说,访问中间件和访问数据库没有区别,事实上在业务服务器看来,中间件就是一个数据库服务器。例如,中间件实现读写分离的基本架构如下图所示

在这里插入图片描述
数据库中间件的方式具备如下特点:

  • 能够支持多种编程语言 ,因为数据库中间件对业务服务器提供的是标准SQL接口
  • 数据库中间件要支持完整的SQL语法和数据库服务器的协议(例如,MySQL客户端和服务器的连接协议),这是一个很复杂的事情,而且细节特别多,很容易出现bug
  • 数据库中间件自己不执行真正的读写操作,但所有的数据库操作请求都要经过中间件,中间件的性能要求也很高
  • 数据库主从切换对业务服务器无感知,数据库中间件可以探测数据库服务器的主从状态,例如,向某个测试表写入1条数据,成功的就是主机,失败的就是从机。

由于数据库中间件的复杂度要比程序代码封装高出一个数量级, 一般情况下建议采用程序语言封装的方式,或者使用成熟的开源数据库中间件,这个系统一旦做好,接入的业务系统越多,节省的程序开发投入也就越多价值也越大

目前的开源数据库中间件方案中, MySQL官方先是提供了mysql-proxy ,但 mysql-proxy一直没有正式 GA ,现在 MySQL官方推荐 MySQL Router,MySQL Router 的主要功能有读写分离、故障自动切换、负载均衡、连接池等,其基本架构如下图所示
在这里插入图片描述
奇虎360公司也开源了自己的数据库中间件Atlas,它基于MySQL proxy实现的,基本架构如下图所示
在这里插入图片描述

Atlas 是一个位于应用程序与 MySQL 之间的中间件 。 在后端DB看来,Atlas相当于连接它的客户端,在前端应用看来,Atlas相当于一个DB,Atlas作为服务端与应用程序通信,它实现了MySQL的客户端和服务端协议,同时作为客户端与MySQL通信。它对应用程序屏蔽了DB的细节,同时为了降低MySQL负担,它还维护了连接池

实现复杂度

  • 读写分离实现时只要识别SQL操作是读操作还是写操作即可,通过简单地判断SELECT、UPDATE、INSERT、DELETE几个关键字就可以实现
  • 分库分表的实现除了要判断操作类型, 还要判断SQL中的具体需要操作的表、操作函数(例如, count函数)、order by 、 group by 操作等,然后根据不同的操作进行不同的处理 。 例如order by操作需要先从多个库查询各个库的数据,然后重新执行order by才能得到最终的结果 相比来说,分库分表的实现要复杂得多

猜你喜欢

转载自blog.csdn.net/dawei_yang000000/article/details/108520454
今日推荐