欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

MySQL锁模型与事物 博客分类: SQL 锁事物 

程序员文章站 2024-03-11 12:29:31
...

 MySQL中不同的存储引擎支持不同的锁机制,比如,MySAM和MEMORY存储引擎采用表级锁,InnoDB支持表级锁和行级锁(默认采用行级锁),BDB(被InnoDB取代)支持表级锁和页面锁。

 

查看mysq提供的存储引擎:mysql> show engines;
查看mysql当前默认的存储引擎:mysql> show variables like '%storage_engine%';
查看某个表用的引擎:mysql> show create table 表名;

 

一、表级锁

 

1、表级锁特点

 

开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。当需要频繁对大部分数据做 GROUP BY 操作或者需要频繁扫描整个表时,推荐使用表级锁。

 

命令mysql> show status like 'Table%';

 

Table_locks_immediate 指的是能够立即获得表级锁的次数,而Table_locks_waited指的是不能立即获取表级锁而需要等待的次数。如果 Table_locks_waited的值比较大的话,则说明存在着较严重的表级锁争用情况,这是可能需要创建一个专有的缓存表,或者通过其它方式来减小表的大小,或者降低表级锁命令调用的频率。

 

 

2、表级锁的锁模式

 

读锁:成功申请读锁的前提是当前没有线程对该表使用写锁,否则该语句会被阻塞。申请读锁成功后,其他线程也可以对该表进行读操作,但不允许有线程对其进行写操作,包括当前线程。 用法:申请——LOCK TABLE table_name [ AS alias_name ] READ,释放——UNLOCK tables。    

 

写锁:成功申请写锁的前提是当前没有线程对表加读锁和其他写锁,否则会被阻塞。写锁可以加优先级,当多个线程同时申请多种锁(LOW_PRIORITY,READ,WRITE)时,LOW_PRIORITY的优先级最低。 用法:申请——LOCK TABLE table_name [AS alias_name] [ LOW_PRIORITY ] WRITE。

 

 

 

3、如何给表加锁

 

MyISAM执行SELECT前会自动把涉及的所有的表加读锁,在执行UPDATE、DELETE、INSERT前会自动把涉及的所有的表加写锁。用户一般不需要直接用LOCK TABLE命令给MyISAM表加锁。

 

显式地给MyISAM表加锁,一般是为了模拟事物操作,实现在某一个时间点多个表的一致性读取。例如,order表记录了每个订单的总金额(total),order_detail记录了每个订单中每个产品的金额小计(subtotal),如果需要检查俩个表的金额是否一致,则:

 

Lock tables orders read local, order_detail read local;

 

Select sum(total) from orders;

 

Select sum(subtotal) from order_detail;

 

Unlock tables;

 

注:local选项作用是在满足MyISAM表并发插入条件的情况下,允许其他用户在表尾并发插入记录

 

 

 

4、一个表锁导致阻塞的例子

 

Session1

Session2

CREATE TABLE my_contacts # 创建表联系人 

(

  id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,

  NAME VARCHAR (30) NOT NULL,

  gender CHAR(1) NOT NULL DEFAULT 'M',  #'M'或'F' 

  birthday DATE,   # 1980-09-15 

  phone VARCHAR (11) NOT NULL,

  information BLOB

) ENGINE = MYISAM ;

 

INSERT  INTO my_contacts(NAME,gender,birthday,phone,information) VALUES ('ZhangSan','F','1990-09-09','13513513513','Friend'); 

INSERT  INTO my_contacts(NAME,gender,birthday,phone,information) VALUES ('LiSi','F','1991-08-12','18989107021','strange boy');

 

LOCK TABLE my_contacts WRITE;

 

SELECT * FROM my_contacts WHERE NAME="ZhangSan";

返回查询结果

SELECT * FROM my_contacts WHERE NAME="LiSi"; 阻塞… …

UNLOCK TABLES;

阻塞… …

 

返回查询结果

 

 

 

5、注意事项

 

Lock table时,如果查询语句中用到别名以及其他的表,那么别名和其他的表均需要锁住。如果锁住了别名,那么查询语句中就不能直接用表名了。在执行LOCK TABLES后,只能访问显式加锁的这些表,不能访问未加锁的表;同时,如果加的是读锁,那么只能执行查询操作,而不能执行更新操作。

 

