大型分布式业务平台数据库优化方法(上)

微信版的原文转载出处

文章摘要:一个小小的MySQL数据库B-Tree索引可能会带来意想不到的性能优化提升……

一、数据库优化的必要性

目前在云计算、大数据、电商、金融支付等应用领域,几乎所有的业务平台都免不了与数据库打交道。由于服务本身是无状态的,一个涉及全链路的业务操作往往需要访问多个数据库实例来完成。在SOA与微服务架构设计大行其道的今天,虽然解决了业务平台的组件化和服务化的问题,然而业务平台上线运行后积累的业务数据呈指数型增长,将成为系统性能的瓶颈。因此,有必要考虑业务平台数据库的性能优化问题。对于热点数据表往往多添加一个B-Tree/B+Tree索引后对平台吞吐量、平均响应时间的性能提升,往往比通过加机器、提高网络带宽能力等硬件资源水平扩展方式更为有效和节约建设成本。

由于业务快速迭代或发布上线的压力,一般在建设业务平台初期,对后端服务的数据存储设计往往会采用“单库单表”模式。大家会觉得对于业务上线初期,“单库单表”模式往往足够,更多的精力可能会放在业务流程的设计和业务代码编程上。

不知道哪天,当运维同事收到监控告警,显示数据库中存量业务数据在慢慢增多,同时在进行巡检时发现业务平台对业务访问的平均响应时间明显超过往常。研发同事通过系统日志分析发现,系统中执行每一条sql语句都耗时比较长,上线之初空空如也的数据表中不知不觉已经接近500W的数据。研发同事遇到的问题也就是比较常见的“数据库存储优化”问题,当单表数据过大时候将对业务平台的增删改查操作造成巨大的影响。下面我们将通过以下几点从原理出发一步步分析如何进行数据库优化。

二、DB单表优化方法

对于业务平台来说,除非单表存储的数据会持续增长,否则一般不需要考虑拆分库表。因此,拆分库和表虽然可以提高单库和单表的容量以及系统的吞吐量,但是会对系统的逻辑、部署、运维等带来不小的影响并提高复杂度。一般,MySQL数据库的单表通过主键、索引以及分区表等方案可以存储千万级别以下的数据。如果加上本地缓存或者redis这样子的分布式缓存做缓冲,还可以进一步将表的存储容量控制在千万级别以上。下面以MySQL数据库为例,主要介绍数据库单表的优化方法。

1.数据字段类型

一般在创建库和数据表时,就应该考虑每张表的数据类型大小。为每个表的数据字段选择合适的类型会减少数据表每一行的存储大小。设想下,每行节约十几个字节,那么100W行的数据量节约的存储量是相当可观的,且存储量越小的表执行查询的速度也就越快。对于数据表的字段类型常用设计原则如下:

a、尽量使用TINYINT、SMALLINT、MEDIUM_INT作为整数类型而非INT,如果非负则加上UNSIGNED,VARCHAR的长度只分配真正需要的空间;

b、尽量少使用DOUBLE和DECIMAL类型;

c、时间类型上,尽量使用TIMESTAMP而非DATETIME,其存储空间只需要DATETIME类型的一半;

d、单表不要有太多字段,建议在20以内;

e、尽量设置NOT

NULL,避免使用NULL字段,NULL字段很难查询优化且占用额外索引空间;

f、使用枚举或整数代替字符串类型;

2.添加索引以及索引原理

可能读到这个标题的时候,很多童鞋可能会觉得,添加索引嘛,这个很简单,一个MySQL语句或者客户端操作下,应该是SoEasy的事儿。其实不然,数据库的索引作用和如何添加索引相信对于大家来说应该是比较熟悉的。下文将主要从MySQL的两种主要数据库引擎(MyISAM和InnoDB)以及索引的数据结构B-Tree(B+Tree)出发,介绍如何添加正确的索引来提高业务平台的增删改查操作的性能效率。

(1)索引的本质

