千万级数据迁移与分表的技术方案

本篇文章主要讲解在年增长数据量为千万级的一个企业级处理方案。

本文是按照某个企业的实际情况进行的划分,每个企业实际业务不同,技术架构也不同,实际应用时,请根据自己企业实际情况进行。文章仅供参考。

按照千万级这个数据量,只进行水平分表,是完全能够满足划分范围的实时查询需要的。

另外,为了再提高些性能,将热点数据做个缓存即可。

关于一些交易相关的,建议订单表的订单号32位设置:yyyyMMddHHmmssSSS+UUID转10位字符生成+(5位分布式增长数字,初始10001,超过5位数字最大范围从10001重新计数),如果订单号是这种方式,则可以将前面15位数字作为下单的时间范围划分,并且对于对账有很大帮助。

另外,每个表一定得有一个自增ID,当页数较大,分页查询时,自增ID的存在可以极大提高查询的性能。

如订单号中并没有时间的标识,可以取订单创建时间/订单的交易时间字段。

选择一种比较好的分片方式,在这里按照订单创建时间的维度进行划分。

为什么不选择订单号取模的方式进行

  1. 订单的查询绝大多数是通过订单时间进行查询 

  2. 使用取模的方式进行分片会造成后期水平扩展麻烦,给后期增加了不必要的麻烦 

  3. 按照创建时间维度分片的缺点,使用订单号取模分片一样会有,非分片维度的查询问题,所有的分库分表都会有这种问题存在,基本上都需要引入映射冗余才能进行解决。 

所以选择按照时间范围进行一个分库。

假设是一年1000w的数据,MySQL官方文档说单表500W-800W,性能才逐渐下降,但是在实际应用中,MySQL可能在单表300W条左右的记录后性能就开始慢慢下降,当然500W以内的单表数据,MySQL查询性能都是比较理想的。

按照这个数据来说,半年进行一块分表,是比较合理的。不会引起单表数据查询性能过慢,以及半年一次的分表,也不过造成分表过多以及迁移频繁。

连续切分的优点是很大的,单表的大小是可以预估的,另外天然水平扩展,及其方便。

没有分库的缺点很明显:就是无法解决集中写入瓶颈的问题,可能存在热点数据问题。

即使是分库了,该缺点在使用范围切分的方案下还是会存在的。如果数据并发上不高,该缺点的影响非常小。

千万级数据迁移与分表的技术方案

按照订单创建时间维度,半年进行一次分表。

通过创建时间维度进行分片后,对于创建时间上的查询都可以直接路由到库表,假设按照前面划分,例如创建时间为2015年2月3日的订单,可以直接定位到db5。

对于非创建时间的查询,例如订单号,那么就需要另外的处理了。

非创建时间属性查询需求分析

在一般情况下,在产品设计之初都可以要求在查询中带上创建时间属性进行查询,这样可以直接路由到对应的库表。
在某些场景下,只能通过订单号进行查询时。例如查询订单轨迹,一定是根据订单号进行查询的。

有几种解决方案。

使用索引表

创建时间是可以直接定位到库表的,订单号不能定位到库表。可以建立一个索引表记录订单号->创建时间的映射关系;通过订单号访问时,先通过索引表查询到创建时间,再定位到相应的库表;属性比较少,可以容纳非常多的数据,一般是不需要再分库分表;如果数据量实在过大,可以再通过订单号进行分库分表;缺点就是需要多一次数据库的查询。

通过缓存映射(建议)

通过数据库的索引表性能比较低,可以将映射关系放入到缓存里,性能会高很多。
订单号查询先到缓存中查询创建时间,再根据创建时间定为库表;如果缓存没有命中订单号,那么采用查询全库的方式获取到订单号对应的创建时间,放入缓存;订单号与创建时间都是不可更改的数据,一旦放入缓存,不会再改变,缓存命中率极高;如果缓存中数据量过大,可以通过订单号再次进行缓存的水平切分;缺点就是需要多一次缓存的查询。

千万级数据迁移与分表的技术方案


通过融入的方式

思路就是在订单号之中便可以直接得到创建时间,或者通过函数运算可以得到创建时间。这种方法需要在项目设计之初便将关系考虑好,后期再进行找关系基本不太可能。每多一个没有分片属性维度的查询,便需要多一个映射的存储。

迁移方案

目前的订单数据都是在单表中。
并且订单数据有一个订单轨迹的单表,该表与订单表是一对多关系,订单轨迹表数据量肯定是比订单表大的。
既然已经知道了订单表有一个关联表,那么在设计之初便应该将订单轨迹表考虑进去。
为了后续关联查询的方便,订单轨迹的分表跟随订单表的切片key。
分表规则弄好之后,接下来就是数据的迁移,如何做到对业务影响最小的数据迁移是一个比较大的挑战。假设单表的老数据为1亿数据量,那么单纯的将数据从单表迁移到分片的表中,这个时间是比较长的。而且由于我们没有自增ID,在做分页的处理上,当查询到后面的数据时,性能会非常差。即使做倒排的一个处理,可以省一些时间,但也很慢。所以肯定是无法接受未迁移的数据对于用户不可见的这种情况发生。
所以考虑,在最开始如何做到不停服务平滑的过渡到分表,需要一个双写同步的方案。1、在项目上线后,采用双写读旧表的操作,旧数据进行逐步迁移、新增数据双写、修改操作同步到新表即可,查询全部走旧表。
2、CRUD全部走新表还是双写读走旧库查询通过缓存进行配置(或者给一个较长的周期,在代码中写死,一个月后的某个半夜点所有增删改查都走新表),当旧数据全部迁移分片后,可以在半夜某个点直接将配置更换为走分片的查询,旧表就可以下掉了,从而实现用户无感知的切换。

注意事项与解决方案

跨片的事务问题

目前没有进行分库,可以不进行考虑。解决方案一:分布式事务

跨片的联表的问题

目前没有进行分库,可以不进行考虑。好的设计和切分是可以减少甚至杜绝此种情况的发生的。现在都崇尚单表操作,简单的做法就是分两次查询进行,第一次查询出关联关系,第二次就根据关联关系进行请求到关联数据,在应用中再进行组合。

跨片的数据统计问题

在产品设计上,应尽量避免此种的需求,在每种统计的范畴下,都限制为半年一个维度。当然,会有无法避免需要统计年度的数据,就是跨半年的数据统计。
可以在各个切片上得到结果,在服务端再进行合并处理。和单表处理的不同是,这种处理是可以并行执行的,所以基本上都会比单一的大表会快很多,但是如果结果集很大,会给服务端造成比较大的内存消耗。

跨片的排序分页

这个问题比较大,所有的分库分表都会有这个问题。
第一页的数据比较好解决。查询所有分片的第一页数据,再通过服务端进行处理即可。

千万级数据迁移与分表的技术方案

但是跨片查询第二页之后的数据,就要稍微复杂一点。如果需要查询第二页的数据,那么需要将每一个分片的前二页数据都查询出来。因为各个分片的数据可能是随机的,为了排序的准确,必须将分片数据中的前N页数据都排序好后做合并,再做一个整体的排序分页,返回最终结果。随着页数增加,压力越大。
所以一般在这种情况下,产品设计只做排序前面几页的展示,因为排序后,后面页数数据并没有太多的意义,绝大多数人不会翻到排序10页以后的数据去看。

在某些情况下,产品的一个简单"妥协",可以给技术带来极大的便利。