【MySQL】MySQL锁和隔离级别浅析一

《MySQL技术内幕InnoDB存储引擎》第一版中对于MySQL的InnoDB引擎锁进行了部分说明,第二版有部分内容更新。

与MySQL自身MyISAM、MSSQL及其他平台BD锁的对比:

6.1 什么是锁
对于MyISAM引擎来说,其锁是表锁。并发情况下的读没有问题,但是并发插入时的性能就要差一些了,若插入是在“底部”的情况,MyISAM引擎还是可以有一定的并发操作。
对于MSSQL,在MSSQL2005版本之前都是页级锁,相对表级锁的MyISAM引擎来说,并发性能有所提高。到MSSQL2005版本开始支持乐观并发和悲观并发。在乐观并发下开始支持行级锁,但是其实现方式与InnoDB存储引擎的实现方式完全不同。你会发现MSSQL的锁是一种稀有的资源,锁越多开销就越大,因此它会有锁升级。在这种情况下行锁会升级到表锁,这时并发的性能又回到了以前。
InnoDB存储引擎锁的实现和Oracle非常类似,提供一致性的非锁定读,行级锁支持,行级锁没有相关的开销,可以同时得到并发性和一致性。

锁的说明:

6.2 InnoDB存储引擎中的锁
6.2.1 锁的类型
InnoDB存储引擎实现了如下两种标准的行级锁:
共享锁(S Lock),允许事务读一行数据。
排他锁(X Lock),允许事务删除或者更新一行数据。
共享锁和共享锁是兼容的,排他锁和排他锁以及共享锁是不兼容的,可以理解为,行在一个事务共享锁的情况下,其他事务也可以获得这行的共享锁。但是如果另一个事务想获得这一行的排他锁,则必须等待共享锁的释放,排他锁与排他锁的实现同理,需等待前一个事务排他锁释放。
InnoDB存储引擎支持多粒度锁定,这种锁定允许在行级上的锁和表级上的锁同时存在。为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,我们称之为意向锁。意向锁是表级别的锁,其设计目的主要是为了在一个事务中揭示下一行将被请求的锁的类型。InnoDB存储引擎支持两种意向锁:
意向共享锁(IS Lock),事务想要获得一个表中某几行的共享锁。
意向排他锁(IX Lock),事务想要获得一个表中某几行的排他锁。
因为InnoDB存储引擎支持的是行级别的锁,所以意向锁其实不会阻塞全表扫以外的任何请求。

对于意向锁的理解可以参考这篇文章的说明,比较好理解,参考:http://www.cnblogs.com/ggjucheng/archive/2012/11/14/2770445.html

总的来说,Innodb的锁定机制和Oracle数据库有不少相似之处。Innodb的行级锁定同样分为两种类型,共享锁和排他锁,而在锁定机制的实现过程中为了让行级锁定和表级锁定共存,Innodb也同样使用了意向锁(表级锁定)的概念,也就有了意向共享锁和意向排他锁这两种。
当一个事务需要给自己需要的某个资源加锁的时候,如果遇到一个共享锁正锁定着自己需要的资源的时候,自己可以再加一个共享锁,不过不能加排他锁。但是,如果遇到自己需要锁定的资源已经被一个排他锁占有之后,则只能等待该锁定释放资源之后自己才能获取锁定资源并添加自己的锁定。而意向锁的作用就是当一个事务在需要获取资源锁定的时候,如果遇到自己需要的资源已经被排他锁占用的时候,该事务可以在需要锁定行的表上面添加一个合适的意向锁。如果自己需要一个共享锁,那么就在表上面添加一个意向共享锁。而如果自己需要的是某行(或者某些行)上面添加一个排他锁的话,则先在表上面添加一个意向排他锁。意向共享锁可以同时并存多个,但是意向排他锁同时只能有一个存在。所以,可以说Innodb的锁定模式实际上可以分为四种:共享锁(S),排他锁(X),意向共享锁(IS)和意向排他锁(IX),我们可以通过以下表格来总结上面这四种所的共存逻辑关系:
  共享锁 排他锁 意向共享锁 意向排他锁
