吃透MySQL(八):锁机制详细介绍

一,MySQL锁的基本介绍

锁是用于管理不同事务对共享资源的并发访问,保证数据的完整性和一致性。

在数据库中,锁保护的对象是事务,锁持续的时间是整个事务过程,只有在整个事务 commit 之后,锁才会释放,也就是说如果当前事务执行时间很长,锁将会被一直持有。

相对其他数据库而言,MySQL的锁机制比较简单,其最 显著的特点是不同的存储引擎支持不同的锁机制。比如,MyISAM和MEMORY存储引擎采用的是表级锁(table-level locking);InnoDB存储引擎既支持行级锁(row-level locking),也支持表级锁,但默认情况下是采用行级锁。

  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
  • 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

从上述特点可见,很难笼统地说哪种锁更好,只能就具体应用的特点来说哪种锁更合适!仅从锁的角度 来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有 并发查询的应用,如一些在线事务处理(OLTP)系统。

MyISAM只支持表级锁(table-level locking),InnoDB既支持表级锁,也支持行级锁(row-level locking)。

二,表锁

1,分类

MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)表独占写锁(Table Write Lock)

  • 表共享读锁(Table Read Lock):不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求。
  • 表独占写锁(Table Write Lock):会阻塞其他用户对同一表的读和写操作。

2,使用

  • 隐式加锁:在执行查询语句(select)之前,会自动给涉及的所有表加读锁,在执行更新操作(insert,update,delete)前,会自动给涉及的表加写锁,操作完成自动释放,这个过程并不需要用户干预。

  • 显示加锁(手动):lock table tableName read(读锁),lock table tableName write(写锁)。

    unlock table tableName(单表解锁),unlock tables(所有表解锁)。

3,表锁使用示例

由于MyISAM存储引擎默认使用表锁,所以我们这里使用MyISAM存储引擎来演示,当然用InnoDB是一样的。

数据准备:

mysql> create table mylock(
    -> id int(11) not null auto_increment,
    -> name varchar(20) default null,
    -> primary key (id)
    -> ) engine=MyISAM default charset=utf8;
Query OK, 0 rows affected, 2 warnings (0.12 sec)

mysql> insert into mylock(id,name) values(1,'a'),(2,'b'),(3,'c'),(4,'d');
Query OK, 4 rows affected (0.05 sec)
Records: 4  Duplicates: 0  Warnings: 0

mysql> select * from mylock;
+----+------+
| id | name |
+----+------+
|  1 | a    |
|  2 | b    |
|  3 | c    |
|  4 | d    |
+----+------+
4 rows in set (0.00 sec)

3.1,读阻塞写案例

Time session1 session2
T1 mysql> lock table mylock read;
Query OK, 0 rows affected (0.00 sec)
T2 当前session可以获取到该表记录
select * from mylock
当前session可以获取到该表记录
select * from mylock
T3 当前session不能查询未锁定的表
select * from user;
Table ‘user’ was not locked with LOCK TABLES
当前session可以查询或更新未锁定的表
select * from user;
insert into user(id,name,age) values(5,‘aa’,‘yes’);
T4 当前session插入或者更新表会提示错误
insert into mylock(id,name) values(5,‘e’);
Table ‘mylock’ was locked with a READ lock and can’t be updated
当前session插入或者更新表会被阻塞
insert into mylock(id,name) values(5,‘2’);
T5 释放锁
unlock tables;
获得锁,更新成功
Query OK, 1 row affected (4 min 5.12 sec)

结论:当获取当前表共享读锁时,不允许自己访问其它表,不允许自己对本表写操作,别人对本表写操作将会被阻塞,只允许自己或别人读对当前表读操作

3.2,写阻塞读案例

Time session1 session2
T1 获取表的write锁
lock table mylock write;
T2 当前session对表的查询,插入,更新操作都可以执行
select * from mylock;
insert into mylock(id,name) values(6,‘f’);
当前session对表的查询,插入,更新操作都会被阻塞
select * from mylock;
insert into mylock(id,name) values(7,‘g’);
T3 释放锁
unlock tables;
当前session立即执行,并返回结果

总结:当获取当前表排他写锁时,不允许自己访问其它表,只允许自己对本表读写操作,别人对本表读写操作将会被阻塞

4,意向共享锁(Intention Shared Locks)& 意向排它锁(Intention Exclusive Locks)

