数据库的水平拆分
数据库的水平拆分
比如说订单,你第一要考虑业务场景,查询订单是哪些用户:其一是前端的用户;其二是后端的用户商家和客服。
第二,它的存储量,订单的数据量是非常大的。但对商品和库存来说,它是有一定的范围的,不会无限的大,因为一个网站或者一个商店,你卖的SKU数量是
有限的。一个大超市可能是几万个SKU,一个小门店可能是几百个,它不会无限扩展的。
数据增量也是如此,一个大超市卖的SKU也就是几万个,电商平台可能是百万级千万级,但是它也不是无限增长的,这更多取决于商家的体量,所以它的数据
量即使有增长也是非常缓慢的。这和订单不一样,订单是几何式的增长。
再看读和写,订单、库存的读和写频率都很高。但是对于商品、价格来讲,读肯定是很高的,因为不停地在浏览,但是写是很少的,改价格的机率很低,不
停地改商品信息的机率也是很低的。
另外是事务的一致性。对于订单和库存一定是要保持一致性,商品信息写的话比较少,不太涉及到事务,除非是批量修改,相对来说事务性一致性稍微弱一
些。
还有缓存,库存可以有缓存,但是缓存的时间是很短的,库存的缓存时效不可能是以天、以小时为级别的,几分钟级别已经是不错了。很多时候前端显示还
是有库存,后面可能已经没有了,所以库存有时效性的要求。但为了减轻数据库压力,在前端展示会有库存的缓存,比如有时候大家会遇到,在浏览的时候
发现它是有库存的,但是下单就没有了,那就是因为前端是缓存的,但是下单的是实时的库存,已经没有了。但对商品和价格信息来说,缓存时效就可以长
一些,可以通过缓存技术减轻数据库的压力。
热点数据也是一样的。数据库的水平拆分怎么拆,从哪些维度去拆,比如说订单,可以有几个维度,你可以根据订单号去拆,根据产用户、商家去拆。对响
应速度来说,用户要求响应速度是最高的;而对商家来说,用户下完订单之后,稍微延迟一会儿他也能接受。
如果按照用户去拆,热点数据的概率就很低,很难出现一个用户一下子出现几千个几万个订单;但是如果按对商家来拆,有一些大的商家,一个双十一可能
几个小时就有上千万的单,这个量就非常大。
而对商品信息来说,如果说你的量没有像天猫、淘宝级别的话,并且主要是靠缓存来读,一般的电商网站,是不需要拆分的。
在做订单水平拆库的时候,不可能网站停下来去做这个项目,所以我们说是飞机开的时候换发动机,在汽车跑的时候换轮胎。
一个是Service化做了技术架构上的拆分,一个是做了数据库的水平拆分。这是刚刚提到的准备工作,Service化和水平拆库的同时,我们的很多中间件技术
也发展起来了,因为你的量上来了、架构调整了,配套设施也要上来,不是说简单的教室一拆分就完了,学校没有保安,要上体育课没有操场是不行的,因
此没有相应的中间件没有是不行的。
数据库拆分有两种:
1)垂直分库
数据库里的表太多,拿出部分到新的库里,一般是根据业务划分表,关系密切的表放同一数据库,应用修改数据库连接即可,比较简单。
2)水平分库
某张表太大,单个数据库存储不下或访问性能有压力,把一张表拆成多张,每张表存放部分记录,保存在不同的数据库里,水平分库需要对系统做大的改造。
1)Scale up,升级Oracle数据库所在的物理机,提升内存/存储/IO性能,但这种升级费用昂贵,并且只能满足短期需要。
2)Scale out,把订单库拆分为多个库,分散到多台机器进行存储和访问,这种做法支持水平扩展,可以满足长远需要。
订单库主要包括订单主表/订单明细表(记录商品明细)/订单扩展表,水平分库即把这3张表的记录分到多个数据库中,订单水平分库效果如下图所示:
原来一个Oracle库被多个MySQL库取代,支持1主多备和读写分离,主备之间通过MySQL自带的数据同步机制(SLA<1秒),所有应用通过订单服务访问订单数据。
分库维度
水平分库首先要考虑根据哪个字段作为分库维度,选择标准是尽量避免应用代码和SQL性能受影响,这就要求当前SQL在分库后,访问尽量落在单个库里,否则单库访问变成多库扫描,读写性能和应用逻辑都会受较大影响,。
对于订单拆分,大家首先想到的是按照用户Id拆分,结论没错,但最好还是数据说话,不能拍脑袋。好的做法是首先收集所有SQL,挑选where语句最常出现的过滤字段,比如用户Id/订单Id/商家Id,每个字段在SQL中有三种情况:
- 单Id过滤,如用户Id=?
- 多Id过滤,如用户Id IN (?,?,?)
- 该Id不出现
然后进一步统计,假设共有500个SQL访问订单库,3个过滤字段出现情况如下:
结论明显,应该选择用户Id进行分库。
等一等,这只是静态分析,每个SQL访问的次数是不一样的,因此还要分析每个SQL的访问量。我们分析了Top15执行最多的SQL (它们占总执行次数85%),如果按照用户Id分库,这些SQL 85%落到单个数据库, 13%落到多个数据库,只有2%需要遍历所有数据库,明显优于使用其他Id进行分库。
通过量化分析,我们知道按照用户Id分库是最优的,同时也大致知道分库对现有系统的影响,比如这个例子中,85%的SQL会落到单个数据库,这部分的访问性能会优化,坚定了各方对分库的信心。
分库策略
分库维度确定后,如何把记录分到各个库里呢?一般有两种方式:
- 根据数值范围,比如用户Id为1-9999的记录分到第一个库,10000-20000的分到第二个库,以此类推。
- 根据数值取模,比如用户Id mod n,余数为0的记录放到第一个库,余数为1的放到第二个库,以此类推。
两种分法的优劣比较如下:
实践中,为了处理简单,选择mod分库的比较多。同时二次分库时,为了数据迁移方便,一般是按倍数增加,比如初始4个库,二次分裂为8个,再16个。这样对于某个库的数据,一半数据移到新库,剩余不动,对比每次只增加一个库,所有数据都要大规模变动。
补充下,mod分库一般每个库记录数比较均匀,但也有些数据库,存在超级Id,这些Id的记录远远超过其他Id,比如在广告场景下,某个大广告主的广告数可能占总体很大比例。如果按照广告主Id取模分库,某些库的记录数会特别多,对于这些超级Id,需要提供单独库来存储记录。
分库数量
分库数量首先和单库能处理的记录数有关,一般来说,Mysql 单库超过5000万条记录,Oracle单库超过1亿条记录,DB压力就很大(当然处理能力和字段数量/访问模式/记录长度有进一步关系)。
在满足上述前提下,如果分库数量少,达不到分散存储和减轻DB性能压力的目的;如果分库的数量多,好处是每个库记录少,单库访问性能好,但对于跨多个库的访问,应用程序需要访问多个库,如果是并发模式,要消耗宝贵的线程资源;如果是串行模式,执行时间会急剧增加。
最后分库数量还直接影响硬件的投入,一般每个分库跑在单独物理机上,多一个库意味多一台设备。所以具体分多少个库,要综合评估,一般初次分库建议分4-8个库。
路由透明
分库从某种意义上来说,意味着DB schema改变了,必然影响应用,但这种改变和业务无关,所以要尽量保证分库对应用代码透明,分库逻辑尽量在数据访问层处理。当然完全做到这一点很困难,具体哪些应该由DAL负责,哪些由应用负责,这里有一些建议:
1)对于单库访问,比如查询条件指定用户Id,则该SQL只需访问特定库。此时应该由DAL层自动路由到特定库,当库二次分裂时,也只要修改mod 因子,应用代码不受影响。
2)对于简单的多库查询,DAL负责汇总各个数据库返回的记录,此时仍对上层应用透明。
3)对于带聚合运算的多库查询,如带groupBy/orderby/min/max/avg等关键字,建议DAL汇总单个库返回的结果,上层应用做进一步处理。一方面DAL全面支持各种case,实现很复杂;另一方面,从1号店实践来看,这样的例子不多,在上层应用作针对性处理,更加灵活。
DAL可进一步细分为JDBC和DAL两层,基于JDBC层面实现分库路由,系统开发难度大,灵活性低,目前也没有很好的成功案例;一般是基于持久层框架进一步封装成DDAL(分布式数据访问层),实现分库路由,1号店DAL即基于iBatis进行上层封装而来。
分页处理
分库后,有些分页查询需要遍历所有库,这些case是分库最大的受害者。
举个分页的例子,比如要求按时间顺序展示某个商家的订单,每页100条记录,由于是按商家查询,需要遍历所有数据库,假设库数量是8,我们来看下分页处理逻辑:
1)如果取第1页数据,则需要从每个库里按时间顺序取前100条记录,8个库汇总后有800条,然后对这800条记录在应用里进行二次排序,最后取前100条。
2)如果取第10页数据,则需要从每个库里取前1000(100*10)条记录,汇总后有8000条记录,然后对这8000条记录二次排序后取(900,1000)条记录。
分库情况下,对于第k页记录,每个库要多取100*(k-1)条记录,所有库加起来,多取的记录更多,所以越是靠后的分页,系统要耗费更多内存和执行时间。
对比没分库的情况,无论取那一页,都只要从单个DB里取100条记录,而且无需在应用内部做二次排序,非常简单。
那如何解决分库情况下的分页问题呢?有以下几种办法:
1)如果是在前台应用提供分页,则限定用户只能看前面n页,这个限制在业务上也是合理的,一般看后面的分页意义不大(如果一定要看,可以要求用户缩小范围重新查询)。
2)如果是后台批处理任务要求分批获取数据,则可以加大page size,比如每次获取5000条记录,有效减少分页数(当然离线访问一般走备库,避免冲击主库)。
3)分库设计时,一般还有配套大数据平台汇总所有分库的记录,有些分页查询可以考虑走大数据平台。
Lookup映射
分库字段只有一个,比如这里是用户Id,但订单表还有其他字段可唯一区分记录,比如订单Id,给定一个订单Id,相应记录一定在某个库里。如果盲目地查询所有分库,则带来不必要的开销,Lookup映射可根据订单Id,找到相应的用户Id,从而实现单库定位。
可以事先检索所有订单Id和用户Id,保存在Lookup表里,Lookup表的记录数和订单库记录总数相等,但它只有2个字段,所以存储和查询性能都不是问题。实际使用时,一般通过分布式缓存来优化Lookup性能。对于新增的订单,除了写订单表,同时要写Lookup表。
1)上层应用通过订单服务/分库代理和DAL访问数据库。
2)代理对订单服务实现功能透明,包括聚合运算,非用户Id到用户Id的映射。
3)Lookup表用于订单Id/用户Id映射,保证按订单Id访问时,可以直接落到单个库,Cache是Lookup的内存数据映像,提升性能,cache故障时,直接访问Lookup表。
4)DAL提供库的路由,根据用户Id定位到某个库,对于多库访问,DAL支持可选的并发访问模式,并支持简单记录汇总。
5)Lookup表初始化数据来自于现有分库数据,新增记录时,直接由代理异步写入。
本文来自博客园,作者:{咏南中间件},转载请注明原文链接:https://www.cnblogs.com/hnxxcxg/p/5311151.html