关于大型网站技术演进的思考(五)-存储的瓶颈5

上文里我遗留了两个问题,一个问题是数据库做了水平拆分以后,如果我们对主键的设计采取一种均匀分布的策略,那么它对于被水平拆分出的表后续的查询操作将有何种影响,第二个问题就是水平拆分的扩容问题。这两个问题在深入下去,本系列就越来越技术化了,可能最终很多朋友读完后还是没有找到解决实际问题的启迪,而且我觉得这些问题都是像BAT这样巨型互联网公司才会认真思考的,因此本篇我打算换个角度来阐述本文的后续内容。

  这里我们首先要明确一个问题,到底是什么因素促使我们去做数据库的垂直拆分和水平拆分的呢?答案很简单就是业务发展的需求,前文里的水平拆分技术方案基本都是抛弃千变万化的业务规则的限制,尽量将水平拆分的问题归为一个简单的技术实现方案,而纯技术手段时常是看起来很美,但是到了面对现实问题时候,常常会变得那么苍白和无力。

  水平拆分的难题里我还有个难题没有讲述,就是水平拆分后对查询操作的影响,特别是对单表查询的影响,这点估计也是大伙最为关心的问题,今天我不在延着水平拆分的技术手段演进是阐述上文的遗留问题,而是我要把前面提到的技术手段和一些典型场景结合起来探讨如何解决网站存储的瓶颈问题。

  前文中我总结过一个解决存储瓶颈的脉络,具体如下:

  单库数据库-->数据库读写分离-->缓存技术-->搜索技术-->数据的垂直拆分-->数据的水平拆分

  这个脉络给一些朋友产生了误解,就是认为这个过程应该是个串行的过程,其实在实际的场景下这个过程往往是并行的,但是里面有一个元素应该是串行的或者说思考时候有个先后问题,那就是对数据库层的操作,具体如下:

  单库数据库-->数据库读写分离-->数据的垂直拆分-->数据的水平拆分

  而缓存技术和搜索技术在数据库的任意阶段里都可以根据实际的业务需求随时切入其中帮助数据库减轻不必要的压力。例如,当网站的后台数据库还是单库的时候,数据库渐渐出现了瓶颈问题,而这个瓶颈又没有达到需要采取大张旗鼓做读写分离方案的程度,那么我这个时候可以考虑引入缓存机制。不过要合理的使用缓存我们首先要明确缓存本身的特点,这些特点如下所示:

  特点一:缓存主要是适用于读操作,并且缓存的读操作的效率要远远高于从数据库以及硬盘读取数据的效率。

  特点二:缓存的数据是存储在内存当中,因此当系统重启,宕机等等异常场景下,缓存数据就会不可逆的丢失,且无法恢复,因此缓存不能作为可靠存储设备,这就导致一个问题,缓存里的数据必须首先从数据库里同步到内存中,而使用缓存的目的就是为了解决数据库的读操作效率低下的问题,数据库的数据同步到缓存的操作会因为数据库的效率低下而在性能上大打折扣,所以缓存适合的场景是那些固定不变的数据以及业务对实时性变化要求不高的数据。

  根据缓存的上述两个特点,我们可以把数据库里和上述描述类似操作的相关数据迁移到缓存里,那样我们就从数据库上剥离了那些对数据库价值不高的操作,让数据库专心做有价值的操作,这样也是减轻数据库压力的一种手段。

  不过这个手段局限性很强,局限性主要是一台计算机了用于存储缓存的内存的大小都是远远要低于硬盘,并且内存的价格要远贵于硬盘,如果我们将大规模的数据从硬盘往内存迁移,从资源成本和利用率角度考虑性价比还是很低的,因此缓存往往都是用于转存那些不会经常变化的数据字典,以及经常会被读,而修改较少的数据,但是这些数据的规模也是有一定限度的,因此当单库数据库出现了瓶颈时候马上就着手进行读写分离方案的设计性价比还是很高的。

  前文我讲到我们之所以选择数据库读写分离是主要原因是因为数据库的读写比例严重失衡所致,但是做了读写分离必然有个问题不可避免,写库向读库同步数据一定会存在一定的时间差,如果我们想减小读库和写库数据的时间差,那么任然会导致读库因为写的粒度过细而发生部分性能的损失,但是时间差过大,或许又会无法满足实际的业务需求,因此这个时间差的设计一定要基于实际的业务需求合理的设计。

  同步的时间差的问题还是个小问题,也比较好解决,但是如何根据实际的业务需求做读写分离这其实还是非常有挑战性的,这里我举个很常见的例子来说明读写分离的难度问题,我们这里以淘宝为例,淘宝是个C2C的电商网站,它是互联网公司提供一个平台,商家自助接入这个平台,在这个平台上卖东西,这个和线下很多大卖场的模式类似。淘宝是个大平台,它的交易表里一定是要记下所有商户的交易数据,但是针对单个商家他们只会关心自己的网店的销售数据,这就有一个问题了,如果某一个商家要查询自己的交易信息,淘宝就要从成千上万的交易信息里检索出该商家的交易信息,那么如果我们把所有交易信息放在一个交易表里,肯定有商家会有这样的疑问,我的网店每天交易额不大,为什么我查询交易数据的速度和那些大商家一样慢了?那么我们到底该如何是解决这样的场景了?

  碰到这样的情况,当网站的交易规模变大后就算我们把交易表做了读写分离估计也是没法解决实际的问题,就算我们做的彻底点把交易表垂直拆分出来估计还是解决不了问题,因为一个业务数据库拥有很多张表,但是真正压力大的表毕竟是少数,这个符合28原则,而数据库大部分的关键问题又都是在那些数据压力大的表里,就算我们把这些表单独做读写分离甚至做垂直拆分,其实只是把数据库最大的问题迁移出原来数据库,而不是在解决该表的实际问题。

  如果我们要解决交易表的问题我们首先要对交易表做业务级的拆分,那么我们要为交易表增加一个业务维度:实时交易和历史交易,一般而言实时交易以当天及当天24小时为界,历史交易则是除去当天交易外的所有历史交易数据。实时交易数据和历史交易数据有着很大不同,实时交易数据读与写是比较均衡的,很多时候估计写的频率会远高于读的频率,但是历史交易表这点上和实时交易就完全不同了,历史交易表的读操作频率会远大于写操作频率,如果我们将交易表做了实时交易和历史交易的拆分后,那么读写分离方案适合的场景是历史交易查询而非实时交易查询,不过历史交易表的数据是从实时交易表里同步过来的,根据这两张表的业务特性,我们可以按如下方案设计,具体如下:

  我们可以把实时交易表设计成两张表,把它们分别叫做a表和b表,a表和b表按天交替进行使用,例如今天我们用a表记录实时交易,明天我们就用b表记录实时交易,当然我们事先可以用个配置表记录今天到底使用那张表进行实时交易记录,为什么要如此麻烦的设计实时交易表了?这么做的目的是为了实时交易数据同步到历史数据时候提供便利,一般我们会在凌晨0点切换实时交易表,过期的实时交易表数据会同步到历史交易表里,这个时候需要数据迁移的实时交易表是全表数据迁移,效率是非常低下,假如实时交易表的数据量很大的时候,这种导入同步操作会变得十分耗时,所以我们设计两张实时交易表进行切换来把数据同步的风险降到最低。由此可见,历史交易表每天基本都只做一次写操作,除非同步出了问题,才会重复进行写操作,但是写的次数肯定是很低的,所以历史交易表的读写比例失衡是非常严重的。不过实时交易表的切换也是有技术和业务风险的,为了保证实时交易表的高效性,我们一般在数据同步操作成功后会清空实时交易表的数据,但是我们很难保证这个同步会不会有问题,因此同步时候我们最好做下备份,此外,两个表切换的时候肯定会碰到这样的场景,就是有人在凌晨0点前做了交易,但是这个交易是在零点后做完,假如实时交易表会记录交易状态的演变过程,那么在切换时候就有可能两个实时表的数据没有做好接力,因此我们同步到历史交易表的数据一定要保持一个原则就是已经完成交易的数据,没有完成的交易数据两张实时交易还要完成一个业务上的接力,这就是业界常说的数据库日切的问题。

  历史交易表本身就是为读使用的,所以我们从业务角度将交易表拆分成实时交易表和历史交易表本身就是在为交易表做读写分离,居然了设计了历史交易表我们就做的干脆点,把历史交易表做垂直拆分,将它从原数据库里拆分出来独立建表,随着历史交易的增大,上文里所说的某个商户想快速检索出自己的数据的难题并没有得到根本的改善,为了解决这个难题我们就要分析下难题的根源在那里。这个根源很简单就是我们把所有商户的数据不加区别的放进了一张表里,不管是交易量大的商户还是交易量小的商户,想要查询出自己的数据都要进行全表检索,而关系数据库单表记录达到一定数据量后全表检索就会变的异常低效,例如DB2当数据量超过了1亿多,mysql单表超过了100万条后那么全表查询这些表的记录都会存在很大的效率问题,那么我们就得对历史交易表进一步拆分,因为问题根源是单表数据量太大了,那我们就可以对单表的数据进行拆分,把单表分成多表,这个场景就和前面说的水平拆分里把原表变成逻辑表,原表的数据分散到各个独立的逻辑表里的方式一致,不过这里我们没有一开始做水平拆分,那是会把问题变麻烦,我们只要在一个数据库下对单表进行拆分即可,这样也能满足我们的要求,并且避免了水平拆分下的跨库写作的难题。接下来我们又有一个问题了那就是我们按什么维度拆分这张单表呢?

  我们按照前文讲到的水平拆分里主键设计方案执行吗?当然不行哦,因为那些方案明显提升不了商户检索数据的效率问题,所以我们要首先分析下商户检索数据的方式,商户一般会按这几个维度检索数据,这些维度分别是:商户号、交易时间、交易类型,当然还有其他的维度,我这里就以这三个维度为例阐述下面的内容,商户查询数据效率低下的根本原因是全表检索,其实商户查询至少有一个维度那就是商户号来进行查询,如果我们把该商户的数据存入到一张单独的表里,自然查询的效率会有很大的提升,但是在实际系统开发里我们很少通过商户号进行拆分表,这是为什么呢?因为一个电商平台的商户是个动态的指标,会经常发生变化,其次,商户号的粒度很细,如果使用商户号拆分表的必然会有这样的后果那就是我们可能要频繁的建表,随着商户的增加表的数量也会增加,造成数据的碎片化,同时不同的商户交易量是不一样的,按商户建表会造成数据存储的严重不平衡。如果使用交易类型来拆分表,虽然维度的粒度比商户号小,但是会造成数据的分散化,也就是说我们查询一个商户的全部交易数据会存在很大问题。由此可见拆表时候如何有效的控制维度的粒度以及数据的聚集度是拆分的关键所在,因为使用交易时间这个维度就会让拆分更加合理,不过时间的维度的设计也是很有学问的,下面我们看看腾讯分析的维度,如下所示:

  腾讯分析的维度是今天这个其实相当于实时交易查询,除此之外都是对历史数据查询,它们分为昨天、最近7天和最近30天,我们如果要对历史交易表进行拆分也是可以参照腾讯分析的维度进行,不过不管我们选择什么维度拆分数据,那么都是牺牲该维度成全了其他维度,例如我们按腾讯分析的维度拆分数据,那么我们想灵活使用时间查询数据将会受到限制。

  我们把历史交易数据通过交易时间维度进行了拆分,虽然得到了效率提升,但是历史交易数据表是个累积表,随着时间推移,首先是月表,接下来是周表都会因为数据累积产生查询效率低下的问题,这个时候我们又该如何解决了?这个时候我们需要再引进一个维度,那么这个时候我们可以选择商户号这个维度,但是商户号作为拆分维度是有一定问题的,因为会造成数据分布不均衡,那么我们就得将维度的粒度由小变粗,其实一个电商平台上往往少数商户是完成了大部分电商平台的交易,因此我们可以根据一定指标把重要商户拆分出来,单独建表,这样就可以平衡了数据的分布问题。

  我们总结下上面的案例,我们会得到很多的启迪,我将这些启迪总结如下:

  启迪一:数据库的读写分离不是简单的把主库数据导入到读库里就能解决问题,读数据库和写数据的分离的目的是为了让读和写操作不能相互影响效率。

  启迪二:解决读的瓶颈问题的本质是减少数据的检索范围,数据检索的范围越小,读的效率也就越高;

  启迪三:数据库的垂直拆分和水平拆分首先不应该从技术角度进行,而是通过业务角度进行,如果数据库进行业务角度的水平拆分,那么拆分的维度往往是要根据该表的某个字段进行的,这个字段选择要有一定原则,这个原则主要是该字段的维度的粒度不能过细,该字段的维度范围不能经常的动态发生变化,最后就是该维度不能让数据分布严重失衡。

  回到现实的开发里,对于一个数据库做拆表,分表的工作其实是一件很让人恼火的工作,这主要是有以下原因所造成的,具体如下所述:

  原因一:一个数据库其实容纳多少张表是有一定限制的,就算没有超过这个限制,如果原库本来有30张表,我们拆分后变成了60张,接着是120张,那么数据库本身管理这么多表也会消耗很多性能,因此公司的DBA往往会控制那些过多分表的行为。

  原因二:每次拆表后,都会牵涉到历史数据的迁移问题,这个迁移风险很大,迁移方案如果设计的不完善可能会导致数据丢失或者损坏,如果关键数据发生了丢失和损坏,结果可能非常致命。因此在设计数据库分表分库方案时候我们要尽量让受影响的数据范围变得最小。

  原因三:每次拆表和分表都会让系统的相关方绷紧神经,方案执行后,会有很长时间的监控和观察期,所以拆数据库时常是一件令人讨厌的事情。

  原因四:为了保证新方案执行后确保系统没有问题,我们常常会让新旧系统并行运行一段时间,这样可以保证如果新方案出现问题,问题的影响面最低,但是这种做法也有一个恶果就是会导致数据迁移方案要进行动态调整,从而增加迁移数据的风险

  因此当公司不得不做这件事情时候,公司都会很自然去考虑第三种解决方案,第三种解决方案是指尽量不改变原数据库的功能,而是另起炉灶,使用新技术来解决我们的问题,例如前文所说的搜索技术解决数据库like的低效问题就是其中方案之一,该方案只要我们将数据库的表按一定时间导入到文件系统,然后对文件建立倒排索引,让like查询效率更好,这样就不用改变原数据库的功能,又能减轻数据库的压力。

  现在常用的第三种解决方案就是使用NoSql数据库,NoSql数据库大多都是针对文件进行的,因此我们可以和使用搜索引擎那样把数据导入到文件里就行了,NoSql基本都采用Key/Value这种简单的数据结构,这种数据结构和关系数据库比起来更加的灵活,对原始数据的约束最少,所以在NoSql数据库里建表我们可以很灵活的把列和行的特性交叉起来用,这句话可能很多人不太理解,下面我举个例子解释下,例如hadoop技术体系里的hbase,hbase是一个基于列族的数据库,使用hbase时候我们就可以通过列来灵活的拆分数据,比如我们可以把中国的省份作为一个列,将该省份的数据都放入到这个列下面,在省这个维度下我们可以接着在定义一个列的维度,例如软件行业,属于软件行业的数据放在这个列下面,最终提供用户查询时候我们就可以减少数据检索的范围,最终达到提升查询效率的目的。由此可见当我们用惯了关系数据库后,学习像hbase这样的Nosql数据库我们会非常的不适应,因为关系数据库的表有固定模式,也就是我们常说的结构化数据,当表的定义好了后,就算里面没有数据,那么这个结构也就固定了,我们使用表的时候都是按这个模型下面,我们几乎感觉不到它,但是到了hbase的使用就不同了,hbase使用时候我们都在不停的为数据增加结构化模型,而且这个维度是以列为维度的,而关系数据库里列确定后我们使用时候是无法改变的,这就是学习hbase的最大困难之一。Hbase之所以这么麻烦的设计这样的计算模型,终极目的就是为了让海量数据按不同维度存储起来,使用时候尽全力检索数据检索的数量,从而达到海量数据快速读取的目的。

