赞
踩
我们的数据库一般都会并发执行多个事务,多个事务可能会并发的对相同的一批数据进行增删改查操作,可能就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。
这些问题的本质都是数据库的多事务并发问题,为了解决多事务并发问题,数据库设计了事务隔离机制、锁机制、MVCC多版本并发控制隔离机制,用一整套机制来解决多事务并发问题。事务隔离机制简单可以理解就是为了满足ACID特性而设计的事务隔离级别,这里不做详细解释。本文主要描述Mysql的锁机制以及MVCC机制。
一、锁
锁的分类有很多种,从加锁思想上分为乐观锁和悲观锁,从加锁粒度上分为表锁和行锁,从对数据的操作类型上分为读锁和写锁(共享锁和排他锁),还有间隙锁和临键锁。下面详细介绍下这些锁。
1、乐观锁和悲观锁
乐观锁是在整个数据处理的过程中,不对数据进行加锁,只在数据处理完成后向数据库提交数据时,才对数据版本进行检查。一般是在表中加一个版本号字段,读取时一并读取数据的版本,提交时对数据的版本做对比,只有此时数据库中的版本与之前读取到的版本一直才进行更新,否则做一些回滚或者重试。
悲观锁是在数据开始处理时就通过数据库本身的读锁或写锁将数据加锁,直到数据处理完成才释放。
2、读写锁(共享锁、排他锁)
如上图可以手动添加读锁,可以看到给当前表加上读锁之后,当前session 对数据的更新操作会报错,而查询操作可以执行。其他session插入或更新则会等待,查询也是可以执行的。
上图中手动添加写锁,当前session对该表的增删改查都没有问题,其他session对该表的所有操作被阻塞。
在Innodb存储引擎中,增删改操作都会自动加写锁,普通的SELECT语句不会加任何锁,如果想加锁,可以使用下面方式:
SELECT * FROM t1 WHERE id = 1 LOCK IN SHARE MODE; – 显式加共享锁
SELECT * FROM t1 WHERE id = 1 FOR UPDATE; – 显式加排他锁
总结:
读锁(共享锁):当事务A给某张表(或行)加上读锁之后,其余事务包括本事务可以读取这个资源,但无法修改。
写锁(排他锁):当事务A对某个资源加上写锁之后,当前事务可以对该资源进行增删改查操作,而其他事务不能对这个资源进行任何操作,直到事务A释放锁。
3、表锁和行锁
表级锁和行级锁只是表示锁的粒度,并不是实际的锁。一般来说,锁粒度越小,锁冲突就越少,系统的并发性能就更高,但同时数据库在管理锁方面的开销也越大,当管理锁的开销比数据存取的开销还要大时,反而可能会影响到系统的性能。
常见的存储引擎中,MyISAM引擎仅支持表级锁,而InnoDB引擎除了支持表级锁外,也支持行级锁(默认)。行级锁可以最大程度地支持并发处理。
注意:无索引行锁会升级为表锁,即对非索引字段更新,行锁可能会变表锁
例如:
第一个Seeion未提交事务,第二个Session会一直阻塞,直到第一个事务提交或第二个事务超时。
InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为
表锁。
4、间隙锁与临键锁
间隙锁,锁的就是两个值之间的空隙。Mysql默认级别是repeatable-read,可能存在幻读的问题,而间隙锁在某些情况下可以解决幻读问题。注意间隙锁只有在可重复读级别下才生效。
如上图中的数据,有间隙(1,4),(4,20),(20,30),(30,正无穷)。
上面第一个Session更新的数据间隙包含(4,30】这个区间,注意最后这个30也是无法操作的,在这个事务没有提交之前,其余的事务无法对这个间隙中的数据进行修改操作(可以查询),会一直阻塞直到前一事务提交为止。如果一直未提交,会超时。
临键锁(Next-key Locks)
Next-Key Locks是行锁与间隙锁的组合。像上面那个例子里的这个(4,30]的整个区间可以叫做临键锁。
锁优化建议
1、尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
2、合理设计索引,尽量缩小锁的范围
3、尽可能减少检索条件范围,避免间隙锁
4、尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行
5、尽可能低级别事务隔离
6、找DBA,让他给出专业的建议,这一方法最有效。
二、MVCC与BufferPool机制
Mysql在可重复读隔离级别下,同样的sql查询语句在一个事务里多次执行查询结果相同,就算其它事务对数据有修改也不会影响当前事务sql语句的查询结果。这个隔离性就是靠MVCC(Multi-Version Concurrency Control)机制来保证的,对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免了频繁加锁互斥。
Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制。
首先要先了解两个概念和一个规则。
undo日志版本链:undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id(事务id)和roll_pointer(前一版本数据的指针)把这些undo日志串联起来形成一个历史记录版本链。这个事务id是在每个事务开启之后,递增生成的一个id。
一致性视图read-view:在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束之前都不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成),这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。
数据是否可见规则:
在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束
之前都不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成),这个视图由执行查询时所有未提交事
务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应
版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。
版本链比对规则:
注意:begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改操作InnoDB表的语句,事务才真正启动,才会向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。
总结:
MVCC机制的实现就是通过read-view机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取
同一条数据在版本链上的不同版本数据。
Innodb引擎SQL执行的BufferPool缓存机制
在Innodb存储引擎中,Mysql设计了一个Buffer Pool存储池,一条sql的实际操作的其实就是这个存储池。例如,当一条update语句执行时,在Mysql中大致有以下几个步骤:
1、从ibd文件中加载被更新数据所在的数据页上的一整页数据到Buffer Pool中。
2、写入原始数据到undo日志中。
3、更新Buffer Pool中的数据。
4、写redo日志到Redo Buffer。
5、准备提交事务,redo日志写入磁盘。
6、准备提交事务,binlog日志写入磁盘。
7、写入commit标记到redo日志文件中,提交事务完成。
8、更新磁盘数据。
上面有几个新的概念:
redo日志:这个日志文件是用来恢复Buffer Pool存储池的。当DB重启时,会将这个文件中的数据加载到缓冲池。如果db在重启时,存在数据未更新到磁盘,这个时候会继续执行上面的第8步。
binlog日志:这个日志文件是用来恢复磁盘数据的。如果数据库ibd文件被人删了,可以通过这个这个日志文件来恢复。
那为什么Mysql不能直接更新磁盘上的数据而且设置这么一套复杂的机制来执行SQL呢?
简单来说,就是因为对ibd文件的写操作是随机的,而对redolog操作是顺序的。顺序的I/O比随机的I/O性能高几个数量级。
末尾附上诸葛老师的SQL执行流程图
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。