MySQL的锁机制

数据库事务必须符合ACID这四种特性,数据库为了维护这些特性,尤其是一致性和隔离性,一般使用加锁这种方式。同时数据库又是个高并发的应用,同一时间会有大量的并发访问,如果加锁过度,会极大的降低并发处理能力。所以对于加锁的处理,可以说就是数据库对于事务处理的精髓所在。

一、锁概念

MySQL中锁的种类很多,有常见的表锁和行锁。

1、表锁

表锁是MySQL中最基本的锁策略,并且是开销最小的策略。但是表锁是对一整张表加锁,虽然可分为读锁和写锁,但毕竟是锁住整张表,会导致并发能力下降,一般是做ddl处理时使用。

2、行锁

行锁则是锁住数据行,这种加锁方法比较复杂,但是由于只锁住有限的数据,对于其它数据不加限制,所以并发能力强,MySQL一般都是用行锁来处理并发事务。

 InnoDB实现了两种类型的行锁。

(1)共享锁(S)

允许一个事务去读一行,阻止其他事务获得相同的数据集的排他锁。 

(2)排他锁(X)

允许获得排他锁的事务更新数据,但是组织其他事务获得相同数据集的共享锁和排他锁。

其实就是我们平常说的读写锁。

3、死锁问题

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。比如:

事务1:

start transaction

update OP_StockPrice set Price = '50.00' where Id = 1 

update OP_StockPrice set Price = '100.00' where Id = 2 

commit

事务2:

start transaction

update OP_StockPrice set Stock = 20 where Id = 2

update OP_StockPrice set Stock = 40 where Id = 1

commit

 

如果两个事务都执行了第一条update语句,更新了一行数据,同时也锁定了该行数据,接着每个事务又同时尝试去执行第二条update语句,却发现该行已经被对方锁定,然后两个事务都等待对方释放锁,同时又都持有对方需要的锁,则陷入死循环。除非有外部因素介入才可以解除死锁。目前数据库系统都有死锁检测和处理机制。

4、阶段锁定协议

存在大量的并发访问时,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。这种方式可以有效的避免循环死锁,但在数据库中却不适用,因为在事务开始阶段,数据库并不知道会用到哪些数据。
InnoDB采用的是两阶段锁定协议。将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)。在事务执行的过程中,随时都可以执行锁定,而只有在事务提交或回滚的时候才会释放锁。

(1)加锁阶段

在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得X锁(排它锁,其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。

(2)解锁阶段

当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。

5、隐式和显示锁定

InnoDB会根据隔离级别在需要的时候自动加锁,这称为隐式加锁。另外,InnoDB也支持通过特定的语句进行显示加锁

 显示加共享锁:

select  ....  lock in share mode

显示加排它锁

 select  ....  for update

二、多版本并发控制(MVCC)

MySQL的大多数事务型存储引擎实现的都不是简单的行锁。基于提升并发性能的考虑,一般都实现了多版本并发控制(MVCC)。可以认为,MVCC是行锁的一个变种,但它在很多情况下避免了加锁操作,减少了开销。MVCC实现了非阻塞的读操作,写操作也只锁定必要的行。

1、MVCC的实现方式

不同的存储引擎的MVCC实现是不同的,典型的有悲观并发控制和乐观并发控制。

(1)悲观并发控制

正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制实现。在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。

(2)乐观并发控制

相对悲观并发控制而言,乐观并发控制采取了更加宽松的加锁机制。悲观并发控制大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观并发控制在一定程度上解决了这个问题。乐观并发控制,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加1。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

2、InnoDB中MVCC的实现

在InnoDB中,使用乐观并发控制来实现MVCC。在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。 在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。

(1)MVCC的具体操作

在可重复读事务隔离(RR)级别下,MVCC的具体操作如下:

SELECT语句

InnoDB会根据以下两个条件检查每行记录:

(a)创建版本号<=当前事务版本号

这样可以确保事务读取的行,要么在事务开始前已经存在,要么是事务自身插入或者修改过的。

(b)删除版本号为空,或者,删除版本号>当前事务版本号

这样可以确保事务读取到的行在事务开始之前未被删除。

INSERT语句

保存当前事务版本号为行的创建版本号。

DELETE语句

保存当前事务版本号为行的删除版本号。

UPDATE语句

插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行。

通过MVCC,虽然每行记录都需要额外的存储空间,更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用,大多数读操作都不用加锁,读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,也只锁住必要行。

(2)快照读与当前读

在RR隔离级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据!这在一些对于数据的时效特别敏感的业务中,就很可能出问题。

对于这种读取历史数据的方式,我们叫它快照读( SNAPSHOT READ),而读取数据库当前版本数据的方式,叫当前读 (CURRENT READ)。很显然,在MVCC中:

快照读:普通的select语句。如下所示:

select * from table ....;

当前读:特殊的读操作,插入、更新、删除操作,都属于当前读,处理的都是当前的数据,需要加锁。具体如下所示:

select * from table where .... lock in share mode;

select * from table where .... for update;

insert  ....;

update  ....;

delete  ....;

事务的隔离级别实际上都是定义了当前读的级别,MySQL为了减少锁处理(包括等待其它锁)的时间,提升并发能力,引入了快照读的概念,使得select不用加锁。而update、insert这些“当前读”就需要另外的模块来解决了。

为了解决“当前读”中的幻读问题,MySQL事务使用了Next-Key锁。

(3)Next-Key锁

Next-Key锁是行锁和间隙锁的合并,行锁上文已经介绍了,接下来说下间隙锁。

间隙锁

锁加在不存在的空闲空间,可以是两个索引记录之间,也可能是第一个索引记录之前或最后一个索引之后的空间。

Next-Key锁是行锁与间隙锁的组合,这样,当InnoDB扫描索引记录的时候,会首先对选中的索引记录加上行锁,再对索引记录两边的间隙(向左扫描扫到第一个比给定参数小的值, 向右扫描扫描到第一个比给定参数大的值, 然后以此为界,构建一个区间)加上间隙锁。如果一个间隙被事务T1加了锁,其它事务是不能在这个间隙插入记录的。这样就防止了幻读,如下演示所示:

时间 

账户更新事务A

账户新增事务B 

T1 

开始事务 

 

T2 

 

开始事务 

T3 把所有账户的余额清0(获取行锁和间隙锁)  

T4


 

插入一个新账户,设置余额为100

(获取不到锁,等待)

T5

 

等待

T6 提交事务,释放锁 等待
T8   获取锁,插入成功
T9   提交事务,释放锁
posted @ 2018-05-03 20:54  saiQsai  阅读(280)  评论(0编辑  收藏  举报