InnoDB锁与事务模型

一、locking 锁

1、Shared and exclusive locks
innodb实现了两种类型的行级锁(锁粒度)
Shared(s)locks 共享锁:允许持有锁的事务去读取行记录。
Exclusive(x)locks 排它锁:允许持有锁的事务更新、删除行记录。
如果事务t1持有了行记录r的 s lock。当另一个事务t2想要对记录r持有一个锁的时候:
T2请求一个s lock:T2立刻获得s lock,t1、t2同时持有记录r的共享锁
T2请求一个x lock:t2 不能立刻获得 排它锁。
如果t1在记录r上持有x lock,那么t2的任何一种锁请求都需要等待,直到t1释放在记录r上的排它锁。


2、Intention locks(意向锁)
Innodb支持多粒度锁,因而允许 row-level 和 table-level 两个粒度的锁共存。
表级锁又称为intention locks(意向锁),意向锁指明一个事务在表a上将要用到的锁l(s lock 或 x lock)去锁定表a的某一行记录。
意向锁分为两种类型:
Intention shared (IS):事务T打算在表t个别的rows上设置s lock.
Select ...LOCK IN SHARE MODE设置一个IS lock。
Intention exclusive (IX):事务T打算在表t个别的rows上设置x lock.
Select ... FOR UPDATE 设置一个排它锁。

加锁原则:
事务a只有在表t上先获取到IS锁,才能进一步对表t的row设置s lock。
同理,事务a只有在表t上先获取到IX锁,才能进一步对表t的row设置x lock。
不同所类型之间的兼容性。
    X     IX              S        IS
X   Conflict   Conflict       Conflict          Conflict
IX    Conflict   Compatible    Conflict         Compatible
S   Conflict   Conflict       Compatible   Compatible
IS    Conflict   Compatible   Compatible    Compatible
如果要请求的锁l与已经存在的锁相互兼容,那么l锁可以立刻设置成功。否则,需要等到已经存在的锁被释放后,才能设置请求的锁l。如果锁l与已经加的锁冲突,并且加锁I的请求一直无法被通过,可能是导致了死锁,程序会出现错误。
意向锁的主要目的是:显示有人正在锁定一个行,或者是将要锁定一个行。


3、Record locks
就是加在一个索引记录上的锁。例如:select c1 from t where c1=10 for update;会阻止任何其他的事务进行插入,修改、删除 t.c1=10的行。
Record locks 总是会锁定索引记录,即便是一个表没有定义索引。在这种情况下,innodb会使用内置的、隐藏的聚集索引作为记录锁。

4、Gap locks
是在索引记录的间隙之间加上的锁,或者在索引记录间隙以外加上的锁。例如:select c1 from t where c1 between 10 and 20 for update 。会阻止其他事务插入10<c1<20的记录,因为在10<c1<20的范围都被加了锁(gap locks)。
一个间隙锁可能会锁定多个索引值、一个甚至一个都没有。
Gap locks 是权衡性能与并发性的部分,并且被使用在事务隔离级别中 。


5、Next-key locks
是记录锁和间隙锁的集合


6、Insert intendtion locks
是间隙锁的一种类型。告诉其它事务


7、Auto-inc locks
自增列上的锁(表级索)

二、transaction model事务模型

1、Transaction isolation level(事务隔离级别)
当多个事务同时执行sql操作时,隔离级别用于平衡InnoDB的性能、可靠性、并发性、结果的可再现性。
可以通过 set transaction 进行单个用户连接的隔离级别设置。通过show variables like ‘tx_isolation’查看当前使用的隔离级别。加上server启动参数--transaction-isolation 或者在 配置文件中设定server level的隔离级别。
InnoDB使用不同的锁策略来实现对不同事务隔离级别的支持。
四个水平的隔离级别:
Read uncommitted

  读取未提交。事务a 读取到 事务b 已经修改但是没有 commit的记录。
Read committed
  使用Consistent read mode(consistent nonlocking reads)。永远都会读取最新的数据库快照。
  Gap locking 被禁用。其仅仅用于外键约束和重复键的检测。因为其它的会话可以将新记录插入到gaps中,因此可能会出现幻读行(phantomas)。
Repeateble read(server默认级别)

  Consistent read
  对于locking reads(锁定读:select for update or lock in share mode),update,delete声明,加锁依赖于sql声明是否使用了带有唯一键搜索条件或者是范围类型的搜索条件的唯一键索引。
  1)对于带有唯一键搜索条件的唯一键索引,InnoDB只会锁定被搜索到的索引近路,而不会对其之前的间隙加锁。
  2)对于其他类型的搜索条件,InnoDB会锁定被扫描到的索引范围。并且使用gap locks或者next-key locks 去阻止其他会话对锁定的范围执行插入操作。
Serializable

  和repeateble read级别保持一致,除了:当禁用autocommit时,InnoDB会隐式的将select 声明 转换成:select  ... block in share mode。

事务隔离级别的查看与设置:

SHOW VARIABLES LIKE  'tx_isolation'

SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED

