关于大型网站技术演进的思考--存储的瓶颈(转)
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于大型网站技术演进的思考--存储的瓶颈(转)相关的知识,希望对你有一定的参考价值。
(一)第一部分
前不久公司请来了位互联网界的技术大牛跟我们做了一次大型网站架构的培训,两天12个小时信息量非常大,知识的广度和难度也非常大,培训完后我很难完整理出全部听到的知识,今天我换了个思路是回味这次培训,这个思路就是通过本人目前的经验和技术水平来思考下大型网站技术演进的过程。
首先我们要思考一个问题,什么样的网站才是大型网站,从网站的技术指标角度考虑这个问题人们很容易犯一个毛病就是认为网站的访问量是衡量的指标,懂点行的人也许会认为是网站在单位时间里的并发量的大小来作为指标,如果按这些标准那么像hao123这样的网站就是大型网站了,如下图所示:
其实这种网站访问量非常大,并发数也非常高,但是它却能用最为简单的web技术来实现:我们只要保持网站的充分的静态化,多部署几台服务器,那么就算地球上所有人都用它,网站也能正常运行。
我觉得大型网站是技术和业务的结合,一个满足某些用户需求的网站只要技术和业务二者有一方难度很大,必然会让企业投入更多的、更优秀的人力成本实现它,那么这样的网站就是所谓的大型网站了。
一个初建的网站往往用户群都是很小的,最简单的网站架构就能解决实际的用户需求,当然为了保证网站的稳定性和安全性,我们会把网站的应用部署到至少两台机器上,后台的存储使用数据库,如果经济实力允许,数据库使用单台服务器部署,由于数据是网站的生命线,因此我们常常会把部署数据库的服务器使用的好点,这个网站结构如下所示:
这个结构非常简单,其实大部分初建网站开发里往往业务逻辑没有企业级系统那么复杂,所以只要有个好的idea,建设一个新网站的成本是非常低的,所使用的技术手段也是非常的基本和简单,不过该图我们要准备三台服务器,而且还要租个机房放置我们的服务器,这些成本对于草根和屌丝还是非常高的,幸运的是当下很多大公司和机构提供了云平台,我们可以花费很少的钱将自己的应用部署到云平台上,这种做法我们甚至不用去考虑把应用、数据库分开部署的问题,更加进一步的降低了网站开发和运维的成本,但是这种做法也有一个问题,就是网站的小命被这个云平台捏住了,如果云平台挂了,俺们的网站服务也就跟着挂了。
这里我先讲讲自己独立使用服务器部署网站的问题,如果我们要把网站服务应用使用多台服务器部署,这么做的目的一般有两个:
- 保证网站的可用性,多台服务器部署应用,那么其中一些服务器挂掉了,只要网站还有服务器能正常运转,那么网站对外任然可以正常提供服务。
- 提高网站的并发量,服务器越多那么网站能够服务的用户,单位时间内能承载的请求数也就越大。
不过要做到以上两点,并不是我们简单将网站分开部署就可以满足的,因为大多数网站在用户使用时候都是要保持用户的状态,具体点就是网站要记住请求是归属到那一个客户端,而这个状态在网站开发里就是通过会话session来体现的。分开部署的web应用服务要解决的一个首要问题就是要保持不同物理部署服务器之间的session同步问题,从而达到当用户第一次请求访问到服务器A,第二个请求访问到服务器B,网站任然知道这两个请求是同一个人,解决方案很直接:服务器A和服务器B上的session信息要时刻保持同步,那么如何保证两台服务器之间session信息的同步呢?
为了回答上面的问题,我们首先要理解下session的机制,session信息在web容器里都是存储在内存里的,web容器会给每个连接它的客户端生成一个sessionid值,这个sessionid值会被web容器置于http协议里的cookie域下,当响应被客户端处理后,客户端本地会存储这个sessionid值,用户以后的每个请求都会让这个sessionid值随cookie一起传递到服务器,服务器通过sessionid找到内存中存储的该用户的session内容,session在内存的数据结构是一个map的格式。那么为了保证不同服务器之间的session共享,那么最直接的方案就是让服务器之间session不断的传递和复制,例如java开发里常用的tomcat容器就采用这种方案,以前我测试过tomcat这种session同步的性能,我发现当需要同步的web容器越多,web应用所能承载的并发数并没有因为服务器的增加而线性提升,当服务器数量达到一个临界值后,整个web应用的并发数甚至还会下降,为什么会这样了?
原因很简单,不同服务器之间session的传递和复制会消耗服务器本身的系统资源,当服务器数量越大,消耗的资源越多,当用户请求越频繁,系统消耗资源也会越来越大。如果我们多部署服务器的目的只是想保证系统的稳定性,采用这种方案还是不错的,不过web应用最好部署少点,这样才不会影响到web应用的性能问题,如果我们还想提升网站的并发量那么就得采取其他的方案了。
时下使用的比较多的方案就是使用独立的缓存服务器,也就是将session的数据存储在一台独立的服务器上,如果觉得存在一台服务器不安全,那么可以使用memcached这样的分布式缓存服务器进行存储,这样既可以满足了网站稳定性问题也提升了网站的并发能力。
不过早期的淘宝在这个问题解决更加巧妙,他们将session的信息直接存储到浏览器的cookie里,每次请求cookie信息都会随着http一起传递到web服务器,这样就避免了web服务器之间session信息同步的问题,这种方案会让很多人诟病,诟病的原因是cookie的不安全性是总所周知的,如果有人恶意截取cookie信息那么网站不就不安全了吗?这个答案还真不好说,但是我觉得我们仅仅是跟踪用户的状态,把session存在cookie里其实也没什么大不了的。
其实如此专业的淘宝这么做其实还是很有深意的,还记得本文开篇提到的hao123网站,它是可以承载高并发的网站,它之所以可以做到这一点,原因很简单它是个静态网站,静态网站的特点就是不需要记录用户的状态,静态网站的服务器不需要使用宝贵的系统资源来存储大量的session会话信息,这样它就有更多系统资源来处理请求,而早期淘宝将cookie存在客户端也是为了达到这样的目的,所以这个方案在淘宝网站架构里还是使用了很长时间的。
在我的公司里客户端的请求到达web服务器之前,会先到F5,F5是一个用来做负载均衡的硬件设备,它的作用是将用户请求均匀的分发到后台的服务器集群,F5是硬件的负载均衡解决方案,如果我们没那么多钱买这样的设备,也有软件的负载均衡解决方案,这个方案就是大名鼎鼎的LVS了,这些负载均衡设备除了可以分发请求外它们还有个能力,这个能力是根据http协议的特点设计的,一个http请求从客户端到达最终的存储服务器之前可能会经过很多不同的设备,如果我们把一个请求比作高速公路上的一辆汽车,这些设备也可以叫做这些节点就是高速路上的收费站,这些收费站都能根据自己的需求改变http报文的内容,所以负载均衡设备可以记住每个sessionid值对应的后台服务器,当一个带有sessionid值的请求通过负载均衡设备时候,负载均衡设备会根据该sessionid值直接找到指定的web服务器,这种做法有个专有名词就是session粘滞,这种做法也比那种session信息在不同服务器之间拷贝复制要高效,不过该做法还是比存cookie的效率低下,而且对于网站的稳定性也有一定影响即如果某台服务器挂掉了,那么连接到该服务器的用户的会话都会失效。
解决session的问题的本质也就是解决session的存储问题,其本质也就是解决网站的存储问题,一个初建的网站在早期的运营期需要解决的问题基本都是由存储导致的。上文里我提到时下很多新建的web应用会将服务器部署后云平台里,好的云平台里或许会帮助我们解决负载均衡和session同步的问题,但是云平台里有个问题很难解决那就是数据库的存储问题,如果我们使用的云平台发生了重大事故,导致云平台存储的数据丢失,这种会不会导致我们在云平台里数据库的信息也会丢失了,虽然这个事情的概率不高,但是发生这种事情的几率还是有的,虽然很多云平台都声称自己多么可靠,但是真实可靠性有多高不是局中人还真不清楚哦,因此使用云平台我们首要考虑的就是要做好数据备份,假如真发生了数据丢失,对于一个快速成长的网站而言可能非常致命。
写到这里一个婴儿般的网站就这样被我们创造出来了,我们希望网站能健康快速的成长,如果网站真的按我们预期成长了,那么一定会有一天我们制造的宝宝屋已经满足不了现实的需求,这个时候我们应该如何抉择了?换掉,全部换掉,使用新的架构例如我们以前长提的SOA架构,分布式技术,这个方法不错,但是SOA和分布式技术是很难的,成本是很高的,如果这时候我们通过添加几台服务器就能解决问题的话,我们绝对不要去选择什么分布式技术,因为这个成本太高了。上面我讲到几种session共享的方案,这个方案解决了应用的水平扩展问题,那么当我们网站出现瓶颈时候就多加几台服务器不就行了吗?那么这里就有个问题了,当网站成长很快,网站首先碰到的瓶颈到底是哪个方面的问题?
本人是做金融网站的,我们所做的网站有个特点就是当用户访问到我们所做的网站时候,目的都很明确就是为了付钱,用户到了我们所做的网站时候都希望能快点,再快点完成本网站的操作,很多用户在使用我们做的网站时候不太去关心网站的其他内容,因此我们所做的网站相对于数据库而言就是读写比例其实非常的均匀,甚至很多场景写比读要高,这个特点是很多专业服务网站的特点,其实这样的网站和企业开发的特点很类似:业务操作的重要度超过了业务展示的重要度,因此专业性网站吸纳企业系统开发的特点比较多。但是大部分我们日常常用的网站,我们逗留时间很长的网站按数据库角度而言往往是读远远大于写,例如大众点评网站它的读写比率往往是9比1。
12306或许是中国最著名的网站之一,我记得12306早期经常出现一个问题就是用户登录老是登不上,甚至在高峰期整个网站挂掉,页面显示503网站拒绝访问的问题,这个现象很好理解就是网站并发高了,大量人去登录网站,购票,系统挂掉了,最后所有的人都不能使用网站了。当网站出现503拒绝访问时候,那么这个网站就出现了最致命的问题,解决大用户访问的确是个超级难题,但是当高并发无法避免时候,整个网站都不能使用这个只能说网站设计上发生了致命错误,一个好的网站设计在应对超出自己能力的并发时候我们首先应该是不让他挂掉,因为这种结果是谁都不能使用,我们希望那些在可接受的请求下,让在可接受请求范围内的请求还是可以正常使用,超出的请求可以被拒绝,但是它们绝对不能影响到全网站的稳定性,现在我们看到了12306网站的峰值从未减少过,而且是越变越多,但是12306出现全站挂掉的问题是越来越少了。通过12036网站改变我们更进一步思考下网站的瓶颈问题。
排除一些不可控的因素,网站在高并发下挂掉的原因90%都是因为数据库不堪重负所致,而应用的瓶颈往往只有在解决了存储瓶颈后才会暴露,那么我们要升级网站能力的第一步工作就是提升数据库的承载能力,对于读远大于写的网站我们采取的方式就是将数据库从读写这个角度拆分,具体操作就是将数据库读写分离,如下图所示:
我们这时要设计两个数据库,一个数据库主要负责写操作我们称之为主库,一个数据库专门负责读操作我们称之为副库,副库的数据都是从主库导入的,数据库的读写分离可以有效的保证关键数据的安全性,但是有个缺点就是当用户浏览数据时候,读的数据都会有点延时,这种延时比起全站不可用那肯定是可以接受的。不过针对12306的场景,仅仅读写分离还是远远不够的,特别是负责读操作的副库,在高访问下也是很容易达到性能的瓶颈的,那么我们就得使用新的解决方案:使用分布式缓存,不过缓存的缺点就是不能有效的实时更新,因此我们使用缓存前首先要对读操作的数据进行分类,对于那些经常不发生变化的数据可以事先存放到缓存里,缓存的访问效率很高,这样会让读更加高效,同时也减轻了数据库的访问压力。至于用于写操作的主库,因为大部分网站读写的比例是严重失衡,所以让主库达到瓶颈还是比较难的,不过主库也有一个读的压力就是主库和副库的数据同步问题,不过同步时候数据都是批量操作,而不是像请求那样进行少量数据读取操作,读取操作特别多,因此想达到瓶颈还是有一定的难度的。听人说,美国牛逼的facebook对数据的任何操作都是事先合并为批量操作,从而达到减轻数据库压力的目的。
上面的方案我们可以保证在高并发下网站的稳定性,但是针对于读,如果数据量太大了,就算网站不挂掉了,用户能很快的在海量数据里检索到所需要的信息又成为了网站的一个瓶颈,如果用户需要很长时间才能获得自己想要的数据,很多用户会失去耐心从而放弃对网站的使用,那么这个问题又该如何解决了?
解决方案就是我们经常使用的百度,谷歌哪里得来,对于海量数据的读我们可以采用搜索技术,我们可以将数据库的数据导出到文件里,对文件建立索引,使用倒排索引技术来检索信息,我们看到了百度,谷歌有整个互联网的信息我们任然能很快的检索到数据,搜索技术是解决快速读取数据的一个有效方案,不过这个读取还是和数据库的读取有所区别的,如果用户查询的数据是通过数据库的主键字段,或者是通过很明确的建立了索引的字段来检索,那么数据库的查询效率是很高的,但是使用网站的人跟喜欢使用一些模糊查询来查找自己的信息,那么这个操作在数据库里就是个like操作,like操作在数据库里效率是很低的,这个时候使用搜索技术的优势就非常明显了,搜索技术非常适合于模糊查询操作。
(二)第二部分
上篇里我讲到某些网站在高并发下会报出503错误,503错误的含义是指网站服务端暂时无法提供服务的含义,503还表达了网站服务端现在有问题但是以后可能会提供正常的服务,对http协议熟悉的人都知道,5开头的响应码表达了服务端出现了问题,在我们开发测试时候最为常见的是500错误,500代表的含义是服务端程序出现了错误导致网站无法正常提供服务,500通常是服务端异常和错误所致,如果生产系统里发现了500错误,那么只能说明网站存在逻辑性的错误,这往往是系统上线前的测试做的不到位所致。回到503错误,我上文解释为拒绝访问,其实更加准确的回答应该是服务不可用,那么为什么我会说503错误在高并发的情况下90%的原因是数据库所致呢?上文我做出了详细的解释,但是今天我回味了一下,发现那个解释还不是太突出重点,问题的重点是在高并发的情况整个网站系统首先暴露出问题的是数据库,如果我们把整个网站系统比作一个盛水的木桶,那么木桶最短的那个板就是数据库了,一般而言网站的服务应用出问题都会是解决存储问题之后才会出现。
数据库出现了瓶颈并不是程序存在逻辑性错误,数据库瓶颈的表现就是数据库因为承受了太多的访问后,数据库无法迅速的做出响应,严重时候数据库会拒绝进一步操作死锁在哪里不能做出任何反应。数据库犹如一把巨型的大锁,很多人争抢这个锁时候会导致这个大锁完全被锁死,最终请求的处理就停留在这个大锁上最终导致网站提示出503错误,503错误最终会传递到所有的客户端上,最终的现象就是全站不可用了。
上文里我讲到session共享的一个方案是将session数据存储在外部一个独立的缓存服务器里,我开始说用一台服务器做缓存服务器,后面提到如果觉得一台服务器做缓存不安全,那么采用分布式缓存服务器例如memcached,那么这里就有一个问题了,为了保证web服务的可用性,我们会把web服务分开部署到不同的服务器上,这些服务器都是对等关系,其中一台服务器不能正常提供服务不会影响到整个网站的稳定性,那么我们采取memcached集群是不是可以达到同样的效果了?即缓存服务器集群中一台服务器挂掉,不会影响到用户对网站的使用了?问题的答案是令人失望了,假如我们使用两台服务器做缓存服务器来存储session信息,那么如果其中一台服务器挂掉了,那么网站将会有一半的用户将不能正常使用网站,原因是他们的session信息丢失了,网站无法正常的跟踪用户的会话状态。我之所以提到这个问题是想告诉大家以memcached为代表的分布式缓存和我们传统理解的分布式系统是有区别的,传统的分布式系统都会包含一个容灾维护系统稳定性的功能,但实际的分布式技术是多种多样的,例如memcached的分布式技术并不是为了解决容灾维护系统稳定性的模式设计,换个说法就是memcached集群的设计是没有过分考虑冗余的问题,而只有适当的冗余才能保证系统的健壮性问题。分布式技术的实现是千差万别的,每个优秀的分布式系统都有自身独有的特点。
全面的讲述memcached技术并非本文的主题,而且这个主题也不是一两句话能说清楚的,这里我简单的介绍下memcached实现的原理,当网站使用缓存集群时候,缓存数据是通过一定的算法将缓存数据尽量均匀分不到不同服务器上,如果用户A的缓存在服务器A上,那么服务器B上是没有该用户的缓存数据,早期的memcache数据分布式的算法是根据缓存数据的key即键值计算出一个hash值,这个hash值再除以缓存服务器的个数,得到的余数会对应某一台服务器,例如1对应服务器A,2对应服务器B,那么余数是1的key值缓存就会存储在服务器A上,这样的算法会导致某一台服务器挂掉,那么网站损失的缓存数据的占比就会比较高,为了解决这个问题,memcached引入了一致性hash算法。关于一致性hash网上有很多资料,这里我就贴出一个链接,本文就不做过多论述了。链接地址如下:
http://blog.csdn.net/kongqz/article/details/6695417
一致性hash可以服务器宕机时候这台服务器对整个缓存数据的影响最小。
上文里我讲到了读写分离的设计方案,而读写分离方案主要是应用于网站读写比例严重失衡的网站,而互联网上绝大部分网站都是读操作的比例远远大于写操作,这是网站的主流,如果一个网站读写比例比较均衡,那么这个网站一般都是提供专业服务的网站,这种网站对于个人而言是一个提供生活便利的工具,它们和企业软件类似。大部分关注大型网站架构技术关心的重点应该是那种对于读写比例失衡的网站,因为它们做起来更加有挑战性。
将数据库进行读写分离是网站解决存储瓶颈的第一步,为什么说是第一步呢?因为读写分离从业务角度而言它是一种粗粒度的数据拆分,因此它所包含的业务复杂度比较低,容易操作和被掌控,从技术而言,实现手段也相对简单,因此读写分离是一种低成本解决存储瓶颈的一种手段,这种方案是一种改良方案而不是革命性的的方案,不管是从难度,还是影响范围或者是经济成本角度考虑都是很容易让相关方接受的。
那么我们仅仅将数据库做读写分离为何能产生好的效率了?回答这个问题我们首先要了解下硬盘的机制,硬盘的物理机制就有一个大圆盘飞速旋转,然后有个磁头不断扫描这个大圆盘,这样的物理机制就会导致硬盘数据的顺序操作比随机操作效率更高,这点对于硬盘的读和写还算公平,但是写操作在高并发情况下会有点复杂,写操作有个特性就是我们要保证写操作的准确性,但是高并发下可能会出现多个用户同时修改某一条数据,为了保证数据能被准确的修改,那么我们通常要把并行的操作转变为串行操作,这个时候就会出现一个锁机制,锁机制的实现是很复杂的,它会消耗很多系统性能,如果写操作掺杂了读操作情况就更复杂,效率会更加低效,相对于写操作读操作就单纯多了,如果我们的数据只有读操作,那么读的性能也就是硬盘顺序读能力和随机读能力的体现,即使掺杂了并发也不会对其有很大的影响,因此如果把读操作和写操作分离,效率自然会得到很大提升。
既然读写分离可以提升存储系统的效率,那么为什么我们又要引入缓存系统和搜索技术了?缓存将数据存在内存中,内存效率是硬盘的几万倍,这样的好处不言而喻,而选择搜索技术的背后的原理就不同了,数据库存储的数据称之为结构化数据,结构化数据的限制很多,当结构化数据遇到了千变万化的随机访问时候,其效率会变得异常低效,但是如果一个网站不能提供灵活、高效的随机访问能力,那么这个网站就会变得单板没有活力,例如我们在淘宝里查找我们想要的商品,但是时常我们并不清楚自己到底想买啥,如果是在实体店里店员会引导我们的消费,但是网站又如何引导我们的消费,那么我们必须要赋予网站通过人们简单意向随机找到各种不同的商品,这个对于数据库就是一个like操作的,但是数据里数据量达到了一定规模以后like的低效是无法让人忍受的,这时候搜索技术在随机访问的能力正好可以弥补数据库这块的不足。
业务再接着的增长下去,数据量也会随之越来越大了,这样发展下去总有一天主库也会产生瓶颈了,那么接下来我们又该如何解决主库的瓶颈了?方法很简单就是我们要拆分主库的数据了,那么我该以什么维度拆分数据了?一个数据库里有很多张表,不同的表都针对不同的业务,网站的不同业务所带来的数据量也不是不同的,这个时候系统的短板就是那些数据量最大的表,所以我们要把那些会让数据库产生瓶颈的表拆出来,例如电商系统里商品表和交易表往往数据量非常大,那么我们可以把这两种表建立在单独的两个数据库里,这样就拆分了数据库的压力,这种做法叫做数据垂直拆分,不过垂直拆分会给原有的数据库查询,特别是有事务的相关操作产生影响,这些问题我们必须要进行改造,关于这个问题,我将在下篇里进行讨论。
当我们的系统做完了读写分离,数据垂直拆分后,我们的网站还在迅猛发展,最终一定又会达到新的数据库瓶颈,当然这些瓶颈首先还是出现在那些数据量大的表里,这些表数据的处理已经超出了单台服务器的能力,这个时候我们就得对这个单库单表的数据进行更进一步的拆分,也就是将一张表分布到两台不同的数据库里,这个做法就是叫做数据的水平拆分了。
Ok,今天内容就讲到这里了,有这两篇文章我们可以理出一个解决大型网站数据瓶颈的一个脉络了,具体如下:
单库数据库-->数据库读写分离-->缓存技术-->搜索技术-->数据的垂直拆分-->数据的水平拆分
以上的每个技术细节在具体实现中可能存在很大的不同,但是问题的缘由大致是一致的,我们理清这个脉络就是想告诉大家我们如果碰到这样的问题应该按何种思路进行思考和设计解决方案,好了,今天就写到这里了,晚安。
(三)第三部分
存储的瓶颈写到现在就要进入到深水区了,如果我们所做的网站已经到了做数据库垂直拆分和水平拆分的阶段,那么此时我们所面临的技术难度的挑战也会大大增强。
这里我们先回顾下数据库的垂直拆分和水平拆分的定义:
垂直拆分:把一个数据库中不同业务单元的数据分到不同的数据库里。
水平拆分:是根据一定的规则把同一业务单元的数据拆分到多个数据库里。
垂直拆分是一个粗粒度的拆分数据,它主要是将原来在一个数据库下的表拆分到不同的数据库里,水平拆分粒度比垂直拆分要更细点,它是将一张表拆到不同数据库里,粒度的粗细也会导致实现技术的难度的也不一样,很明显水平拆分的技术难度要远大于垂直拆分的技术难度。难度意味着投入的成本的增加以及我们需要承担的风险的加大,我们做系统开发一定要有个清晰的认识:能用简单的方案解决问题,就一定要毫不犹豫的舍弃复杂的方案,当系统需要使用高难度技术的时候,我们一定要让自己感受到这是迫不得已的。
我是以java工程师应聘进了我现在的公司,所以在我转到专职前端前,我也做过不少java的应用开发,当时我在公司的前辈告诉我,我们公司的数据库建模很简单,怎么个简单法了,数据库的表之间都没有外键,数据库不准写触发器,可以写写存储过程,但是存储过程决不能用于处理生产业务逻辑,而只能是一些辅助工作,例如导入导出写数据啊,后面听说就算是数据库做到了读写分离,数据之间同步也最好是用java程序做,也不要使用存储过程,除非迫不得已。开始我还不太理解这些做法,这种不理解不是指我质疑了公司的做法,而是我在想如果一个数据库我们就用了这么一点功能,那还不如让数据库公司为咋们定制个阉割版算了,不过在我学习了hadoop之后我有点理解这个背后的深意了,其实作为存储数据的数据库,它和我们开发出的程序的本质是一样的那就是:存储和计算,那么当数据库作为一个业务系统的存储介质时候,那么它的存储对业务系统的重要性要远远大于它所能承担的计算功能,当数据库作为互联网系统的存储介质时候,如果这个互联网系统成长迅速,那么这个时候我们对数据库存储的要求就会越来越高,最后估计我们都想把数据库的计算特性给阉割掉,当然数据库基本的增删改查我们是不能舍弃的,因为它们是数据库和外界沟通的入口,我们如果接触过具有海量数据的数据库,我们会发现让数据库运行的单个sql语句都会变得异常简洁和简单,因为这个时候我们知道数据库已经在存储这块承担了太多的负担,那么我们能帮助数据库的手段只能是尽量降低它运算的压力。
回到关于数据库垂直拆分和水平拆分的问题,假如我们的数据库设计按照我们公司业务数据库为蓝本的话,那么数据库进行了水平拆分我们会碰到什么样的问题了?为了回答这个问题我就要比较下拆分前和拆分后会给调用数据库的程序带来怎样的不同,不同主要是两点:
第一点:被拆出的表和原库的其他表有关联查询即使用join查询的操作需要进行改变;
第二点:某些增删改(注意:一般业务库设计很少使用物理删除,因为这个操作十分危险,这里的删往往是逻辑删除,一般做法就是更新下记录的状态,本质是一个更新操作)牵涉到拆分的表和原库其他表共同完成,那么该操作的事务性就会被打破,如果处理不好,假如碰到操作失败,业务无法做到回滚,这会对业务操作的安全性带来极大的风险。
关于解决第一点的问题还是相对比较简单的,方式方法也很多,下面我来讲讲我所知道的一些方法,具体如下:
方法一:在垂直拆表时候,我们先梳理下使用到join操作sql查询,梳理的维度是以被拆分出的表为原点,如果是弱依赖的join表我们改写下sql查询语句,如果是强依赖的join表则随拆分表一起拆分,这个方法很简单也很可控,但是这个技术方案存在一个问题,就是让拆分粒度变大,拆分的业务规则被干扰,这么拆分很容易犯一个问题就是一个数据库里总会存在这样一些表,就是很多数据库都会和它关联,我们很难拆解这些关联关系,当我们无法理清时候就会把该表做冗余,即不同数据库存在雷同表,随着业务增长,这种表的数据同步就成为了数据库的一个软肋,最终它会演变为整个数据库系统的短板甚至是全系统的短板。
方法二:我们拆表的准则还是按业务按需求在数据库层面进行,等数据库拆好后,再改写原来受到影响的join查询语句,这里我要说明的是查询语句修改的成本很低,因为查询操作是个只读操作,它不会改变任何底层的东西,如果数据表跨库,我们可以把join查询拆分为多次查询,最后将查询结果在内存中归纳和合并,其实我们如果主动拆库,绝不会把换个不同的数据库产品建立新库,肯定是使用相同数据库,同类型的数据库基本都支持跨库查询,不过跨库查询听说效率不咋地,我们可以有选择的使用。这种方案也有个致命的缺点,我们做数据库垂直拆分绝不可能一次到位,一般都是多次迭代,而该方案的影响面很大,关联方过多,每次拆表几乎要检查所有相关的sql语句,这会导致系统不断累积不可预知的风险。
以下三段内容是方法三:
不管是方法一还是方法二,都有一个很根本的缺陷就是数据库和上层业务操作耦合度很高,每次数据库的变迁都导致业务开发跟随做大量的同步工作,这样的后果就是资源浪费,做服务的人不能天天被数据库牵着鼻子走,这样业务系统的日常维护和业务扩展会很存问题,那么我们一定要有一个服务和数据库解耦方案,那么这里我们就得借鉴ORM技术了。(这里我要说明下,方法一和方法二我都是以修改sql阐述的,在现实开发里很多系统会使用ORM技术,互联网一般用ibatis和mybatis这种半ORM的产品,因为它们可以直接写sql和数据库最为亲近,如果使用hibernate则就不同了,但是hibernate虽然大部分不是直接写sql,但是它只不过是对数据库操作做了一层映射,本质手段是一致,所以上文的sql可以算是一种指代,它也包括ORM里的映射技术)
传统的ORM技术例如hibernate还有mybatis都是针对单库进行的,并不能帮我们解决垂直拆分的问题,因此我们必须自己开发一套解决跨库操作的ORM系统,这里我只针对查询的ORM谈谈自己的看法(讲到这里是不是有些人会有种似成相识的感觉,这个不是和分布式系统很像吗)。
其实具体怎么重构有问题的sql不是我想讨论的问题,因为这是个技术手段或者说是一个技术上的技巧问题,我这里重点讲讲这个ORM与服务层接口的交互,对于服务层而言,服务层最怕的就是被数据库牵着鼻子走,因为当数据库要进行重大改变时候,服务层总是想方设法让自己不要发生变化,对于数据库层而言服务层的建议都应该是合理,数据库层要把服务层当做自己的需求方,这样双方才能齐心协力完成这件重要的工作,那么服务层一般是怎样和数据库层交互的呢?
从传统的ORM技术我们可以找到答案,具体的方式有两种:
第一种:以hibernate为代表的,hibernate框架有一套自己的查询语言就是hql,它类似于sql,自定义一套查询语言看起来很酷,也非常灵活,但是实现难度非常之高,因为这种做法相当于我们要自己编写一套新的编程语言,如果这个语言设计不好,使用者又理解不深入,最后往往会事与愿违,就像hibernate的hql,我们经常令可直接使用sql也不愿意使用hql,这其中的缘由用过的人一定很好理解的。
第二种:就是数据层给服务层提供调用方法,每个方法对应一个具体的数据库操作,就算底层数据库发生重大变迁,只要提供给服务端的方法定义不变,那么数据库的变迁对服务层影响度也会最低。
前面我提到技术难度是我们选择技术的一个重要指标,相比之下第二种方案将会是我们的首选。
垂直拆分数据库还会带来另一个问题就是对事务的影响,垂直拆分数据库会导致原来的事务机制变成了分布式事务,解决分布式事务问题是非常难的,特别是如果我们想使用业界推出的解决分布式事务方案,那么要自己实现个分布式事务就更难了,不过这里我要说明一下,我这里说的更难是和我写本文有关,我本篇文章之所以现在才写是因为我想先研究下业界推出的分布式解决方案,但是这些方案的原理看得我很沮丧,我就想如果我们直接用方案的接口实现了它,因为还是不懂他的很多原理,那么这些方案其实就是不可控方案,说不定使用过多就会给系统埋下定时炸弹,因此这里我就只提提这些方案,有兴趣的童鞋可以去研究下:
一、X/OPEN组织推出的分布式事务规范XA,其中还包括该组织定义的分布式事务处理模型X/OPEN;
二、大型网站一致性理论CAP/BASE
三、 PAXOS协议。
这里特别要提的是PAXOS协议,我以前写过好几篇关于zookeeper的文章,zookeeper框架有一个特性就是它本身是一个分布式文件系统,当我们往zookeeper写数据时候,zookeeper集群能保证我们的写操作的可靠性,这个可靠性和我们使用线程安全来控制写数据一样,绝对不会让写操作出错,之所以zookeeper能做到这点,是因为zookeeper内部有一个类似PAXOS协议的协议,这个协议类似一个选举方案,它能保证写入操作的原子性。
其实事务也是和线程安全技术类似,只不过事务是要保证一个业务操作的原子性问题,当然事务还要有个特点就是回滚机制即业务操作失败,事务可以保证系统恢复到业务操作前的状态,回滚机制的本质其实是维护业务操作的状态性,具体点我这里列举个例子:当系统将要执行一个业务操作时候,我们首先为业务系统定义一个初始状态,业务执行操作时候我们可以定义一个执行状态,操作成功就是一个成功状态,操作失败就是一个操作失败状态,如果业务操作是失败状态,我们可以让业务回滚到初始状态,更进一步如果执行状态超时也可以将整个业务状态回退到初始状态,其实所有事务回滚机制的本质基本都是如此。记得不久前,在群里有个群友就问大家如何实现分布式事务,他想要知道的分布式事务是有没有一种技术能像我们操作数据库或者是jdbc那样一个commit,一个rollback就搞定,但是现实中的分布式事务比commit和rollback复杂的多,不可能简单的让我们写几个标记就能实现分布式事务,当然业界是有方案的,就是我上面提到的,如果有人真想知道可以自己研究下,不过我本人现在还是不太懂上面这些技术的原理和思想。
其实当时我马上给那位群友一个解答,我说我们开发时候是经常碰到分布式事务,但是我们解决分布式事务大多数从业务角度来解决的,而没去选择纯技术手段,因为技术手段太复杂难以控制。这个答案可能不会令提问者满意,但是我现在还是坚持这个观点,这个观点符合我提到的原则,当技术方案难度过高,我们就不要轻易选择使用它,因为这么做是很危险的,今天我就举个例子吧,这样可能更有说服力。我现在做的系统很多业务操作经常要和其他系统共同完成,其他系统有我们公司自己的系统,也有其他企业的系统,这里我还是把业务操作比作一辆在高速公路的汽车,那么每个系统就是高速公路上的一个收费站,业务每到一个收费站,该系统的数据库就会在对应的数据库的某张表里某条记录上记录一个状态,当汽车跑完全程,各个收费站就会相互通知,告诉大家任务完成,最终将所有的状态置为已完成,如果失败,就废掉这辆汽车,收费站之间也会相互通知,让所有的记录状态回归到初始状态,就当从来没有这辆汽车来过。这个做法的原理就是使用了事务回滚的本质,状态的变迁和回退,这个做法在业务系统开发里也有个专有术语就是工作流。其实大多数问如何实现分布式事务如何实现的问题的本质就是想解决事务的回滚问题,我们其实不要被这个分布式事务的名字给吓住了,其实有很多不起眼的技术手段和业务手段都能达到相同的目的。
晚上11点了,看来本文今天写不完了,今天就到此为止,最后我要总结下本文的内容,具体如下:
1. 大型网站解决存储瓶颈的问题,我们要找准存储这个关键点,因为数据库其实是存储和运算的组合体,但是在我们这个场景下,存储是第一位的,当存储是瓶颈时候我们要狠下心来尽量多的抛弃数据的计算特点,所以上文中我提出我们数据库就不要滥用计算功能了例如触发器、存储过程等等。
2. 数据库剥离计算功能不代表不要数据的计算功能,因为没有数据的计算功能数据库也就没价值了,那么我们要将数据库的计算功能进行迁移,迁移到程序里面,一般大型系统程序和数据库都是分开部署到不同服务器上,因此程序里处理数据计算就不会影响到数据库所在服务器的性能,就可以让安装数据库的服务器专心服务于存储。
3. 我们要尽一切可能的把数据库的变化对服务层的影响降到最低,最好是数据库做拆分后,现有业务不要任何的更改,那么我们就得设计一个全新的数据访问层,这个数据访问层将数据库和服务层进行解耦,任何数据库的变化都由数据访问层消化,数据访问层对外接口要高度统一,不要轻易改变。
4. 如果我们设计了数据访问层来解决数据库拆分的问题,数据访问层加上数据库其实就组合出了一个分布式数据库的解决方案,由此可见拆分数据库的难度是很高的,因为数据库将拥有分布式的特性,而分布式开发就意味开发难度的增加。
5. 对于分布式事务的处理,我们尽量要从具体问题具体分析,不要一感觉这个事务操作本质是分布式事务就去寻找通用的分布式事务技术手段,这样的想法其实是回避困难的思想,结果可能会是把问题搞得更加复杂。
好了,今天就写到这里吧,祝大家晚安,生活愉快!
(四)第四部分
如果数据库需要进行水平拆分,这其实是一件很开心的事情,因为它代表公司的业务正在迅猛的增长,对于开发人员而言那就是有不尽的项目可以做,虽然会感觉很忙,但是人过的充实,心里也踏实。
数据库水平拆分简单说来就是先将原数据库里的一张表在做垂直拆分出来放置在单独的数据库和单独的表里后更进一步的把本来是一个整体的表进一步拆分成多张表,每一张表都用独立的数据库进行存储。当表被水平拆分后,原数据表成为了一个逻辑的概念,而这个逻辑表的业务含义需要多张物理表协同完成,因此数据库的表被水平拆分后,那么我们对这张表的操作已经超出了数据库本身提供给我们现有的手段,换句话说我们对表的操作会超出数据库本身所拥有的处理能力,这个时候我就需要设计相关的方案来弥补数据库缺失的能力,这就是数据库水平拆分最大的技术难点所在。
数据库的水平拆分是数据库垂直拆分的升级版,它和垂直拆分更像继承机制里的父子关系,因此水平拆分后,垂直拆分所遇到的join查询的问题以及分布式事务的问题任然存在,由于表被物理拆解增加了逻辑表的维度,这也给垂直拆分里碰到的两个难题增加了更多的维度,因此水平拆分里join查询的问题和分布式事务会变得更加复杂。水平拆分除了垂直拆分两个难题外,它还会产生新的技术难题,这些难题具体如下:
难题一:数据库的表被水平拆分后,该表的主键设计会变得十分困难;
难题二:原来单表的查询逻辑会面临挑战。
在准备本篇文章时候,我看到一些资料里还提到了一些难题,这些难题是:
难题三:水平拆分表后,外键的设计也会变得十分困难;
难题四:这个难题是针对数据的新增操作的,大致的意思是,我们到底按什么规则把需要存储的数据存储在拆分出的那个具体的物理数据表里。
难题三的问题,我在上篇已经给出了解答,这里我进行一定的补充,其实外键问题在垂直拆分就已经存在,不过在讲垂直拆分时候我们没有讲到这个问题,这主要是我设定了一个前提,就是数据表在最原始的数据建模阶段就要抛弃所有外键的设计,并将外键的逻辑抛给服务层去完成,我们要尽全力减轻数据库承担的运算压力,其实除了减轻数据库运算压力外,我们还要将作为存储原子的表保持相对的独立性,互不关联,那么要做到这点最直接的办法就是去掉表与表之间关联的象征:外键,这样我们就可以从根基上为将来数据库做垂直拆分和水平拆分打下坚实的基础。
至于难题四,其实问题的本质是分库分表后具体的数据在哪里落地的问题,而数据存储在表里的关键障碍其实就是主键,试想一下,我们设计张表,所有字段我们都准许可以为空,但是表里有个字段是绝对不能为空的,那就是主键,主键是数据在数据库里身份的象征,因此我们在主键设计上是可以体现出该数据的落地规则,那么难题四也会随之解决。因此下文我会重点讲解前两个水平拆分的难题。
首先是水平拆分里的主键设计问题,抛开所有主键所能代表的业务含义,数据库里标的主键本质是表达表里的某一条记录的唯一性,在设计数据库的时候我们可以由一个绝对不可重复的字段表示主键,也可以使用多个字段组合起来表达这种唯一性,使用一个字段表示主键,这已经是很原子级的操作,没法做进一步的修改,但是如果使用多个字段表示一个主键对于水平拆分而言就会碰到问题了,这个问题主要是体现在数据到底落地于哪个数据库,关于主键对数据落地的影响我会在把相关知识讲解完毕后再着重阐述,这里要提的是当碰到联合主键时候我们可以设定一个没有任何业务含义的字段来替代,不过这个要看场景了,我倾向于将联合主键各个字段里的值合并为一个字段来表示主键,如果有的朋友认为这样会导致数据冗余,那么可以干脆去掉原来做联合主键的相关字段就是用一个字段表示,只不过归并字段时候使用一个分隔符,这样方便服务层进行业务上的拆分。
由上所述,这里我给出水平拆分主键设计的第一个原则:被水平拆分的表的主键设计最好使用一个字段表示。
如果我们的主键只是表达记录唯一性的话,那么水平拆分时候相对要简单的多,例如在Oracle数据库里有一个sequence机制,这其实就是一个自增数的算法,自增机制几乎所有关系数据库都有,也是我们平时最喜欢使用的主键字段设计方案,如果我们要拆分的表,使用了自增字段,同时这个自增字段只是用来表达记录唯一性,那么水平拆分时候处理起来就简单多了,我这里给出两个经典方案,方案如下:
方案一:自增列都有设定步长的特性,假如我们打算把一张表只拆分为两个物理表,那么我们可以在其中一张表里把主键的自增列的步长设计为2,起始值为1,那么它的自增规律就是1,3,5,7依次类推,另外一张物理表的步长我们也可以设置为2,如果起始值为2,那么自增规律就是2,4,6,8以此类推,这样两张表的主键就绝对不会重复了,而且我们也不用另外做两张物理表相应的逻辑关联了。这种方案还有个潜在的好处,那就是步长的大小和水平数据拆分的粒度关联,也是我们为水平拆分的扩容留有余量,例如我们把步长设计为9,那么理论上水平拆分的物理表可以扩容到9个。
方案二:拆分出的物理表我们允许它最多存储多少数据,我们其实事先通过一定业务技术规则大致估算出来,假如我们估算一张表我们最多让它存储2亿条,那么我们可以这么设定自增列的规律,第一张物理表自增列从1开始,步长就设为1,第二种物理表的自增列则从2亿开始,步长也设为1,自增列都做最大值的限制,其他的依次类推。
那么如果表的主键不是使用自增列,而是业务设计的唯一字段,那么我们又如何处理主键分布问题了?这种场景很典型,例如交易网站里一定会有订单表,流水表这样的设计,订单表里有订单号,流水表里有流水号,这些编号都是按一定业务规则定义并且保证它的唯一性,那么前面的自增列的解决方案就没法完成它们做水平拆分的主键问题,那么碰到这个情况我们又该如何解决了?我们仔细回味下数据库的水平拆分,它其实和分布式缓存何其的类似,数据库的主键就相当于分布式缓存里的键值,那么我们可以按照分布式缓存的方案来设计主键的模型,方案如下:
方案一:使用整数哈希求余的算法,字符串如果进行哈希运算会得出一个值,这个值是该字符串的唯一标志,如果我们稍微改变下字符串的内容,计算的哈希值肯定是不同,两个不同的哈希值对应两个不同字符串,一个哈希值有且只对应唯一一个字符串,加密算法里的MD5,SHA都是使用哈希算法的原理计算出一个唯一标示的哈希值,通过哈希值的匹配可以判断数据是否被篡改过。不过大多数哈希算法最后得出的值都是一个字符加数字的组合,这里我使用整数哈希算法,这样计算出的哈希值就是一个整数。接下来我们就要统计下我们用于做水平拆分的服务器的数量,假如服务器的数量是3个,那么接着我们将计算的整数哈希值除以服务器的数量即取模计算,通过得到的余数来选择服务器,该算法的原理图如下所示:
方案二:就是方案一的升级版一致性哈希,一致性哈希最大的作用是保证当我们要扩展物理数据表的数量时候以及物理表集群中某台服务器失效时候才会体现,这个问题我后续文章会详细讨论物理数据库扩容的问题,因此这里先不展开讨论了。
由上所述,我们发现在数据库进行水平拆分时候,我们设定的算法都是通过主键唯一性进行的,根据主键唯一性设计的特点,最终数据落地于哪个物理数据库也是由主键的设计原则所决定的,回到上文里我提到的如果原库的数据表使用联合字段设计主键,那么我们就必须首先合并联合主键字段,然后通过上面的算法来确定数据的落地规则,虽然不合并一个字段看起来也不是太麻烦,但是在我多年开发里,把唯一性的字段分割成多个字段,就等于给主键增加了维度,字段越多,维度也就越大,到了具体的业务计算了我们不得不时刻留心这些维度,结果就很容易出错,我个人认为如果数据库已经到了水平拆分阶段了,那么就说明数据库的存储的重要性大大增强,为了让数据库的存储特性变得纯粹干净,我们就得尽力避免增加数据库设计的复杂性,例如去掉外键,还有这里的合并联合字段为一个字段,其实为了降低难度,哪怕做点必要的冗余也是值得。
解决数据库表的水平拆分后的主键唯一性问题有一个更加直接的方案,这也是很多人碰到此类问题很自然想到的方法,那就是把主键生成规则做成一个主键生成系统,放置在单独一台服务器上统一生成,每次新增数据主键都从这个服务器里获取,主键生成的算法其实很简单,很多语言都有计算UUID的功能,UUID是根据所在服务器的相关的硬件信息计算出的全球唯一的标示,但是这里我并没有首先拿出这个方案,因为它相比如我前面的方案缺点太多了,下面我要细数下它的缺点,具体如下:
缺点一:把主键生成放到外部服务器进行,这样我们就不得不通过网络通信完成主键值的传递,而网络是计算机体系里效率最低效的方式,因此它会影响数据新增的效率,特别是数据量很大时候,新增操作很频繁时候,该缺点会被放大很多;
缺点二:如果我们使用UUID算法做主键生成的算法,因为UUID是依赖单台服务器进行,那么整个水平拆分的物理数据库集群,主键生成器就变成整个体系的短板,而且是关键短板,主键生成服务器如果失效,整个系统都会无法使用,而一张表需要被水平拆分,而且拆分的表是业务表的时候,那么这张表在整个系统里的重要度自然很高,它如果做了水平拆分后出现单点故障,这对于整个系统都是致命的。当然有人肯定说,既然有单点故障,那么我们就做个集群系统,问题不是解决了吗?这个想法的确可以解决我上面阐述的问题,但是我前文讲到过,现实的软件系统开发里我们要坚守一个原则那就是有简单方案尽量选择简单的方案解决问题,引入集群就是引入了分布式系统,这样就为系统开发增加了开发难度和运维风险,如果我们上文的方案就能解决我们的问题,我们何必自讨苦吃做这么复杂的方案呢?
缺点三:使用外部系统生成主键使得我们的水平拆分数据库的方案增加了状态性,而我上面提到的方案都是无状态的,有状态的系统会相互影响,例如使用外部系统生成主键,那么当数据操作增大时候,必然会造成在主键系统上资源竞争的事情发生,如果我们对主键系统上的竞争状态处理不好,很有可能造成主键系统被死锁,这也就会产生我前文里说到的503错误,而无状态的系统是不存在资源竞争和死锁的问题,这洋就提升了系统的健壮性,无状态系统另一个优势就是水平扩展很方便。
这里我列出单独主键生成系统的缺点不是想说明我觉得这种解决方案完全不可取,这个要看具体的业务场景,根据作者我的经验还没有找到一个很合适使用单独主键生成器的场景。
上文里我提出的方案还有个特点就是能保证数据在不同的物理表里均匀的分布,均匀分布能保证不同物理表的负载均衡,这样就不会产生系统热点,也不会让某台服务器比其他服务器做的事情少而闲置资源,均匀分配资源可以有效的利用资源,降低生产的成本提高生产的效率,但是均匀分布式数据往往会给我们业务运算带来很多麻烦。
水平拆分数据库后我们还要考虑水平扩展问题,例如如果我们事先使用了3台服务器完成了水平拆分,如果系统运行到一定阶段,该表又遇到存储瓶颈了,我们就得水平扩容数据库,那么如果我们的水平拆分方案开始设计的不好,那么扩容时候就会碰到很多的麻烦。
以上问题将是我下篇文章里进行讨论的,今天就写到这里,祝大家生活愉快。
(五)第五部分
上文里我遗留了两个问题,一个问题是数据库做了水平拆分以后,如果我们对主键的设计采取一种均匀分布的策略,那么它对于被水平拆分出的表后续的查询操作将有何种影响,第二个问题就是水平拆分的扩容问题。这两个问题在深入下去,本系列就越来越技术化了,可能最终很多朋友读完后还是没有找到解决实际问题的启迪,而且我觉得这些问题都是像BAT这样巨型互联网公司才会认真思考的,因此本篇我打算换个角度来阐述本文的后续内容。
这里我们首先要明确一个问题,到底是什么因素促使我们去做数据库的垂直拆分和水平拆分的呢?答案很简单就是业务发展的需求,前文里的水平拆分技术方案基本都是抛弃千变万化的业务规则的限制,尽量将水平拆分的问题归为一个简单的技术实现方案,而纯技术手段时常是看起来很美,但是到了面对现实问题时候,常常会变得那么苍白和无力。
水平拆分的难题里我还有个难题没有讲述,就是水平拆分后对查询操作的影响,特别是对单表查询的影响,这点估计也是大伙最为关心的问题,今天我不在延着水平拆分的技术手段演进是阐述上文的遗留问题,而是我要把前面提到的技术手段和一些典型场景结合起来探讨如何解决网站存储的瓶颈问题。
前文中我总结过一个解决存储瓶颈的脉络,具体如下:
单库数据库-->数据库读写分离-->缓存技术-->搜索技术-->数据的垂直拆分-->数据的水平拆分
这个脉络给一些朋友产生了误解,就是认为这个过程应该是个串行的过程,其实在实际的场景下这个过程往往是并行的,但是里面有一个元素应该是串行的或者说思考时候有个先后问题,那就是对数据库层的操作,具体如下:
单库数据库-->数据库读写分离-->数据的垂直拆分-->数据的水平拆分
而缓存技术和搜索技术在数据库的任意阶段里都可以根据实际的业务需求随时切入其中帮助数据库减轻不必要的压力。例如,当网站的后台数据库还是单库的时候,数据库渐渐出现了瓶颈问题,而这个瓶颈又没有达到需要采取大张旗鼓做读写分离方案的程度,那么我这个时候可以考虑引入缓存机制。不过要合理的使用缓存我们首先要明确缓存本身的特点,这些特点如下所示:
特点一:缓存主要是适用于读操作,并且缓存的读操作的效率要远远高于从数据库以及硬盘读取数据的效率。
特点二:缓存的数据是存储在内存当中,因此当系统重启,宕机等等异常场景下,缓存数据就会不可逆的丢失,且无法恢复,因此缓存不能作为可靠存储设备,这就导致一个问题,缓存里的数据必须首先从数据库里同步到内存中,而使用缓存的目的就是为了解决数据库的读操作效率低下的问题,数据库的数据同步到缓存的操作会因为数据库的效率低下而在性能上大打折扣,所以缓存适合的场景是那些固定不变的数据以及业务对实时性变化要求不高的数据。
根据缓存的上述两个特点,我们可以把数据库里和上述描述类似操作的相关数据迁移到缓存里,那样我们就从数据库上剥离了那些对数据库价值不高的操作,让数据库专心做有价值的操作,这样也是减轻数据库压力的一种手段。
不过这个手段局限性很强,局限性主要是一台计算机了用于存储缓存的内存的大小都是远远要低于硬盘,并且内存的价格要远贵于硬盘,如果我们将大规模的数据从硬盘往内存迁移,从资源成本和利用率角度考虑性价比还是很低的,因此缓存往往都是用于转存那些不会经常变化的数据字典,以及经常会被读,而修改较少的数据,但是这些数据的规模也是有一定限度的,因此当单库数据库出现了瓶颈时候马上就着手进行读写分离方案的设计性价比还是很高的。
前文我讲到我们之所以选择数据库读写分离是主要原因是因为数据库的读写比例严重失衡所致,但是做了读写分离必然有个问题不可避免,写库向读库同步数据一定会存在一定的时间差,如果我们想减小读库和写库数据的时间差,那么任然会导致读库因为写的粒度过细而发生部分性能的损失,但是时间差过大,或许又会无法满足实际的业务需求,因此这个时间差的设计一定要基于实际的业务需求合理的设计。
同步的时间差的问题还是个小问题,也比较好解决,但是如何根据实际的业务需求做读写分离这其实还是非常有挑战性的,这里我举个很常见的例子来说明读写分离的难度问题,我们这里以淘宝为例,淘宝是个C2C的电商网站,它是互联网公司提供一个平台,商家自助接入这个平台,在这个平台上卖东西,这个和线下很多大卖场的模式类似。淘宝是个大平台,它的交易表里一定是要记下所有商户的交易数据,但是针对单个商家他们只会关心自己的网店的销售数据,这就有一个问题了,如果某一个商家要查询自己的交易信息,淘宝就要从成千上万的交易信息里检索出该商家的交易信息,那么如果我们把所有交易信息放在一个交易表里,肯定有商家会有这样的疑问,我的网店每天交易额不大,为什么我查询交易数据的速度和那些大商家一样慢了?那么我们到底该如何是解决这样的场景了?
碰到这样的情况,当网站的交易规模变大后就算我们把交易表做了读写分离估计也是没法解决实际的问题,就算我们做的彻底点把交易表垂直拆分出来估计还是解决不了问题,因为一个业务数据库拥有很多张表,但是真正压力大的表毕竟是少数,这个符合28原则,而数据库大部分的关键问题又都是在那些数据压力大的表里,就算我们把这些表单独做读写分离甚至做垂直拆分,其实只是把数据库最大的问题迁移出原来数据库,而不是在解决该表的实际问题。
如果我们要解决交易表的问题我们首先要对交易表做业务级的拆分,那么我们要为交易表增加一个业务维度:实时交易和历史交易,一般而言实时交易以当天及当天24小时为界,历史交易则是除去当天交易外的所有历
以上是关于关于大型网站技术演进的思考--存储的瓶颈(转)的主要内容,如果未能解决你的问题,请参考以下文章