LOCK TABLE my_contacts READ;

 

SELECT a.name ,b.math FROM my_contacts a, scores b WHERE a.`name`=b.`name`;

 

以上查询语句中,my_contacts被锁住了,但使用了别名a,a没有被锁住;表scores也没有被锁住,且scores也用了别名,修改为:

 

LOCK TABLE my_contacts as a READ, scores as b READ;

 

SELECT a.name ,b.math FROM my_contacts a, scores b WHERE a.`name`=b.`name`;

 

 

 

6、并发插入

 

一般而言,MyISAM表的读和写是串行的。在一定条件下,MyISAM表也支持查询和插入并发执行。

 

MyISAM有一个系统变量concurrent_insert,专用于控制并行插入的行为,其值可以为012。为0时,不允许并发插入;为1时,(默认设置),如果MyISAM表中没有空洞(即表的中间没有被删除的行),则允许一个进程读表,另一个进程在表尾插入;为2时,允许在表尾并发插入记录。

 

 

 

Session1

Session2

SHOW VARIABLES LIKE "%current_insert%";

current_insert的值为AUTO,(1)

 

LOCK TABLE my_contacts READ LOCAL;

当前session可以查询,不能更新;其它session可以并发插入(如果无空洞),不能删除更新

 

INSERT INTO my_contacts (NAME,gender,birthday,phone,information) VALUES ('LiHong','F','1960-01-01','18956234756','');

插入失败,my_contacts被locked;

UPDATE my_contacts SET gender = 'M' WHERE NAME = 'ZhangSan';

更新失败,my_contacts被locked;

INSERT INTO my_contacts (NAME,gender,birthday,phone,information) VALUES ('WangHui','M','1990-01-01','13758694231','');

并发插入成功;

 

SELECT id, gender FROM my_contacts WHERE NAME  = 'ZhangSan';

查询当前session内的记录成功;

SELECT * FROM my_contacts WHERE NAME  = 'WangHui';

无法查询到并发插入的记录;

UPDATE my_contacts SET gender = 'M' WHERE NAME = 'LiSi';

更新操作被阻塞… …

UNLOCK TABLES;

被阻塞… …

SELECT * FROM my_contacts WHERE NAME  = 'WangHui';

查询其它session插入的记录成功

更新记录成功

 

当一个表获得READ LOCAL锁后,该线程可以对表进行查询,不能更新,插入等,但其它线程可以并发插入,(concurrent_insert=2或者concurrent_insert=1且无空洞),但不能删除和更新。

 

MyISAM的并发插入特性可用来解决对同一表查询和插入的锁争用。例如,设置concurrent_insert=2,且通过空闲时段执行OPTIMIZE TABLE语句来整理空间碎片,收回因删除记录产生的中间空洞。

 

 

 

7、MyISAM的粒调度

 

