美文网首页
MySQL 对于千万级的大表要怎么优化?

MySQL 对于千万级的大表要怎么优化?

作者: 老鼠AI大米_Java全栈 | 来源:发表于2022-03-29 11:05 被阅读0次

首先采用Mysql存储千亿级的数据,确实是一项非常大的挑战。Mysql单表确实可以存储10亿级的数据,只是这个时候性能非常差,项目中大量的实验证明,Mysql单表容量在500万左右,性能处于最佳状态。

针对大表的优化,主要是通过数据库分库分表来解决,目前比较普遍的方案有三个:分区,分库分表,NoSql/NewSql。实际项目中,这三种方案是结合的,目前绝大部分系统的核心数据都是以RDBMS存储为主,NoSql/NewSql存储为辅。

分区

首先来了解一下分区方案。[分区表]是由多个相关的底层表实现的。这些底层表也是由句柄对象表示,所以我们也可以直接访问各个分区,存储引擎管理分区的各个底层表和管理普通表一样(所有的底层表都必须使用相同的存储引擎),分区表的索引只是在各个底层表上各自加上一个相同的索引。这个方案对用户屏蔽了sharding的细节,即使查询条件没有[sharding column],它也能正常工作(只是这时候性能一般)。不过它的缺点很明显:很多的资源都受到单机的限制,例如[连接数],网络吞吐等。如何进行分区,在实际应用中是一个非常关键的要素之一。

下面开始举例:以客户信息为例,客户数据量5000万加,项目背景要求保存客户的银行卡绑定关系,客户的证件绑定关系,以及客户绑定的业务信息。此业务背景下,该如何设计数据库呢。项目一期的时候,我们建立了一张客户业务绑定关系表,里面冗余了每一位客户绑定的业务信息。基本结构大致如下:

image.png

查询时,对银行卡做索引,业务编号做索引,证件号做索引。随着需求大增多,这张表的索引会达到10个以上。而且客户解约再签约,里面会保存两条数据,只是绑定的状态不同。假设我们有5千万的客户,5个[业务类型],每位客户平均2张卡,那么这张表的数据量将会达到惊人的5亿,事实上我们系统用户量还没有过百万时就已经不行了。这样的设计绝对是不行的,无论是插入,还是查询,都会让系统崩溃。

image.png

从上图我们可以看出,有2中类型的文件,.frm文件和.ibd文件

  • frm文件:表结构文件
  • ibd文件:InnoDB中,索引和数据都在同个文件.ibdata(你的执行结果可能是.MYD索引文件和.MYI数据文件,没关系,这是MyIsAm存储引擎,对应着InnoDB的.ibd文件)。因为Order这张表分为5个区,所以有5个这样的文件
  • par文件:你执行的结果可能有.par文件也可能没有。注意:从MySql 5.7.6开始,不再创建.par分区定义文件。分区定义存储在内部数据字典中。

[mysql数据库]中的数据是以文件的形势存在磁盘上的,默认放在/mysql/data下面(可以通过my.cnf中的datadir来查看), 一张表主要对应着三个文件,一个是frm存放表结构的,一个是myd存放表数据的,一个是myi存表索引的。这三个文件都非常的庞大,尤其是.myd文件,快5个G了。下面进行第一次分区优化,Mysql支持的分区方式有四种:

image.png

在我们的项目中,range分区和[list分区]没有使用场景,如果基于绑定编号做range或者list分区,绑定编号没有实际的业务含义,无法通过它进行查询,因此,我们就剩下 HASH 分区和 KEY 分区了,HASH分区仅支持int类型列的分区,且是其中的一列。 KEY 分区倒是可以支持多列,但也要求其中的一列必须是int类型;看我们的库表结构,发现没有哪一列是int类型的,如何做分区呢?增加一列,绑定时间列,将此列设置为int类型,然后按照绑定时间进行分区,将每一天绑定的用户分到同一个区里面去。

这次优化之后,我们的插入快了许多,但是查询依然很慢,为什么?

因为在做查询的时候,我们也只是根据银行卡或者证件号进行查询,并没有根据时间查询,相当于每次查询,mysql都会将所有的分区表查询一遍。进行第二次方案优化,既然 HASH 分区和 KEY分区要求其中的一列必须是int类型的,那么创造出一个int类型的列出来分区是否可以?分析发现,银行卡的那串数字有秘密。银行卡一般是16位到19位不等的数字串,我们取其中的某一位拿出来作为表分区是否可行呢,通过分析发现,在这串数字中,其中确实有一位是0到9随机生成的,我们基于银行卡号+随机位进行KEY分区,每次查询的时候,通过计算截取出这位随机位数字,再加上卡号,联合查询,达到了分区查询的目的,需要说明的是,分区后,建立的索引,也必须是分区列,否则Mysql还是会在所有的分区表中查询数据。