共享锁 兼容 冲突 兼容 冲突
排他锁 冲突 冲突 冲突 冲突
意向共享锁 兼容 冲突 兼容 兼容
意向排他锁 冲突 冲突 兼容 兼容

也有人把InnoDB意向锁理解是引擎层对于目前事务锁后面事务锁的记录,这也是为什么叫“意向锁”很好的阐述。
MSSQL的意向锁与InnoDB引擎区别比较大,可以单独学习下。

数据的“快照”

6.2.2 一致性的非锁定读操作
一致性非锁定行读(consistent nonlocking read)是指InnoDB存储引擎通过行多版本并发控制(multi versioning)的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行DELETE、UPDATE操作,这时读取操作不会因此而会等待行上锁的释放,相反InnoDB存储引擎会去读取行的一个快照数据。
称其为非锁定读,因为不需要等待访问的行上X锁的释放。快照数据是指该行之前版本的数据,该实现是通过Undo段来实现的。而Undo用来在事务中回滚数据,因此快照数据本身是没有额外的开销。此外,读取快照数据是不需要上锁的,因为没有必要对历史的数据进行修改。
非锁定读的机制很大程度提高数据读取的并发性,在InnoDB存储引擎默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。但是在不同事务隔离级别下,读取的方式不同,并不是每个事务隔离级别下读取的都是一致性数据。同样,即使都是使用一致性读,但是对于快照数据的定义也不相同。
在Read-Committed和Repeatable-Read两种事务隔离级别下,对于一致性非锁定读获得的结果并不一样。
假设事务A begin;select * from t1 where c1=1;
假设事务B begin;update t1 set c1=2 where c1=1;commit;
事务A先于事务B begin,但是事务B先结束:
在Read-Committed隔离级别下,在事务A中再进行查询select * from t1 where c1=1无数据。
在Repeatable-Read隔离级别下,在事务A中再进行查询select * from t1 where c1=1获得原数据。
这个例子看到,这两个隔离级别对于快照数据的定义不一样。
在Read-Committed隔离级别下,读取行的最新版本快照数据。
在Repeatable-Read隔离级别下,读取事务开始时的行快照数据。 

下图是对于非锁定一致性读的逻辑结构的说明。

【MySQL】MySQL锁和隔离级别浅析一

锁的算法:

InnoDB存储引擎有3钟行锁算法设计,分别是:
Record Lock:单个行记录上的锁。
Gap Lock:间隙锁,锁定一个范围,但不包含记录本身。
Next-Key Lock:Record Lock+Gap Lock,锁定一个范围,并锁定记录本身。
Record Lock总是会去锁住索引记录。如果InnoDB存储引擎表建立的时候没有设置任何一个索引,这是InnoDB存储引擎会使用隐式的主键来进行锁定。
Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这中锁定算法(REPEATABEL READ),对于不同SQL查询语句,可能设置共享的(Share)Next-Key Lock和排他的(Exlusive)Next-Key Lock。

可以自己做一些测试,我的测试验证了以下几个场景

、不同事务可以使用相同的索引锁定不同的行
、表有多个索引,如果多个事务使用不同的索引指向同一行,由于聚集索引结构,行会被锁住
、使用非唯一索引的事务更新,行以及行前后的间隙会被加锁,不仅影响目标行,同时无法insert相同条件键值
、不是用索引的更新会锁定住整个表

针对于表结构、索引结构以及其他隔离级别情况下的触发锁类型,可以参考网易何登成网盘中“MySQL 加锁处理分析.pdf”这篇文章,很细致。

何登成百度网盘:http://pan.baidu.com/share/home?uk=4265849107&view=share

查看处理过程中的锁

在MySQL中能够通过information_schema中的innodb_trx、innodb_locks、innodb_lock_waits查看存在的锁,如下:

