MySQL的一些总结

最近拜读了58同城的大神沈剑的关于mysql的一些文章,现在做一个总结如下:

一、索引到底是怎么实现的

这里要思考“为什么设计成这样”,而不是“是怎样的”。

数据库的索引用于提升数据库的查找速度。加速查找速度的数据结构有两种:一种是哈希,一种是树型。哈希的查询、插入、修改、删除的平均时间复杂度都是O(1),而树的平均时间复杂度都是O(lg(n))。这里可以看到哈希结构查找速度会快一点,但Mysql数据库一般使用的是树型结构,这是跟SQL的需求相关的。对于单行查询的SQL需求确实哈希索引更快,每次只查询一条记录。但对于排序查询的SQL需求,比如分组group by或者排序order by又或者比较<>等,哈希索引时间复杂度会退化为O(n),而树型的有序特性,依然能保持O(log(n))的高效率。所以说任何脱离需求的设计都是耍流氓,故InnoDB并不支持哈希索引。

那么数据库索引为什么要使用B+树呢?

首先要了解一下二叉搜索树,它不适合作为数据库索引是因为数据量大的时候树的高度会比较高,查询慢,而且每个节点只存储一个记录,可能导致一次查询多次磁盘IO。

然后就是B树,它的特点是:

1)不再是二叉搜索,而是n叉搜索;

2)叶子节点,非叶子节点,都存储数据;

3)中序遍历,可以获取所有的节点。

B树能够完美的利用“局部性原理”,局部性原理是:

1)内存读写块,磁盘读写慢

2)磁盘预读,并不是按需读取,而是按页预读。一页数据通常是4k。

3)软件设计尽量遵循“数据读取集中”,充分提高磁盘IO。

最后一种是B+树,它是在B树的基础上做了一些改进。

1)非叶子节点不再存储数据,数据只存储在同一层的叶子节点上;B+树中根到每一个节点的路径长度都一样,而B树不是。

2)叶子之间,增加了链表,获取所有节点不再需要中序遍历。

B+树比B树更优的特性:

1)范围查找,定位min和max之后,中间叶子节点,就是结果集,不用中序回溯。很适用范围查询SQL。

2)叶子节点存储实际的记录行,记录行相对比较紧密的存储,适合大数据量磁盘存储;非叶子节点存储记录的pk,用于查询加速,适合内存存储。

3)非叶子节点,不存储实际记录只存储记录的key时,相同内存的情况下B+树能够存储更多索引。

二、MyISAM与InnoDB的索引差异与实践

1.MyISAM的索引

MyISAM的索引与行记录是分开存储的,叫做非聚集索引。其主键索引与普通索引没有本质差异:有连续聚集的区域单独存储行记录;主键索引的叶子节点,存储主键,与对应行记录的指针;普通索引的叶子节点,存储索引列,与对应行记录的指针。

MyISAM的表可以没有主键。主键索引与普通索引是两棵独立的索引B+树,通过索引列查找时,先定位到B+树的叶子节点,再通过指针定位到行记录。

2.InnoDB的索引

InnoDB的主键索引与行记录是存储在一起的,叫做聚集索引:没有单独区域存储行记录;主键索引叶子节点,存储主键,与对应行记录而不是指针。

因为这个特性,InnoDB的表必须要有聚集索引:如果表定义了PK,则PK就是聚集索引;如果表没有定义PK,则第一个非空的unique列是聚集索引;否则InnnoDB会创建一个隐藏的row-id作为聚集索引。聚集索引也只能有一个,因为数据行在物理磁盘上只能有一份聚集存储。普通索引可以有多个,它与聚集索引是不同的,普通索引的叶子节点,只存储主键。

对于InnoDB表有几个要注意的:

1)不建议使用较长的列做主键,因为所有的普通索引都会存储主键,这样会导致普通索引过于庞大;

2)建议使用趋势递增的列做主键,由于数据行与索引一体,这样不至于插入记录时有大量索引分裂,行记录移动;

在SQL查找一条记录时往往是通过普通索引得到主键,然后通过聚集索引定位到行记录,所以其实是扫了两遍索引树。

三、InnoDB的并发为什么这么高

1.并发控制

并发任务对同一个临界资源进行操作,如果不采取措施,可以导致不一致,所以需要进行并发控制。技术上实现手段有:锁和数据多版本。

2.锁

普通锁能够操作数据前实现互斥保证一致性,但性能太低。于是出现共享锁和排他锁,读数据时加S锁,修改数据时加X锁。