通过银行卡号查询绑定关系的问题解决了,那么证件号呢,如何通过证件号来查询绑定关系。前面已经讲过,做索引一定是要在分区健上进行,否则会引起全表扫描。我们再创建了一张新表,保存客户的证件号绑定关系,每位客户的证件号都是唯一的,新的证件号绑定关系表里,证件号作为了主键,那么如何来计算这个分区健呢,客户的证件信息比较庞杂,有身份证号,港澳台通行证,[机动车驾驶证]等等,如何在无序的证件号里找到分区健。为了解决这个问题,我们将证件号绑定关系表一分为二,其中的一张表专用于保存身份证类型的证件号,另一张表则保存其他证件类型的证件号,在身份证类型的证件绑定关系表中,我们将身份证号中的月数拆分出来作为了分区健,将同一个月出生的客户证件号保存在同一个区,这样分成了12个区,其他证件类型的证件号,数据量不超过10万,就没有必要进行分区了。这样每次查询时,首先通过证件类型确定要去查询哪张表,再计算分区健进行查询。作了分区设计之后,保存2000万用户数据时银行卡表的数据保存文件就分成了10个小文件,证件表的数据保存文件分成了12个小文件,解决了这两个查询的问题,还剩下一个问题:业务编号怎么办?一个客户有多个签约业务,如何进行保存?这时候,采用分区的方案就不太合适了,它需要用到分表的方案。

分表

我们前面有提到过对于mysql,其数据文件是以文件形式存储在磁盘上的。当一个数据文件过大时,操作系统对大文件的操作就会比较麻烦耗时,且有的操作系统就不支持大文件,这个时候就必须分表了。另外对于mysql常用的存储引擎是Innodb,它的底层数据结构是B+树。当其数据文件过大的时候,查询一个节点可能会查询很多层次,而这必定会导致多次IO操作进行装载进内存,肯定会耗时的。除此之外还有Innodb对于B+树的锁机制。对每个节点进行加锁,那么当更改表结构的时候,这时候就会树进行加锁,当表文件大的时候,这可以认为是不可实现的。所以综上我们就必须进行分表与分库的操作。

如何进行分库分表,目前互联网上有许多的版本,比较知名的一些方案:阿里的TDDL,DRDS和cobar,京东金融的sharding-jdbc;民间组织的MyCAT;360的Atlas;美团的zebra;其他比如网易,58,京东等公司都有自研的中间件。

这么多的分库分表中间件方案归总起来,就两类:client模式和proxy模式。

image.png

client模式

image.png

proxy模式

无论是client模式,还是proxy模式。几个核心的步骤是一样的:SQL解析,重写,路由,执行,结果归并。个人比较倾向于采用client模式,它架构简单,性能损耗也比较小,运维成本低。

如何对业务类型进行分库分表。分库分表最重要的一步,即sharding column的选取,sharding column选择的好坏将直接决定整个分库分表方案最终是否成功。而sharding column的选取跟业务强相关。在我们的项目场景中,sharding column无疑最好的选择是[业务编号]。通过业务编号,将客户不同的绑定签约业务保存到不同的表里面去,根据业务编号路由到相应的表中进行查询,达到进一步优化sql的目的。

数据库的优化策略

  • 第一优化sql和索引;

  • 第二加缓存,memcached,redis;

  • 第三以上都做了后,还是慢,就做主从复制或主主复制,[读写分离],可以在应用层做,效率高,也可以用三方工具,第三方工具推荐360的atlas,其它的要么效率不高,要么没人维护;

  • 第四如果以上都做了还是慢,不要想着去做切分,mysql自带分区表,先试试这个,对你的应用是透明的,无需更改代码,但是sql语句是需要针对[分区表]做优化的,sql条件中要带上分区条件的列,从而使查询定位到少量的分区上,否则就会扫描全部分区,另外分区表还有一些坑,在这里就不多说了;

  • 第五如果以上都做了,那就先做垂直拆分,其实就是根据你模块的耦合度,将一个大的系统分为多个小的系统,也就是分布式系统;

  • 第六才是水平切分,针对数据量大的表,这一步最麻烦,最能考验技术水平,要选择一个合理的sharding key,为了有好的查询效率,表结构也要改动,做一定的冗余,应用也要改,sql中尽量带sharding key,将数据定位到限定的表上去查,而不是扫描全部的表;

mysql数据库一般都是按照这个步骤去演化的,成本也是由低到高;

