热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

MySQL亿级数据量水平分库实践

需求背景水平拆分和垂直拆分一直是最常见的数据库优化方式,笔者所在的部门所使用的数据库一直是主从热备的架构,但数据量在一年前就已经破亿,并以飞快的增长速度不断增加。为了减小数据库的负

需求背景

水平拆分和垂直拆分一直是最常见的数据库优化方式,笔者所在的部门所使用的数据库一直是主从热备的架构,但数据量在一年前就已经破亿,并以飞快的增长速度不断增加。为了减小数据库的负担,提高数据库的效率,缩短查询时间,水平拆分的工作已经必不可免。分库前最重要的工作便是先对数据库进行迁移拆分,将原来的源库按照自身的业务需求和逻辑拆分成多个分库。笔者所在的部门采用的方案为基于自研客户端中间件的分片+不停机数据迁移的方式。

拆分策略

范围拆分

按照时间区间或 ID 区间来切分。例如:按日期将不同月甚至是日的数据分散到不同的库中;将 UserID 为1~9999的记录分到第一个库,10000~20000的分到第二个库,以此类推。

优点:单表大小可控;天然便于水平扩展,后期如果想对整个分片集群扩容时,只需要添加节点即可,无需对其他分片的数据进行迁移;使用分片字段进行范围查找时,连续分片可快速定位分片进行快速查询,有效避免跨分片查询的问题。
缺点:连续分片可能存在数据热点,例如按时间字段分片,有些分片存储最近时间段内的数据,可能会被频繁的读写,而有些分片存储的历史数据,则很少被查询;也有可能造成分片不均,如使用 UserID 区间切分,某些用户产生的数据远远超过别的用户时,将面临分片大小不均匀的问题

Hash 拆分

一般采用 hash 取模 mod 的切分方式,例如:将 Order 表根据 user_id 字段切分到4个库中,余数为0的放到第一个库,余数为1的放到第二个库,以此类推。这样同一个用户的数据会分散到同一个库中,如果查询条件带有分区键的字段,则可明确定位到相应库去查询。采用这样的拆分策略,数据分片相对比较均匀,不容易出现热点和并发访问的瓶颈。

一致性 Hash与哈希槽

普通 Hash 后取模的方式在从库需要再次数据迁移时需要所有从库都打乱重新映射,迁移效率不高问题,可采用mod 2^n 的取模方式优化,虽然解决了迁移过程中效率不高的问题,但是每次新增数据库都得增加(2×原有的数据库数量),不能单个单个增加,显然不够灵活。我们将选择缩小在一致性 Hash 和哈希槽这种伸缩性强的方案之间。一致性哈希算法是分布式缓存系统中常用的算法,解决了普通余数 Hash 算法伸缩性差的问题,解决了分布式环境下机器增加或者减少时,简单的取模运算无法获取较高命中率的问题。哈希槽是在 Redis Cluster 集群方案中采用的,Redis Cluster 集群没有采用一致性哈希方案,而是采用数据分片中的哈希槽来进行数据存储与读取的。我们选择了简单高效的哈希槽方案,相比于一致性哈希需要新增多个虚拟节点来平衡负载,并且需要多一次查询 hash 后离值最近的节点,哈希槽显得更加简单高效。我们将所有数据分为1024个槽,不管以后系统将来如何发展,1024个槽将保持不变,就算考虑将来最极端的情况,1024个槽分别都对应了1024个库后无法再新增槽,我们还可对每个槽的数据进行再次分库迁移拆分。采用分槽的形式后,将数据和数据库实例分离开来,数据与槽绑定,数据在哪个槽是永恒不变的,而数据库实例可以通过配置动态得与槽绑定,将来要扩容只需重新设定每个数据库实例对应了哪些槽,经过人工计算平均划分即可实现分片均匀。

有关于一致性 Hash 算法的可见这篇博文:一致性Hash(Consistent Hashing)原理剖析

水平拆分带来的问题

而水平拆分必然会带来一些问题,例如:

  1. 原本依赖于数据库自增 id 的主键在分库的场景下,多个分库下 id 做不到全局唯一;
  2. 引入了分布式事务的问题,如果同一个逻辑事务里,涉及的数据跨多个数据库实例,本地事务将不生效;
  3. 需要将原本的源库做拆分迁移,如果数据量很大的情况下,不停机的数据迁移也将成为一个难点;
  4. 引入了跨库聚合的问题,分库分表后,表之间的关联操作将受到限制,就无法 join 位于不同数据库实例的表,结果原本一次查询能够完成的业务,可能需要多次查询才能完成;
  5. 同时当拆分后的数据库再次达到瓶颈,如何扩容也成为一个问题。