意向共享锁(Intention Shared Locks)和 意向排它锁(Intention Exclusive Locks)都是属于表锁。

  • 意向共享锁(IS):表示事务准备给数据行加入共享锁,即一个数据行加共享锁前必须先取得该表的 IS 锁,意向共享锁之间是可以相互兼容的。
  • 意向排它锁(IX):表示事务准备给数据行加入排他锁,即一个数据行加排他锁前必须先取得该表的 IX 锁,意向排它锁之间是可以相互兼容的。

加意向锁的目的是:在给行加锁的时候,如果当前行加了其它锁的时候,我需要读取到某一行进行具体的判断,而加意向锁的目的是简化了此判断,如果表中有其它锁的话就直接给拒绝掉了,就不需要再判断行里面的锁了,减少了一次判断,判断的粒度变大了,提高了判断效率。

意向锁(IS 、IX)是 InnoDB 数据操作之前自动加的,不需要用户干预。

三,行锁

InnoDb默认使用的行锁。

1,分类

共享锁:又称为读锁,简称 S 锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

排他锁:又称为写锁,简称 X 锁,排他锁不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的锁(共享锁、排他锁),只有该获取了排他锁的事务是可以对数据行进行读取和修改。(其他事务要读取数据可来自于快照)

2,行锁加锁过程

在这里插入图片描述

假如此时有一个事务 tx1 需要对记录 A 加 X 锁,它的执行执行过程如下所示:

  1. 在该记录所在的数据库上加一把意向锁 IX
  2. 在该记录所在的表上加一把意向锁 IX
  3. 在该记录所在的页上加一把意向锁 IX
  4. 最后在该记录 A 上加上一把 X 锁

假如此时有一个事物 tx2 需要对记录 B(假设和记录 A 在同一个页中)加 S 锁,它的执行执行过程如下所示:

  1. 在该记录所在的数据库上加一把意向锁 IS
  2. 在该记录所在的表上加一把意向锁 IS
  3. 在该记录所在的页上加一把意向锁 IS
  4. 最后在该记录 B 上加上一把 S 锁

一般数据库中的加锁操作是从上往下,逐层进行加锁的,它不是只对某条记录进行加锁。

3,使用

  • 隐式加锁:默认,自动加锁自动释放,select不会自动加锁,insert,update,delete自动加排他锁。

  • 显示加锁(手动):select * from user lock in share mode(共享锁),select * from user for update(排他锁)

    释放锁:commit/rollback事物。

所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select …from…查询数据,因为普通查询没有任何锁机制。

4,行锁到底锁了什么

InnoDB 的行锁是通过给索引上的索引项加锁来实现的。

只有通过索引条件进行数据检索,InnoDB 才使用行级锁,否则,InnoDB 将使用表锁(锁住索引的所有记录)

通过普通索引进行数据检索,会在 普通字段索引上面加一把行锁,同时会在该唯一索引对应的主键索引上面也会加上一把行锁,总共会加两把行锁。

5,行锁使用案例

5.1,在不通过索引条件查询的时候,innodb使用的是表锁而不是行锁

mysql> create table mylock( id int, name varchar(10)) engine=innodb;
Query OK, 0 rows affected (0.33 sec)

mysql> insert into mylock(id,name) values(1,'bobo'),(2,'zhangsan'),(3,'lisi');
Query OK, 3 rows affected (0.08 sec)
Records: 3  Duplicates: 0  Warnings: 0
Time session1 session2
T1 设置不自动提交事物
set autocommit=0;
可以查询到数据
select * from mylock where id=1;
设置不自动提交事物
set autocommit=0;
可以查询到数据
select * from mylock where id=2;
T2 当前会话查询id=1的数据开启排他锁
select * from mylock where id=1 for update;
T3 当前会话查询id=2的数据开启排他锁
可以发现当前会话处于阻塞状态
select * from mylock where id=2 for update;
T4 当前会话提交,释放排他锁
commit;
当前会话获取到排他锁,查询到结果

总结:session1只给一行加了排他锁,但是session2在请求其他行的排他锁的时候,会出现锁等待。原因是在没有索引的情况下,innodb只能使用表锁。

5.2,创建带索引的表进行条件查询,innodb使用的是行锁

给上面表id列创建索引:

