当前位置: 首页 > 科技观察

MySQL分库分表会带来哪些问题?

时间:2023-03-12 00:50:50 科技观察

分库分表可以有效消除单机单库带来的性能瓶颈和压力,突破网络IO、硬件资源、连接数瓶颈,但同时也带来了一些问题。这些技术挑战和相应的解决方案如下所述。1.事务一致性问题分布式事务当更新的内容同时分布在不同的数据库中时,必然会导致跨库事务问题。跨分片交易也是分布式交易,没有简单的解决方案。一般可以使用“XA协议”和“两阶段提交”进行处理。分布式事务可以最大化数据库操作的原子性。但提交交易时需要多个节点协同,延误了提交交易的时间点,延长了交易的执行时间。这导致事务访问共享资源时发生冲突或死锁的可能性更高。随着数据库节点的增多,这种趋势会越来越严重,从而成为系统在数据库层面横向扩展的桎梏。最终一致性对于那些对性能要求高但对一致性要求不高的系统,往往不需要系统的实时一致性。只要在允许的时间段内达到最终一致性,就可以使用事务补偿。不同于交易执行出错后立即回滚的方式,交易补偿是一种事后检查和修复的措施。一些常见的实现方式包括:数据的对账校验、基于日志的比对、定期与标准数据源比对同步等。交易补偿也要结合业务系统来考虑。2、跨节点关联查询join在问题划分之前,系统中很多列表和详情页需要的数据都可以通过sqljoin来完成。拆分后,数据可能分布在不同的节点上。这时候join带来的问题就比较麻烦了。考虑到性能,尽量避免使用连接查询。跨节点关联查询join在问题划分之前,系统中很多列表和详情页需要的数据都可以通过sqljoin来完成。拆分后,数据可能分布在不同的节点上。这时候join带来的问题就比较麻烦了。考虑到性能,尽量避免使用连接查询。解决这个问题的一些方法:1)全局表全局表也可以看作是“数据字典表”,是系统中所有模块都可能依赖的一些表。为了避免跨库join查询,这样的表可以放在每个数据库中各保存一份。这些数据通常很少被修改,因此无需担心一致性问题。2)字段冗余是典型的反范式设计,以空间换时间,避免join查询以求性能。例如:在order表中保存userId时,也冗余保存了一份userName,这样查询订单明细时就不用去查询“买家用户表”了。但是这种方式适用场景有限,更适合依赖字段较少的情况。冗余字段的数据一致性也难以保证。就像上面订单表的例子,买家修改userName后,是否需要在历史订单中同步更新?这个也要结合实际的业务场景来考虑。3)数据在系统层面组装,查询分两次进行。在第一次查询的结果中找到关联数据id,然后根据id发起第二次请求,获取关联数据。最后,对获得的数据进行现场组装。4)在ER分片关系型数据库中,如果能够先确定表之间的关联关系,将具有关联关系的表记录存储在同一个分片中,则可以更好的避免cross-shardjoin问题。在1:1或者1:n的情况下,通常是根据主表的ID主键来划分。如下图所示:这样DataNode1上的order订单表和orderdetail订单明细表可以通过orderId部分关联查询,DataNode2上也是如此。3.跨节点分页、排序、功能问题跨节点、多数据库查询时,会出现限制分页、排序排序等问题。分页需要根据指定的字段进行排序。当排序字段为分片字段时,更容易通过分片规则定位到指定的分片;当排序字段不是分片字段时,它变得更加复杂。需要先对不同分片节点中的数据进行排序返回,然后对不同分片返回的结果集进行汇总和重新排序,最后返回给用户。如图:上图只取了第一页的数据,对性能影响不大。但是如果获取的page数量很大,情况就复杂很多,因为每个shard节点的数据可能是随机的。为了排序的准确性,需要对所有节点的前N页数据进行排序进行合并,最后再进行整体排序,比较消耗CPU和内存资源,所以页数越大,系统性能越差将。在使用Max、Min、Sum、Count等函数进行计算时,也需要先在每个分片上执行相应的函数,然后对每个分片的结果集进行汇总和重新计算,最后返回结果。如图:4.全局主键规避问题在分库分表环境下,由于表中的数据同时存在于不同的数据库中,主键值的自增长usuallyused将无用,并且不能保证某个分区数据库ID的自生成数据是全局唯一的。因此需要单独设计全局主键,避免跨数据库主键重复。有一些常见的主键生成策略:1)UUIDUUID标准形式包含32个十六进制数,分为5段,36个字符,形式为8-4-4-4-12,例如:550e8400-e29b-41d4-a716-446655440000UUID是最简单的主键解决方案。本地生成,性能高,不费时网络。但缺点也很明显。因为UUID很长,会占用很大的存储空间;另外,建立索引作为主键,根据索引进行查询也会有性能问题。在InnoDB下,UUID的乱序会导致数据位置的频繁变化,导致分页。2)在数据库中创建sequence表结合数据库维护主键id表:CREATETABLE`sequence`(`id`bigint(20)unsignedNOTNULLauto_increment,`stub`char(1)NOTNULLdefault'',PRIMARYKEY(`id`),UNIQUEKEY`stub`(`stub`))ENGINE=MyISAM;存根字段设置为唯一索引,同一个存根值在序列表中只有一条记录,可以同时为多张表生成全局ID。序列表内容如下:+--------------------+------+|id|stub|+-------------------+------+|72157623227190423|a|+--------------------+------+使用MyISAM存储引擎代替InnoDB以获得更高的性能。MyISAM使用表级锁,对表的读写都是序列化的,所以并发时不用担心读取同一个ID值两次。当需要全局唯一的64位ID时,执行:REPLACEINTOsequence(stub)VALUES('a');SELECTLAST_INSERT_ID();这两个语句是Connection级别的,selectlast_insert_id()必须和replaceinto连接到同一个数据库才能得到刚刚插入的新ID。使用replaceinto而不是insertinto的好处是避免了表行过多,不需要定期清理。这种方案比较简单,但缺点也很明显:存在单点问题,对DB的依赖性强。当DB异常时,整个系统不可用。配置主从可以提高可用性,但是当主库挂掉,主从切换时,特殊情况下数据一致性很难保证。另外,性能瓶颈仅限于单个MySQL的读写性能。flickr团队使用的一种主键生成策略和上面的序列表方案类似,但是更好的解决了单点和性能瓶颈的问题。本方案的总体思路是:建立2台以上的服务器进行全局ID的生成,每台服务器上只部署一个数据库,每个数据库都有一个序列表记录当前的全局ID。表中ID增长的步长为数据库个数,起始值依次错开,这样ID的生成可以hash到每个数据库。如下图:ID由两台数据库服务器生成,设置不同的auto_increment值。第一个序列的起始值为1,每一步递增2,另一个序列的起始值为2,每一步递增2。这样一来,第一台机器生成的ID都是奇数(1,3,5,7...),第二台机器生成的ID都是偶数(2,4,6,8...)。该方案将生成ID的压力平均分配到两台机器上。同时提供系统容错能力。如果第一台机器出现错误,可以自动切换到第二台机器获取ID。但是,也有几个缺点:向系统中添加机器使得水平扩展更加复杂;每获取一个ID,都需要对DB进行读写,对DB的压力还是很大的,只能通过堆机来提升性能。可以在flickr方案的基础上继续优化,使用batch的方式减少数据库的写入压力,每次获取一个范围的身份证号,用完再去数据库获取,可以大大降低数据库的压力。如下图所示:仍然使用两个DB来保证可用性,数据库中只存储当前最大的ID。ID生成服务一次批量取6个ID,先将max_id改为5,应用访问ID生成服务时,不需要访问数据库,从0到5依次从编号开始分配ID段缓存。发送完这些ID后,将max_id改为11,下次分发6~11的ID。这样一来,数据库的压力降低到原来的1/6。3)Snowflake分布式自增ID算法Twitter的snowflake算法解决了分布式系统生成全局ID的需求,生成64位Long数。组成部分为:1、接下来的41位是毫秒级别的时间,41位的长度可以表示69年的时间,5位datacenterId,5位workerId。10位长度支持最多部署1024个节点。最后12位是毫秒内的计数。12位计数序列号支持每个节点每毫秒产生4096个ID序列。随时间趋势递增;不依赖第三方系统,稳定性和效率高。理论上QPS约为409.6w/s(1000*2^12),整个分布式系统不会发生ID冲突;可以根据自己的业务灵活分配比特位。缺点是对机器时钟有很强的依赖性,如果时钟拨回,可能会导致产生重复的ID。综上,结合数据库和snowflake独有的ID方案,可以参考业界比较成熟的方案:Leaf——美团点评分布式ID生成系统,兼顾了高可用、容灾、分布式等问题计时。5、数据迁移和扩容当业务快速发展,面临性能和存储瓶颈时,就会考虑分片设计。这时候就不可避免地要考虑历史数据迁移的问题。一般的做法是先读取历史数据,然后按照指定的分片规则将数据写入各个分片节点。另外需要根据当前数据量、QPS、业务发展速度进行容量规划,计算大概需要的分片数量(一般建议单表数据量单个分片不超过1000W)。如果采用数值范围分片,只需要增加节点扩容,不需要迁移分片数据。如果采用数值取模分片,考虑后期扩容问题相对麻烦。