当然对应的是一些解决方案:

  1. 全局唯一 id 的问题也有自研开发的分布式 id 生成器提供全局唯一的有序id以及其他云文档存储部门生成提供的唯一 file_id 保证。
  2. 由于我们的业务中,现有的所有事务都是针对同一个 file_id 的,因此基于 file_id 分库后的数据都落在同一数据库实例上,不存在跨库乃至分布式事务的问题。当然,如果有跨库事务与分库这种需求同时存在时,分布式事务将不可避免,有关于分布式事务的方案,可见我写的这篇文章:对于 MySQL 分布式事务的几个看法
  3. 跨库 join 的问题,首先我们应当尽量避免跨库 join 操作,这种操作因为需要中间件支持跨库查询并且跨库聚合的操作,不仅增加了中间件开发的复杂度和耦合度,而且十分没有必要。可以从以下几个方面进行优化:
    3.1 字段的冗余,将本来需要跨库查询的字段冗余在一张表里;
    3.2 设计表结构和分库时,需要 join 的表尽量采用同一个唯一 id 使得需要联表的数据落在同一库里;
    3.3 在业务层进行数据的聚合,分别查询后自行聚合筛选;实在没有其他办法时再考虑是否让中间件支持跨库操作。

中间件的开发

《MySQL 亿级数据量水平分库实践》

当前主流的分库方案既有基于客户端中间件的,也有引入 MySQL 代理中间层。大部分公司的分库方案都是使用中间件的形式,基于中间件的方式是分库方案中最快的,没有代理中间层,仅需要客户端进行一次哈希计算,不需要经过代理便可直接操作分库节点,不需要多余的 Proxy 机器,不用考虑 Proxy 部署与高可用的维护,并且引入 Proxy 将加多一层网络延迟。基于客户端中间件的方式则需要每种语言都实现一遍客户端中间件的逻辑,维护和开发的成本较高,耦合度相较于中间层的形式来说更高。

由于我们所有的业务代码统一使用了 Golang 编写,因此并无需要重复开发客户端中间件的问题。采用自研的方式是由于我们的需求并不复杂,并不需要引入一些重量级的分库中间件。

具体的 Golang 分库中间件执行一条 sql 流程分为三个步骤,解析 sql,通过解析语法树看中间件是否支持这条 sql ,并获取 sql 中配置的分区键值或者位置(分区键即为每张表中的某一列,这一列是根据自身义务决定的,一般为主键);获取分区键的值进行 hash 映射,获取分库的 db 实例执行 sql。

大概开发思路如下,由于标准库 sql 包中已经维护了一套非常完善的连接池机制以及数据操作流程,并且有非常优秀的 MySQL 驱动包 go-mysql-driver,我们决定对 go-mysql-driver 包进行封装,我们的中间件是基于 go-mysql-driver 实现 sql 包的一套驱动,对 sql 进行解析,根据开始时的配置,拿取 sql 中的分区键的值( sql 中已经包含分区键的值,即如 select * from user where id = '1')或者是分区键所处的位置( sql 中分区键的值为?,即如 select * from user where id = ?),并对最终获取到的分区键的值做hash计算,计算出映射的槽,并操作槽对应的 db 实例执行这行 sql,db 实例是使用 go-mysql-driver 驱动打开的,无需重复从头开始造轮子。

db,err := sql.open("new_driver")

通过这种实现标准库 sql 包的驱动,从旧驱动改成新驱动,只需要改动一行代码便可轻松切换。

迁移工具的开发

介绍完中间件,下面详细得阐述下,我们的迁移工具的开发以及开发过程中遇到的一些问题:

我们决定整体方案对业务是无感知的,因此我们采用了一种无侵入的方式,迁移过程中的数据拆分的方式我们借鉴了 Redis 扩容的方式,采用分槽的概念对所有数据根据配置进行分槽映射,并根据每个表设立的分区键与总槽数的 hash 值决定每条数据的 slot 节点。此处采用分槽的设计目的是为了当数据容量过大时可以进行灵活伸缩,而不管是在初次迁移过程中,还是为了以后的扩容迁移,开发一套在线迁移工具势在必行。

由于在迁移的过程中,必须加入自身的业务逻辑,如分库分表等,因此类似于 MySQLDumper 的这种全量迁移工具我们无法使用,我们采用的是类 MySQLDumper 不锁表的全量分库迁移+建立主从复制的增量复制迁移/基于 Binlog 的增量复制迁移。由于我们使用的云数据库的特殊性,无法直接与源库建立主从复制关系,我们采用了基于 Binlog 增量复制的方案。

如何实现全量迁移呢?将 MySQLDumper 过程中执行的命令拿出去分析即可得知

FLUSH /*!40101 LOCAL */ TABLES
FLUSH TABLES WITH READ LOCK
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ
START TRANSACTION /*!40100 WITH CONSISTENT SNAPSHOT */
SHOW VARIABLES LIKE 'gtid\_mode'
SHOW MASTER STATUS
UNLOCK TABLES
show create table `your_table`
SELECT /*!40001 SQL_NO_CACHE */ * FROM `your_table`

FLUSH TABLES WITH READ LOCK 先对所有的 Tables 加锁处理

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ 设置会话隔离级别为可重复读(RR)隔离级别

START TRANSACTION /*!40100 WITH CONSISTENT SNAPSHOT */ 保存一份当前所有数据的快照