mysql> create index idx1 on mylock(id);
Query OK, 0 rows affected (2 min 14.11 sec)
Time session1 session2
T1 设置不自动提交事物
set autocommit=0;
可以查询到数据
select * from mylock where id=1;
设置不自动提交事物
set autocommit=0;
可以查询到数据
select * from mylock where id=2;
T2 当前会话查询id=1的数据开启排他锁
select * from mylock where id=1 for update;
T3 当前会话查询id=2的数据开启排他锁
可以发现当前会话可以查询到数据
select * from mylock where id=2 for update;

总结:session1对索引id=1进行查询并开启排他锁,session2对索引id=2进行查询并开启排他锁,仍然可以查询成功,证明,针对索引进行查询并加锁,innodb使用的是行锁。

5.3,由于mysql的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是依然无法访问到具体的数据

添加id=1的另外一条数据

mysql> insert into mylock values(1,'xiaowang');
Query OK, 1 row affected (0.00 sec)

mysql> select * from mylock;
+------+----------+
| id   | name     |
+------+----------+
|    1 | bobo     |
|    2 | zhangsan |
|    3 | lisi     |
|    1 | xiaowang |
+------+----------+
4 rows in set (0.00 sec)
Time session1 session2
T1 set autocommit=0 set autocommit=0
T2 select * from mylock where id=1 and name=‘bobo’ for update;
T3 select * from mylock where id=1 and name=‘xiaowang’ for update;
虽然session2访问的是和session1不同的记录,但是因为使用了相同的索引,所以需要等待锁

总结:虽然session2访问的是和session1不同的记录,但是因为使用了相同的索引,所以需要等待锁

四,行锁实现算法

我们在手动开启独占锁(悲观锁/写锁)的时候,mysql会根据影响的数据范围,开启不同的锁的算法,由此形成了锁算法的分类,大致分为三类:记录锁(Record Locks)、间隙锁(Gap Locks)和临键锁(Next-key Locks)。

  • 记录锁(Record Locks):单个记录上的锁,锁总会锁住索引记录,锁住的是key。如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB会使用隐式的主键进行锁定。

  • 间隙锁(Gap Locks):间隙锁,锁定一个范围,但不包含记录本身,锁定索引记录间隙,确保索引记录的间隙不变,间隙锁是针对事务隔离级别为可重复读或以上级别而配的。Gap Lock在InnoDB的唯一作用就是防止其他事务的插入操作,以此防止幻读。

  • 临键锁(Next-key Locks):锁定一个范围,并且锁定记录本身,在Next-Key Lock 算法下,InnoDB对于行的查询都是采用这种锁定的算法。可以有效的防止幻读。当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。当查询的索引为辅助索引时,默认使用Next-Key Locking技术进行加锁,锁定范围是前一个索引到后一个索引之间范围。

数据准备:

mysql> create table t(
    -> id int(11) not null,
    -> name varchar(10) default null,
    -> primary key(id)
    -> )engine=innodb default charset=utf8;
Query OK, 0 rows affected, 2 warnings (0.29 sec)

mysql> insert into t(id,name) values(1,'1'),(4,'4'),(7,'7'),(10,'10');
Query OK, 4 rows affected (0.07 sec)
Records: 4  Duplicates: 0  Warnings: 0

mysql> select * from t;
+----+------+
| id | name |
+----+------+
|  1 | 1    |
|  4 | 4    |
|  7 | 7    |
| 10 | 10   |
+----+------+
4 rows in set (0.00 sec)

1,记录锁(Record Locks)

当 SQL 执行按照唯一性(Primary key、Unique key)索引进行数据的检索时,查询条件等值匹配且查询的数据是存在,这时 SQL 语句加上的锁即为记录锁 Record Locks,锁住具体的索引项

演示案例:

在这里插入图片描述

Time session1 session2
T1 设置事物不自动提交
set autocommit=0;
设置事物不自动提交
set autocommit=0;
T2 查询id=4的数据,并加排他锁
select * from t where id=4 for update;
此时使用的是记录锁,锁住的是主键4
T3 查询id=7的数据,并加排他锁,可以查询成功
select * from t where id=7 for update;
当查询id=4的数据并加排他锁时发现被阻塞住了,因为session1已经把id=4的索引锁住了
select * from t where id=4 for update;
T4 提交事物
commit;
T5 获得查询结果