2、Consistent nonlocking reads(就是普通的select操作)
一致读:InnoDB使用多版本控制 来查询 在某一个时间点上的数据库快照。查询语句t只能看到其开始时间点之前的最新快照。但是在同一事务里,修改了一些记录,那么select查询可能看到最新的数据,也可能看到之前版本的数据。
当隔离级别为repeateble read 时,在同一事务内,一致读只会读取事务开始时间点之前最新的数据快照。在你提交或回滚事务之后,才能读取到最新的的数据快照。
在隔离级别为read committed级别下,consistent read每次都会读取最新的数据快照。
在repeateble read和read committed两种事务隔离级别模式下,默认使用consistent reads模式进行select处理。Consistent read不会对访问的表加任和锁,因此其它会话可以随意的修改这些表。
如果想查看到最新的数据库快照。可以使用read committed隔离级别,或者是使用locking read : SELECT * FROM t LOCK IN SHARE MODE;(查询操作会停止一直到包含最新数据快照的事务结束)

3、Locking reads
如果在一个事务t里,需要先查询一些记录,然后再根据查询到的记录来进行插入或者更新操作的时候,普通的select 查询操作无法提供足够的数据保护。因为此时,其它事务tx可以对事务t查询到数据进行修改或删除。为了提供额外的安全机制,InnoDB提供了两种类型的锁定读:
SELECT ... LOCK IN SHARE MODE(查看最新的数据快照)
对读取到的行记录加上共享锁(s-lock)。此时,除非当前事务结束,否则,其它会话只可以读取这些记录,但是不能够修改他们。 又如果,其它事务正在表记录还没有提交,那么你的查询会一直等到其它事务结束,然后使用最新的数据。
SELECT ... FOR UPDATE
会锁定相关的索引记录条目,就像对这些记录条目发出update操作一样。其它的事务将不能进行对这些记录的更新,不能执行 select...lock in share mode ,也不能读取这些数据。
一旦事务结束,locking reads加的锁就会被释放。
Example 1:(子表信息依赖于父表信息)
Select * from parent where name=’jones’ lock in share mode;
在锁定读状态下获取到父节点之后,便可以安全的执行子节点的插入,然后提交事务即可。在这期间其它事务只能读父节点‘jones’,而修改操作只能等待...。
Example 2:(手动设置计数器字段)
问题:多个事务同事访问到计数器的最大值x,然后最新添加的几条记录数量标记都是x+1而造成错误。
Select counter_field from t for update;
获取最新的值,并对其获取的每一行加上x-lock。此时其它事务,无法进行修改 或 读取 操作。
然后执行计数器增加操作就不会出错:
Update t set counter_field=counter_field+1;

4、Locks set by different sql statemetn in InnoDB

在使用不同的sql 语句执行命令是,命令会对所操作的表加上不同类型锁。
不论where condition 有没有记录行的过滤操作,Locking read ,update,或者delete 通常会对sql扫描到的每一个索引记录设置记录锁。InnoDB不会记住where 条件,但是会记住哪些索引范围被表扫描过了。

InnoDB通常会使用next-key locks。
如果没有适合的用于查询的索引,那么mysql必须要扫描全表,此时整张表的每一个记录都会被加锁。因此,设置、使用合适的索引,来避免很多不必要的表扫描十分重要。


在不同情况下,InnoDB是如何加锁的呢?
1)select ... from
使用一致读模式(consistent read mode),会读取数据库快照,而且在非serializable隔离级别的事务中不会给表加锁。
当隔离级别为serializable时,查询会把扫描到的索引记录设置为 shared next-key locks(共享的下一键锁)。
当使用唯一索引搜索唯一行时,Innodb只对扫描行加 index record lock。
2)select ... from ... lock in share mode
对扫描的所有index records加shared next-key locks。
当使用唯一索引搜索唯一行时,Innodb只对扫描行加 index record lock。

3)select ...from...for update
对扫描的所有index records加exclusive next-key locks。
当使用唯一索引搜索唯一行时,Innodb只对扫描行加 index record lock。
会阻止其它会话执行locking read。
4)update...where...
对扫描的所有index records加exclusive next-key locks。
当使用唯一索引搜索唯一行时,Innodb只对扫描行加 index record lock。
当update修改了聚集索引记录的时候,二级索引记录会被加上一个隐含的锁。
5)Delete from ...where...
对扫描的所有index records加exclusive next-key locks。
当使用唯一索引搜索唯一行时,Innodb只对扫描行加 index record lock。
6)Insert
在插入行上设置exclusive lock(是一种索引记录锁,not next-key not gap key,因此不会阻止其它会话插入到之前的间隙)。
当插入到一条记录会导致一个duplicate-key error并且已经被加了x-lock(重复键错误的时候),当前会话由请求exclusive锁变成请求一个共享锁。
7)insert...on duplicate key update
当发生重复键错误的时候,会设置排他锁。如果是一个重复的主键,那么它将会被加上exclusive index-record lock。如果是一个重复唯一键,那么它将会被加上exclusive next-key lock。
8)Replace
如果没有唯一键冲突,replace和insert的加锁原则保持一致。
否则,会加exclusive next-key lock。
9)insert into t select ...from s where ...
为每个插入到表t的行加上exclusive index record lock (not gap lock)。
如果事务隔离级别为 read committed, 或者开启innodb_locks_unsafe_for_binlog并且隔离级别不是serializable。那么InnoDB会以一致性读的方式来搜索表s。
否则,InnoDB对表s上的记录设置shared next-key locks 。
10)如果一个表初始化了一个 auto_increment column
InnoDB使用一个 AUTO-INC 表锁。加锁持续到当前sql的结束,而不是事务的结束。当表持有AUTO-INC锁时,其它会话不能向表里插入数据。
11)foreign key
如果表存在外键约束。
12)lock tables
设置一个表锁。但是它是比InnoDBb级别更高的mysql级别锁。
当innodb_table_locsk=1(默认值)并且 autocommit=0时,引擎可以使用表锁。
否则,INNODB的死锁自动检测无法检查到与表锁相关的死锁。

