MySQL中锁详解(行锁、表锁、页锁、悲观锁、乐观锁等)
原文地址:http://blog.csdn.net/mysteryhaohao/article/details/51669741
锁,在现实生活中是为我们想要隐藏于外界所使用的一种工具。在计算机中,是协调多个进程或线程并发访问某一资源的一种机制。在数据库当中,除了传统的计算资源(CPU、RAM、I/O等等)的争用之外,数据也是一种供许多用户共享访问的资源。如何保证数据并发访问的一致性、有效性,是所有数据库必须解决的一个问题,锁的冲突也是影响数据库并发访问性能的一个重要因素。从这一角度来说,锁对于数据库而言就显得尤为重要。
MySQL锁
相对于其他的数据库而言,MySQL的锁机制比较简单,最显著的特点就是不同的存储引擎支持不同的锁机制。根据不同的存储引擎,MySQL中锁的特性可以大致归纳如下:
| 行锁 | 表锁 | 页锁 | |
| MyISAM | √ | ||
| BDB | √ | √ | |
| InnoDB | √ | √ | 
开销、加锁速度、死锁、粒度、并发性能
- 表锁:开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低
- 行锁:开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高
- 页锁:开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般
从上述的特点课件,很难笼统的说哪种锁最好,只能根据具体应用的特点来说哪种锁更加合适。仅仅从锁的角度来说的话:
表锁更适用于以查询为主,只有少量按索引条件更新数据的应用;行锁更适用于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用。(PS:由于BDB已经被InnoDB所取代,我们只讨论MyISAM表锁和InnoDB行锁的问题)
mysql> show status like 'table%'; +-----------------------+-------+ | Variable_name | Value | +-----------------------+-------+ | Table_locks_immediate | 2979 | | Table_locks_waited | 0 | +-----------------------+-------+ 2 rows in set (0.00 sec))
MySQL表级锁的锁模式
| 请求锁模式          是否兼容 当前锁模式 | None | 读锁 | 写锁 | 
| 读锁 | 是 | 是 | 否 | 
| 写锁 | 是 | 否 | 否 | 
| session_1 | session_2 | 
| 获得表film_text的WRITE锁定 mysql> lock table film_text write; Query OK, 0 rows affected (0.00 sec) | |
| 当前session对锁定表的查询、更新、插入操作都可以执行: mysql> select film_id,title from film_text where film_id = 1001; +---------+-------------+ | film_id | title       | +---------+-------------+ | 1001    | Update Test | +---------+-------------+ 1 row in set (0.00 sec) mysql> insert into film_text (film_id,title) values(1003,'Test'); Query OK, 1 row affected (0.00 sec) mysql> update film_text set title = 'Test' where film_id = 1001; Query OK, 1 row affected (0.00 sec) Rows matched: 1  Changed: 1  Warnings: 0 | 其他session对锁定表的查询被阻塞,需要等待锁被释放: mysql> select film_id,title from film_text where film_id = 1001; 等待 | 
| 释放锁: mysql> unlock tables; Query OK, 0 rows affected (0.00 sec) | 等待 | 
| Session2获得锁,查询返回: mysql> select film_id,title from film_text where film_id = 1001; +---------+-------+ | film_id | title | +---------+-------+ | 1001    | Test  | +---------+-------+ 1 row in set (57.59 sec) | 
如何加表锁
Select sum(total) from orders; Select sum(subtotal) from order_detail; 这时,如果不先给两个表加锁,就可能产生错误的结果,因为第一条语句执行过程中,order_detail表可能已经发生了改变。因此,正确的方法应该是: Lock tables orders read local, order_detail read local; Select sum(total) from orders; Select sum(subtotal) from order_detail; Unlock tables;
- 上面的例子在LOCK TABLES时加了“local”选项,其作用就是在满足MyISAM表并发插入条件的情况下,允许其他用户在表尾并发插入记录,有关MyISAM表的并发插入问题,在后面的章节中还会进一步介绍。
- 在用LOCK TABLES给表显式加表锁时,必须同时取得所有涉及到表的锁,并且MySQL不支持锁升级。也就是说,在执行LOCK TABLES后,只能访问显式加锁的这些表,不能访问未加锁的表;同时,如果加的是读锁,那么只能执行查询操作,而不能执行更新操作。其实,在自动加锁的情况下也基本如此,MyISAM总是一次获得SQL语句所需要的全部锁。这也正是MyISAM表不会出现死锁(Deadlock Free)的原因。
在如下表所示的例子中,一个session使用LOCK TABLE命令给表film_text加了读锁,这个session可以查询锁定表中的记录,但更新或访问其他表都会提示错误;同时,另外一个session可以查询表中的记录,但更新就会出现锁等待。
| session_1 | session_2 | 
| 获得表film_text的READ锁定 mysql> lock table film_text read; Query OK, 0 rows affected (0.00 sec) | |
| 当前session可以查询该表记录 mysql> select film_id,title from film_text where film_id = 1001; +---------+------------------+ | film_id | title            | +---------+------------------+ | 1001    | ACADEMY DINOSAUR | +---------+------------------+ 1 row in set (0.00 sec) | 其他session也可以查询该表的记录 mysql> select film_id,title from film_text where film_id = 1001; +---------+------------------+ | film_id | title            | +---------+------------------+ | 1001    | ACADEMY DINOSAUR | +---------+------------------+ 1 row in set (0.00 sec) | 
| 当前session不能查询没有锁定的表 mysql> select film_id,title from film where film_id = 1001; ERROR 1100 (HY000): Table 'film' was not locked with LOCK TABLES | 其他session可以查询或者更新未锁定的表 mysql> select film_id,title from film where film_id = 1001; +---------+---------------+ | film_id | title         | +---------+---------------+ | 1001    | update record | +---------+---------------+ 1 row in set (0.00 sec) mysql> update film set title = 'Test' where film_id = 1001; Query OK, 1 row affected (0.04 sec) Rows matched: 1  Changed: 1  Warnings: 0 | 
| 当前session中插入或者更新锁定的表都会提示错误: mysql> insert into film_text (film_id,title) values(1002,'Test'); ERROR 1099 (HY000): Table 'film_text' was locked with a READ lock and can't be updated mysql> update film_text set title = 'Test' where film_id = 1001; ERROR 1099 (HY000): Table 'film_text' was locked with a READ lock and can't be updated | 其他session更新锁定表会等待获得锁: mysql> update film_text set title = 'Test' where film_id = 1001; 等待 | 
| 释放锁 mysql> unlock tables; Query OK, 0 rows affected (0.00 sec) | 等待 | 
| Session获得锁,更新操作完成: mysql> update film_text set title = 'Test' where film_id = 1001; Query OK, 1 row affected (1 min 0.71 sec) Rows matched: 1  Changed: 1  Warnings: 0 | 
注意,当使用LOCK TABLES时,不仅需要一次锁定用到的所有表,而且,同一个表在SQL语句中出现多少次,就要通过与SQL语句中相同的别名锁定多少次,否则也会出错!举例说明如下。
(1)对actor表获得读锁: mysql> lock table actor read; Query OK, 0 rows affected (0.00 sec) (2)但是通过别名访问会提示错误: mysql> select a.first_name,a.last_name,b.first_name,b.last_name from actor a,actor b where a.first_name = b.first_name and a.first_name = 'Lisa' and a.last_name = 'Tom' and a.last_name <> b.last_name; ERROR 1100 (HY000): Table 'a' was not locked with LOCK TABLES (3)需要对别名分别锁定: mysql> lock table actor as a read,actor as b read; Query OK, 0 rows affected (0.00 sec) (4)按照别名的查询可以正确执行: mysql> select a.first_name,a.last_name,b.first_name,b.last_name from actor a,actor b where a.first_name = b.first_name and a.first_name = 'Lisa' and a.last_name = 'Tom' and a.last_name <> b.last_name; +------------+-----------+------------+-----------+ | first_name | last_name | first_name | last_name | +------------+-----------+------------+-----------+ | Lisa | Tom | LISA | MONROE | +------------+-----------+------------+-----------+ 1 row in set (0.00 sec)
并发插入(Concurrent Inserts)
- 当concurrent_insert设置为0时,不允许并发插入。
- 当concurrent_insert设置为1时,如果MyISAM表中没有空洞(即表的中间没有被删除的行),MyISAM允许在一个进程读表的同时,另一个进程从表尾插入记录。这也是MySQL的默认设置。
- 当concurrent_insert设置为2时,无论MyISAM表中有没有空洞,都允许在表尾并发插入记录。
| session_1 | session_2 | 
| 获得表film_text的READ LOCAL锁定 mysql> lock table film_text read local; Query OK, 0 rows affected (0.00 sec) | |
| 当前session不能对锁定表进行更新或者插入操作: mysql> insert into film_text (film_id,title) values(1002,'Test'); ERROR 1099 (HY000): Table 'film_text' was locked with a READ lock and can't be updated mysql> update film_text set title = 'Test' where film_id = 1001; ERROR 1099 (HY000): Table 'film_text' was locked with a READ lock and can't be updated | 其他session可以进行插入操作,但是更新会等待: mysql> insert into film_text (film_id,title) values(1002,'Test'); Query OK, 1 row affected (0.00 sec) mysql> update film_text set title = 'Update Test' where film_id = 1001; 等待 | 
| 当前session不能访问其他session插入的记录: mysql> select film_id,title from film_text where film_id = 1002; Empty set (0.00 sec) | |
| 释放锁: mysql> unlock tables; Query OK, 0 rows affected (0.00 sec) | 等待 | 
| 当前session解锁后可以获得其他session插入的记录: mysql> select film_id,title from film_text where film_id = 1002; +---------+-------+ | film_id | title | +---------+-------+ | 1002    | Test  | +---------+-------+ 1 row in set (0.00 sec) | Session2获得锁,更新操作完成: mysql> update film_text set title = 'Update Test' where film_id = 1001; Query OK, 1 row affected (1 min 17.75 sec) Rows matched: 1  Changed: 1  Warnings: 0 | 
- 通过指定启动参数low-priority-updates,使MyISAM引擎默认给予读请求以优先的权利。
- 通过执行命令SET LOW_PRIORITY_UPDATES=1,使该连接发出的更新请求优先级降低。
- 通过指定INSERT、UPDATE、DELETE语句的LOW_PRIORITY属性,降低该语句的优先级。
- 原子性(Atomicity):事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
- 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引或双向链表)也都必须是正确的。
- 隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
- 持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。
- 更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题--最后的更新覆盖了由其他事务所做的更新。例如,两个编辑人员制作了同一文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同一文件,则可避免此问题。
- 脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做"脏读"。
- 不可重复读(Non-Repeatable Reads):一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。
- 幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
- 一种是在读取数据前,对其加锁,阻止其他事务对数据进行修改。
- 另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。从用户的角度来看,好像是数据库可以提供同一数据的多个版本,因此,这种技术叫做数据多版本并发控制(MultiVersion Concurrency Control,简称MVCC或MCC),也经常称为多版本数据库。
| 读数据一致性及允许的并发副作用 隔离级别 | 读数据一致性 | 脏读 | 不可重复读 | 幻读 | 
| 未提交读(Read uncommitted) | 最低级别,只能保证不读取物理上损坏的数据 | 是 | 是 | 是 | 
| 已提交度(Read committed) | 语句级 | 否 | 是 | 是 | 
| 可重复读(Repeatable read) | 事务级 | 否 | 否 | 是 | 
| 可序列化(Serializable) | 最高级别,事务级 | 否 | 否 | 否 | 
mysql> show status like 'innodb_row_lock%'; +-------------------------------+-------+ | Variable_name | Value | +-------------------------------+-------+ | InnoDB_row_lock_current_waits | 0 | | InnoDB_row_lock_time | 0 | | InnoDB_row_lock_time_avg | 0 | | InnoDB_row_lock_time_max | 0 | | InnoDB_row_lock_waits | 0 | +-------------------------------+-------+ 5 rows in set (0.01 sec)
如果发现锁争用比较严重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高,还可以通过设置InnoDB Monitors来进一步观察发生锁冲突的表、数据行等,并分析锁争用的原因。
具体方法如下: mysql> CREATE TABLE innodb_monitor(a INT) ENGINE=INNODB; Query OK, 0 rows affected (0.14 sec) 然后就可以用下面的语句来进行查看: mysql> Show innodb status\G; *************************** 1. row *************************** Type: InnoDB Name: Status: … … ------------ TRANSACTIONS ------------ Trx id counter 0 117472192 Purge done for trx's n:o < 0 117472190 undo n:o < 0 0 History list length 17 Total number of lock structs in row lock hash table 0 LIST OF TRANSACTIONS FOR EACH SESSION: ---TRANSACTION 0 117472185, not started, process no 11052, OS thread id 1158191456 MySQL thread id 200610, query id 291197 localhost root ---TRANSACTION 0 117472183, not started, process no 11052, OS thread id 1158723936 MySQL thread id 199285, query id 291199 localhost root Show innodb status … 监视器可以通过发出下列语句来停止查看: mysql> DROP TABLE innodb_monitor; Query OK, 0 rows affected (0.05 sec)
- 共享锁(S):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
- 排他锁(X):允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
- 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
- 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
| 请求锁模式    是否兼容 当前锁模式 | X | IX | S | IS | 
| X | 冲突 | 冲突 | 冲突 | 冲突 | 
| IX | 冲突 | 兼容 | 冲突 | 兼容 | 
| S | 冲突 | 冲突 | 兼容 | 兼容 | 
| IS | 冲突 | 兼容 | 兼容 | 兼容 | 
- 共享锁(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE。
- 排他锁(X):SELECT * FROM table_name WHERE ... FOR UPDATE。
| session_1 | session_2 | 
| mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) mysql> select actor_id,first_name,last_name from actor where actor_id = 178; +----------+------------+-----------+ | actor_id | first_name | last_name | +----------+------------+-----------+ | 178      | LISA       | MONROE    | +----------+------------+-----------+ 1 row in set (0.00 sec) | mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) mysql> select actor_id,first_name,last_name from actor where actor_id = 178; +----------+------------+-----------+ | actor_id | first_name | last_name | +----------+------------+-----------+ | 178      | LISA       | MONROE    | +----------+------------+-----------+ 1 row in set (0.00 sec) | 
| 当前session对actor_id=178的记录加share mode 的共享锁: mysql> select actor_id,first_name,last_name from actor where actor_id = 178 lock in share mode; +----------+------------+-----------+ | actor_id | first_name | last_name | +----------+------------+-----------+ | 178      | LISA       | MONROE    | +----------+------------+-----------+ 1 row in set (0.01 sec) | |
| 其他session仍然可以查询记录,并也可以对该记录加share mode的共享锁: mysql> select actor_id,first_name,last_name from actor where actor_id = 178 lock in share mode; +----------+------------+-----------+ | actor_id | first_name | last_name | +----------+------------+-----------+ | 178      | LISA       | MONROE    | +----------+------------+-----------+ 1 row in set (0.01 sec) | |
| 当前session对锁定的记录进行更新操作,等待锁: mysql> update actor set last_name = 'MONROE T' where actor_id = 178; 等待 | |
| 其他session也对该记录进行更新操作,则会导致死锁退出: mysql> update actor set last_name = 'MONROE T' where actor_id = 178; ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | |
| 获得锁后,可以成功更新: mysql> update actor set last_name = 'MONROE T' where actor_id = 178; Query OK, 1 row affected (17.67 sec) Rows matched: 1  Changed: 1  Warnings: 0 | 
| session_1 | session_2 | 
| mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) mysql> select actor_id,first_name,last_name from actor where actor_id = 178; +----------+------------+-----------+ | actor_id | first_name | last_name | +----------+------------+-----------+ | 178      | LISA       | MONROE    | +----------+------------+-----------+ 1 row in set (0.00 sec) | mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) mysql> select actor_id,first_name,last_name from actor where actor_id = 178; +----------+------------+-----------+ | actor_id | first_name | last_name | +----------+------------+-----------+ | 178      | LISA       | MONROE    | +----------+------------+-----------+ 1 row in set (0.00 sec) | 
| 当前session对actor_id=178的记录加for update的排它锁: mysql> select actor_id,first_name,last_name from actor where actor_id = 178 for update; +----------+------------+-----------+ | actor_id | first_name | last_name | +----------+------------+-----------+ | 178      | LISA       | MONROE    | +----------+------------+-----------+ 1 row in set (0.00 sec) | |
| 其他session可以查询该记录,但是不能对该记录加共享锁,会等待获得锁: mysql> select actor_id,first_name,last_name from actor where actor_id = 178; +----------+------------+-----------+ | actor_id | first_name | last_name | +----------+------------+-----------+ | 178      | LISA       | MONROE    | +----------+------------+-----------+ 1 row in set (0.00 sec) mysql> select actor_id,first_name,last_name from actor where actor_id = 178 for update; 等待 | |
| 当前session可以对锁定的记录进行更新操作,更新后释放锁: mysql> update actor set last_name = 'MONROE T' where actor_id = 178; Query OK, 1 row affected (0.00 sec) Rows matched: 1  Changed: 1  Warnings: 0 mysql> commit; Query OK, 0 rows affected (0.01 sec) | |
| 其他session获得锁,得到其他session提交的记录: mysql> select actor_id,first_name,last_name from actor where actor_id = 178 for update; +----------+------------+-----------+ | actor_id | first_name | last_name | +----------+------------+-----------+ | 178      | LISA       | MONROE T  | +----------+------------+-----------+ 1 row in set (9.59 sec) | 
mysql> create table tab_no_index(id int,name varchar(10)) engine=innodb; Query OK, 0 rows affected (0.15 sec) mysql> insert into tab_no_index values(1,'1'),(2,'2'),(3,'3'),(4,'4'); Query OK, 4 rows affected (0.00 sec) Records: 4 Duplicates: 0 Warnings: 0
| session_1 | session_2 | 
| mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select * from tab_no_index where id = 1 ; +------+------+ | id   | name | +------+------+ | 1    | 1    | +------+------+ 1 row in set (0.00 sec) | mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select * from tab_no_index where id = 2 ; +------+------+ | id   | name | +------+------+ | 2    | 2    | +------+------+ 1 row in set (0.00 sec) | 
| mysql> select * from tab_no_index where id = 1 for update; +------+------+ | id   | name | +------+------+ | 1    | 1    | +------+------+ 1 row in set (0.00 sec) | |
| mysql> select * from tab_no_index where id = 2 for update; 等待 | 
mysql> create table tab_with_index(id int,name varchar(10)) engine=innodb; Query OK, 0 rows affected (0.15 sec) mysql> alter table tab_with_index add index id(id); Query OK, 4 rows affected (0.24 sec) Records: 4 Duplicates: 0 Warnings: 0
| session_1 | session_2 | 
| mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select * from tab_with_index where id = 1 ; +------+------+ | id   | name | +------+------+ | 1    | 1    | +------+------+ 1 row in set (0.00 sec) | mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) mysql> select * from tab_with_index where id = 2 ; +------+------+ | id   | name | +------+------+ | 2    | 2    | +------+------+ 1 row in set (0.00 sec) | 
| mysql> select * from tab_with_index where id = 1 for update; +------+------+ | id   | name | +------+------+ | 1    | 1    | +------+------+ 1 row in set (0.00 sec) | |
| mysql> select * from tab_with_index where id = 2 for update; +------+------+ | id   | name | +------+------+ | 2    | 2    | +------+------+ 1 row in set (0.00 sec) | 
mysql> alter table tab_with_index drop index name; Query OK, 4 rows affected (0.22 sec) Records: 4 Duplicates: 0 Warnings: 0 mysql> insert into tab_with_index values(1,'4'); Query OK, 1 row affected (0.00 sec) mysql> select * from tab_with_index where id = 1; +------+------+ | id | name | +------+------+ | 1 | 1 | | 1 | 4 | +------+------+ 2 rows in set (0.00 sec)
| session_1 | session_2 | 
| mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) | mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) | 
| mysql> select * from tab_with_index where id = 1 and name = '1' for update; +------+------+ | id   | name | +------+------+ | 1    | 1    | +------+------+ 1 row in set (0.00 sec) | |
| 虽然session_2访问的是和session_1不同的记录,但是因为使用了相同的索引,所以需要等待锁: mysql> select * from tab_with_index where id = 1 and name = '4' for update; 等待 | 
mysql> alter table tab_with_index add index name(name); Query OK, 5 rows affected (0.23 sec) Records: 5 Duplicates: 0 Warnings: 0
| session_1 | session_2 | 
| mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) | mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) | 
| mysql> select * from tab_with_index where id = 1 for update; +------+------+ | id   | name | +------+------+ | 1    | 1    | | 1    | 4    | +------+------+ 2 rows in set (0.00 sec) | |
| Session_2使用name的索引访问记录,因为记录没有被索引,所以可以获得锁: mysql> select * from tab_with_index where name = '2' for update; +------+------+ | id   | name | +------+------+ | 2    | 2    | +------+------+ 1 row in set (0.00 sec) | |
| 由于访问的记录已经被session_1锁定,所以等待获得锁。: mysql> select * from tab_with_index where name = '4' for update; | 
mysql> alter table tab_no_index add index name(name); Query OK, 4 rows affected (8.06 sec) Records: 4 Duplicates: 0 Warnings: 0 mysql> explain select * from tab_with_index where name = 1 \G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: tab_with_index type: ALL possible_keys: name key: NULL key_len: NULL ref: NULL rows: 4 Extra: Using where 1 row in set (0.00 sec) mysql> explain select * from tab_with_index where name = '1' \G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: tab_with_index type: ref possible_keys: name key: name key_len: 23 ref: const rows: 1 Extra: Using where 1 row in set (0.00 sec)
| session_1 | session_2 | 
| mysql> select @@tx_isolation; +-----------------+ | @@tx_isolation  | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row in set (0.00 sec) mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) | mysql> select @@tx_isolation; +-----------------+ | @@tx_isolation  | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row in set (0.00 sec) mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) | 
| 当前session对不存在的记录加for update的锁: mysql> select * from emp where empid = 102 for update; Empty set (0.00 sec) | |
| 这时,如果其他session插入empid为102的记录(注意:这条记录并不存在),也会出现锁等待: mysql>insert into emp(empid,...) values(102,...); 阻塞等待 | |
| Session_1 执行rollback: mysql> rollback; Query OK, 0 rows affected (13.04 sec) | |
| 由于其他session_1回退后释放了Next-Key锁,当前session可以获得锁并成功插入记录: mysql>insert into emp(empid,...) values(102,...); Query OK, 1 row affected (13.35 sec) | 
| session_1 | session_2 | 
| mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) mysql> select * from target_tab; Empty set (0.00 sec) mysql> select * from source_tab where name = '1'; +----+------+----+ | d1 | name | d2 | +----+------+----+ |  4 | 1    |  1 | |  5 | 1    |  1 | |  6 | 1    |  1 | |  7 | 1    |  1 | |  8 | 1    |  1 | +----+------+----+ 5 rows in set (0.00 sec) | mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) mysql> select * from target_tab; Empty set (0.00 sec) mysql> select * from source_tab where name = '1'; +----+------+----+ | d1 | name | d2 | +----+------+----+ |  4 | 1    |  1 | |  5 | 1    |  1 | |  6 | 1    |  1 | |  7 | 1    |  1 | |  8 | 1    |  1 | +----+------+----+ 5 rows in set (0.00 sec) | 
| mysql> insert into target_tab select d1,name from source_tab where name = '1'; Query OK, 5 rows affected (0.00 sec) Records: 5  Duplicates: 0  Warnings: 0 | |
| mysql> update source_tab set name = '1' where name = '8'; 等待 | |
| commit; | |
| 返回结果 commit; | 
| session_1 | session_2 | 
| mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) mysql>set innodb_locks_unsafe_for_binlog='on' Query OK, 0 rows affected (0.00 sec) mysql> select * from target_tab; Empty set (0.00 sec) mysql> select * from source_tab where name = '1'; +----+------+----+ | d1 | name | d2 | +----+------+----+ |  4 | 1    |  1 | |  5 | 1    |  1 | |  6 | 1    |  1 | |  7 | 1    |  1 | |  8 | 1    |  1 | +----+------+----+ 5 rows in set (0.00 sec) | mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) mysql> select * from target_tab; Empty set (0.00 sec) mysql> select * from source_tab where name = '1'; +----+------+----+ | d1 | name | d2 | +----+------+----+ |  4 | 1    |  1 | |  5 | 1    |  1 | |  6 | 1    |  1 | |  7 | 1    |  1 | |  8 | 1    |  1 | +----+------+----+ 5 rows in set (0.00 sec) | 
| mysql> insert into target_tab select d1,name from source_tab where name = '1'; Query OK, 5 rows affected (0.00 sec) Records: 5  Duplicates: 0  Warnings: 0 | |
| session_1未提交,可以对session_1的select的记录进行更新操作。 mysql> update source_tab set name = '8' where name = '1'; Query OK, 5 rows affected (0.00 sec) Rows matched: 5  Changed: 5  Warnings: 0 mysql> select * from source_tab where name = '8'; +----+------+----+ | d1 | name | d2 | +----+------+----+ |  4 | 8    |  1 | |  5 | 8    |  1 | |  6 | 8    |  1 | |  7 | 8    |  1 | |  8 | 8    |  1 | +----+------+----+ 5 rows in set (0.00 sec) | |
| 更新操作先提交 mysql> commit; Query OK, 0 rows affected (0.05 sec) | |
| 插入操作后提交 mysql> commit; Query OK, 0 rows affected (0.07 sec) | |
| 此时查看数据,target_tab中可以插入source_tab更新前的结果,这符合应用逻辑: mysql> select * from source_tab where name = '8'; +----+------+----+ | d1 | name | d2 | +----+------+----+ |  4 | 8    |  1 | |  5 | 8    |  1 | |  6 | 8    |  1 | |  7 | 8    |  1 | |  8 | 8    |  1 | +----+------+----+ 5 rows in set (0.00 sec) mysql> select * from target_tab; +------+------+ | id   | name | +------+------+ | 4    | 1.00 | | 5    | 1.00 | | 6    | 1.00 | | 7    | 1.00 | | 8    | 1.00 | +------+------+ 5 rows in set (0.00 sec) | mysql> select * from tt1 where name = '1'; Empty set (0.00 sec) mysql> select * from source_tab where name = '8'; +----+------+----+ | d1 | name | d2 | +----+------+----+ |  4 | 8    |  1 | |  5 | 8    |  1 | |  6 | 8    |  1 | |  7 | 8    |  1 | |  8 | 8    |  1 | +----+------+----+ 5 rows in set (0.00 sec) mysql> select * from target_tab; +------+------+ | id   | name | +------+------+ | 4    | 1.00 | | 5    | 1.00 | | 6    | 1.00 | | 7    | 1.00 | | 8    | 1.00 | +------+------+ 5 rows in set (0.00 sec) | 
...... SET TIMESTAMP=1169175130; BEGIN; # at 274 #070119 10:51:57 server id 1 end_log_pos 105 Query thread_id=1 exec_time=0 error_code=0 SET TIMESTAMP=1169175117; update source_tab set name = '8' where name = '1'; # at 379 #070119 10:52:10 server id 1 end_log_pos 406 Xid = 5 COMMIT; # at 406 #070119 10:52:14 server id 1 end_log_pos 474 Query thread_id=2 exec_time=0 error_code=0 SET TIMESTAMP=1169175134; BEGIN; # at 474 #070119 10:51:29 server id 1 end_log_pos 119 Query thread_id=2 exec_time=0 error_code=0 SET TIMESTAMP=1169175089; insert into target_tab select d1,name from source_tab where name = '1'; # at 593 #070119 10:52:14 server id 1 end_log_pos 620 Xid = 7 COMMIT; ......
- 一是采取上面示例中的做法,将innodb_locks_unsafe_for_binlog的值设置为“on”,强制MySQL使用多版本数据一致性读。但付出的代价是可能无法用binlog正确地恢复或复制数据,因此,不推荐使用这种方式。
- 二是通过使用“select * from source_tab ... Into outfile”和“load data infile ...”语句组合来间接实现,采用这种方式MySQL不会给source_tab加锁。
| 隔离级别         一致性读和锁 SQL | Read Uncommited | Read Commited | Repeatable Read | Serializable | |
| SQL | 条件 | ||||
| select | 相等 | None locks | Consisten read/None lock | Consisten read/None lock | Share locks | 
| 范围 | None locks | Consisten read/None lock | Consisten read/None lock | Share Next-Key | |
| update | 相等 | exclusive locks | exclusive locks | exclusive locks | Exclusive locks | 
| 范围 | exclusive next-key | exclusive next-key | exclusive next-key | exclusive next-key | |
| Insert | N/A | exclusive locks | exclusive locks | exclusive locks | exclusive locks | 
| replace | 无键冲突 | exclusive locks | exclusive locks | exclusive locks | exclusive locks | 
| 键冲突 | exclusive next-key | exclusive next-key | exclusive next-key | exclusive next-key | |
| delete | 相等 | exclusive locks | exclusive locks | exclusive locks | exclusive locks | 
| 范围 | exclusive next-key | exclusive next-key | exclusive next-key | exclusive next-key | |
| Select ... from ... Lock in share mode | 相等 | Share locks | Share locks | Share locks | Share locks | 
| 范围 | Share locks | Share locks | Share Next-Key | Share Next-Key | |
| Select * from ... For update | 相等 | exclusive locks | exclusive locks | exclusive locks | exclusive locks | 
| 范围 | exclusive locks | Share locks | exclusive next-key | exclusive next-key | |
| Insert into ... Select ... (指源表锁) | innodb_locks_unsafe_for_binlog=off | Share Next-Key | Share Next-Key | Share Next-Key | Share Next-Key | 
| innodb_locks_unsafe_for_binlog=on | None locks | Consisten read/None lock | Consisten read/None lock | Share Next-Key | |
| create table ... Select ... (指源表锁) | innodb_locks_unsafe_for_binlog=off | Share Next-Key | Share Next-Key | Share Next-Key | Share Next-Key | 
| innodb_locks_unsafe_for_binlog=on | None locks | Consisten read/None lock | Consisten read/None lock | Share Next-Key | |
- 第一种情况是:事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。
- 第二种情况是:事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。
SET AUTOCOMMIT=0; LOCK TABLES t1 WRITE, t2 READ, ...; [do something with tables t1 and t2 here]; COMMIT; UNLOCK TABLES;
| session_1 | session_2 | 
| mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) mysql> select * from table_1 where where id=1 for update; ... 做一些其他处理... | mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) mysql> select * from table_2 where id=1 for update; ... | 
| select * from table_2 where id =1 for update; 因session_2已取得排他锁,等待 | 做一些其他处理... | 
| mysql> select * from table_1 where where id=1 for update; 死锁 | 
| session_1 | session_2 | 
| mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) | mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) | 
| mysql> select first_name,last_name from actor where actor_id = 1 for update; +------------+-----------+ | first_name | last_name | +------------+-----------+ | PENELOPE   | GUINESS   | +------------+-----------+ 1 row in set (0.00 sec) | |
| mysql> insert into country (country_id,country) values(110,'Test'); Query OK, 1 row affected (0.00 sec) | |
| mysql>  insert into country (country_id,country) values(110,'Test'); 等待 | |
| mysql> select first_name,last_name from actor where actor_id = 1 for update; +------------+-----------+ | first_name | last_name | +------------+-----------+ | PENELOPE   | GUINESS   | +------------+-----------+ 1 row in set (0.00 sec) | |
| mysql>  insert into country (country_id,country) values(110,'Test'); ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | 
| session_1 | session_2 | 
| mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) | mysql> set autocommit=0; Query OK, 0 rows affected (0.00 sec) | 
| mysql> select first_name,last_name from actor where actor_id = 1 for update; +------------+-----------+ | first_name | last_name | +------------+-----------+ | PENELOPE   | GUINESS   | +------------+-----------+ 1 row in set (0.00 sec) | |
| mysql> select first_name,last_name from actor where actor_id = 3 for update; +------------+-----------+ | first_name | last_name | +------------+-----------+ | ED         | CHASE     | +------------+-----------+ 1 row in set (0.00 sec) | |
| mysql> select first_name,last_name from actor where actor_id = 3 for update; 等待 | |
| mysql> select first_name,last_name from actor where actor_id = 1 for update; ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | |
| mysql> select first_name,last_name from actor where actor_id = 3 for update; +------------+-----------+ | first_name | last_name | +------------+-----------+ | ED         | CHASE     | +------------+-----------+ 1 row in set (4.71 sec) | 
| session_1 | session_2 | 
| mysql> select @@tx_isolation; +-----------------+ | @@tx_isolation  | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row in set (0.00 sec) mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) | mysql> select @@tx_isolation; +-----------------+ | @@tx_isolation  | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row in set (0.00 sec) mysql> set autocommit = 0; Query OK, 0 rows affected (0.00 sec) | 
| 当前session对不存在的记录加for update的锁: mysql> select actor_id,first_name,last_name from actor where actor_id = 201 for update; Empty set (0.00 sec) | |
| 其他session也可以对不存在的记录加for update的锁: mysql> select actor_id,first_name,last_name from actor where actor_id = 201 for update; Empty set (0.00 sec) | |
| 因为其他session也对该记录加了锁,所以当前的插入会等待: mysql> insert into actor (actor_id , first_name , last_name) values(201,'Lisa','Tom'); 等待 | |
| 因为其他session已经对记录进行了更新,这时候再插入记录就会提示死锁并退出: mysql> insert into actor (actor_id, first_name , last_name) values(201,'Lisa','Tom'); ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | |
| 由于其他session已经退出,当前session可以获得锁并成功插入记录: mysql> insert into actor (actor_id , first_name , last_name) values(201,'Lisa','Tom'); Query OK, 1 row affected (13.35 sec) | 
| session_1 | session_2 | session_3 | 
| mysql> select @@tx_isolation; +----------------+ | @@tx_isolation | +----------------+ | READ-COMMITTED | +----------------+ 1 row in set (0.00 sec) mysql> set autocommit=0; Query OK, 0 rows affected (0.01 sec) | mysql> select @@tx_isolation; +----------------+ | @@tx_isolation | +----------------+ | READ-COMMITTED | +----------------+ 1 row in set (0.00 sec) mysql> set autocommit=0; Query OK, 0 rows affected (0.01 sec) | mysql> select @@tx_isolation; +----------------+ | @@tx_isolation | +----------------+ | READ-COMMITTED | +----------------+ 1 row in set (0.00 sec) mysql> set autocommit=0; Query OK, 0 rows affected (0.01 sec) | 
| Session_1获得for update的共享锁: mysql> select actor_id, first_name,last_name from actor where actor_id = 201 for update; Empty set (0.00 sec) | 由于记录不存在,session_2也可以获得for update的共享锁: mysql> select actor_id, first_name,last_name from actor where actor_id = 201 for update; Empty set (0.00 sec) | |
| Session_1可以成功插入记录: mysql> insert into actor (actor_id,first_name,last_name) values(201,'Lisa','Tom'); Query OK, 1 row affected (0.00 sec) | ||
| Session_2插入申请等待获得锁: mysql> insert into actor (actor_id,first_name,last_name) values(201,'Lisa','Tom'); 等待 | ||
| Session_1成功提交: mysql> commit; Query OK, 0 rows affected (0.04 sec) | ||
| Session_2获得锁,发现插入记录主键重,这个时候抛出了异常,但是并没有释放共享锁: mysql> insert into actor (actor_id,first_name,last_name) values(201,'Lisa','Tom'); ERROR 1062 (23000): Duplicate entry '201' for key 'PRIMARY' | ||
| Session_3申请获得共享锁,因为session_2已经锁定该记录,所以session_3需要等待: mysql> select actor_id, first_name,last_name from actor where actor_id = 201 for update; 等待 | ||
| 这个时候,如果session_2直接对记录进行更新操作,则会抛出死锁的异常: mysql> update actor set last_name='Lan' where actor_id = 201; ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction | ||
| Session_2释放锁后,session_3获得锁: mysql> select first_name, last_name from actor where actor_id = 201 for update; +------------+-----------+ | first_name | last_name | +------------+-----------+ | Lisa       | Tom       | +------------+-----------+ 1 row in set (31.12 sec) | 
mysql> show innodb status \G ……. ------------------------ LATEST DETECTED DEADLOCK ------------------------ 070710 14:05:16 *** (1) TRANSACTION: TRANSACTION 0 117470078, ACTIVE 117 sec, process no 1468, OS thread id 1197328736 inserting mysql tables in use 1, locked 1 LOCK WAIT 5 lock struct(s), heap size 1216 MySQL thread id 7521657, query id 673468054 localhost root update insert into country (country_id,country) values(110,'Test') ……… *** (2) TRANSACTION: TRANSACTION 0 117470079, ACTIVE 39 sec, process no 1468, OS thread id 1164048736 starting index read, thread declared inside InnoDB 500 mysql tables in use 1, locked 1 4 lock struct(s), heap size 1216, undo log entries 1 MySQL thread id 7521664, query id 673468058 localhost root statistics select first_name,last_name from actor where actor_id = 1 for update *** (2) HOLDS THE LOCK(S): ……… *** (2) WAITING FOR THIS LOCK TO BE GRANTED: ……… *** WE ROLL BACK TRANSACTION (1) ……
 
                    
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号