总结:当session1执行select * from t where id=4 for update只会把id=4的数据行锁住。

2,间隙锁(Gap Locks)

当 SQL 执行按照索引进行数据的检索时,查询条件的数据不存在,这时 SQL 语句加上的锁即为 Gap locks,锁住数据不存在的区间(左开右开)

Gap 只在 REPEATABLE-READ 事务隔离级别存在。因为幻读问题是在 REPEATABLE-READ 事务通过临键锁和 MVCC 解决的,而临键锁=间隙锁+记录锁,所以间隙锁只在REPEATABLE-READ 事务隔离级别存在。

示例演示:

在这里插入图片描述

1,session1和session2都设置事物不主动提交

# session1
set autocommit=0;
# session2
set autocommit=0;

2,session1查询id>4 and id<6并加排他锁

# session1
mysql> select * from t where id>4 and id<6 for update;
Empty set (0.00 sec)

3,session2插入id=3和id=6的数据

mysql> insert into t(id,name) values(3,'3');
Query OK, 1 row affected (0.00 sec)

mysql> insert into t(id,name) values(6,'6');

总结:session1中 select * from t where id>4 and id<6 for update;,发现没有命中,会锁住(4,7)区间,如果session2对此区间(4,7)进行操作将会被阻塞,不会限制对其它区间操作

3,临键锁(Next-key Locks)

当 SQL 执行按照索引进行数据的检索时,查询条件为范围查找(between and、<、>等)并有数据命中,则此时 SQL 语句加上的锁为 Next-key locks,锁住索引的记录 + 区间(左开右闭)

临键锁(Next-key Locks):InnoDB 默认的行锁算法。

为什么 InnoDB 选择临键锁作为行锁的默认算法?

防止幻读。当我们把下一个区间也锁住的时候,这个时候我们要新增数据,就会被锁住,这样就可以防止幻读。

在这里插入图片描述

1,session1和session2都设置事物不主动提交

# session1
set autocommit=0;
# session2
set autocommit=0;

2,session1查询id>5 and id<9并加排他锁

mysql> select * from t where id>5 and id<9 for update;
+----+------+
| id | name |
+----+------+
|  7 | 7    |
+----+------+
1 row in set (0.00 sec)

3,session2对id=1和id=4和id=7加排他锁

mysql> select * from t where id=1 for update;
+----+------+
| id | name |
+----+------+
|  1 | 1    |
+----+------+
1 row in set (0.00 sec)

mysql> select * from t where id=4 for update;
+----+------+
| id | name |
+----+------+
|  4 | 4    |
+----+------+
1 row in set (0.00 sec)

mysql> select * from t where id=7 for update;

总结:session1中 select * from t where id>5 and id<9 for update;这条查询语句命中了7这条数据,它会锁住 (4, 7] 这个区间,同时还会锁住下一个区间 (7, 10]。

五,死锁

1,死锁介绍

A事物获取到了锁1,等着获取锁2,B事物获取到了锁2等着获取锁1!这样就造成了死锁!

死锁演示

数据准备:

mysql> select * from t;
+----+------+
| id | name |
+----+------+
|  1 | 1    |
|  3 | 3    |
|  4 | 4    |
|  7 | 7    |
| 10 | 10   |
+----+------+
5 rows in set (0.00 sec)

1,session1获取id=1行锁

# session1
mysql> select * from t where id=1 for update;
+----+------+
| id | name |
+----+------+
|  1 | 1    |
+----+------+
1 row in set (0.00 sec)

2,session2获取id=3行锁

# session2
mysql> select * from t where id=3 for update;
+----+------+
| id | name |
+----+------+
|  3 | 3    |
+----+------+
1 row in set (0.00 sec)

3,session1获取id=3的行锁,阻塞在这里

mysql> select * from t where id=3 for update;

4,session2获取id=1的行锁,结果死锁

mysql> select * from t where id=1 for update;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

2,如何尽量避免死锁

  • 以固定的顺序访问表和行。

  • 大事务拆小。大事务更倾向于死锁,如果业务允许,将大事务拆小。

  • 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率。

  • 降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。

  • 为表添加合理的索引。可以看到如果不走索引将会为表的每一行记录添加上锁,死锁的概率大大增大。

猜你喜欢

转载自blog.csdn.net/u013277209/article/details/113676006
今日推荐