mysql> select * from innodb_trx \G
*************************** 1. row ***************************
trx_id: 2897C4F
trx_state: LOCK WAIT
trx_started: 2014-09-24 14:03:14
trx_requested_lock_id: 2897C4F:355:3:2
trx_wait_started: 2014-09-24 14:03:14
trx_weight: 2
trx_mysql_thread_id: 1092551
trx_query: select * from t1 for update
trx_operation_state: starting index read
trx_tables_in_use: 1
trx_tables_locked: 1
trx_lock_structs: 2
trx_lock_memory_bytes: 376
trx_rows_locked: 1
trx_rows_modified: 0
trx_concurrency_tickets: 0
trx_isolation_level: REPEATABLE READ
trx_unique_checks: 1
trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
trx_adaptive_hash_latched: 0
trx_adaptive_hash_timeout: 10000
*************************** 2. row ***************************
trx_id: 2897BB4
trx_state: RUNNING
trx_started: 2014-09-24 14:02:51
trx_requested_lock_id: NULL
trx_wait_started: NULL
trx_weight: 2
trx_mysql_thread_id: 1092533
trx_query: NULL
trx_operation_state: NULL
trx_tables_in_use: 0
trx_tables_locked: 0
trx_lock_structs: 2
trx_lock_memory_bytes: 376
trx_rows_locked: 8
trx_rows_modified: 0
trx_concurrency_tickets: 0
trx_isolation_level: REPEATABLE READ
trx_unique_checks: 1
trx_foreign_key_checks: 1
trx_last_foreign_key_error: NULL
trx_adaptive_hash_latched: 0
trx_adaptive_hash_timeout: 10000
2 rows in set (0.00 sec) mysql> select * from innodb_locks \G
*************************** 1. row ***************************
lock_id: 2897C4F:355:3:2
lock_trx_id: 2897C4F
lock_mode: X
lock_type: RECORD
lock_table: `test`.`t1`
lock_index: `GEN_CLUST_INDEX`
lock_space: 355
lock_page: 3
lock_rec: 2
lock_data: 0x0000593AAECE
*************************** 2. row ***************************
lock_id: 2897BB4:355:3:2
lock_trx_id: 2897BB4
lock_mode: S
lock_type: RECORD
lock_table: `test`.`t1`
lock_index: `GEN_CLUST_INDEX`
lock_space: 355
lock_page: 3
lock_rec: 2
lock_data: 0x0000593AAECE
2 rows in set (0.02 sec) mysql> select * from innodb_lock_wait \G
ERROR 1109 (42S02): Unknown table 'innodb_lock_wait' in information_schema
mysql> select * from innodb_lock_waits \G
*************************** 1. row ***************************
requesting_trx_id: 2897C4F
requested_lock_id: 2897C4F:355:3:2
blocking_trx_id: 2897BB4
blocking_lock_id: 2897BB4:355:3:2
1 row in set (0.00 sec)

可以通过join三张表使锁lock_id对应线线程trx_mysql_thread_id:

mysql> SELECT  r.trx_id AS waiting_trx_id,r.trx_mysql_thread_id AS waiting_thread,r.trx_query AS waiting_query,t.trx_id AS blocking_trx_id,t.trx_mysql_thread_id AS blocking_thread,t.trx_query AS blocking_query  FROM information_schema.innodb_lock_waits w INNER JOIN information_schema.innodb_trx t ON  t.trx_id=w.blocking_trx_id INNER JOIN information_schema.innodb_trx r  ON r.trx_id=w.requesting_trx_id \G
*************************** 1. row ***************************
waiting_trx_id: 289DABF
waiting_thread: 1093652
waiting_query: select * from t1 for update
blocking_trx_id: 289DAA4
blocking_thread: 1093648
blocking_query: NULL
1 row in set (0.00 sec)

美团技术团队的博文对这块的说明也很不错http://tech.meituan.com/innodb-lock.html

参考:http://imysql.cn/2008_07_10_innodb_tx_isolation_and_lock_mode

本文只是对于“SELECT ... LOCK IN SHARE MODE”和“SELECT ... FORUPDATE”事务中的锁和RR隔离级别内的测试。