这里借用下MySQL官方对索引的定义:“索引(Index)是帮助MySQL高效获取数据的数据结构。提取句子主干,就可以得到索引的本质:索引是数据结构。”

大家都知道数据库查询是DB的最基本的功能之一。任何人都希望查询数据能够尽可能地快,因此数据库的设计者也从查询性能的角度尽可能地去优化查询算法和选择合适的数据结构。在MySQL中,主要采用B-/B+Tree作为索引的结构(至于为什么选择这两种数据结构,限制篇幅就不展开叙述了)。

(2)MySQL两种存储引擎的索引实现

目前,大家用MySQL数据库建数据表时主要用MyISAM和InnoDB两种主流的存储引擎。对于这两种数据库存储引擎索引结构的差异可能不太会去深究,下面先介绍下这两种数据库存储引擎的差异,其中也分析了索引结构的差异。


如上面表格中“索引的结构”一栏所述,MyISAM存储引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。下面是MyISAM中索引的原理图:


MyISAM中索引检索的算法先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,读取相应数据记录。这里我们假设这个表仅有三列,分别是Col1、Col2和Col3列。在MyISAM中,主索引和辅助索引(Secondary

key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。假设我们在Col2列上建立一个辅助索引则索引结构如下:


虽然InnoDB也是使用B+Tree作为索引结构,但是具体实现方式与MyISAM截然不同。InnoDB的索引结构如下图所示:


在上面索引结构的具体实现方式上有两个区别,第一个区别在于InnoDB的数据文件本身就包含了索引部分。而从上文两种存储引擎区别的表格中可以知道,MyISAM索引和数据部分是分离的,索引文件仅保存的是数据记录的地址。而在InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。

第二个与MyISAM索引的不同是InnoDB的辅助索引data域存储相应记录主键的值而不是地址。换句话说,InnoDB的所有辅助索引都引用主键作为data域。例如,下图为定义在Col3上的一个辅助索引示意图:


了解该两种MySQL主流存储引擎的索引实现方式对正确使用和索引优化还是非常有帮助的。比如,了解InnoDB的索引实现后,就容易明白为什么不应该使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再比如,用非单调的字段作为主键在InnoDB存储引擎中并不是个好主意,因为InnoDB数据文件本身是一颗B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。

(3)通过索引优化sql性能方法

a、应尽量避免在WHERE子句中对字段进行NULL值判断,否则将导致数据库存储引擎放弃使用索引而进行全表扫描;

b、索引不是越多越好,根据业务代码的查询有针对性的创建,考虑在WHERE、ORDER/GROUP

BY命令上涉及的列建立索引,同时通过MySQL的执行计划命令EXPLAIN(关于执行计划的EXPLAIN下文会讲到)来查看自己的SQL语句是否落到索引上还是执行全表扫描;字段值分布很稀少的字段不适合建索引,例如"性别"这种只有两个值的枚举字段;

c、字符串值的字段只需建前缀索引(最左前缀匹配);同时该类字段最好不要用做主键;

d、尽量不用外键,由业务代码约束保证;

e、使用多列索引时主意顺序和查询条件保持一致,同时删除不必要的单列索引;

f、合理地使用覆盖索引,避免不必要地回表操作;

3.EXPLAIN的具体用法

上一节在讲通过添加索引优化sql语句时候已经带到过数据库EXPLAIN执行计划,下面将主要叙述下这个EXPLAIN关键词的用户,并通过一个现实开发中遇到的问题示例进行讲解。MySQL的EXPLAIN关键字用于SQL语句的查询执行计划(QEP)。这条命令的输出结果能够让我们了解MySQL优化器是如何执行SQL语句的。然而,这条命令的输出结果并没有提供任何SQL优化的建议,但它提供的重要信息可以帮助我们做出SQL调优决策。

(1)EXPLAIN的基本用法

MySQL的EXPLAIN语法可以运行在SELECT语句或者特定表上。如果作用在表上,那么此命令等同于DESC表命令。UPDATE和DELETE的语句需要进行性能改进时,当这些SQL语句不是直接在表的主键或者索引上执行时,为了确保最优化的索引使用率,需要把它们改写成SELECT语句(以便对它们执行EXPLAIN命令)。

一般我们用EXPLAIN关键字查看SELECT查询SQL语句的QEP时,每个执行的QEP中均会出现如下几个主要结果字段。

a、key:指出优化器选择使用的索引。一般来说SQL查询中的每个表都仅使用一个索引。也存在索引合并的少数例外情况,如给定表上用到了两个或者更多索引;

b、rows:提供了QEP试图分析所有存在于累计结果集中的行数目的MySQL优化器估计值。QEP很容易描述这个很困难的统计量;

c、possible_keys:指出SQL优化器为查询选定的索引;

d、key_len:定义用于SQL语句的连接条件的键的长度;

e、table:EXPLAIN命令输出结果中的一个单独行的唯一标识符。这个值可能是表名、表的别名或者一个为查询产生临时表的标识符;

f、select_type:提供了各种表示table列引用的使用方式的类型。最常见的值包括SIMPLE(不包含子查询和其他复杂语法的简单查询)、PRIMARY(为复杂查询创建的首要表)、DERIVED(当查询的表不是物理表时,那么就用该关键字标识)、UNION(执行union的sql语句的SQL元素)、DEPENDENT SUBQUERY(为使用子查询而定义的);

g、Extra:提供了有关不同种类的MySQL优化器路径的一系列额外信息;

h、type:代表QEP中指定的表使用的连接方式,const(当前表只有一行匹配时出现该关键字)、eq_ref(表示有一行是为每个之前确定的表而读取的)、ref(表示所有具有匹配的索引值的行都被用到)、range(所有符合一个给定范围值的索引行都被用到)、ALL(表示需要一次全表扫描其他类型的值);

i、ref:可以被用来标识那些用来进行索引比较的列或者常量;

(2)EXPLAIN执行计划性能调优的示例

以某业务平台的日志表历史记录查询的SQL语句为例,在没有对数据表添加索引的情况下,对业务逻辑代码中通过MyBatis—ORM框架执行的SQL语句进行EXPLAIN的QEP查询。具体的语句如下:

EXPLAINSELECT * FROM `os_biz_operator_log` WHERE user_id ='CIDC-U-a5b982db32364abbb6ff28c893d19255' AND operator_time BETWEEN"2017-01-01" AND"2017-02-28"

可以得到结果如下:

由于执行计划的结果TYPE为ALL,存在全表扫描问题,同时可以看出该条SQL语句的每次执行查询后返回行数为256656。通过查看日志发现,每次平均执行的时间为258毫秒

因此,可以通过对USER_ID和OPERATOR_TIME两个字段为添加索引:

ALTER TABLE`os_biz_operator_log` ADD INDEX user_id_time_index(USER_ID,OPERATOR_TIME)

添加索引后通过查看执行日志,可以看出执行时间为50毫秒,速率提升超过6

4.分区表解决单表数据过大

当数据库单表存的数据量过大时候,可以考虑采用分区表的方案来解决。MySQL的分区表是由多个相关的物理子表组成,这些表也是可以由句柄对象表示,所以对于用户也可以直接访问各个分区,存储引擎管理分区的各个物理表和管理普通表一样(所有的底层表都必须使用相同的存储引擎),分区表的索引只是在各个底层表上各自加上一个相同的索引。

MySQL在创建表时使用PARTITION BY子句定义每个分区存放的数据。在执行查询时,优化器会根据分区定义过滤那些没有需要数据的分区,这样查询就无须扫描所有分区—只需查找包含数据的分区即可。

在分区表上操作按照下面的操作逻辑进行:

a、select查询:当查询一个分区表的时候,分区层先打开并锁住所有的底层表,优化器判断是否可以过滤部分分区,然后再调用对应的存储引擎接口访问各个分区的数据;

b、insert操作:当写入一条记录时,分区层打开并锁住所有的底层表,然后确定哪个分区接受这条记录,再将记录写入对应的底层表;

c、delete操作:当删除一条记录时,分区层先打开并锁住所有的底层表,然后确定数据对应的分区,最后对相应底层表进行删除操作;

d、update操作:当更新一条数据时,分区层先打开并锁住所有的底层表,mysql先确定需要更新的记录在哪个分区,然后取出数据并更新,再判断更新后的数据应该放在哪个分区,然后对底层表进行写入操作,并对原数据所在的底层表进行删除操作;

虽然每个SQL操作都会打开并锁住所有的底层表,但这并不是说分区表在处理过程中是锁住全表的,如果存储引擎能够自己实现行级锁,如:innodb,则会在分区层释放对应的表锁,这个加锁和解锁过程与普通innodb上的查询类似。

(1)MySQL分区表的优点:

a、单表可以存储更多的数据;

b、分区表的数据更容易维护,可以通过清除整块分区以批量删除大量数据,也可以增加新的分区来支持新插入的数据;

c、部分查询能够从查询条件确定只落在少数分区上,查询执行速度比较快;

d、分区表的数据还可以分布在不同的物理设备上,从而高效地利用多个硬件设备;

e、可以使用分区表来避免某些特殊瓶颈,例如InnoDB单个索引的互斥访问、ext3文件系统的inode锁竞争;

f、可以备份和恢复单个分区;

(2)分区表的限制和缺点

a、一个表最多只能分1024个区;

b、如果分区字段中有主键或者唯一索引的列,那么所有主键列和唯一索引都必须包含;

c、分区表中无法使用外键约束;

下面是根据时间字段来建立分区表的一个示例:

CREATETABLE IF NOT EXISTS`cloudmaster`.`session_history_month` (

`ID` INT NOT NULL AUTO_INCREMENT COMMENT '主键id自增',

`USER_NAME` VARCHAR(50) NOT NULL COMMENT '用户登录名,这里是用户名唯一',

`LOGIN_DATE`DATETIME NULL COMMENT '登录时间',

`LOGOUT_DATE` DATETIME NULL COMMENT '登出时间',

`EXPIRE_DATE` DATETIME NULL COMMENT '登录过期时间',

`CREATE_DATE` DATETIME NULL COMMENT '创建时间',

`MODIFY_DATE` DATETIME NULL COMMENT '修改时间',

`VERSION` INT NULL DEFAULT 1 COMMENT '版本号',

PRIMARY KEY (`ID`,`CREATE_DATE`))

ENGINE = INNODB

PARTITION BY RANGE (TO_DAYS(CREATE_DATE)) (

PARTITION y2016_m12 VALUES LESS THAN(TO_DAYS('2017-10-01')),

PARTITION y2017_m01 VALUES LESS THAN(TO_DAYS('2017-11-01')),

PARTITION y2017_m02 VALUES LESS THAN (TO_DAYS('2017-12-01')),

PARTITION y2017_m03 VALUES LESS THAN(TO_DAYS('2018-01-01')),

PARTITION m_d VALUES LESS THAN MAXVALUE);

(3)分区表适合的场景

a、最适合的场景数据的时间序列性比较强,则可以按时间来分区,查询时加上时间范围条件效率会非常高,同时对于不需要的历史数据能很容易批量删除;

b、如果数据有明显的热点,而且除了这部分数据,其他数据很少被访问到,那么可以将热点数据单独放在一个分区,让这个分区的数据能够有机会都缓存在内存中,查询时只访问一个很小的分区表,能够有效使用索引和缓存;

对于数据库的单表在千万级以内的数据量,通过以上的DB单表优化方法一般都可以应应对,但是当数据量超过千万级别时,就需要通过分库分表、读写分离数据和缓存并用等方案进行技术架构优化改造来解决。限于篇幅原因,将在下篇文章继续为大家进行阐述。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容