MySQL一般认为写请求比度请求更重要,所以一个进程请求某个MyISAM表的读锁,同时另一个进程也请求同一表的写锁,写进程会先获得锁,不仅如此,即使读请求先到锁等待队列,写请求后到,写锁也会插到读锁请求之前!这也正是MyISAM表不太适合于有大量更新操作和查询操作应用的原因,因为,大量的更新操作会造成查询操作很难获得读锁,从而可能永远阻塞。这时,可以通过一些设置来调节MyISAM 的调度行为。

 

  • 通过指定启动参数low-priority-updates,使MyISAM引擎默认给予读请求以优先的权利。

  • 通过执行命令SET LOW_PRIORITY_UPDATES=1,使该连接发出的更新请求优先级降低。

  • 通过指定INSERT、UPDATE、DELETE语句的LOW_PRIORITY属性,降低该语句的优先级。

  • 给系统参数max_write_lock_count设置一个合适的值,当一个表的读锁达到这个值后,MySQL就暂时将写请求的优先级降低,给读进程一定获得锁的机会。

    需要注意的是,一些需要长时间运行的查询操作,也会使写进程“饿死”!应用中应尽量避免长时间运行的查询操作,可以通过使用中间表等措施对SQL语句做一定的“分解”,使每一步查询都能在较短时间完成,从而减少锁冲突。如果复杂查询不可避免,应尽量安排在数据库空闲时段执行,比如一些定期统计可以安排在夜间执行。

     

    二、行级锁

    InnoDB与MyISAM的最大不同点是:1、支持事物(Transaction),2、支持行级锁。锁定粒度最细的一种锁,能大大减少数据库操作的冲突,由于其粒度小,加锁的开销最大。

     

    1、事物
    事务是由一组SQL语句组成的逻辑处理单元,具有四个属性,即ACID:

  • 原子性(Atomicity):对数据的修改,要么全都执行,要么全都不执行。

  • 一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构(如B树索引、双向链表)是正确的。

  • 隔离性(Isolation):事务在不受外部并发操作影响,事务处理过程中的中间状态对外不可见,反之亦然。

  • 持久性(Durable):事务完成后,它对于数据的修改是永久的,即使出现系统故障。

     

    事务的并发执行带来的问题:

  • 更新丢失:多个事物更新同一行时,由于每个事务都不知道其他事务的存在,就会导致最后的更新覆盖了由其他事务所做的更新。

  • 脏读:一个事务正对一条记录进行修改,在提交事物之前,该记录的数据处于不一致状态,如果另一事物不加控制地读取同一条记录,就会读到 “脏”数据。

  • 不可重复读:一个事务读取以前读过的数据,却发现该数据已发生了改变,甚至被删除。

  • 幻读:一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据。

     

    事务的隔离性

    MyISAM不支持事物,InnoDB支持事物。定义隔离级别的语句如下:
    SET TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED | READ COMMITTED  |REPEATABLE READ  |SERIALIZABLE ]

    查询事物的隔离级别:

    1.查看当前会话隔离级别:select @@tx_isolation;

    2.查看系统当前隔离级别:select @@global.tx_isolation;

    3.设置当前会话隔离级别:set session transaction isolatin level repeatable read;

    4.设置系统当前隔离级别:set global transaction isolation level repeatable read;

    5.命令行开始事务:set autocommit=off 或者 start transaction

    各个级别存在问题如下:

 

隔离级

脏读

不可重读

幻读

读未提交(Read uncommitted)

可能

可能

可能

读提交(Read committed)

不可能

可能

可能

可重复读(Repeatable read)

不可能

不可能

可能

可串行化(Serializable)

不可能

不可能

不可能

 

注:Oracle支持read commited、serializable以及自定义的read only;SQL Server支持以上四个以及自定义的“快照”;MySQL支持以上四个级别,默认为repeatable read。

 

 

 

Read uncommitted存在脏读

 

Session1

Session2

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

设置为读未提交

SELECT @@session.tx_isolation,@@global.tx_isolation;

 

START TRANSACTION;

SET autocommit = 0;

开始事物

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

设置为读未提交

 

START TRANSACTION;

SET autocommit = 0;

开始事物

SELECT * FROM my_contacts;

 

 

INSERT  INTO my_contacts(NAME,gender,birthday,phone,information) VALUES ('ZhaoXin','M','1990-01-01','1585652123','');

Session2插入记录,但是没有提交

SELECT * FROM my_contacts;

查询到Session2插入的记录。(脏读)

 

 

ROLLBACK; 回滚提交

COMMIT; 提交

 

 

 

 

Read uncommited不会出现脏读,但是不可重复读

 

Session1

Session2

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

设置读提交

START TRANSACTION;

SET autocommit = 0;

开始事物

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

设置读提交

START TRANSACTION;

SET autocommit = 0;

开始事物

SELECT * FROM my_contacts;

 

 

INSERT  INTO my_contacts(NAME,gender,birthday,phone,information) VALUES ('ZhaoXin','M','1990-01-01','1585652123','');

Session2插入记录,但是没有提交

SELECT * FROM my_contacts;

查询不到Session2 插入的记录(不存在脏读)

 

 

COMMIT; 提交,插入成功

SELECT * FROM my_contacts;

查询到Session2 插入的记录(不可重读)

 

COMMIT; 提交

 

 

 

 

Repeatable Read不存在不可重读,但存在幻读

 

Session1

Session2

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

设置重复读

 

START TRANSACTION;

SET autocommit = 0;

开始事物

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

设置重复读

 

START TRANSACTION;

SET autocommit = 0;

开始事物

SELECT * FROM my_contacts;

 

 