这个测试基于RR这个隔离级别,在《高性能MySQL》的1.5.1 InnoDB存储引擎章节InnoDB概览中对于RR防止幻读是通过间隙锁(next-key locking)实现的,上面提到的何登成“MySQL 加锁处理分析.pdf”这篇文章对于某些条件下操作的间隙锁解释非常易懂。在《高性能MySQL》的1.5.1 InnoDB存储引擎章节中对于间隙锁的说明如下:

间隙锁使得InnoDB不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。

《MySQL参考手册》中对于这块的说明是:

15.2.10.5. 锁定读SELECT ... FOR UPDATE和SELECT ... LOCK IN SHARE MODE
在一些环境中,一个持续读是不方便的。比如,你可能想要往表的子表里添加一个新行,并确信该子表在父表中有一个根。下列例子显示如何在你应用程序代码中实现参考的完整性。
假设你使用一个持续读去读取父表并且看到表中子表的根。不能安全地往子表添加子行吗?不,因为可能同时发生一些其它用户从父表删除父行,而你没有注意到它的情况。
解决办法是在使用LOCK IN SHARE MODE的锁定模式执行SELECT:
SELECT * FROM parent WHERE NAME = 'Jones' LOCK IN SHARE MODE;
在共享模式执行一个读意味着我们读最新的可用数据,并在我们读的行设置一个共享锁定。共享模式锁防止其它人更新或删除我们已读的行。同时,如果最新的数据属于其它客户端尚未提交的事务,我们等着知道那个事务被提交。我们看到前述的查询返回父'Jones',我们可以安全地 往子表添加子记录并提交我们的事务。
让我们看另外一个例子:我们在表child_codes 中有一个整数计数器域,我们用该表给每个添加到子表里的子项指派一个唯一的识别符。显然,使用持续读或者共享模式读去读取当前计数器的值并是一个好主意, 因为数据库的两个用户可能看到计数器的同一个值,如果两个用户试着用同一识别符往该表添加子项,就会发生一个重复键(duplicate-key)错误。
在这里,如果两个用户同时读计数器,当试图更新计数器之时,至少它们中有一个会发生死锁错误并终止,因此LOCK IN SHARE MODE并不是一个好的解决方法。
在这种情况下,有两个好方法去实现读计数器和增长计数器值:(1) 先更新计数器,让计数器值增1,之后读计数器,或者(2)用锁定模式FOR UPDATE先读计数器,之后计数器值增加。后一个途径可被如下实现:
SELECT counter_field FROM child_codes FOR UPDATE;
UPDATE child_codes SET counter_field = counter_field + 1;
SELECT ... FOR UPDATE读最新的可见数据,在每个它读取的行设置独占锁定。因此,它设置与搜索的SQL UPDATE可能会在行上设置的锁定同样的锁定。
请注意,以上仅是一个SELECT ... FOR UPDATE如何起作用的例子。在MySQL中,事实上生成一个唯一识别符的特殊任务可被用对该表的单独访问来完成:
UPDATE child_codes SET counter_field = LAST_INSERT_ID(counter_field + 1);
SELECT LAST_INSERT_ID();
SELECT语句仅仅取回识别符信息(专门对当前连接)。它不访问任何表。

翻译成普通话意思就是:

SELECT ... LOCK IN SHARE MODE sets a shared mode lock on the rows read. A shared mode lock enables other sessions to read the rows but not to modify them. The rows read are the latest available, so if they belong to another transaction that has not yet committed, the read blocks until that transaction ends.
在读取的行上设置一个共享模式的锁。这个共享锁允许其它session读取数据但不允许修改它。 行读取的是最新的数据,如果他被其它事务使用中而没有提交,读取锁将被阻塞直到那个事务结束。 SELECT ... FOR UPDATE sets an exclusive lock on the rows read. An exclusive lock prevents other sessions from accessing the rows for reading or writing.
在读取行上设置一个排他锁。组织其他session读取或者写入行数据

下面的内容是参考上面链接博文测试的内容。

测试一:

  Variable_name Value      