UNLOCK TABLES 获取完快照即可解锁,整个过程锁定时间不超过1s

SHOW VARIABLES LIKE 'gtid\_mode' ,SHOW MASTER STATUS 保存当前快照的 binlog pos位置

后面即可使用 select * 进行全表扫描,对扫描的每一行进行 hash 分库后执行 insert,即可实现全量的分库迁移。

而至于增量复制迁移,基本原理即是将自身模拟成 MySQL 从库,进而接受主库传来的 binlog,对传来的 binlog 做解析,最终实现对每一行 hash 分库后也执行相对应的 insert/update/delete 操作即可实现增量分库迁移。


推荐阅读
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • 一句话解决高并发的核心原则
    本文介绍了解决高并发的核心原则,即将用户访问请求尽量往前推,避免访问CDN、静态服务器、动态服务器、数据库和存储,从而实现高性能、高并发、高可扩展的网站架构。同时提到了Google的成功案例,以及适用于千万级别PV站和亿级PV网站的架构层次。 ... [详细]
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • 基于PgpoolII的PostgreSQL集群安装与配置教程
    本文介绍了基于PgpoolII的PostgreSQL集群的安装与配置教程。Pgpool-II是一个位于PostgreSQL服务器和PostgreSQL数据库客户端之间的中间件,提供了连接池、复制、负载均衡、缓存、看门狗、限制链接等功能,可以用于搭建高可用的PostgreSQL集群。文章详细介绍了通过yum安装Pgpool-II的步骤,并提供了相关的官方参考地址。 ... [详细]
  • 一、Hadoop来历Hadoop的思想来源于Google在做搜索引擎的时候出现一个很大的问题就是这么多网页我如何才能以最快的速度来搜索到,由于这个问题Google发明 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • Java学习笔记之使用反射+泛型构建通用DAO
    本文介绍了使用反射和泛型构建通用DAO的方法,通过减少代码冗余度来提高开发效率。通过示例说明了如何使用反射和泛型来实现对不同表的相同操作,从而避免重复编写相似的代码。该方法可以在Java学习中起到较大的帮助作用。 ... [详细]
  • Todayatworksomeonetriedtoconvincemethat:今天在工作中有人试图说服我:{$obj->getTableInfo()}isfine ... [详细]
  • 上图是InnoDB存储引擎的结构。1、缓冲池InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。因此可以看作是基于磁盘的数据库系统。在数据库系统中,由于CPU速度 ... [详细]
  • 解决Sharepoint 2013运行状况分析出现的“一个或多个服务器未响应”问题的方法
    本文介绍了解决Sharepoint 2013运行状况分析中出现的“一个或多个服务器未响应”问题的方法。对于有高要求的客户来说,系统检测问题的存在是不可接受的。文章详细描述了解决该问题的步骤,包括删除服务器、处理分布式缓存留下的记录以及使用代码等方法。同时还提供了相关关键词和错误提示信息,以帮助读者更好地理解和解决该问题。 ... [详细]
  • 2021最新总结网易/腾讯/CVTE/字节面经分享(附答案解析)
    本文分享作者在2021年面试网易、腾讯、CVTE和字节等大型互联网企业的经历和问题,包括稳定性设计、数据库优化、分布式锁的设计等内容。同时提供了大厂最新面试真题笔记,并附带答案解析。 ... [详细]
  • php缓存ri,浅析ThinkPHP缓存之快速缓存(F方法)和动态缓存(S方法)(日常整理)
    thinkPHP的F方法只能用于缓存简单数据类型,不支持有效期和缓存对象。S()缓存方法支持有效期,又称动态缓存方法。本文是小编日常整理有关thinkp ... [详细]
  • Python脚本编写创建输出数据库并添加模型和场数据的方法
    本文介绍了使用Python脚本编写创建输出数据库并添加模型数据和场数据的方法。首先导入相应模块,然后创建输出数据库并添加材料属性、截面、部件实例、分析步和帧、节点和单元等对象。接着向输出数据库中添加场数据和历程数据,本例中只添加了节点位移。最后保存数据库文件并关闭文件。文章还提供了部分代码和Abaqus操作步骤。另外,作者还建立了关于Abaqus的学习交流群,欢迎加入并提问。 ... [详细]
  • 云原生应用最佳开发实践之十二原则(12factor)
    目录简介一、基准代码二、依赖三、配置四、后端配置五、构建、发布、运行六、进程七、端口绑定八、并发九、易处理十、开发与线上环境等价十一、日志十二、进程管理当 ... [详细]
  • 本文介绍了在PostgreSQL中批量导入数据时的优化方法。包括使用unlogged表、删除重建索引、删除重建外键、禁用触发器、使用COPY方法、批量插入等。同时还提到了一些参数优化的注意事项,如设置effective_cache_size、shared_buffer等,并强调了在导入大量数据后使用analyze命令重新收集统计信息的重要性。 ... [详细]
author-avatar
月舞B的啊
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有