Mysql数据库事务的隔离级别和锁的实现原理分析

mysql数据库的事务隔离级别相信不少同窗都知道.mysql

你们有没有想过它是如何实现的呢?带着这些问题我翻阅了相关数据库的书籍和资料,把个人理解写下来.算法

一:事务隔离级别

mysql数据库的隔离界别以下:sql

1, READ UNCOMMITTED(未提交读)

事务中的修改,即便没有提交,对其它事务也是可见的.  脏读(Dirty Read).数据库


2, READ COMMITTED(提交读)

一个事务开始时,只能"看见"已经提交的事务所作的修改. 这个级别有时候也叫不可重复读(nonrepeatable read).服务器


3, REPEATABLE READ(可重复读)

该级别保证了同一事务中屡次读取到的一样记录的结果是一致的. 但理论上,该事务级别仍是没法解决另一个幻读的问题(Phantom Read). 并发

幻读:  当某个事务读取某个范围内的记录时,另一个事务又在该范围内插入了新的记录.当以前的事务再次读取该范围时,会产生幻行.(Phantom Row).oracle

幻读的问题理应由更高的隔离级别来解决,但mysql和其它数据不同,它一样在可重复读的隔离级别解决了这个问题. 优化

也就是说, mysql的可重复读的隔离级别解决了   "不可重复读" 和 “幻读” 2个问题. 稍后咱们能够看见它是如何解决的.spa

而oracle数据库,可能须要在 “SERIALIZABLE ” 事务隔离级别下才能解决 幻读问题.对象

mysql默认的隔离级别也是: REPEATABLE READ(可重复读)


4, SERIALIZABLE (可串行化) 

强制事务串行执行,避免了上面说到的 脏读,不可重复读,幻读 三个的问题.


二:  MVCC(Multi-Version Concurrency Control多版本并发控制

MVCC的实现,是经过保存数据在某个时间点的快照来实现的. 

InnoDB的MVCC是经过在每行记录后面保存2个隐藏的列来实现的,一列保存了行的建立时间,一列保存了行的过时时间(或删除时间).但它们都存储的是系统版本号

MVCC最大的做用是:  实现了非阻塞的读操做,写操做也只锁定了必要的行.

MYSQL的MVCC 只在 read committed  和 repeatable read  2个隔离级别下工做. 


在MVCC的机制下,mysql InnoDB(默认隔离级别)的增删改查变成了以下模式:

SELECT:  
1, InnoDB只查找版本早于当前事务版本的数据行(行的系统版本号小于等于事务的系统版本号)
2, 行的删除号要么未定义,要么大于当前事务版本号,这样能够确保事务读取到的行,在事务开始以前未被删除.

INSERT:
InnoDB 为新插入的每一行保存当前系统版本号作为行版本号。


DELETE: 
INNODB 为删除的每一行保存当前系统版本号做为行删除标识


UPDATE:
InnoDB 为插入的每一行新记录,保存当前系统版本号做为行版本号,同时保存当前系统版本号到原来的行做为行删除标识.


注意:  上面的读取方式只在InnoDB默认隔离级别下工做,其它的隔离级别会有很大的差别,稍后会看到.


三, InnoDB锁.

InnoDB的锁大体分为:

3.1 行锁

支持并发高,带来最大的锁开销. 在存储引擎层实现,服务器感知不到

3.2 表锁

服务器会为诸如: ALTER Table 之类的语句使用表锁,忽略存储引擎的锁机制


但锁的类型又分为:

(1). 共享锁(S Lock) , 容许事务读取一行数据

(2). 排他锁(X Lock),容许事务删除或更新一行数据.


3.3 意向锁

InnoDB还实现了一种锁,叫意向锁(Intention Lock).意向锁是将锁定的对象分为多个层次.

意向锁的类型分为:

(1). 意向共享锁(IS Lock) 

(2). 意向排他锁(IX Lock)

好比:  须要对页上的记录加X锁,那么须要分别对 数据库A,表,页 上加意向锁IX,最后对记录r上加X锁.

一旦对数据库A,表,页上加IX锁失败,则阻塞.



四: 一致性非锁定读(consistent nonlocking read) 

4.1 不加锁的读.

是InnoDB存储引擎下的读取数据的方式( read committed  和 repeatable read).

一致性非锁定读,个人理解是它的读取方式是把: 事务隔离级别,MVCC,InnoDB锁结合起来运用到实现 Mysql读的一种方式.

咱们前面提到,mysql读取数据的方式是读MVCC下的快照数据.

具体来讲, 读取mysql数据库时,若是读取的行正在执行DELETE,UPDATE等操做,这时,读取操做不会所以去等待行上的X锁释放,相反,InnoDB会读取行的一个快照数据.

这样利用MVCC,InnoDB实现了非阻塞读的实现.极大的提升了数据库的并发性.

但在不一样的事务隔离级别下读取的数据的方式也不同

(1).  在read committed隔离级别下: 

一致性非锁定读老是读取被锁定行的最新一份快照数据.  产生了不可重复读的问题.

(2).  在repeatable read 事务隔离级别下:

 一致性非锁定读老是读取事务开始时的行数据版本.  解决不可重复读的问题

4.2 一致性锁定读

还有一种读的方式叫:  一致性锁定读(加锁的读).

1). select .... for update. 加X锁