数据库设计和表创建时就要考虑性能

mysql数据库本身高度灵活,造成性能不足,严重依赖开发人员能力。也就是说开发人员能力高,则mysql性能高。这也是很多关系型数据库的通病,所以公司的dba通常工资巨高。

设计表时要注意:

  • 表字段避免null值出现,null值很难查询优化且占用额外的索引空间,推荐默认数字0代替null。
  • 尽量使用INT而非BIGINT,如果非负则加上UNSIGNED(这样数值容量会扩大一倍),当然能使用TINYINT、SMALLINT、MEDIUM_INT更好。
  • 使用枚举或整数代替字符串类型
  • 尽量使用TIMESTAMP而非DATETIME
  • 单表不要有太多字段,建议在20以内
  • 用整型来存IP

索引

  • 索引并不是越多越好,要根据查询有针对性的创建,考虑在WHERE和ORDER BY命令上涉及的列建立索引,可根据EXPLAIN来查看是否用了索引还是全表扫描
  • 应尽量避免在WHERE子句中对字段进行NULL值判断,否则将导致引擎放弃使用索引而进行全表扫描
  • 值分布很稀少的字段不适合建索引,例如"性别"这种只有两三个值的字段
  • 字符字段只建前缀索引
  • 字符字段最好不要做主键
  • 不用外键,由程序保证约束
  • 尽量不用UNIQUE,由程序保证约束
  • 使用多列索引时主意顺序和查询条件保持一致,同时删除不必要的单列索引
  • 简言之就是使用合适的数据类型,选择合适的索引

选择合适的数据类型
(1)使用可存下数据的最小的数据类型,整型 < date,time < char,varchar < blob
(2)使用简单的数据类型,整型比字符处理开销更小,因为字符串的比较更复杂。如,int类型存储时间类型,bigint类型转ip函数
(3)使用合理的字段属性长度,固定长度的表会更快。使用enum、char而不是varchar
(4)尽可能使用not null定义字段
(5)尽量少用text,非用不可最好分表

选择合适的索引列
(1)查询频繁的列,在where,group by,order by,on从句中出现的列
(2)where条件中<,<=,=,>,>=,between,in,以及like 字符串+通配符(%)出现的列
(3)长度小的列,索引字段越小越好,因为数据库的存储单位是页,一页中能存下的数据越多越好
(4)离散度大(不同的值多)的列,放在联合索引前面。查看离散度,通过统计不同的列值来实现,count越大,离散程度越高:

sql的编写需要注意优化

  • 使用limit对查询结果的记录进行限定
  • 避免select *,将需要查找的字段列出来
  • 使用连接(join)来代替子查询
  • 拆分大的delete或insert语句
  • 可通过开启慢查询日志来找出较慢的SQL
  • 不做列运算:SELECT id WHERE age + 1 = 10,任何对列的操作都将导致表扫描,它包括数据库教程函数、计算表达式等等,查询时要尽可能将操作移至等号右边
  • sql语句尽可能简单:一条sql只能在一个cpu运算;大语句拆小语句,减少锁时间;一条大sql可以堵死整个库
  • OR改写成IN:OR的效率是n级别,IN的效率是log(n)级别,in的个数建议控制在200以内
  • 不用函数和触发器,在应用程序实现
  • 避免%xxx式查询
  • 少用JOIN
  • 使用同类型进行比较,比如用'123'和'123'比,123和123比
  • 尽量避免在WHERE子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描
    -对于连续数值,使用BETWEEN不用IN:SELECT id FROM t WHERE num BETWEEN 1 AND 5
  • 列表数据不要拿全表,要使用LIMIT来分页,每页数量也不要太大

为什么不建议使用join

  1. 对于mysql,不推荐使用子查询和join是因为本身join的效率就是硬伤,一旦数据量很大效率就很难保证,强烈推荐分别根据索引单表取数据,然后在程序里面做join,merge数据。

  2. 子查询就更别用了,效率太差,执行子查询时,MYSQL需要创建临时表,查询完毕后再删除这些临时表,所以,子查询的速度会受到一定的影响,这里多了一个创建和销毁临时表的过程。

  3. 如果是JOIN的话,它是走嵌套查询的。小表驱动大表,且通过索引字段进行关联。如果表记录比较少的话,还是OK的。大的话业务逻辑中可以控制处理。

  4. 数据库是最底层的,瓶颈往往是数据库。建议数据库只是作为数据store的工具,而不要添加业务上去。

相关文章

网友评论

      本文标题:MySQL 对于千万级的大表要怎么优化?

      本文链接:https://www.haomeiwen.com/subject/jmsvjrtx.html