INSERT  INTO my_contacts(NAME,gender,birthday,phone,information) VALUES ('ZhaoXin','M','1990-01-01','1585652123','');

Session2插入记录,但是没有提交

SELECT * FROM my_contacts;

查询不到Session2 插入的记录(不存在脏读)

 

 

COMMIT; 提交,插入成功

SELECT * FROM my_contacts;

查询不到Session2 插入的记录(当前session内多次查询的结果保持不变,可重复读)

 

COMMIT; 提交

 

SELECT * FROM my_contacts;

查询到Session2 插入的记录

 

 

 

 

Session1

Session2

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

设置重复读

 

START TRANSACTION;

SET autocommit = 0;

开始事物

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

设置重复读

 

START TRANSACTION;

SET autocommit = 0;

开始事物

SELECT * FROM my_contacts;

 

 

INSERT  INTO my_contacts(id,NAME,gender,birthday,phone,information) VALUES (5,'ZhaoXin','M','1990-01-01','1585652123','');

 

COMMIT; 提交,插入成功

SELECT * FROM my_contacts;

查询不到Session2 插入的记录(当前session内多次查询的结果保持不变,可重复读)

 

INSERT  INTO my_contacts(id,NAME,gender,birthday,phone,information) VALUES (5,'ZhouQi','M','1990-01-01','13966623200','');

不知道id=5被session2插入记录,导致Duplicate entry '5' for key 'PRIMARY'

 

 

       Repeatable Read下的幻读现象可以用间隙锁解决,即先把某个范围内的(例如id=5)不存在的记录锁住,其它事物插入记录时就被阻塞,当前事物就可以正常插入。

 

Serializable不存在幻读,它是串行化执行读和写、写与写。

 

Session1

Session2

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

设置可串行化

 

START TRANSACTION;

SET autocommit = 0;

开始事物

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;

SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;

设置可串行化

 

START TRANSACTION;

SET autocommit = 0;

开始事物

SELECT * FROM my_contacts;

 

 

INSERT  INTO my_contacts(id,NAME,gender,birthday,phone,information) VALUES (5,'ZhaoXin','M','1990-01-01','1585652123','');

被阻塞… …

COMMIT;

 

 

阻塞解除

SELECT * FROM my_contacts;

被阻塞… …

 

 

COMMIT;

阻塞解除

 

 

 

 

2InnoDB的行级锁的锁模型

 

  • 共享锁(shared lock,S): SELECT ...LOCK IN SHARE MODE; 当没有其他线程对查询结果集中的任何一行使用排他锁时,可以对查询结果中的每行都加共享锁,否则会被阻塞。其他线程也可以读取使用了共享锁的表,而且这些线程读取的是同一个版本的数据,但是不能获取该数据集合的排它锁。

  • 排它锁(exclusive lock,X):SELECT ...LOCK FOR UPDATE; 当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功对查询结果中的每行都加排他锁,否则会被阻塞。获得排它锁的事务可以更新数据,而其他事务不能获取相同数据集的共享读锁和排他写锁。

  • 意向共享锁(IS):事务在给一个数据行加共享锁前必须先取得该表的IS锁。

  • 意向排他锁(IX):事务在给一个数据行加排他锁前必须先取得该表的IX锁。

     

    统一数据集的两个行级锁的兼容性:

 

 

S

IS

X

IX

S

兼容

兼容

冲突

冲突

IS

 

兼容

冲突

兼容

X

 

 

冲突

冲突

IX

 

 

 

兼容

 

 

 

InnoDB共享锁的例子

 

Session1

Session2

alter table my_contacts engine=innodb;

SET autocommit = 0;

SELECT * FROM my_contacts WHERE id=2;

SET autocommit = 0;

 SELECT * FROM my_contacts WHERE id=2;

SELECT * FROM my_contacts WHERE id=2 LOCK IN SHARE MODE;  加共享锁成功

 

 

SELECT * FROM my_contacts WHERE id=2 LOCK IN SHARE MODE;

加共享锁成功

INSERT  INTO my_contacts(NAME,gender,birthday,phone,information) VALUES ('ZhouLiang','F','1990-01-01','13645685552','');

插入数据(没有被锁住)成功

UPDATE my_contacts SET  gender='M' WHERE id = 2;