2). select .... lock in share mode. 加S锁


五: InnoDB 锁的算法

5.1  Record Lock: 但个行记录的锁

5.2  GAP Lock: 间隙锁,锁定一个范围,但不包含记录自己.

5.3  Next-Key Lock: Gap Lock+Record Lock 锁定一个范围并锁定记录自己.

上面所说的锁定的对象均为: 索引记录. 若是InnoDB存储引擎在创建的时候没有设置任何一个索引,那么这时,InnoDB存储引擎会使用隐式的主键进行锁定.

当查询的索引含有惟一属性时,InnoDB存储引擎会对Next-Key Lock进行优化,降级为Record Lock.


下面的2句话是InnoDB在不一样隔离级别下产生"不可重复读" 和 "幻读" 和解决它 的根本缘由: 

InnoDB存储引擎默认的事务隔离级别(repeatable read)下,采用的是 Next-Key Locking的方式来加锁.

read committed隔离级别下采用的是: Record Lock 的方式来加锁.


下面咱们来看下 Next-key lock的具体实现:

默认存储引擎下, 好比表A 上的id字段有索引abc, 而且id有 3,8,12,20这几个值,那么该索引可能被Next-key locking区间为:

(负无穷,3)

[3,8)

[8,12),

[12,20),

[20,正无穷)

当事务T1锁定了 [8,12),[12,20)这2个区间时,当插入15时,上面的区间变成:

[8,12),[12,15),[15,20). 

但查询索引含有惟一属性时,Next-Key Lock 降级为 Record Lock,仅锁住索引自己. 

好,如今表A的id值变成了: 3,8,12,15,20

若是执行下列语句: 

select * from A where id>16 for update. 

InnoDB会对(16,正无穷) 加锁,

但在 read committed的事务隔离级别下,由于采用Record Lock,只会锁定20这个值.

若是在此时另一个事务T2,插入了22这个值,此时,  read committed 隔离级别下就会产生"幻读"的问题.

但在InnoDB默认存储引擎下的Next-key Lock 模式下,22是插入是会被阻塞的,直到事务T1提交后,释放X锁,才能提交22这值.这样,InnoDB就这样解决了幻读的问题.


如今,咱们应该清楚的知道,在不一样的事务隔离级别下,mysql InnoDB是如何实现解决 "不可重复读" 和 “幻读” 的问题了吧.


六: 总结: 

1, InnoDB用MVCC来实现非阻塞的读操做,不一样隔离级别下,MVCC经过读取不一样版本的数据来解决"不可重复读" 的问题.

2, InnoDB的默认隔离级别解决2个问题,"不可重复读" 和 "幻读", oracle须要在串行读中解决"幻读"问题. InnoDB的实现方式和通常隔离级别的定义不一致.

3, InnoDB的默认隔离级别采用Next-key Lock(间隙锁) 来解决幻读问题. 而 read committed隔离级别采用Record锁,所以会产生"幻读"问题.

4, InnoDB的存储引擎不存在锁升级的问题(太多的行锁升级为表锁),来下降锁的开销. 由于不是根据记录来产生行锁的,根据页对锁进行管理.