tx_isolation REPEATABLE-READ session 1 session 2  
1 update未提交 select update t1 set b='z'
where a=1
select
* from t1
where a=1
session
1 commit之前,普通select返回的结果都是session 1 commit提交前结果
2 update未提交 select
… lock in share mode
update
t1 set b='y'
where a=1
select
* from t1
where a=1 lock in share mode
session
1 commit以后session 2返回结果
3 update未提交 select
… for update
update
t1 set b='x'
where a=1
select
* from t1
where a=1 for update
session
1 commit以后session 2返回结果
  RR的隔离级别,对于a=1行的update操作会给行加排他锁
1、普通的select只是对于session 1事务提交前的行数据快照查询
2、select … lock in share mode属于共享锁,与session 1的排他锁互斥,需要等待session
1提交或者回滚
3、select … for update属于排他锁,与session 1的排它锁互斥,所以也需要等待需要等待session 1提交或者回滚

测试二:

  Variable_name Value      
tx_isolation REPEATABLE-READ      
session 1 session 2  
query result query result  
1 begin        
2     begin    
3 select * from t1 where a=1 for update        
4 update t1 set b='u' where a=1     session 2查询需要等待session 1事务处理完成或者回滚
5     select * from t1 where a=1 for update

select * from t1 where a=1 lock in share mode
无返回,等待  
6 select
* from t1 where a=1 for update

select * from t1 where a=1 lock in share mode
 

+---+------+
| a | b |
+---+------+
| 1 | u |
+---+------+
1 row in set (0.00 sec)

  无返回,等待 session
2查询需要等待session 1事务处理完成或者回滚
7 commit    

+---+------+
| a | b |
+---+------+
| 1 | u |
+---+------+
1 row in set (33.02 sec)

 
8     update t1 set b='w' where a=1   session
1事务处理完成或者回滚后session 2获得查询结果
9     select
* from t1 where a=1 for update

select * from t1 where a=1 lock in share mode

+---+------+
| a | b |
+---+------+
| 1 | w |
+---+------+
1 row in set (0.00 sec)

 
10 select
* from t1 where a=1 for update

select * from t1 where a=1 lock in share mode
无返回,等待     session
2事务处理完成或者回滚后session 1获得查询结果
11     commit    
12  

+---+------+
| a | b |
+---+------+
| 1 | w |
+---+------+
1 row in set (10.46 sec)

select
* from t1 where a=1 for update

select * from t1 where a=1 lock in share mode

+---+------+
| a | b |
+---+------+
| 1 | w |
+---+------+
1 row in set (0.00 sec)

 

测试三:

  Variable_name Value      
tx_isolation REPEATABLE-READ      
session 1 session 2  
query result query result  
1 begin        
2 select * from t1 where a=1 lock in share mode

+---+------+
| a | b |
+---+------+
| 1 | w |
+---+------+
1 row in set (0.00 sec)

     
3     begin    
4     select * from t1 where a=1 lock in share mode

+---+------+
| a | b |
+---+------+
| 1 | w |
+---+------+
1 row in set (0.00 sec)

session 2事务虽然只有一个select但是由于update和select两个所持有的共享锁、排他锁互斥,所以session 1的update事务需要等到session 2提交以后完成
5 update t1 set b='m' where a=1 无返回,等待      
6   Query OK, 1 row affected (17.49 sec)
Rows matched: 1  Changed: 1  Warnings: 0
commit    
7     select * from t1
where a=1 lock in share mode
无返回,等待 session 1未提交事务,等待
8 commit    

+---+------+
| a | b |
+---+------+
| 1 | m |
+---+------+
1 row in set (7.16 sec)

 

此后又做了几个测试,总结如下:

type 类型
select 快照
select … lock in share mode 共享锁
select … for update 排它锁
DML 排它锁
  select select … lock in share mode select … for update DML
select 快照 快照 快照 快照
select … lock in share mode 快照 共享实时 互斥等待 互斥等待
select … for update 快照 互斥等待 互斥等待 互斥等待
DML 快照 互斥等待 互斥等待 互斥等待
上一篇:win10下安装java jdk,tomcat


下一篇:[转]Oracle ORA-01403: no data found Exception SYS_REFCURSOR