千万级数据迁移与分表的技术方案 - 企业产品实战
本篇文章主要讲解在年增长数据量为千万级的一个企业级技术处理方案。
本文是按照某个企业的实际情况进行的划分,每个企业实际业务不同,技术架构也不同,实际应用时,请根据自己企业实际情况进行。文章仅供参考。
按照千万级这个数据量,只进行水平分表,是完全能够满足划分范围的实时查询需要的。
另外,为了再提高些性能,将热点数据做个缓存即可。
关于一些交易相关的,建议订单表的订单号 32 位设置:yyyyMMddHHmmssSSS+UUID 转 10 位字符生成+(5 位分布式增长数字,初始 10001,超过 5 位数字最大范围从 10001 重新计数),如果订单号是这种方式,则可以将前面 15 位数字作为下单的时间范围划分,并且对于对账有很大帮助。
另外,每个表一定得有一个自增 ID,当页数较大,分页查询时,自增 ID 的存在可以极大提高查询的性能。
如订单号中并没有时间的标识,可以取订单创建时间/订单的交易时间字段。
选择一种比较好的分片方式,在这里按照订单创建时间的维度进行划分。
为什么不选择订单号取模的方式进行
订单的查询绝大多数是通过订单时间进行查询
使用取模的方式进行分片会造成后期水平扩展麻烦,给后期增加了不必要的麻烦
按照创建时间维度分片的缺点,使用订单号取模分片一样会有,非分片维度的查询问题,所有的分库分表都会有这种问题存在,基本上都需要引入映射冗余才能进行解决。
所以选择按照时间范围进行一个分库。
假设是一年 1000w 的数据,MySQL 官方文档说单表 500W-800W,性能才逐渐下降,但是在实际应用中,MySQL 可能在单表 300W 条左右的记录后性能就开始慢慢下降,当然 500W 以内的单表数据,MySQL 查询性能都是比较理想的。
按照这个数据来说,半年进行一块分表,是比较合理的。不会引起单表数据查询性能过慢,以及半年一次的分表,也不过造成分表过多以及迁移频繁。
连续切分的优点是很大的,单表的大小是可以预估的,另外天然水平扩展,及其方便。
没有分库的缺点很明显:就是无法解决集中写入瓶颈的问题,可能存在热点数据问题。
即使是分库了,该缺点在使用范围切分的方案下还是会存在的。如果数据并发上不高,该缺点的影响非常小。
按照订单创建时间维度,半年进行一次分表。
通过创建时间维度进行分片后,对于创建时间上的查询都可以直接路由到库表,假设按照前面划分,例如创建时间为 2015 年 2 月 3 日的订单,可以直接定位到 db5。
对于非创建时间的查询,例如订单号,那么就需要另外的处理了。
非创建时间属性查询需求分析
在一般情况下,在产品设计之初都可以要求在查询中带上创建时间属性进行查询,这样可以直接路由到对应的库表。
在某些场景下,只能通过订单号进行查询时。例如查询订单轨迹,一定是根据订单号进行查询的。
有几种解决方案。
使用索引表
创建时间是可以直接定位到库表的,订单号不能定位到库表。可以建立一个索引表记录订单号->创建时间的映射关系;通过订单号访问时,先通过索引表查询到创建时间,再定位到相应的库表;属性比较少,可以容纳非常多的数据,一般是不需要再分库分表;如果数据量实在过大,可以再通过订单号进行分库分表;缺点就是需要多一次数据库的查询。
通过缓存映射(建议)
通过数据库的索引表性能比较低,可以将映射关系放入到缓存里,性能会高很多。
订单号查询先到缓存中查询创建时间,再根据创建时间定为库表;如果缓存没有命中订单号,那么采用查询全库的方式获取到订单号对应的创建时间,放入缓存;订单号与创建时间都是不可更改的数据,一旦放入缓存,不会再改变,缓存命中率极高;如果缓存中数据量过大,可以通过订单号再次进行缓存的水平切分;缺点就是需要多一次缓存的查询。
通过融入的方式
思路就是在订单号之中便可以直接得到创建时间,或者通过函数运算可以得到创建时间。这种方法需要在项目设计之初便将关系考虑好,后期再进行找关系基本不太可能。每多一个没有分片属性维度的查询,便需要多一个映射的存储。
迁移方案
目前的订单数据都是在单表中。
并且订单数据有一个订单轨迹的单表,该表与订单表是一对多关系,订单轨迹表数据量肯定是比订单表大的。
既然已经知道了订单表有一个关联表,那么在设计之初便应该将订单轨迹表考虑进去。
为了后续关联查询的方便,订单轨迹的分表跟随订单表的切片 key。
分表规则弄好之后,接下来就是数据的迁移,如何做到对业务影响最小的数据迁移是一个比较大的挑战。假设单表的老数据为 1 亿数据量,那么单纯的将数据从单表迁移到分片的表中,这个时间是比较长的。而且由于我们没有自增 ID,在做分页的处理上,当查询到后面的数据时,性能会非常差。即使做倒排的一个处理,可以省一些时间,但也很慢。所以肯定是无法接受未迁移的数据对于用户不可见的这种情况发生。
所以考虑,在最开始如何做到不停服务平滑的过渡到分表,需要一个双写同步的方案。1、在项目上线后,采用双写读旧表的操作,旧数据进行逐步迁移、新增数据双写、修改操作同步到新表即可,查询全部走旧表。
2、CRUD 全部走新表还是双写读走旧库查询通过缓存进行配置(或者给一个较长的周期,在代码中写死,一个月后的某个半夜点所有增删改查都走新表),当旧数据全部迁移分片后,可以在半夜某个点直接将配置更换为走分片的查询,旧表就可以下掉了,从而实现用户无感知的切换。
注意事项与解决方案
跨片的事务问题
目前没有进行分库,可以不进行考虑。解决方案一:分布式事务
跨片的联表的问题
目前没有进行分库,可以不进行考虑。好的设计和切分是可以减少甚至杜绝此种情况的发生的。现在都崇尚单表操作,简单的做法就是分两次查询进行,第一次查询出关联关系,第二次就根据关联关系进行请求到关联数据,在应用中再进行组合。
跨片的数据统计问题
在产品设计上,应尽量避免此种的需求,在每种统计的范畴下,都限制为半年一个维度。当然,会有无法避免需要统计年度的数据,就是跨半年的数据统计。
可以在各个切片上得到结果,在服务端再进行合并处理。和单表处理的不同是,这种处理是可以并行执行的,所以基本上都会比单一的大表会快很多,但是如果结果集很大,会给服务端造成比较大的内存消耗。
跨片的排序分页
这个问题比较大,所有的分库分表都会有这个问题。
第一页的数据比较好解决。查询所有分片的第一页数据,再通过服务端进行处理即可。
但是跨片查询第二页之后的数据,就要稍微复杂一点。如果需要查询第二页的数据,那么需要将每一个分片的前二页数据都查询出来。因为各个分片的数据可能是随机的,为了排序的准确,必须将分片数据中的前 N 页数据都排序好后做合并,再做一个整体的排序分页,返回最终结果。随着页数增加,压力越大。
所以一般在这种情况下,产品设计只做排序前面几页的展示,因为排序后,后面页数数据并没有太多的意义,绝大多数人不会翻到排序 10 页以后的数据去看。
在某些情况下,产品的一个简单"妥协",可以给技术带来极大的便利。
版权声明: 本文为 InfoQ 作者【谙忆】的原创文章。
原文链接:【http://xie.infoq.cn/article/1a1cce6995b1706d590b52122】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论