更行数据(被锁住),等待

 

 

UPDATE my_contacts SET  gender='M' WHERE id = 2;

更新数据(被锁住),造成死锁,执行失败,释放锁

获得锁后更行成功

 

 

 

 

InnoDB排它锁的例子

 

Session1

Session2

SET autocommit = 0;

SELECT * FROM my_contacts WHERE id=2;

SET autocommit = 0;

SELECT * FROM my_contacts WHERE id=2;

SELECT * FROM my_contacts WHERE id=2 FOR UPDATE;

 申请排它锁成功

 

 

SELECT * FROM my_contacts WHERE id=2;查询成功;可以查询,更新、加锁会阻塞

SELECT * FROM my_contacts WHERE id=2 LOCK IN SHARE MODE; 加共享锁,阻塞… …

UPDATE my_contacts SET  gender='M' WHERE id = 2;

更新操作,成功

 COMMIT; 释放锁

 

 

阻塞解除,添加共享锁成功

 

 

 

3、InnoDB行级锁争用情况

 

mysql> show status like 'innodb_row_lock%';

 

如果发现锁争用比较严重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高,还可以通过设置InnoDB Monitors来进一步观察发生锁冲突的表、数据行等,并分析锁争用的原因。

 

mysql> CREATE TABLE innodb_monitor(a INT) ENGINE=INNODB;

 

mysql> Show innodb status\G;

 

停止监视器:mysql> DROP TABLE innodb_monitor;

 

在SHOW INNODB STATUS的显示内容中,会有详细的当前锁等待的信息,包括表名、锁类型、锁定记录的情况等,便于进行进一步的分析。长时间打开打开监视器会导致.err文件变得非常大,所以确认问题原因后,要记得删除监控表以关闭监视器。

 

 

 

4、InnoDB行级锁的实现方式

 

InnoDB行锁是通过给索引项加锁来实现的,这一点与Oracle不同,Oracle是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。

 

 

 

Session1

Session2

CREATE TABLE tab_no_index(id INT,NAME VARCHAR(10)) ENGINE=INNODB;

INSERT INTO tab_no_index VALUES(1,'1'),(2,'2'),(3,'3'),(4,'4');

创建无索引表

SET autocommit=0;

SELECT * FROM tab_no_index WHERE id = 1 ;

SET autocommit=0;

SELECT * FROM tab_no_index WHERE id = 2;

SELECT * FROM tab_no_index WHERE id = 1 FOR UPDATE;

对id=1设置行级锁,由于无索引,导致实际上设置了表级锁

 

 

SELECT * FROM tab_no_index WHERE id = 2 FOR UPDATE;

被阻塞了… … 

 

 

 

Session1

Session2

CREATE TABLE tab_with_index(id INT,NAME VARCHAR(10)) ENGINE=INNODB;

ALTER TABLE tab_with_index ADD INDEX id(id);

INSERT INTO tab_with_index VALUES(1,'1'),(2,'2'),(3,'3'),(4,'4');

创建有索引表

SET autocommit=0;

SELECT * FROM tab_with_index WHERE id = 1 ;

SET autocommit=0;

SELECT * FROM tab_with_index WHERE id = 2;

SELECT * FROM tab_with_index WHERE id = 1 FOR UPDATE;

对id=1设置行级锁,设置成功

 

 

SELECT * FROM tab_with_index WHERE id = 2 FOR UPDATE;

对id=2申请行级锁成功

 

 

 

由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。

 

Session1

Session2

insert into tab_with_index  values(1,'4');

set  autocommit=0;

set  autocommit=0;

select * from tab_with_index where id = 1 and name = '1' for update;

申请行级锁成功,id有索引,name没有索引

 

 

SELECT * FROM tab_with_index WHERE id = 1 AND NAME = '4' FOR UPDATE;

尽管查询的行记录与session1中的不同,但是使用了相同的索引,所以需要等待,阻塞… …

 

 

 

当表有多个索引(无论是主键索引、唯一索引或普通索引)的时候,不同的事务可以使用不同的索引锁定不同的行。

 

Session1

Session2

alter table tab_with_index add index name(name);

id和name均有索引

set  autocommit=0;

set  autocommit=0;

select * from tab_with_index where id = 1 for update;