主要特点:共享锁之间不互斥、排他锁与任何锁互斥。

3.数据多版本

为了进一步提高并发性, 加入了数据多版本,它的核心原理是:

1)写任务发生时,将数据克隆一份,以版本号区分;

2)写任务操作新克隆的数据,直至提交;

3)并发读任务可以继续读取旧版本的数据不至于阻塞。

提高并发的演进思路是:

1)普通锁,本质是串行执行

2)读写锁,实现读读并发

3)数据多版本,实现读写并发

4.redo、undo、回滚段

redo日志是数据库事务提交后,必须将更新后数据刷到磁盘上保证ACID特性,但磁盘随机写性能太低,所以优化方式是先将修改行为写到redo日志里变成顺序写,再定期将数据刷到磁盘上,这样能极大的提高性能。如果出现数据库崩溃,还没来得及刷盘的数据,在数据库重启后,会重做redo日志的内容,保证已提交事务对数据产生的影响都刷到磁盘上。所以redo日志用于保障已提交事务的ACID特性。

undo日志是数据库事务未提交时,会将事务修改数据的镜像存放到undo日志里,当事务回滚时或者数据库崩溃时可以利用undo日志,即旧版本数据,撤销未提交事务对数据库产生的影响。对于insert操作,undo日志记录新数据的pk,回滚时直接删除;对于delete/update操作,undo日志记录旧数据的row,回滚时直接恢复,它们分别存放在不同的buffer里。所以undo日志用于保障未提交事务不会对数据库的ACID特性产生影响。

回滚段是存储undo日志的地方。undo日志和回滚段和InnoDB的MVCC密切相关。

5.InnoDB为何能做到高并发

回滚段里的数据,其实就是历史数据的快照,这些数据是不会被修改,select可以高并发读取他们。

快照读是一种一致性情不加锁的读,一般普通的select语句都是快照读,而非快照读则指加了lock in share mode或者for update这种的select语句。

四、InnoDB的七种锁

1)自增锁是一种特殊的表级别锁(table-level lock),专门针对事务插入AUTO_INCREMENT类型的列。最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。与此同时,InnoDB提供了innodb_autoinc_lock_mode配置,可以调节与改变该锁的模式与行为。

2)共享/排它锁:

(1)事务拿到某一行记录的共享S锁,才可以读取这一行;

(2)事务拿到某一行记录的排它X锁,才可以修改或者删除这一行;

共享/排它锁的潜在问题是,不能充分的并行,解决思路是数据多版本。

3)意向锁是指,未来的某个时刻,事务可能要加共享/排它锁了,先提前声明一个意向。InnoDB支持多粒度锁(multiple granularity locking),它允许行级锁与表级锁共存,实际应用中,InnoDB使用的是意向锁。

意向锁有这样一些特点:

(1)首先,意向锁,是一个表级别的锁(table-level locking);

(2)意向锁分为:

  • 意向共享锁(intention shared lock, IS),它预示着,事务有意向对表中的某些行加共享S锁

  • 意向排它锁(intention exclusive lock, IX),它预示着,事务有意向对表中的某些行加排它X锁

举个例子:

select ... lock in share mode,要设置IS锁

select ... for update,要设置IX锁

(3)意向锁协议(intention locking protocol)并不复杂:

  • 事务要获得某些行的S锁,必须先获得表的IS锁

  • 事务要获得某些行的X锁,必须先获得表的IX锁

(4)由于意向锁仅仅表明意向,它其实是比较弱的锁,意向锁之间并不相互互斥,而是可以并行,其兼容互斥表如下:

          IS          IX

IS      兼容      兼容

IX      兼容      兼容

(5)额,既然意向锁之间都相互兼容,那其意义在哪里呢?它会与共享锁/排它锁互斥,其兼容互斥表如下:

          S          X

IS      兼容      互斥

IX      互斥      互斥

插入意向锁,是间隙锁(Gap Locks)的一种(所以,也是实施在索引上的),它是专门针对insert操作的。

多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会阻塞彼此。

4)记录锁是封锁索引记录。

5)间隙锁(Gap Locks)是封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。间隙锁的主要目的,就是为了防止其他事务在间隔中插入数据,以导致“不可重复读”。

6)临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。更具体的,临键锁会封锁索引记录本身,以及索引记录之前的区间。

五、事务的隔离级别

隔离性是指,多个用户的并发事务访问同一个数据库时,一个用户的事务不应该被其他用户的事务干扰,多个并发事务之间要相互隔离。