5、Phantom Rows(幻读行)
执行两次select,但是第二次select出现了一行第一次select结果里没有出现的记录。这一行数据,成为幻读行。
InnoDB采用next-key locking算法来实现消除幻读行的出现。
Create table a (id int,primary ke(id));
Insert into a values (1),(3),(6),(9);
Session 1:
Select * from a where id>9 for update;
sql对扫描记录设置exclusive next-key lock。(6,9),(9,+无穷)两个区间被加了排他锁(gap lock),id=9的记录被加了排他锁(index record lock)。

6、InnoDB中的死锁问题

 死锁就是不同事务之间持有对方需要的锁,从而导致不同的事务无法继续进行。因为两个事务都在等待可用的资源,所以都不愿意释放自身所拥有的锁。

死锁举例:

client a:                                                                    client b:

mysql> CREATE TABLE t (i INT) ENGINE = InnoDB;

mysql> INSERT INTO t (i) VALUES(1);

mysql> START TRANSACTION;

mysql> SELECT * FROM t WHERE i = 1 LOCK IN SHARE MODE;

                                    mysql> delete from t where i=1;     //操作进入队列等待获取x lock

mysql> delete from t where i=1;  

+------+

  |  i  |

+------+

  |  1  |

+------+

因为client a持有的 s lock,还没有被释放,因此client b 要删除i=1记录的x lock无法立即获取,进入队列请求 x lock ,并进入等待状态.......

然后,client a也要删除i=1的记录。此时,即发生了deadlock (出现了死锁现象)

死锁解释:

 a 需要 x lock来删除行,但是因为 b已经请求了x lock,并且等待a 释放s lock,因此 a 不能获得 x lock。

因为b在a 之前请求了x lock,故a 持有的s lock也不能升级为 x lock。此时 InnoDB引擎会向其中的一个客户端发送错误信息,并且释放它的锁。错误信息如下:

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

Deadlock detection and rollback

当死锁检测被启动时(InnoDB默认启动),InnoDB会自动检测 事务死锁,并且回滚一个或多个事务以终止死锁。InnoDB会尽可能的回滚数据量较小的事务(事务的大小由事务所要insert、update、delete的行数有关)。

如果innodb_table_locks = 1(默认值)并且 autocommit = 0。InnoDB就会知道表锁的存在,并且跟高级别的MySQL层知道行级锁的存在。 否则,InnoDB无法检测到由MySQL LOCK TABLES语句设置的表锁 或者 由除InnoDB之外的存储引擎设置的锁定的死锁。 通过设置innodb_lock_wait_timeout系统变量的值来解决这些无法意识表锁存在情况。

当事务完全回滚之后,其所设置的所有锁都会被释放。但是,由于一个单独的sql语句因为出错而回滚的情况,有一些锁还会被保留,这是因为InnoDB不知道哪些锁分别是由哪些语句设置的。

如何select 在事务中调用了一个存储函数,但是函数执行失败,该语句将会回滚。此外,如果以后再执行rollback,整个事务将会全部回滚。

如果InnoDB监控器输出的最新检测到的死锁部分包含一条消息:“TOO DEEP OR LONG SEARCH IN THE LOCK TABLE WAITS-FOR GRAPH,WE WILL ROLL BACK FOLLOWING TRANSACTION,”,这表示等待列表上的事务次数已达到200个的限制。超过200个事务的等待列表被视为死锁,并且将试图检查等待列表的事务进行回滚。 如果等待列表上的事务拥有超过1,000,000个锁线程,则也可能会发生相同的错误。

Disabling Deadlock Detection

在高并发系统上,当许多线程等待同一个锁时,死锁检测可能导致server运行速度减慢。 有时,禁用死锁检测,并且在发生死锁时依靠innodb_lock_wait_timeout设置进行事务回滚可能是一种更好的方案。 我们可以使用innodb_deadlock_detect配置选项禁用死锁检测。

How to Minimize and Handle Deadlocks

原文地址:https://www.cnblogs.com/ahguSH/p/7246552.html