在讲数据库水平拆分时候,我列出了水平拆分数据库需要解决的两个难题,它们分别是主键的设计问题和单表查询的问题,主键问题前文已经做了比较详细的讲述了,但是第二个问题我没有讲述,今天我将会讲讲如何解决数据表被垂直拆分后的单表查询问题。

  要解决数据表被水平拆分后的单表查询问题,我们首先要回到问题的源头,我们为什么需要将数据库的表进行水平拆分。下面我们来推导下我们最终下定决心做水平拆分表的演进过程,具体如下:

  第一个演进过程:进行了读写分离的表在数据增长后需要进行水平拆分吗?回答这个疑问我们首先要想想进行读写分离操作的表真的是因为数据量大吗?答案其实是否定的。最基本的读写分离的目的是为了解决数据库的某张表读写比率严重失衡的问题,举个例子,有一张表每天会增加1万条数据,也就是说我们的系统每天会向这张表做1万次写的操作,当然也有可能我们还会更新或者删除这张表的某些已有的记录,这些操作我们把它归并到写操作,那么这张表一天我们随意定义个估值吧2万5千次写操作,其实这种表的数据量并不大,一年下来也就新增的几百万条数据,一个大型的商业级别的关系数据库,当我们为表建立好索引和分区后,查询几百万条数据它的效率并不低,这么说来查询的效率问题还不一定是读写分离的源头。其实啊,这张表除了写操作每天还承受的读操作可能会是10万,20万甚至更高,这个时候问题来了,像oracle和mysql这样鼎鼎大名的关系数据库默认的最大连接数是100,一般上了生产环境我们可能会设置为150或者200,这些连接数已经到了这些关系数据库的最大极限了,如果再加以提升,数据库性能会严重下降,最终很有可能导致数据库由于压力过大而变成了一个巨锁,最终导致系统发生503的错误,如是我们就会想到采用读写分离方案,将数据库的读操作迁移到专门的读库里,如果系统的负载指标和我列举的例子相仿,那么迁移的读库甚至不用做什么垂直拆分就能满足实际的业务需求,因为我们的目的只是为了减轻数据库的连接压力。

  第二个演进过程:随着公司业务的不断增长,系统的运行的压力也越来越大了,我们已经了解了系统的第一个瓶颈是从存储开始了,如是我们开始谈论方案如何解决存储的问题,这时我们发现我们已经做了读写分离,也使用了缓存,甚至连搜索技术也用上了,那么下个阶段就是垂直拆分了,垂直拆分很简单就是把表从数据库里拆出来,单独建库建表,但是这种直截了当的方案想想就能感到这样的做法似乎没有打中系统的痛点,那么系统的痛点到底是什么呢?根据数据库本身的特性,我们会发现痛点主要是三个方面组成:

  第一个方面:数据库的连接数的限制。原库的某些表可能承担数据库80%的连接,极端下甚至可以超过90%的连接,而且这些表的业务操作十分的频繁,当其他小众业务的表需要进行操作时候,搞不好因为连接数被全部占用而不得不排队等待空闲连接的出现,那么这个时候我们就会考虑把这张表做垂直拆分,这样就减轻了原数据库连接的压力,使得数据库连接负载变得比较均衡。

  第二个方面是数据库的读操作,第三个方面是数据库的写操作,虽然把读和写分成两个方面,但是这两个方面在我们做垂直拆分时候要结合起来考虑。首先我们要分析下数据库的写操作,单独的写操作效率都是很高的,不管我们的写是单条记录的写操作,还是批量的写操作,这些写操作的数据量就是我们要去写的数据的大小,因此控制写的数据量的大小是一件很容易很天然的操作,所以这些操作不会造成数据库太大负担,详细点的话,对于数据库而言,新增操作无非是在原来数据后面追加些记录,而修改操作或者删除操作一般都是通过建立了高效索引的字段来定位数据后再进行的操作,因此它的性能也是非常高的。而读操作看起来比写操作简单(例如:读操作不存在像事务这些乌七八糟因素的干扰),但是当读操作面对海量数据时候就严重挑战着数据库和硬盘的极限能力,因此读操作很容易产生瓶颈问题,而且这个瓶颈不管问题表是否读写失衡都会面临的。前文里我详细列举了一个交易表设计的案例,其中我们可以看到数据库垂直拆分在实际应用里的运用,在例子里我们首先根据业务特点将交易表分成了实时交易表和历史交易表,这个做法其实就是将原交易表的读和写进行分离,但是这种分离和纯粹的读写分离相比会更加有深意,这个深意就是拆分实时和历史交易表也就是在分拆原表的读写操作的关联性,换句话说,如果我们不这么做的话,那么交易表的每次写和每次读几乎等价,这样我们没法单独解决读的性能问题,分出了历史交易表后我们再对历史交易表来做读的优化,那么这也不会影响到写操作,这样把问题的复杂度给降低了。在案例里我们对历史交易表进行了业务级别的水平拆分,但是这个拆分是以如何提升读的效率进行的,因此前文讲到的水平拆分里主键设计方案基本上派不上用场,因为这两种水平拆分的出发点是不同的,那么使用的手段和达到效果也将不一样。

  由上所述,我们可以把数据库的水平拆分重新定义下,我在这几篇文章里一直讲述的水平拆分本质是从数据库技术来定义的,我把它们称为狭义的水平拆分,与狭义相对的就是广义的水平拆分,例如上文例子里把交易表根据业务特性分为实时交易表和历史交易表,这种行为也是一种水平拆分,但是这个拆分不会遵守我前面讲到主键设计方案,但是它的确达到水平拆分的目的,所以这样的水平拆分就属于广义的水平拆分了。

  第三个演进过程:到了三个演进过程我们就会考虑到真正的水平拆分了,也就是上面提到的狭义的水平拆分了,狭义的水平拆分执行的理由有两个,一个那就是数据量太大了,另一个是数据表的读写的关联性很难进行拆分了,这点和垂直拆分有所不同,做垂直拆分的考虑不一定是因为数据量过大,例如某种表数据量不大,但是负载过重,很容易让数据库达到连接的极限值,我们也会采取垂直拆分手段来解决问题,此外,我们想减轻写操作和读操作的关联性,从而能单独对有瓶颈的写操作或读操作做优化设计,那么我们也会考虑到垂直拆分,当然数据量实在是太大的表我们想优化,首先也会考虑到垂直拆分,因为垂直拆分是针对海量数据优化的起始手段,但是垂直拆分可不一定能解决海量数据的问题。

  狭义水平拆分的使用的前提是因为数据量太大,到底多大了,我们举个例子来说明下,假如某个电商平台一天的交易笔数有2亿笔,我们用来存储数据的关系数据库单表记录到了5千万条后,查询性能就会严重下降,那么如果我们把这两亿条数据全部存进这个数据库,那么随着数据的累积,实时交易查询基本已经没法正常完成了,这个时候我们就得考虑把实时交易表进行狭义的水平拆分,狭义的水平拆分首先碰到的难点就是主键设计的问题,主键设计问题也就说明狭义水平拆分其实解决的是海量数据写的问题,如果这张表读操作很少,或者基本没有,这个水平拆分是很好设计的,但是一张表只写不读,对于作为业务系统的后台数据库那基本是非常罕见的,。

  前文讲到的主键设计方案其实基本没有什么业务上的意义,它解决的主要问题是让写入的数据分布均匀,从而能合理使用存储资源,但是这个合理分布式存储资源却会给查询操作带来极大的问题,甚至有时可以说狭义水平拆分后数据查询变得困难就是由这种看起来合理的主键设计方案所致。

  我们还是以实时交易表的实例来说明问题,一个电商平台下会接入很多不同的商户,但是不同的商户每天产生的交易量是不同,也就是说商户的维度会让我们使交易数据变得严重的不均衡,可能电商平台下不到5%的商户完成了全天交易量的80%,而其他95%的商户仅仅完成20%的交易量,但是作为业务系统的数据表,进行读操作首先被限制和约束的条件就是商户号,如果要为我们设计的实时交易表进行狭义的水平拆分,做拆分前我们要明确这个拆分是由交易量大的少量商户所致,而不是全部的商户所致的。如果按照均匀分布主键的设计方案,不加商户区分的分布数据,那么就会发生产生少量交易数据的商户的查询行为也要承受交易量大的商户数据的影响,而能产生大量交易数据的商户也没有因为自己的贡献度而得到应有的高级服务,碰到这个问题其实非常好解决,就是在做狭义水平拆分前,我们先做一次广义的水平拆分,把交易量大的商户交易和交易量小的商户交易拆分出来,交易量小的商户用一张表记录,这样交易量小的商户也会很happy的查询出需要的数据,心里也是美滋滋的。接下来我们就要对交易量大的商户的交易表开始做狭义的水平拆分了,为这些重点商户做专门的定制化服务。

  做狭义水平拆分前,我们有个问题需要过一下,在狭义水平拆分前我们需要先做一下广义的水平拆分吗?这个我这里不好说,具体要看实际的业务场景,但是针对我列举的实时交易的例子而言,我觉得没那个必要,因此拆分出的重点商户交易量本来就很大,每个都在挑战数据库读能力的极限,更重要的是实时交易数据的时间粒度已经很小了,再去做广义水平拆分难度很大,而且很难做好,所以这个时候我们还是直接使用狭义的水平拆分。拆分完毕后我们就要解决查询问题了。

  做实时查询的标准做法就是分页查询了,在讲述如何解决分页查询前,我们看看我们在淘宝里搜索【衣服】这个条件的分页情况,如下图所示:

 

  我们看到一共才100页,淘宝上衣服的商品最多了,居然搜索出来的总页数只有100页,这是不是在挑战我们的常识啊,淘宝的这个做法也给我们在实现水平拆分后如何做分页查询一种启迪。要说明这个启迪前我们首先要看看传统的分页是如何做的,传统分页的做法是首先使用select count(1) form table这样的语句查询出需要查询数据的总数,然后再根据每页显示的记录条数,查询出需要显示的记录,然后页面根据记录总数,每页的条数,和查询的结果来完成分页查询。回到我们的交易表实例里,有一个重要商户在做实时交易查询,可是这个时候该商户已经产生了1千万笔交易了,假如每页显示10条,记录那么我们就要分成100万页,这要是真显示在页面上,绝对能让我们这些开发人员像哥伦布发现新大陆那样惊奇,反正我见过的最多分页也就是200多页,还是在百度搜索发现的。其实当数据库一张表的数据量非常大的时候,select的count查询效率就非常低下,这个查询有时也会近似个全表检索,所以count查询还没结束我们就会失去等待结果的耐心了,更不要是说等把数据查询出来了,所以这个时候我们可以学习下淘宝的做法,当商户第一次查询我们准许他查询有限的数据。我自己所做的一个项目的做法就是这样的,当某个商户的交易量实在是很大时候我们其实不会计算数据的总笔数,而是一次性查询出1000条数据,这1000条数据查询出来后存入到缓存里,页面则只分100页,当用户一定要查询100页后的数据,我们再去追加查询,不过实践下来,商户基本很少会查询100页后的数据,常常看了5,6页就会停止查询了。不过商户也时常会有查询全部数据的需求,但是商户有这种需求的目的也不是想在分页查询里看的,一般都是为了比对数据使用的,这个时候我们一般是提供一个发起下载查询全部交易的功能页面,商户根据自己的条件先发起这样的需求,然后我们系统会在后台单独起个线程查询出全部数据,生成一个固定格式的文件,最后通过一些有效手段通知商户数据生成好了,让商户下载文件即可。

  对于进行了狭义水平拆分的表做分页查询我们通常都不会是全表查询,而是抽取全局的数据的一部分结果呈现给用户,这个做法其实和很多市场调查的方式类似,市场调查我们通常是找一些样本采集相关数据,通过分析这些样本数据推导出全局的一个发展趋势,那么这些样本选择的合理性就和最终的结论有很大关系,回到狭义水平拆分的表做分页查询,我们为了及时满足用户需求,我们只是取出了全部数据中的一部分,但是这一部分数据是否满足用户的需求,这个问题是很有学问的,如果是交易表,我们往往是按时间先后顺序查询部分数据,所以这里其实使用到了一个时间的维度,其他业务的表可能这个维度会不一样,但肯定是有个维度约束我们到底返回那些部分的数据。这个维度可以用一个专有的名词指代那就是排序,具体点就是要那个字段进行升序还是降序查询,看到这里肯定有人会有异议,那就是这种抽样式的查询,肯定会导致查询的命中率的问题,即查出来的数据不一定全部都是我们要的,其实要想让数据排序正确,最好就是做全量排序,但是一到全量排序那就是全表查询,做海量数据的全表排序查询对于分页这种场景是无法完成的。回到淘宝的例子,我们相信淘宝肯定没有返回全部数据,而是抽取了部分数据分页,也就是淘宝查询时候加入了维度,每个淘宝的店家都希望自己的商户放在搜索的前列,那么淘宝就可以让商家掏钱,付了钱以后淘宝改变下商家在这个维度里的权重,那么商家的商品就可以排名靠前了。

  狭义水平拆分的本身对排序也有很大的影响,水平拆分后我们一个分页查询可能要从不同数据库不同的物理表里去取数据,单表下我们可以先通过数据库的排序算法得到一定的数据,但是局部的排序到了全局可能就不正确了,这个又该怎么办了?其实由上面内容我们可以知道要满足对海量数据的所有查询限制是非常难的,时常是根本就无法满足,我们只能做到尽量多满足些查询限制,也就是海量查询只能做到尽量接近查询限制的条件,而很难完全满足,这个时候我前面提到的主键分布方案就能起到作用了,我们在设计狭义水平拆分表主键分布时候是尽量保持数据分布均衡,那么如果我们查询要从多张不同物理表里取的时候,例如我们要查1000条数据,而狭义水平拆分出了两个物理数据库,那么我们就可以每个数据库查询500条,然后在服务层归并成1000条数据,在服务层排序,这种场景下如果我们的主键设计时候还包含点业务意义,那么这个排序的精确度就会得到很大提升。假如用户对排序不敏感,那就更好做了,分页时候如果每页规定显示10条,我们可以把10条数据平均分配给两个数据库,也就是显示10条A库的数据,再显示5条B库的数据。

  看到这里有些细心的朋友可能还会有疑问,那就是居然排序是分页查询的痛点,那么我们可以不用数据库查询,而使用搜索技术啊,NoSql数据库啊,的确这些技术可以更好的解决分页问题,但是关系数据库过渡到搜索引擎和NoSql数据库首先需要我们转化数据,而狭义的水平拆分的数据表本身数据量很大,这个转化过程我们是没法快速完成的,如果我们对延时容忍度那么高,其实我们就没必要去做数据库的狭义水平拆分了。这个问题反过来说明了使用狭义拆分数据表的业务场景,那就是:针对数据量很大的表同时该表的读写的关联性是没法有效拆分的。

  最后我要讲的是,如果系统到了狭义水平拆分都没法解决时候,我们就要抛弃传统的关系数据方案了,将该业务全部使用NoSql数据库解决或者像很多大型互联网公司那样,改写开源的mysql数据库。文章写道这里,我还是想说一个观点,如果一个系统有很强烈需求去做狭义的水平拆分,那么这个公司的某个业务那肯定是非常的大了,所以啊,这个方案以公司为单位应该有点小众了。

原文地址:https://www.cnblogs.com/NullPointException/p/5053614.html