InnoDB实现了哪几种事务的隔离级别?

按照SQL92标准,InnoDB实现了四种不同事务的隔离级别:

  • 读未提交(Read Uncommitted)

  • 读提交(Read Committed, RC)

  • 可重复读(Repeated Read, RR)

  • 串行化(Serializable)

不同事务的隔离级别,实际上是一致性与并发性的一个权衡与折衷。

InnoDB使用不同的锁策略(Locking Strategy)来实现不同的隔离级别。

1)读未提交(Read Uncommitted)

这种事务隔离级别下,select语句不加锁。

此时,可能读取到不一致的数据,即“读脏”。这是并发最高,一致性最差的隔离级别。

2)串行化(Serializable)

这种事务的隔离级别下,所有select语句都会被隐式的转化为select ... in share mode。

这可能导致,如果有未提交的事务正在修改某些行,所有读取这些行的select都会被阻塞住。

这是一致性最好的,但并发性最差的隔离级别。

3)可重复读(Repeated Read, RR)

(1)普通的select使用快照读(snapshot read),这是一种不加锁的一致性读(Consistent Nonlocking Read),底层使用MVCC来实现。

(2)加锁的select(select ... in share mode / select ... for update), update, delete等语句,它们的锁,依赖于它们是否在唯一索引(unique index)上使用了唯一的查询条件(unique search condition),或者范围查询条件(range-type search condition):

  • 在唯一索引上使用唯一的查询条件,会使用记录锁(record lock),而不会封锁记录之间的间隔,即不会使用间隙锁(gap lock)与临键锁(next-key lock)。

  • 范围查询条件,会使用间隙锁与临键锁,锁住索引记录之间的范围,避免范围间插入记录,以避免产生幻影行记录,以及避免不可重复的读。

4)读提交(Read Committed, RC)

(1)普通读是快照读;

(2)加锁的select, update, delete等语句,除了在外键约束检查(foreign-key constraint checking)以及重复键检查(duplicate-key checking)时会封锁区间,其他时刻都只使用记录锁;

此时,其他事务的插入依然可以执行,就可能导致,读取到幻影记录。

六、各种SQL到底加了什么锁

1)普通select

(1)在读未提交(Read Uncommitted),读提交(Read Committed, RC),可重复读(Repeated Read, RR)这三种事务隔离级别下,普通select使用快照读(snpashot read),不加锁,并发非常高;

(2)在串行化(Serializable)这种事务的隔离级别下,普通select会升级为select ... in share mode;

2)加锁select

加锁select主要是指:

  • select ... for update

  • select ... in share mode

1.在唯一索引(unique index)上使用唯一的查询条件(unique search condition),会使用记录锁(record lock),而不会封锁记录之间的间隔,即不会使用间隙锁(gap lock)与临键锁(next-key lock);

2.其他的查询条件和索引条件,InnoDB会封锁被扫描的索引范围,并使用间隙锁与临键锁,避免索引范围区间插入记录;

3)update与delete

(1)和加锁select类似,如果在唯一索引上使用唯一的查询条件来update/delete,例如:

update t set name=xxx where id=1;

也只加记录锁;

(2)否则,符合查询条件的索引记录之前,都会加排他临键锁(exclusive next-key lock),来封锁索引记录与之前的区间;

(3)尤其需要特殊说明的是,如果update的是聚集索引(clustered index)记录,则对应的普通索引(secondary index)记录也会被隐式加锁,这是由InnoDB索引的实现机制决定的:普通索引存储PK的值,检索普通索引本质上要二次扫描聚集索引。

4)insert

同样是写操作,insert和update与delete不同,它会用排它锁封锁被插入的索引记录,而不会封锁记录之前的范围。同时,会在插入区间加插入意向锁(insert intention lock),但这个并不会真正封锁区间,也不会阻止相同区间的不同KEY插入。

六、InnoDB调试死锁的方法

配置的确认与修改】

要测试InnoDB的锁互斥,以及死锁,有几个配置务必要提前确认:

  • 区间锁是否关闭

  • 事务自动提交(auto commit)是否关闭

  • 事务的隔离级别(isolation level)

这几个参数,会影响实验结果。

间隙锁是否关闭

区间锁(间隙锁,临键锁)是InnoDB特有施加在索引记录区间的锁,MySQL5.6可以手动关闭区间锁,它由innodb_locks_unsafe_for_binlog参数控制:

  • 设置为ON,表示关闭区间锁,此时一致性会被破坏(所以是unsafe)

  • 设置为OFF,表示开启区间锁

