走近科学之《MySQL 的秘密》
mysql 存储引擎、索引、执行计划、事务、锁、分库分表、优化
1、存储引擎(storage engines)
存储引擎规定了数据存储时的不同底层实现,如存储机制、索引、锁、事务等。
可以通过 show engines 命令查看当前服务器的存储引擎信息:
引擎名称 | 服务器是否支持 | 组件 | 事务 | 分布式事务 | 保存点 |
---|---|---|---|---|---|
InnoDB | 支持(默认引擎) | 支持事务、行级锁、外键 | 支持 | 支持 | 支持 |
MRG_MYISAM | 支持 | 相同 MyISAM 表的集合 | 不支持 | 不支持 | 不支持 |
MEMORY | 支持 | 基于哈希、存储在内存中、对临时表很有用 | 不支持 | 不支持 | 不支持 |
BLACKHOLE | 支持 | 写入的任何内容都会消失 ? ? ? | 不支持 | 不支持 | 不支持 |
MyISAM | 支持 | 不支持 | 不支持 | 不支持 | |
CSV | 支持 | 不支持 | 不支持 | 不支持 | |
ARCHIVE | 支持 | 不支持 | 不支持 | 不支持 | |
PERFORMANCE_SCHEMA | 支持 | 不支持 | 不支持 | 不支持 | |
FEDERATED | 不支持 |
注:保存点指的是事务过程中的一个逻辑点,用于取消部分事务。当事务结束后,会自动删除该事务的所有保存点。当事务回滚时,可以通过指定保存点使其回退到指定的点。
1.1、InnoDB
InnoDB 存储引擎是一个事务型存储引擎,其特点是支持事务、行锁设计、支持外键、支持非锁定读。
InnoDB 使用 mvcc (多版本并发控制)来获得高并发性;且实现了 SQL 标准的四种隔离级别;使用 next-key locking 的策略来避免幻读现象的产生;提供了 insert buffer(插入缓冲)、double write(二次写)、adaptive hash index(自适应哈希索引)read ahead(预读)等高性能和高可用功能。
对于表中数据的存储,InnoDB 采用了聚集索引的方式,因此每张表中数据的存储都是按主键的存储进行存放(若没有显式创建聚集索引则主键为聚集索引)。若没有显式指定主键,则 InnoDB 会为每行数据生成一个 6 字节的 ROWID(隐藏列)作为主键,且插入数据时自增。
1.2、MRG_MYISAM
MRG_MYISAM 存储引擎实际上时 MyISAM 的组合,它是多个使用 MyISAM 引擎的表的集合。它内部没有数据,真正的数据在 MyISAM 引擎表中。
MRG_MYISAM 解决了在对分库分表数据进行查询时,不确定数据放在那个表里的问题。它将多个表聚合成一个表统一查询,且不会影响原有数据。
1.3、MEMORY
MEMORY 引擎是将表中的数据存放在内存中,若数据库崩溃或重启则表中数据将丢失。它适用于存放临时数据的临时表,以及数据仓库中的维度表。MEMORY 引擎默认使用哈希索引,而不是 B+tree 索引。
虽然 MEMORY 引擎速度非常快,但其在使用上还是有一定的限制。如,只支持表锁,并发性能差;不支持 text 和 blob 类型;存储变长字段字段(varchar)时是按照定长字段(char)的方式进行的,因此会浪费内存。
mysql 使用 MEMORY 引擎作为临时表来存放查询的中间结果集。如果中间结果集大于 MEMORY 引擎表的容量设置,又或者中间结果集含有 text 或 blob 列类型字段,则 mysql 会将其转换到 MyISAM 引擎表存放到而存放到磁盘中。
1.4、BLACKHOLE
黑洞存储引擎,擎如其名。
1.5、MyISAM
MyISAM 存储引擎表级锁、支持全文索引,但不支持事务,主要面向一些 OLAP(联机分析处理)数据库应用。历史上,曾作为 mysql 的默认存储引擎出现过。MyISAM 引擎的缓冲池只缓存索引文件,而不缓存数据,这点不同于大多数存储引擎。
MyISAM 引擎表由 MYD 和 MYI 组成,MYD 用来存放数据文件,MYI 用来存放索引文件。其系统兼容性好,查询速度快。
1.6、CSV
CSV 存储引擎,其特点是不支持索引、不能为 null、不能自增;可对数据文件直接编辑;以 csv 格式存储数据;数据以文本方式存储在文件中;更新和删除先写入到临时文件,然后在 rnd_end() 函数中重新生成数据文件。
CSV 引擎可以将 csv 文件当作 mysql 的表进行处理。其 .frm 为表结构描述,.csv 为数据,.csm 为状态、当前记录数量等。
1.7、ARCHIVE
ARCHIVE 存储引擎只支持 insert 和 select 操作,从 mysql 5.1 开始支持索引。其使用 zlib 算法将数据行进行压缩后存储,压缩比一般达到 1:10。ARCHIVE 非常适合用于存储归档数据,如日志信息。ARCHIVE 引擎使用行锁来实现高并发的插入操作,但其并不是事务安全的存储引擎,其设计目的是提供告诉的插入和压缩功能。
1.8、PERFORMANCE_SCHEMA
PERFORMANCE_SCHEMA 为 mysql 5.5 新出的一个存储引擎,主要用来收集数据库服务器的性能参数。用户无法主动创建该引擎表,都是由 mysql 自己创建。
其提供了进程的锁、文件、互斥变量等信息;保持历史事件的汇总信息,为 mysql 服务器的性能作出详细判断。
1.9、FEDERATED
FEDERATED 存储引擎的表并不存放数据,而是指向一台远程 mysql 服务器上的表。非常类似于 sql server 的链接服务器和 oracle 的透明网关,不同的是,目前 FEDERATED 引擎只支持 mysql 数据库表,不支持异构数据库表。
2、索引(index)
索引是关系型数据库中给数据库表的一列或多列的值排序后的存储结构,其主要目的是为了提高数据库的检索性能。
下文所有概念性全都基于 mysql 数据库 InnoDB 存储引擎。
2.1、索引分类及定义
mysql 索引分类及定义如下:
- 主键索引:
是一种特殊的唯一索引,一个表只能有一个主键,且不允许有空值,字段值唯一。一般在建表时创建主键索引。如果没有指定主键,则系统会为每行数据自动生成一个 6 字节的隐藏列 ROWID 作为主键。 - 唯一索引:
字段值唯一,允许有空值,若为联合索引,则联合字段的值须唯一。 - 普通索引:
最基本的索引,无任何限制。 - 联合索引:
在多个字段上创建的索引,此外也无任何限制。 - 全文索引:
用于在文章中检索文本信息,尽可在 MyISAM 引擎表中使用。 - 聚集索引:
数据行的物理顺序与索引字段值的逻辑顺序一致,一个表中有且只能有一个聚集索引。若不显式指定,则默认为主键索引;若没有主键,则第一个唯一非空索引为聚集索引;若没有唯一非空索引,则自动生成一个 6 字节的隐藏列 ROWID 作为聚集索引。 - 非聚集索引:
除却一个聚集索引外,其余都是非聚集索引。
主流的索引数据结构为 B + Tree 和 Hash。聚集索引和非聚集索引都基于 B + Tree 结构。
2.2、B + Tree
B + Tree 为传统意义上的索引,且其是目前关系型数据库中最为常用和最为有效的索引。B + Tree 索引的构造类似于二叉树,根据键值对快速查找数据。
B + Tree 由 B Tree 演化而来, B 树为父,B + 树为子,B 树矮胖,B + 树更矮胖;平衡二叉树算是一种特殊的 B 树(B 树又名多路平衡树);平衡二叉树是基于二分法一种二叉树数据结构。(注:这里的 B 指 banlance,而不是 binary)
B Tree
一个 m 阶 B 树有以下特征:
- 关键字分布在整棵树中,且每一个出现且只出现在一个节点中(包括根节点、非叶子节点、叶子节点)。
- 拥有 n 个关键字的节点将把子节点分成 n + 1 段,即拥有 n + 1 个子节点,同时满足查找树的大小关系。
- 子节点的关键字个数的范围是 [m/2 - 1, m -1],非叶子节点的关键字个数等于子节点数 - 1。
- 非叶子节点最多只有 m 个子节点,且 m > 2。
- 根节点的子节点数范围为 [2, m];除根节点外的非叶子节点的子节点数范围为 [m/2, m] 向上取整。
关于上面这个 3 阶 B 树的查询、插入、删除的过程如下:
1、查询元素 4 的过程如下:
- 获取根节点并与 4 比较,4 < 15,则找到左侧子节点。
- 获取左侧子节点并与 4 比较,4 < 6 ,则找到左侧子节点。
- 获取左侧子节点并于 4 比较,4 = 4,返回。
2、插入元素 3 的过程如下:
- 通过类似查询的方式找到元素 3 应该插入的位置。
- 但 m 阶 b 树的节点关键字个数范围为 [m/2 - 1, m - 1],即 [1, 2],所以 关键字 2、3、4 所在的节点的中间元素需上移。
- 父节点插入 元素 3,父节点也超载,继续上移。
- 上移过程中要始终保持查找树的大小关系。
3、删除元素 17的过程如下:
- 通过查询找到 17 元素,然后删除。
- 删除后 关键字为 20 的节点只有一个子节点了,不满足 b 树条件(非叶子节点最多只有 m 个子节点,且 m > 2),所以需要左旋,20 元素作为子节点,23 元素作为父节点。
B + Tree
一个 m 阶 B + 树的特征如下:
- 非叶子节点不存放关键字的指针,只存放关键字及子节点指针,且只进行数据索引。
- 非叶子节点的关键字个数与子树个数相等。
- 所有叶子节点中包含了全部的关键字信息,以及这些关键字对应数据的存放的指针,且叶子节点的关键字从小到大顺序排列,左边节点结尾数据会存放右边节点开始数据的指针。
- 通常 b + 树上会有两个头指针,一个指向根节点,一个指向关键字最小的叶子节点。
关于上述 m 阶 b + tree 的查询、插入、删除的过程如下:
过程?过程和 b 树差不太多。
b + 树相对于 b 树查询时的优点:
- b + 树磁盘 IO 次数小。每一此获取节点数据都会进行磁盘 IO,由于 b + 树的非叶子节点中不保存数据,只保存索引,所以对于同等大小的磁盘页可以容纳更多的索引,进而降低了磁盘 IO,这也是 b + 树更加矮胖的原因。
- b + 树查询稳定。由于 b + 树的数据指针只保存在叶子节点,所以每一次查询都必须查找到叶子节点;但 b 树可能刚开始就结束了,又可能得查到末尾。
- 范围查找时,b + 树只需要遍历叶子节点的链表即可,但 b 树需要通过重复的中序遍历来确定上限。
B + 树索引与哈希索引的区别
b + 树索引:
- 支持同时匹配多个索引。
- 支持同时匹配多个索引中的部分索引。
- 支持索引列左前缀查找。
- 支持索引列等值、范围查找。
哈希索引:
- 不支持排序。
- 不支持范围查找。
- 不支持联合索引的最左匹配原则。
区别:
- 在等值查询中,哈希索引占有绝对优势,速度极快;但若存在大量哈希冲突,则哈希等值查找效率极低。
- 由于哈希索引是无序的,所以哈希索引不支持范围查找;b+树索引的叶子节点是有序链表,有利于范围查找。
- 由于哈希索引是对索引列的完整值做哈希,所以哈希索引不支持 like ‘aa%’ 模糊查询。
- 哈希索引不支持联合索引的最左匹配原则,因为对于联合索引是将索引字段值合并后再哈希。
- b+树索引检索时间较平均;若出现大量哈希冲突,则哈希索引时间波动较大。
b + 树索引适合大多数场景,如组合查询、范围查询、排序、分组、模糊等;hash 索引适合离散性高、数据基数大、等值查询时。
2.3、名词解释
-
最左匹配原则
最左匹配原则指的是 where 子句中的条件顺序要与联合索引的列顺序保持一致,否则索引不生效。# 给列 a、b、c 建立联合索引 (a, b, c) # 下列查询索引生效(后五个查询索引生效是因为 mysql 有查询优化器,会自动优化顺序) select * from table_name where a = 'a' and b = 'b' and c = 'c' select * from table_name where a = 'a' and c = 'c' and b = 'b' select * from table_name where b = 'b' and a = 'a' and c = 'c' select * from table_name where b = 'b' and c = 'c' and a = 'a' select * from table_name where c = 'c' and b = 'b' and a = 'a' select * from table_name where c = 'c' and a = 'a' and b = 'b' # 下列查询索引生效 select * from table_name where a = 'a' select * from table_name where a = 'a' and b = 'b' select * from table_name where a = 'a' and b = 'b' and c = 'c' # 下列查询索引生效(但用到的是 a 列索引,b、c 列都没用到) select * from table_name where a = 'a' and c = 'c' # 下列查询索引不生效 select * from table_name where b = 'b' select * from table_name where c = 'c' select * from table_name where b = 'b' and c = 'c' select * from table_name where c = 'c' and b = 'b'
顺带说一下最左前缀原则:
最左前缀原则指的是在模糊查询中,只有 like ‘aa%’ 时索引才会生效。其原因是,对于字符类型,其比较规则是一个字符一个字符比较,只有第一个字符匹配到了,才会比较第二个字符。# 索引生效 select * from table_name where a like 'aa%' # 索引不生效 select * from table_name where a like '%aa' select * from table_name where a like '%aa%'
-
回表
如果 select 所需的列包含非索引列或者根据一次索引不能获得所需数据,则需要到对应表中的对应行找到需要的列值,这个过程就叫回表。# 如存在表 table_name 含有 id、name、gender、age 字段 且 id 为主键 并为 name 字段建立了普通索引 # 以下查询不需要回表 # 因为通过搜索主键索引树就可以确定对应记录的位置 # 因为主键索引默认为聚集索引,而聚集索引指的是数据行的物理顺序跟字段值的逻辑顺序保持一致 # 也就是搜索了主键索引就相当于搜索了聚集索引,直接确定了数据行的地址 select * from table_name where id = 1 # 以下查询需要回表 # 因为 select 列包含了非索引列,查询时需要先搜索 name 索引树找到主键 id # 然后搜索主键索引树找到具体数据行 select * from table_name where name = 'momo'
-
覆盖索引
索引覆盖指的是在一颗索引树上就能获取到 select 所需的数据。即无需回表。
无需回表时,查询速度会更快,所以尽可能的实现索引覆盖将会在很大程度上提高查询效率。
实现索引覆盖最常见的方式就是为被查询的列建立联合索引。 -
索引下推
索引下推是 mysql 在 5.6 版本添加的查询优化策略(ICP 优化)。其具体实现是在索引搜索过程中,对索引列包含的条件字段先做判断,过滤掉不符合条件的数据,以此来减少回表次数。# 如存在表 table_name 含有 id、name、gender、age 字段 且 id 为主键 并为 name、gender 字段建立了联合索引 # 表中存在以下记录 # (1, 'aa', 1, 21) # (2, 'aabb', 1, 21) # (3, 'aacc', 2, 23) # (4, 'aadd', 2, 23) # 对于以下查询会出现索引下推 select * from table_name where name like 'aa%' and gender = 1 and age = 23 # mysql 5.6 之前的版本: # 查询时首先会搜索联合索引树找出 name 以 'aa' 开头的四条记录的 id,然后逐个回表比较 gender 和 age 字段的值 # mysql 5.6 版本: # 查询时首先会搜索索引树,根据最左匹配原则,会先匹配到 name like 'aa%' 的记录,同时过滤掉 gender != 1 的值 # 得到 id 为 3、4,然后回两次表再比较 age 的值。
-
索引失效
以下几种情况会导致索引失效:- 全表扫描,会使索引失效。
- where 子句中使用 !=、<、> 操作符会使索引失效。
- where 子句中对 null 值判断会使索引失效,如 select * from table_name where name = null。
- where 子句中使用 or 连接条件会使索引失效。
- where 子句中使用 in、not in 关键字会使索引失效。
- where 子句中使用 like ‘%aa’、like ‘%aa%’ 会使索引失效。
- where 子句中对字段进行表达式操作会使索引失效,如 select * from table_name where age/2 = 20。
- where 子句中进行函数操作会使索引失效,如 select * from table_name where substring(name, 1, 3) = ‘abc’。
- where 子句中第一个条件不是联合索引第一个索引列时会使索引失效(最左匹配原则)。
2.4、索引的创建
- 创建索引可以给检索带来性能上的很大提升(当发现检索慢时首先想到的应该是建立索引)。
- 首先应该考虑在 where、order by 所涉及到的列上建立索引。
- 在经常需要检索的字段上建立索引,比如用户名、商品名等。
- 一个表的索引最好不要超过 6 个,索引固然可以提高检索效率,但同时也降低了 insert 和 update 的效率,因为 insert 或 update 时可能会重建索引。
- 对于建立了索引的表要尽可能的避免全表扫描,否则会使索引失效。
3、执行计划
通过 mysql 的执行计划可以获取到 sql 执行时表的访问顺序、数据读取操作的操作类型、那些索引可以用、实际使用了那些索引、表之间的引用等。但其也有一定局限性,如不会告诉你触发器、存储过程或用户自定义函数对查询的影响;不能直到 mysql 执行 sql 时对 sql 的自动优化情况;只能作用于 select 操作。
mysql 的执行计划虽然不一定准确,但却在我们分析 sql 时能提供具有很大参考价值的信息。
3.1、explain 说明
可以通过 explain 关键字加 sql 语句来查看执行计划。
其中 id、type、key、rows、Extra 对分析执行情况有很大参考价值。
字段解释如下:
-
id:
执行 id,表示执行 select 子句或操作表的顺序。
id 相同,则表示执行顺序从上往下;如果存在子查询,则 id 值递增,值越大,执行优先级越高;id 若相同,则可认为是一组,组内从上往下执行,在所有族中,id 值越大,执行优先级越高。 -
select_type:
select_type 表示执行的类型。select_type 描述 备注 SIMPLE 简单查询 查询中不包含子查询或联合查询 PRIMARY 最外层查询 查询中若包含复杂的子查询,则最外层查询被标记为 primary SUBQUERY 子查询 在 select 或 where 列表中包含子查询时,该子查询被标记为 subquery DERIVED 衍生查询 在 from 列表中包含子查询时,该子查询被标记为 derived UNION 联合查询 若第二个 select 出现在 union 关键字之后,则被标记为 union UNION RESULT 联合结果查询 从 union 表中获取结果的 select 被标记为 union result -
table:
查询时访问的表名。 -
partitions:
若查询是基于分区表的,则代表查询时的分区。 -
type:
type 表示访问类型,又可理解为 mysql 在表中找到所需行的方式。type 描述 备注 ALL 全表扫描 扫描全表找到匹配行 index 全索引树扫描 扫描全索引树 range 索引树范围扫描 常见于 between、<、> 等 index_merge 索引合并 使用多个单列索引及逆行搜索 ref 非唯一索引扫描 即会通过索引找到一个或多个记录 eq_ref 唯一索引扫描 即会通过索引找到一个记录,常见于主键索引扫描或唯一索引扫描 const 常量 即 mysql 会将某些查询条件优化且转换为常量,如主键条件 system 特殊常量 const 的特殊情况,当表中只有一行记录时为 system BULL 最高境界 mysql 在优化过程中分解语句,执行时甚至不用访问表或索引 其性能从上到下依次增强,且 range 之前的都可以尝试优化。
-
possible_key:
查询时可能使用的索引。 -
key:
查询时实际使用的索引。 -
key_len:
使用的索引字节长度。 -
ref:
表示当前查询的连接匹配条件,即那些列或常量被用于查找索引列上的值。 -
rows:
mysql 根据表统计信息即索引选用信息,预估的找到所需记录需要读取的行数。 -
filtered:
符合某条件的记录数百分比。 -
Extra:
额外重要信息。Extra 描述 Using index 表示当前 select 中使用了覆盖索引 Using where 表示 mysql 存储引擎在接收到记录后进行 “后过滤(Post-filter)”,又可理解为当前 select 没有使用,将使用 where 子句中条件进行过滤 Using temporary 表示当前 select 会使用临时表来存放结果集,常见于排序和分组查询 Using filesort 表示当前排序使用 “文件排序”,无法利用索引排序时就会利用文件排序
4、数据库事务
4.1、事务的 ACID
- 原子性:
原子性是指一个事务中的所有操作要么全部成功执行,要么不执行。当其中一个操作发生异常时,事务回滚。 - 一致性:
一致性是指当一个事务完成后,使数据从一个状态转换为另一个状态,但是数据的完整性保持一致。 - 隔离性:
隔离性是指当多个用户并发访问数据库时,数据库会给每个用户开启事务,以事务为单位访问,事务之间互不影响。 - 持久性:
持久性是指事务执行成功后,对数据的修改是持久性的。
4.2、并发事务问题
- 丢失更新:
- 第一类丢失是指当两个事务同时修改同一行数据,其中一个事务撤销时会覆盖另一个已完成事务更新的数据。(比如浩浩去银行存100元,这时候银行刚好要扣除年费5元,当扣除成功后,浩浩突然想起晚上越好撸友去网吧通宵,所以不存了。这就会导致账户变为原来的100元)。
- 第二类丢失是指当两个事务同时更新同一行数据时,其中一个事务的更新结果会被另一个事务覆盖。(比如浩浩跟女友去逛街,浩浩说你去买衣服吧,我在这坐会儿,当女友选好衣服准备用浩浩的银行卡结账时,浩浩突然想起劫出了新皮肤,那一定得买一个,于是两个人同时支付,结果由于事务问题造成银行只扣了皮肤的钱(可把这货乐坏了))。
- 脏读:
脏读是指一个事务读取了另一个事务未提交的数据。(比如浩浩的银行卡有100元,好好在深夜加班敲代码,需要一杯咖啡提提神,10元,这时浩浩的女友看中了一个口红,需要97元,两人同时支付,但系统提示浩浩余额不足只有3元了,无奈。但浩浩的女友由于密码错误也支付失败。于是浩浩郁闷,他的女友则纳闷,越想越生气(他竟然偷偷改了支付密码不告诉我)。于是这将会是一个不平静的夜晚。)。 - 幻读:
幻读是指一个事务执行两次相同的查询操作,却得到不同的结果。这是因为两次查询执行中间执行了一个更新数据的事务。 - 不可重复读:
不可重复读是指一个事务两次查询同一行数据,却得到不同的结果。这是因为两次查询执行中间执行了一个更新数据的事务。
注:
- 脏读和不可重复度的区别:脏读是一个事务读了另一个事务未提交的数据;不可重复读是一个事务读取了前一个事务已提交的数据。
- 幻读和不可重复读的区别:幻读是两次查询大量数据;不可重复读是两次查询一行数据。
4.3、事务隔离级别
- Read_uncommited-只读未提交:
事务最低级别的隔离,允许一个事务可以看到另一个事务未提交的数据。只能解决第一类丢失更新的问题。 - Read_commited-读并提交:
保证了一个事务更新的数据提交后才能被另一个事务看到。解决了第一类更新丢失和脏读的问题。 - Repeatable_read-重复读:
保证了同一个事务在相同条件下前后两次能获取到一致的数据。解决了第一类丢失更新、第二类丢失更新、脏读和不可重复读问题。 - Serilizable-序列化:
事务串行序列化。解决了脏读、幻读和不可重复读,但效率差,实际开发中一般不用。
5、数据库锁
InnoDB 存储引擎实现了两种标准行级锁:
- 共享锁(S Lock):允许事务读一行数据。
- 排它锁(X Lock):允许事务删除或更新一行数据。
6、分库分表
分库分表是程序员三高(高并发、高可用、高性能)中高并发和高性能的一种实现方式或解决方式(它并不能实现高可用)。
6.1、分库分表的背景
对于 “小” 应用(用户量小、数据量小),单机数据库就能够满足;但是对于大型应用(用户量大、数据量大),由于其并发量大、业务数据增长率高、累积的数据量大,数据库的读写速度将会是限制应用性能的最直接原因。
最简单的解决方法就是读写分离,实现方式则是数据库的主从同步。即主库负责写,从库负责读。但随着用户量的增长,随之而来的就是大量用户请求。对于读操作,则可以考虑水平扩展从库;对于写操作来说,由于得保证数据的一致性,主库不能水平扩展。这时候,就需要考虑分库分表。
6.2、分库分表的目的
分库分表的直接目的是解决高并发下的处理速度问题以及数据库服务器的性能问题。
由于每个服务器的 TPS、IO、内存等都是有限的,所以架构设计主要的目的就是在合理利用服务器、网络等资源的前提下尽可能解决高并发问题。当请求并发量大时。可以考虑集群处理,如上文提到的数据库主从同步等;当单库数据量大时,可以考虑切分成更多更小的库(库中的表少,表里的数据少);当单表数据量大时,可以考虑切分成更多更小的表(表的字段少,表里的数据少)。
6.3、分库分表的方法
分库分表一般分为垂直切分和水平切分。当需要分库分表时,可以考虑先垂直后水平,因为垂直切分较水平切分在实现上相对来说较简单且更容易理解。
- 垂直切分:
- 垂直分表:
垂直分表是基于表字段进行的,即对于表字段过多的表,可以考虑将其中不常用、长度较大(如 text 类型)的字段放到明细表中。当一张表字段过百时就可以考虑垂直分表,同时也可以避免数据量大造成的 “跨页问题”。 - 垂直分库:
垂直分库是基于系统业务的,即根据不同业务将系统库切分成多个业务库。如电商系统中可以分为用户库、商品库、订单库等。
垂直分库最重要的一点是业务的拆分,合理的业务拆分有利于具体的实现,且便于后期扩展。同时,分库也意味着增加数据库服务器,以此避免由服务器性能而造成并发请求处理速度慢的问题。
- 垂直分表:
- 水平切分:
- 水平分表:
水平分表是指将数据量过大的单张表根据某种规则拆分成多张表,这种规则可以是时间、地域、业务类型等。如月表、年表、地域表、企业表等。 - 水平分库:
水平分库是在水平分表的基础上,添加多个数据库节点,每个库中有系统的所有表,但是表中的数据却是属于同一种规则的。 - 水平分库分表的切分规则:
- range:即按照范围来拆分,如 1 ~ 10000 一个表,10001 ~ 20000 一个表。
- 时间:即按照时间来拆分,如月表、年表等,同样的也是月库、年库。
- 地域:即按照地理区域来拆分,如西北表、东南表等,同样对应的是西北库、东南库等。
- 哈希取模:即将数据唯一值的哈希值取模映射到数据库节点哈希值取模上。
- 水平分表:
6.4、分库分表后的问题
分库分表后也会产生新的问题:
- 事务:
分库分表后事务将变成分布式事务。如果依赖数据库本身的事务支持去处理事务,那将会造成高昂的性能代价;如果在程序中处理事务,则会造成代码复杂等问题。 - 聚合查询:
如 group by、order by 等。 - 跨库 join:
分库分表后关联查询将受到限制,无法 join 位于不同分库的表,也无法 join 分表粒度不同的表,原来只需要一次的查询,现在可能需要多次查询才能完成。可以通过这几种方法解决:全局表:即一些基础数据所有库都存放一份;字段冗余:即将一些冗余信息同时存放在多张表中,以减少关联查询的可能(冗余字段修改时,也就需要修改多张表);程序中组装:即程序中分多次查询,然后再组装。
6.5、关于哈希取模
水平分表的拆分规则中有一种方法是哈希取模,其实现原理是对数据唯一值进行哈希取模,得到的结果将是存放该数据的数据库节点。如分库分表后有三个数据库节点,分别是 1、2、3,当 userId.hashCode()%n == 1 时(n 代表数据库节点个数)则表示该数据对应数据库 2,userId.hashCode()%n == 0 时,则对应数据库 1。
这种方式存在缺陷,即当新增节点或删除节点时,userId.hashCode()%n 将与原来不同,就会造成历史数据全部失效,此时就需要进行数据迁移。于是就有了一致性哈希。
一致性哈希实现原理,首先通过自定义哈希算法求出所有数据库节点的哈希值(如数据库节点命名唯一值的哈希值),使其较均匀的分布在一个 0 ~ 2^32-1 的数字圆环中,然后求出数据唯一值哈希,同样落在圆环上,其对应数据库将是圆环上顺时针方向上最近的一个节点。这样可以减少数据迁移的操作,当某个节点宕掉,只需要迁移这个节点对应的数据即可。
7、优化
数据库方面优化可以从 sql、数据库表结构、系统配置、硬件等这几个点进行。
7.1、SQL 优化
- 全表扫描,会使索引失效。所以要尽量避免全表扫描。
- where 子句中使用 !=、<、> 操作符会使索引失效。
- where 子句中对 null 值判断会使索引失效,如 select * from table_name where name = null。所以对于数据库字段尽量设置默认值,用 name = default_value 来代替 name = null。
- where 子句中使用 or 连接条件会使索引失效。对于 select age from user where age = 10 or age = 20,可以修改为 select age from user where age = 10 union all select age from user where age = 20。
- where 子句中使用 in、not in 关键字会使索引失效。可以替换为 between and。
- where 子句中使用 like ‘%aa’、like ‘%aa%’ 会使索引失效。尽量使用 like ‘aa%’。
- where 子句中对字段进行表达式操作会使索引失效,如 select * from table_name where age/2 = 20。可替换为 select age from user where age = 40。
- where 子句中进行函数操作会使索引失效,如 select * from table_name where substring(name, 1, 3) = ‘abc’。可替换为 select age from user where age = 40。
- where 子句中第一个条件不是联合索引第一个索引列时会使索引失效(最左匹配原则)。
- 正确使用 exists 与 in。当查询 A 表,子查询 B 表,A 表的数量多于 B 表时可考虑使用 in,反之则使用 exists。
- 尽量使用数字型字段,也就是字段值只有数字时将其设计为数字型字段,而不是字符型,这样会降低查询和连接的性能。因为系统在查询和连接时对于字符的比较是一个一个的比较,而数字只比较一次。
- 尽量使用 varchar 代替 char,因为存储空间会直接影响查询效率和性能开销。
- 任何时候都不要使用 select * ,可使用具体字段代替 *。
- 尽量避免使用游标,因为游标效率底(极低,大数据量时)。
- 尽量将多条 sql 语句压缩到一条 sql 中。因为每次 sql 执行前都会进行 网络连接、权限校验等,而这个过程是非常耗时的。
- 尽量使用 where 代替 having。因为 having 是先检索出所有记录再进行筛选,而 where 是在聚合前就进行筛选。
- 尽量使用表的别名。当查询需要进行多表连接时,用表的别名,这会减少解析时间。
- 对于 insert 优化。新建临时表时,如果插入的记录特别多则可考虑使用 select into 代替 create table,效率高且不会产生大量日志,如果数据量小则先 create table,再 insert。
- 对于 update 优化。如果值 update 一个两个字段则避免 update 全部字段,否则会造成明显的性能消耗,且会产生大量日志。
7.2、数据库表结构优化
数据库表结构优化则指分库分表。
7.3、系统配置优化
系统配置优化指通过修改数据库服务器参数配置来提高其性能。
7.4、硬件优化
买个更牛逼的数据库服务器。
听见奶奶说什么了吗?
奶奶总说…
艾欧尼亚 昂扬不灭!!!