申请行级锁成功,(1,’1’),(1,’4’)被锁定,即id=1,name=’1’,name=’4’被锁定

 

 

select * from tab_with_index where name = '2' for update;

name=’2’没有被锁定,申请排它锁成功

 

SELECT * FROM tab_with_index WHERE id=4 AND NAME = '4' FOR UPDATE;

尽管查询的记录中与session1的记录无交集,但是name=’4’被锁定了,阻塞… …

 

是否使用索引来检索数据是由MySQL通过判断不同执行计划的代价来决定的,如果MySQL认为全表扫描效率更高,比如对一些很小的表,它就不会使用索引,这种情况下InnoDB将使用表锁,而不是行锁。因此,在分析锁冲突时,别忘了通过用explain检查SQL的执行计划,以确认是否真正使用了索引。

 

ALTER TABLE tab_no_index ADD INDEX NAME(NAME);

 

EXPLAIN SELECT * FROM tab_no_index WHERE NAME = 1;  #有索引,但是没有使用

 

 

EXPLAIN SELECT * FROM tab_with_index WHERE NAME = '1'; #使用了索引

 

 

 

 

5、间隙锁(Next-Key

 

当使用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB不仅会给符合条件的记录的索引项加锁,而且还会对键值在条件范围内但并不存在的记录加锁,这种锁机制叫间隙锁(Next-Key锁)。例如,Select * from my_contacts where id > 5 for update;会将id>5的所有记录(即使不存在)加锁。

 

InnoDB使用间隙锁的目的,一是为了防止幻读,以满足相关隔离级别的要求,如果上面的select语句不使用间隙锁,如果其他事务插入id大于5的记录,那么本事务如果再次执行上述语句,就会发生幻读;另一方面是为了满足其恢复和复制的需要。

 

显然,使用范围条件检索并锁定记录时,间隙锁会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。因此,在实际应用开发中,尽量避免使用范围条件.

 

 

 

Session1

Session2

select @@tx_isolation; 隔离级别为REPEATABLE-READ

set autocommit = 0;

select @@tx_isolation; 隔离级别为REPEATABLE-READ

set autocommit = 0;

select * from temp where id = 6 for update;

当前没有id=6的记录,但是该不存在的记录仍然被锁住

 

 

insert into temp(id,...) values(6,...);

阻塞… …

rollback;

 

 

Session1回退后释放了Next-Key锁,当前session获得锁并成功插入记录

 

 

 

恢复和复制对InnoDB锁机制的影响MySQL

 

通过BINLOG录执行成功的INSERT、UPDATE、DELETE等更新数据的SQL语句,并由此实现MySQL数据库的恢复和主从复制。MySQL的恢复是SQL语句级的,也就是重新执行BINLOG中的SQL语句。MySQL的Binlog是按照事务提交的先后顺序记录的,恢复也是按这个顺序进行的。由此可知,MySQL的恢复机制要求:在一个事务未提交前,其他并发事务不能插入满足其锁定条件的任何记录(不允许出现幻读)。这也是许多情况下,无论在Read Commited或是Repeatable Read隔离级别下,InnoDB都要使用间隙锁。

 

另外,对于“insert  into target_tab select * from source_tab where ...”和“create  table new_tab ...select ... From  source_tab where ...(CTAS)”这种SQL语句,用户并没有对source_tab做任何更新操作,但MySQL对这种SQL语句做了特别处理。

 

 

 

Session1

Session2

mysql> set autocommit = 0;

mysql> select * from target_tab;

 mysql> select * from source_tab where name = '1';

mysql> set autocommit = 0;

mysql> select * from target_tab;

mysql> select * from source_tab where name = '1';

insert into target_tab select d1,name from source_tab where name = '1';

 

 

update source_tab set name = '1' where name = '8';

等待… …  因为source_table被session1锁住了

commit;

 

 

commit;

 

 

 

这里InnoDB给source_tab加了共享锁,为什么要这么做呢?其原因是为了保证恢复和复制的正确性。因为不加锁的话,如果在上述语句执行过程中,其他事务对source_tab做了更新操作,就可能导致数据恢复的结果错误。

 

为了演示这一点,重复一下前面的例子,不同的是在session1执行事务前,先将系统变量 innodb_locks_unsafe_for_binlog的值设置为“on”(其默认值为off),这时执行update source_tab set name = '1' where name = '8'时不会被锁住。最后执行的结果是:source_tab的内容update了,target_tab插入update前的内容,(也就是所期望的先执行session1的insert,后执行session2的update)。分析binlog发现update语句在insert前执行,如果按照binlog恢复数据,那么恢复的数据与实际应用不符。

 

这也就是为什么没有使用MySQL的多版本数据一致性读技术,而是用共享锁锁住source_tab。

 

附注:一致性读是相对于脏读而言的,如果查询一个有10000条记录的表T需要10min,在9:00读表T,9:10返回结果,但是9:05的时候另外一个事物删除了表T的一条记录,那么当前事物的查询结果是10000条记录还是9999条记录呢?如果是9999条,则发生了脏读,如果是10000条,则发生了一致性读。

 

INSERT...SELECT... CREATE TABLE...SELECT...语句,可能会阻止对源表的并发更新,造成对源表锁的等待。如果查询比较复杂的话,会造成严重的性能问题,在应用中应尽量避免使用。如果必须要实现这样的逻辑,要么牺牲binlog的恢复数据,要么组合间接实现该逻辑。

 

 

 

InnoDB存储引擎中不同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

 

 

 

 

对于许多SQL,隔离级别越高,InnoDB给记录集加的锁就越严格(尤其是使用范围条件的时候),产生锁冲突的可能性也就越高,从而对并发性事务处理性能的影响也就越大。因此,应该尽量使用较低的隔离级别,以减少锁争用的机率。实际上,通过优化事务逻辑,大部分应用使用Read Commited隔离级别就足够了。对于一些确实需要更高隔离级别的事务,可以通过在程序中执行SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ或SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE动态改变隔离级别的方式满足需求。

 

 

 

6、什么时候使用表锁

 

对于InnoDB表,在绝大部分情况下都应该使用行级锁,因为事务和行锁往往是我们之所以选择InnoDB表的理由。但在个别特殊事务中,也可以考虑使用表级锁。

 

1、事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。

 

2、事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。

 

当然,应用中这两种事务不能太多,否则,就应该考虑使用MyISAM表了。

 

在InnoDB下,使用表锁要注意以下两点。

 

1、使用LOCK TABLES虽然可以给InnoDB加表级锁,但必须说明的是,表锁不是由InnoDB存储引擎层管理的,而是由其上一层──MySQL Server负责的。仅当autocommit=0、innodb_table_locks=1(默认设置)时,InnoDB才能自动识别涉及表级锁的死锁;。

 

2、用 LOCK TABLES对InnoDB表加锁时,要将AUTOCOMMIT设为0,否则MySQL不会给表加锁; COMMIT或ROLLBACK并不能释放用LOCK TABLES加的表级锁,必须用UNLOCK TABLES释放表锁。正确的方式见如下语句:

 

SET AUTOCOMMIT=0;

 

LOCK TABLES t1 WRITE, t2 READ, ...;

 

[do something with tables t1 and t2 here];

 

COMMIT;

 

UNLOCK TABLES;

 

 

 

7、关于死锁

 

MyISAM总是一次获得所需的全部锁,要么全部满足,要么等待,因此不会出现死锁。

 

InnoDB中,除单个SQL组成的事务外,锁是逐步获得的,了在InnoDB中发生死锁是可能的。

 

 

 

Session1

Session2

mysql> set autocommit = 0;

mysql> select * from table_1 where where id=1 for update;

mysql> set autocommit = 0;

mysql> select * from table_2 where id=1 for update;

select * from table_2 where id =1 for update;

因Session2已取得排他锁,等待

 

 

mysql> select * from table_1 where where id=1 for update;

死锁

 

 

 

在上面的例子中,两个事务都需要获得对方持有的排他锁才能继续完成事务,这种循环锁等待就是典型的死锁。发生死锁后,InnoDB一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务。但在涉及外部锁或表锁的情况下,InnoDB并不能完全自动检测到死锁,这需要设置锁等待超时参数 innodb_lock_wait_timeout来解决。但是如果有大量事物,仍然会造成挂死,拖累性能。

 

通常来说,死锁都是应用设计的问题,通过调整业务流程、数据库对象设计、事务大小,以及访问数据库的SQL语句,绝大部分死锁都可以避免。

 

1、如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。

 

2、批量方式处理数据时,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能。

 

 

 

session1

session2

mysql> set autocommit=0;

mysql> set autocommit=0;

mysql> select first_name,last_name from actor where actor_id = 1 for update;

 

 

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 = 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

等待解除

 

 

 

 

3、在事务中,如果要更新记录,应该直接申请足够级别的锁,即排他锁,而不应先申请共享锁,更新时再申请排他锁。

 

4、在REPEATABLE-READ隔离级别下,如果两个线程同时对相同条件记录用SELECT...FOR UPDATE加排他锁,在没有符合该条件记录情况下,两个线程都会加锁成功。程序发现记录尚不存在,就试图插入一条新记录,如果两个线程都这么做,就会出现死锁。这种情况下,将隔离级别改成READ COMMITTED,就可避免问题。

 

 

 

session1

session2

mysql> select @@tx_isolation;

| REPEATABLE-READ |

mysql> set autocommit = 0;

mysql> select @@tx_isolation;

| REPEATABLE-READ |

mysql> set autocommit = 0;

对不存在的记录加排它锁成功

mysql> select actor_id,first_name,last_name from actor where actor_id = 201 for update;

 

 

对不存在的记录加排它锁成功

mysql> select actor_id,first_name,last_name from actor where actor_id = 201 for update;

因为其他session也对该记录加了锁,所以当前的插入会等待:

mysql> insert into actor (actor_id , first_name , last_name) values(201,'Lisa','Tom');

 

 

 

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

插入成功

 

 

 

 

5、当隔离级别为READ COMMITTED时,如果两个线程都先执行SELECT...FOR UPDATE,判断是否存在符合条件的记录,如果没有,就插入记录。此时,只有一个线程能插入成功,另一个线程会出现锁等待,当第1个线程提交后,第2个线程会因主键重出错,但虽然这个线程出错了,却会获得一个排他锁!这时如果有第3个线程又来申请排他锁,也会出现死锁。

 

对于这种情况,可以直接做插入操作,然后再捕获主键重异常,或者在遇到主键重错误时,总是执行ROLLBACK释放获得的排他锁

 

 

 

Session1

Session2

Session3

mysql> select @@tx_isolation;

| READ-COMMITTED |

mysql> set autocommit=0;

mysql> select @@tx_isolation;

| READ-COMMITTED |

mysql> set autocommit=0;

mysql> select @@tx_isolation;

| READ-COMMITTED |

mysql> set autocommit=0;

mysql> select actor_id, first_name,last_name from actor where actor_id = 201 for update;

空记录

mysql> select actor_id, first_name,last_name from actor where actor_id = 201 for update;

空记录

 

mysql> insert into actor (actor_id,first_name,last_name) values(201,'Lisa','Tom');

成功插入

 

 

 

mysql> insert into actor (actor_id,first_name,last_name) values(201,'Lisa','Tom');

等待锁

 

mysql> commit;

 

 

 

Session2获得锁,发现插入记录主键重,这个时候抛出了异常,但是并没有释放锁:

mysql> insert into actor (actor_id,first_name,last_name) values(201,'Lisa','Tom');

ERROR 1062 (23000): Duplicate entry '201' for key 'PRIMARY'

 

 

 

Session3申请获锁,因为session_2已经锁定该记录,所以session_3需要等待:

mysql> select actor_id, first_name,last_name from actor where actor_id = 201 for update;

等待

 

这个时候,如果Session2直接对记录进行更新操作,则会抛出死锁的异常:

mysql> update actor set last_name='Lan' where actor_id = 201;

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

 

 

 

Session2释放锁后,Session3获得锁:

mysql> select first_name, last_name from actor where actor_id = 201 for update;

 

 

 

尽管通过上面介绍的设计和SQL优化等措施,可以大大减少死锁,但死锁很难完全避免。因此,在程序设计中总是捕获并处理死锁异常是一个很好的编程习惯。

 

如果出现死锁,可以用SHOW INNODB STATUS命令来确定最后一个死锁产生的原因。返回结果中包括死锁相关事务的详细信息,如引发死锁的SQL语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等。据此可以分析死锁产生的原因和改进措施。

 

 

 

相关标签: 事物