目录
MySQL的锁机制
1、锁的分类
1.1、共享锁
1.2、排他锁
1.3、意向共享锁和意向排他锁
1.4、自增锁
2、MyISAM表锁
MyISAM写锁阻塞读的案例:
MyISAM读阻塞写的案例:
MyISAM的并发插入问题
3、InnoDB表锁
1、事务及其ACID属性
2、并发事务带来的问题
3、InnoDB的行锁模式及加锁方法
锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中,除传统的 计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一 个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。
相对其他数据库而言,MySQL的锁机制比较简单,其最显著的特点是不同的存储引擎支持不同的锁机制。比如,MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
从上述特点可见,很难笼统地说哪种锁更好,只能就具体应用的特点来说哪种锁更合适!仅从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。
共享锁 Shared Locks(简称S锁,属于行锁)
排他锁 Exclusive Locks(简称X锁,属于行锁)
意向共享锁 Intention Shared Locks(简称IS锁,属于表锁)
意向排他锁 Intention Exclusive Locks(简称IX锁,属于表锁)
自增锁 AUTO-INC Locks
共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据库,但是只能读不能修改;
create table student(id int,name varchar(10)) engine=innodb;
insert into student values(1,'1'),(2,'2'),(3,'3'),(4,'4');
事务A:
set autocommit = 0;
select * from student where id=1 lock in share mode;
事务B:
set autocommit = 0;
select * from student where id=1;(读取数据没问题)
update student set name='hh' where id=1;(无法修改,当事务A提交之后,会立刻修改成功)
排他锁不能与其他锁共存,如一个事务获取了一个数据行的排他锁,其他事务就不能获取该行的锁,只有当前获取了排他锁的事务可以对数据进行读取和修改。
delete、update、insert默认是排他锁。
事务A:
set autocommit = 0;
select * from student where id=1 for update;
事务B:
set autocommit = 0;
select * from student where id=1 for update;(会卡死,当事务A提交之后,会立刻成功)
select * from student where id=1 lock in share mode;(会卡死,当事务A提交之后,会立刻成功)
意向共享锁:表示事务准备给数据行加入共享锁,也就是说一个数据行在加共享锁之前必须先取得该表的IS锁
意向排他锁:表示事务准备给数据行加入排他锁,也就是说一个数据行在加排他锁之前必须先取得该表的IX锁
意向锁是InnoDB数据操作之前自动加的,不需要用户干预。
针对自增列自增长的一个特殊的表级别锁
mysql> show variables like 'innodb_autoinc_lock_mode';
+--------------------------+-------+
| Variable_name | Value |
+--------------------------+-------+
| innodb_autoinc_lock_mode | 2 |
+--------------------------+-------+
1 row in set, 1 warning (0.00 sec)
--1代表连续,事务未提交则id永久丢失
--2代表interleaved,插入
MySQL文档
MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。
对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;对 MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;MyISAM表的读操作与写操作之间,以及写操作之间是串行的!
CREATE TABLE `mylock` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`NAME` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `mylock` (`id`, `NAME`) VALUES ('1', 'a');
INSERT INTO `mylock` (`id`, `NAME`) VALUES ('2', 'b');
INSERT INTO `mylock` (`id`, `NAME`) VALUES ('3', 'c');
INSERT INTO `mylock` (`id`, `NAME`) VALUES ('4', 'd');
当一个线程获得对一个表的写锁之后,只有持有锁的线程可以对表进行更新操作。其他线程的读写操作都会等待,直到锁释放为止。
开启2个链接DB会话,进行如下顺序操作。
session1 | session2 |
1、获取表的write锁定 lock table mylock write; |
|
2、当前session对表的查询,插入,更新操作都可以执行 select * from mylock; insert into mylock values(5,'e'); |
3、当前session对表的查询会被阻塞 select * from mylock; |
4、释放锁: unlock tables; |
5、当前session能够立刻执行,并返回对应结果 |
--session1
--1、session1
mysql> lock table mylock write;
Query OK, 0 rows affected (0.00 sec)
--2、session1
mysql> select * from mylock;
+----+------+
| id | NAME |
+----+------+
| 1 | a |
| 2 | b |
| 3 | c |
| 4 | d |
+----+------+
4 rows in set (0.00 sec)
mysql> insert into mylock values(5,'e');
Query OK, 1 row affected (0.01 sec)
--session2
--3、session2
mysql> select * from mylock;
--4、session1
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
--5、session2
mysql> select * from mylock;
+----+------+
| id | NAME |
+----+------+
| 1 | a |
| 2 | b |
| 3 | c |
| 4 | d |
| 5 | e |
+----+------+
5 rows in set (7.79 sec)
一个session使用lock table给表加读锁,这个session可以锁定表中的记录,但更新和访问其他表都会提示错误,同时,另一个session可以查询表中的记录,但更新就会出现锁等待。
session1 | session2 |
1、获得表的read锁定 lock table mylock read; |
|
2、当前session可以查询该表记录: select * from mylock; |
3、当前session可以查询该表记录: select * from mylock; |
4、当前session不能查询没有锁定的表 select * from dept; ERROR 1100 (HY000): Table 'dept' was not locked with LOCK TABLES |
5、当前session可以查询或者更新未锁定的表 select * from mylock; insert into dept values(50,'X50','Y50'); |
6、当前session插入或者更新表会提示错误 insert into mylock values(6,'f'); ERROR 1099 (HY000): Table 'mylock' was locked with a READ lock and can't be updated update mylock set name='aa' where id = 1; ERROR 1099 (HY000): Table 'mylock' was locked with a READ lock and can't be updated |
7、当前session插入数据会等待获得锁 insert into mylock values(6,'f'); |
8、释放锁 unlock tables; |
9、获得锁,更新成功 |
--1.session1
mysql> lock table mylock read;
Query OK, 0 rows affected (0.00 sec)
--2、session1
mysql> select * from mylock;
+----+------+
| id | NAME |
+----+------+
| 1 | a |
| 2 | b |
| 3 | c |
| 4 | d |
| 5 | e |
+----+------+
5 rows in set (0.00 sec)
--3、session2
mysql> select * from mylock;
+----+------+
| id | NAME |
+----+------+
| 1 | a |
| 2 | b |
| 3 | c |
| 4 | d |
| 5 | e |
+----+------+
5 rows in set (0.00 sec)
--4、session1
mysql> select * from dept;
ERROR 1100 (HY000): Table 'dept' was not locked with LOCK TABLES
--5、session2
mysql> select * from mylock;
+----+------+
| id | NAME |
+----+------+
| 1 | a |
| 2 | b |
| 3 | c |
| 4 | d |
| 5 | e |
+----+------+
5 rows in set (0.00 sec)
mysql> insert into dept values(50,'X50','Y50');
Query OK, 1 row affected (0.01 sec)
--6、session1
mysql> insert into mylock values(6,'f');
ERROR 1099 (HY000): Table 'mylock' was locked with a READ lock and can't be updated
mysql> update mylock set name='aa' where id = 1;
ERROR 1099 (HY000): Table 'mylock' was locked with a READ lock and can't be updated
--7、session2 等待锁
mysql> insert into mylock values(6,'f');
--8、session1
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
9、session2
mysql> insert into mylock values(6,'f');
Query OK, 1 row affected (28.08 sec)
注意:
MyISAM在执行查询语句之前,会自动给涉及的所有表加读锁,在执行更新操作前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此用户一般不需要使用命令来显式加锁,上例中的加锁时为了演示效果。
MyISAM表的读和写是串行的,这是就总体而言的,在一定条件下,MyISAM也支持查询和插入操作的并发执行
session1 | session2 |
1、获取表的read local锁定 lock table mylock read local; |
|
2、当前session不能对表进行更新或者插入操作 insert into mylock values(6,'f'); ERROR 1099 (HY000): Table 'mylock' was locked with a READ lock and can't be updated update mylock set name='aa' where id = 1; ERROR 1099 (HY000): Table 'mylock' was locked with a READ lock and can't be updated |
3、其他session可以查询该表的记录 select * from mylock; |
4、当前session不能查询没有锁定的表 select * from dept; ERROR 1100 (HY000): Table 'dept' was not locked with LOCK TABLES |
5、其他session可以进行插入操作,但是更新会阻塞 insert into mylock values(7,'g'); |
6、当前session不能访问其他session插入的记录; select* from mylock; |
|
7、释放锁资源:unlock tables; | 8、当前session获取锁,更新操作完成 |
9、当前session可以查看其他session插入的记录 select* from mylock; |
--1、session1
mysql> lock table mylock read local;
Query OK, 0 rows affected (0.00 sec)
--2、session1
mysql> insert into mylock values(6,'f');
ERROR 1099 (HY000): Table 'mylock' was locked with a READ lock and can't be updated
mysql> update mylock set name='aa' where id = 1;
ERROR 1099 (HY000): Table 'mylock' was locked with a READ lock and can't be updated
--3、session2
mysql> select* from mylock;
+----+------+
| id | NAME |
+----+------+
| 1 | a |
| 2 | b |
| 3 | c |
| 4 | d |
| 5 | e |
| 6 | f |
+----+------+
6 rows in set (0.00 sec)
--4、session1
mysql> select * from dept;
ERROR 1100 (HY000): Table 'dept' was not locked with LOCK TABLES
--5、session2
mysql> insert into mylock values(7,'g');
Query OK, 1 row affected (0.01 sec)
mysql> update mylock set name = 'aa' where id = 1;
--6、session1
mysql> select* from mylock;
+----+------+
| id | NAME |
+----+------+
| 1 | a |
| 2 | b |
| 3 | c |
| 4 | d |
| 5 | e |
| 6 | f |
+----+------+
6 rows in set (0.00 sec)
--7、session1
mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)
--8、session2
mysql> update mylock set name = 'aa' where id = 1;
Query OK, 1 row affected (30.79 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--9、session1
mysql> select* from mylock;
+----+------+
| id | NAME |
+----+------+
| 1 | aa |
| 2 | b |
| 3 | c |
| 4 | d |
| 5 | e |
| 6 | f |
| 7 | g |
+----+------+
7 rows in set (0.00 sec)
可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定争夺:
mysql> show status like 'table_locks%';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Table_locks_immediate | 24 |
| Table_locks_waited | 2 |
+-----------------------+-------+
2 rows in set (0.00 sec)
--如果Table_locks_waited的值比较高,则说明存在着较严重的表级锁争用情况。
事务是由一组SQL语句组成的逻辑处理单元,事务具有4属性,通常称为事务的ACID属性。
相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持更多用户的并发操作,但与此同时,会带来一下问题:
脏读: 一个事务正在对一条记录做修改,在这个事务并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”的数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做“脏读”
不可重复读:一个事务在读取某些数据已经发生了改变、或某些记录已经被删除了!这种现象叫做“不可重复读”。
幻读: 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”
上述出现的问题都是数据库读一致性的问题,可以通过事务的隔离机制来进行保证。
数据库的事务隔离越严格,并发副作用就越小,但付出的代价也就越大,因为事务隔离本质上就是使事务在一定程度上串行化,需要根据具体的业务需求来决定使用哪种隔离级别
脏读 | 不可重复读 | 幻读 | |
read uncommitted | √ | √ | √ |
read committed | √ | √ | |
repeatable read | √ | ||
serializable |
可以通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况:
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.00 sec)
--如果发现锁争用比较严重,如InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高
共享锁(s):又称读锁。允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
排他锁(x):又称写锁。允许获取排他锁的事务更新数据,阻止其他事务取得相同的数据集共享读锁和排他写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。
mysql InnoDB引擎默认的修改数据语句:update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select …for update语句,加共享锁可以使用select … lock in share mode语句。所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。
InnoDB行锁实现方式
InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
1、在不通过索引条件查询的时候,innodb使用的是表锁而不是行锁
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');
session1 | session2 |
1、set autocommit=0; select * from tab_no_index where id = 1; |
2、set autocommit=0; select * from tab_no_index where id =2; |
3、select * from tab_no_index where id = 1 for update; | |
4、select * from tab_no_index where id = 2 for update; 锁等待 | |
5、commit; | 6、锁等待结束 |
--查看是否是自动提交 1表示开启,0表示关闭
mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
| 1 |
+--------------+
1 row in set (0.00 sec)
--设置关闭
mysql> set autocommit = 0;
Query OK, 0 rows affected (0.00 sec)
--1、session1
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)
--2、session2
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)
--3、session1
mysql> select * from tab_no_index where id = 1 for update;
+------+------+
| id | name |
+------+------+
| 1 | 1 |
+------+------+
1 row in set (0.00 sec)
--4、session2 锁等待,时间长超时
mysql> select * from tab_no_index where id = 2 for update;
--5、session1
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
--6、session2
mysql> select * from tab_no_index where id = 2 for update;
+------+------+
| id | name |
+------+------+
| 2 | 2 |
+------+------+
1 row in set (8.00 sec)
session1只给一行加了排他锁,但是session2在请求其他行的排他锁的时候,会出现锁等待。原因是在没有索引的情况下,innodb只能使用表锁。
2、创建带索引的表进行条件查询,innodb使用的是行锁
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');
session1 | session2 |
1、set autocommit=0; select * from tab_with_index where id = 1; |
2、set autocommit=0; select * from tab_with_index where id =2; |
3、select * from tab_with_index where id = 1 for update; | |
4、select * from tab_with_index where id = 2 for update; |
--1、session1
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)
--2、session2
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)
--3、session1
mysql> select * from tab_with_index where id = 1 for update;
+------+------+
| id | name |
+------+------+
| 1 | 1 |
+------+------+
1 row in set (0.00 sec)
--4、session2
mysql> select * from tab_with_index where id = 2 for update;
+------+------+
| id | name |
+------+------+
| 2 | 2 |
+------+------+
1 row in set (0.00 sec)
3、由于mysql的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,
但是依然无法访问到具体的数据
insert into tab_with_index values(1,'4');
session1 | session2 |
1、set autocommit=0; | 2、set autocommit=0; |
3、select * from tab_with_index where id = 1 and name='1' for update; | |
4、select * from tab_with_index where id = 1 and name='4' for update; 虽然session2访问的是和session1不同的记录,但是因为使用了相同的索引,所以需要等待锁 |
|
5、commit; | 6、锁释放 |
--1、session1
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
--2、session2
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
--3、session1
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)
--4、session2 锁等待
mysql> select * from tab_with_index where id = 1 and name='4' for update;
--5、session1
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
--6、session2
mysql> select * from tab_with_index where id = 1 and name='4' for update;
+------+------+
| id | name |
+------+------+
| 1 | 4 |
+------+------+
1 row in set (13.71 sec)
死锁
session1 | session2 |
1、set autocommit=0; select * from tab_with_index where id = 1 for update; |
2、set autocommit=0; select * from tab_with_index where id = 2 for update; |
3、select * from tab_with_index where id = 2 for update; 锁等待 |
4、select * from tab_with_index where id = 1 for update; 死锁,退出 ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction |
5、第4步死锁退出,运行结束 |
--1、session1
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.01 sec)
--2、session2
mysql> set autocommit=0;
Query OK, 0 rows affected (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)
--3、session1
mysql> select * from tab_with_index where id = 2 for update;
--4、session2
mysql> select * from tab_with_index where id = 1 for update;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
--5、session1
mysql> select * from tab_with_index where id = 2 for update;
+------+------+
| id | name |
+------+------+
| 2 | 2 |
+------+------+
1 row in set (7.36 sec)
总结
对于MyISAM的表锁,主要讨论了以下几点:
(1)共享读锁(S)之间是兼容的,但共享读锁(S)与排他写锁(X)之间,以及排他写锁(X)之间是互斥的,也就是说读和写是串行的。
(2)在一定条件下,MyISAM允许查询和插入并发执行,我们可以利用这一点来解决应用中对同一表查询和插入的锁争用问题。
(3)MyISAM默认的锁调度机制是写优先,这并不一定适合所有应用,用户可以通过设置LOW_PRIORITY_UPDATES参数,或在INSERT、UPDATE、DELETE语句中指定LOW_PRIORITY选项来调节读写锁的争用。
(4)由于表锁的锁定粒度大,读写之间又是串行的,因此,如果更新操作较多,MyISAM表可能会出现严重的锁等待,可以考虑采用InnoDB表来减少锁冲突。
对于InnoDB表,本文主要讨论了以下几项内容:
(1)InnoDB的行锁是基于索引实现的,如果不通过索引访问数据,InnoDB会使用表锁。
(2)在不同的隔离级别下,InnoDB的锁机制和一致性读策略不同。
在了解InnoDB锁特性后,用户可以通过设计和SQL调整等措施减少锁冲突和死锁,包括:
MySQL Optimization Learning(一)
MySQL Optimization Learning(五)
不断学习才能不断提高!
生如蝼蚁,当立鸿鹄之志,命比纸薄,应有不屈之心。
乾坤未定,你我皆是黑马,若乾坤已定,谁敢说我不能逆转乾坤?
努力吧,机会永远是留给那些有准备的人,否则,机会来了,没有实力,只能眼睁睁地看着机会溜走。