可以这么查询该参数:

show global variables like "innodb_locks%";

事务自动提交

MySQL默认把每一个单独的SQL语句作为一个事务,自动提交。

可以这么查询事务自动提交的参数:

show global variables like "autocommit";

事务的隔离级别

不同事务的隔离级别,InnoDB的锁实现是不一样。

可以这么查询事务的隔离级别:

show global variables like "tx_isolation";

可以这么设置事务的隔离级别:

set session transaction isolation level X;

X取:

read uncommitted 

read committed 

repeatable read

serializable 

这三个参数,MySQL5.6的默认值如上:

  • OFF,表示使用区间锁

  • On,表示事务自动提交

  • RR,事务隔离级别为可重复读

要模拟并发事务,需要修改事务自动提交这个选项,每个session要改为手动提交。

任何连上MySQL的session,都要手动执行:

set session autocommit=0;

以手动控制事务的提交。

 查看死锁情况可以使用命令:show engine innodb status。

七、主键与唯一索引约束

触发约束检测的时机:

  • insert

  • update

当检测到违反约束时,不同存储引擎的处理动作是不一样的。

如果存储引擎支持事务,SQL会自动回滚

如果存储引擎不支持事务,SQL的执行会中断,此时可能会导致后续有符合条件的行不被操作,出现不符合预期的结果。

另外,对于insert的约束冲突,可以使用:

insert … on duplicate key

指出在违反主键或唯一索引约束时,需要进行的额外操作

总结,对于主键与唯一索引约束:

  • 执行insert和update时,会触发约束检查

  • InnoDB违反约束时,会回滚对应SQL

  • MyISAM违反约束时,会中断对应的SQL,可能造成不符合预期的结果集

  • 可以使用 insert … on duplicate key 来指定触发约束时的动作

  • 通常使用 show warnings; 来查看与调试违反约束的ERROR

八、5项最佳实践

1)关于count(*)
知识点:MyISAM会直接存储总行数,InnoDB则不会,需要按行扫描。

实践:数据量大的表,InnoDB不要轻易select count(*),性能消耗极大。

常见坑:只有查询全表的总行数,MyISAM才会直接返回结果,当加了where条件后,两种存储引擎的处理方式类似。

2)关于全文索引
知识点:MyISAM支持全文索引,InnoDB5.6之前不支持全文索引。

实践:不管哪种存储引擎,在数据量大并发量大的情况下,都不应该使用数据库自带的全文索引,会导致小量请求占用大量数据库资源,而要使用索引外置的架构设计方法。

启示:大数据量+高并发量的业务场景,全文索引,MyISAM也不是最优之选。

3)关于事务
知识点:MyISAM不支持事务,InnoDB支持事务。

实践:事务是选择InnoDB非常诱人的原因之一,它提供了commit,rollback,崩溃修复等能力。在系统异常崩溃时,MyISAM有一定几率造成文件损坏,这是非常烦的。但是,事务也非常耗性能,会影响吞吐量,建议只对一致性要求较高的业务使用复杂事务。

小技巧:MyISAM可以通过lock table表锁,来实现类似于事务的东西,但对数据库性能影响较大,强烈不推荐使用。

4)关于外键
知识点:MyISAM不支持外键,InnoDB支持外键。

实践:不管哪种存储引擎,在数据量大并发量大的情况下,都不应该使用外键,而建议由应用程序保证完整性。

5)关于行锁与表锁
知识点:MyISAM只支持表锁,InnoDB可以支持行锁。

分析
MyISAM:执行读写SQL语句时,会对表加锁,所以数据量大,并发量高时,性能会急剧下降。
InnoDB:细粒度行锁,在数据量大,并发量高时,性能比较优异。

实践:网上常常说,select+insert的业务用MyISAM,因为MyISAM在文件尾部顺序增加记录速度极快。楼主的建议是,绝大部分业务是混合读写,只要数据量和并发量较大,一律使用InnoDB。

常见坑
InnoDB的行锁是实现在索引上的,而不是锁在物理行记录上。潜台词是,如果访问没有命中索引,也无法使用行锁,将要退化为表锁。

启示:InnoDB务必建好索引,否则锁粒度较大,会影响并发。

结论
在大数据量,高并发量的互联网业务场景下,请使用InnoDB:

  • 行锁,对提高并发帮助很大

  • 事务,对数据一致性帮助很大

这两个点,是InnoDB最吸引人的地方。

猜你喜欢

转载自blog.csdn.net/guotufu/article/details/87336976