MySQL完全笔记【数据库锁篇】
数据库锁(Lock)
数据库的锁机制是由存储引擎提供的,而每个存储引擎的实现策略不相同,这里重点对比下MyIsAM 和 InnoDB 存储引擎。
对比项 | InnoDB | MyIsAM |
---|---|---|
事务 | 支持 | 不支持 |
锁 | 行锁、表锁 | 表锁 |
外键 | 支持 | 不支持 |
隐式和显式锁定
InnoDB采用两阶段锁定协议:隐式锁和显式锁。
隐式锁:
在事务执行过程中,随时都可以执行锁定,锁只有在commit 或 rollback 时才会释放,并且所有的锁是在同一时刻被释放。
这就是隐式锁,InnoDB 会根据隔离级别在需要的时候自动加锁。
显式锁:
同样,InnoDB 也支持通过特定语句进行显式锁定。(这些语句不属于SQL规范)
SELECT ... LOCK IN SHARE MODE; -- 显式上共享锁
SELECT ... FOR UPDATE; -- 显式上排他锁
MySQL也支持
LOCK TABLES
和UNLOCK TABLES
语句,这是在服务层实现的,和存储引擎无关。它们有自己的用途,但并不能代替事务处理。
注:
上面显式的通过SQL上共享锁和排他锁,锁住的资源是整张表还是某几行,得视情况分析。
LOCK TABLES
和 UNLOCK TABLES
常在数据库备份场景下使用,保证数据备份时的一致性。
LOCK TABLES
和事务锁之间互相影响的话,情况会变复杂,而且影响性能,所以一般情况下没必要使用该语句。
读写锁
在 Mysql 中实现了两种基本的锁:
共享锁:
又叫读锁(read lock),其他事务可以继续加共享锁,但是不能继续加排他锁。
排他锁:
又叫写锁(write lock),一旦加了写锁之后,其他事务就不能加任何锁了。
读锁和写锁的兼容关系:
读锁 | 写锁 | |
---|---|---|
读锁 | 兼容 | 冲突 |
写锁 | 冲突 | 冲突 |
锁兼容是指事务 A 获得锁之后,事务 B 也尝试获取某种锁,如果能立即获取,则称锁兼容,反之叫冲突。
纵轴是代表已有的锁,横轴是代表尝试获取的锁。
锁力度
加锁一定是有开销的,一般来说,加锁的粒度越小,并发度越高,开销也就越大。MySQL的InnoDB 存储引擎提供了两种锁策略:表锁和行锁。
表锁
表锁是Mysql中最基本的锁策略,是开销最小的锁策略:它会直接锁住整张表的数据。
场景:
当对一个表进行当前读操作(insert, delete, update, select...for update),并且没有使用索引时,事务操作期间就会锁住整张表,这会阻塞其他事务对该表的所有当前读操作,表锁的并发效率低。
注:写操作一定是当前读操作,当前读并不一定是写操作,比如 select...for update 就是当前读,但不是写操作。
行锁
行级锁可以最大程度的支持并发处理,同时也带来了较大的锁开销。
行锁是建立在使用索引的基础之上的。事务操作只有使用了索引,才有可能使用行锁。
MVCC
如果每次读或写都需要加相应的锁来保证数据的安全性,那么并发性能其实是不高的,InnoDB 通过MVCC 提高并发性能(这是重头戏)。
MVCC(Multi-Version Concurrency Control)多版本并发控制:
在InnoDB 表中,每一行记录的后面增加两个隐藏列,创建版本号和删除版本号。
每开始一个新的事务,系统版本号都会递增,事务开始时刻的系统版本号会作为事务的版本号。对于查询,事务版本号会和每一行的两个隐藏列作对比;对于增删改,事务版本号都会新增到相应的隐藏列中。
下面看看在 可重复读
隔离级别下,MVCC具体是如何操作的:
SELECT
InnoDB会根据以下两个条件检查每行记录:
InnoDB只查找创建版本号 小于等于 当前事务版本号的数据行。
这样可以确保事务读取到的行,要么是在事务开始之前就存在的,要么是事务自身插入或修改过的。
行的删除版本要么未定义,要么 大于 当前事务版本号。
这样可以确保事务读取到的行,要么没被删除,要么是在当前事务开启之后被删除的。
只有符合上述两个条件的记录,才能返回作为查询结果。
INSERT
InnoDB 为新插入的每一行保存当前系统版本号(事务版本号)作为创建版本号。
DELETE
InnoDB 为删除的每一行保存当前系统版本号(事务版本号)作为删除版本号。
UPDATE
InnoDB 插入一行新纪录,保存当前系统版本号(事务版本号)作为创建版本号;同时保存当前系统版本号(事务版本号)作为原来行的删除版本号。
保存这两个额外的系统版本号,使绝大多数的读操作都可以不用加共享锁。这样的设计使得读操作很简单,性能也很好,并且能够保证只会读取到符合标准的行。
不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作,以及一些额外的维护工作。
在 MVCC 中,分为两种读操作:
当前读:
需要加锁的语句,update,insert,delete,select...for update 等都是当前读。
快照读:
MVCC 实现了可重复读,在 可重复读
隔离级别下的快照读,事务不是以 begin
开始的时间点作为快照建立时间点,而是以第一条 select 语句的时间点作为快照的时间点,以后的 select 都会读取当前时间点的快照值,已达到可重复读的目的。
MVCC最大的好处:读不加锁,读写不冲突。读写不冲突极大的增加了系统的并发性能。
小结:
MVCC 只在 读已提交 和 可重复读 两个隔离级别下工作。其他两个隔离级别都和MVCC不兼容,因为读未提交总是读取最新的数据行,而不是符合当前事务版本的数据行;而串行化则会对所有读取的行都加锁。
间隙锁
MySQL默认的事务隔离级别是可重复读
,可重复读存在幻读的并发问题,MySQL使用间隙锁来防止幻读。
下面用例子来一睹间隙锁的风采:
(student_sign:学生签到表;id为主键,其他字段都没有索引)
mysql> select * from student_sign;
+----+---------+------+-----+---------+
| id | version | name | age | is_sign |
+----+---------+------+-----+---------+
| 1 | 1 | wyc | 25 | 1 |
| 2 | 2 | ybw | 45 | 0 |
| 3 | 3 | jjj | 42 | 1 |
| 4 | 3 | www | 56 | 1 |
| 5 | 3 | www | 22 | 1 |
| 7 | 7 | xxx | 33 | 1 |
| 8 | 4 | wws | 77 | 0 |
| 9 | 9 | rrr | 56 | 1 |
| 10 | 7 | ttt | 65 | 1 |
+----+---------+------+-----+---------+
场景一
事务A:
begin;
update student_sign
set is_sign = 1
where id >=4 and id <=8;
Query OK, 4 rows affected (0.00 sec)
事务A在没有提交的情况下,开启事务B:(模拟并发)
begin;
mysql> insert into student_sign (id,version,name,age) VALUES (6,3,'www',43);
-- 会一直阻塞该SQL的执行,直到锁等待超时;
mysql> update student_sign set age =88 where id =3;
-- 执行成功,不会阻塞;
mysql> update student_sign set age =88 where id =9;
-- 会一直阻塞该SQL的执行,直到锁等待超时;
mysql> update student_sign set age =88 where id =10;
-- 执行成功,不会阻塞;
结论:
where条件对索引字段进行范围写操作,范围[m,n], 间隙锁会锁定 [m, n+1] 范围内的连续数据(存在和不存在的)。
所以第1、3条SQL语句会一直阻塞,因为它们操作的数据在间隙锁范围之内。倘若第一条SQL执行成功,那么就发生了幻读。
第2、4条SQL语句可以执行成功,它们操作的数据不在间隙锁范围之内。在解决幻读的情况下,提高了并发效率。
场景二
事务A:
begin;
mysql> update student_sign set is_sign = 1;-- 没有where条件,进行全表更新
事务A在没有提交的情况下,开启事务B:
mysql> begin;
mysql> insert into student_sign (id,version,name,age) VALUES (16,3,'www',43);
-- 会一直阻塞该SQL的执行,直到锁等待超时;
1205 - Lock wait timeout exceeded; try restarting transaction
结论:
不带where条件的update 和 delete 语句,事务期间会锁整张表。其他事务对该表的所有写操作,都会被阻塞。
场景三
事务A:
begin;
update student_sign
set is_sign = 1
where age>18 and age < 60 ;
Query OK, 1 row affected (0.00 sec)
7 Changed: 1 Warnings: 0 :
mysql>
事务A在没有提交的情况下,开启事务B:
mysql> begin;
mysql> insert into student_sign
(id,version,name,age)
VALUES
(16,3,'www',43);-- 会一直阻塞该SQL的执行,直到锁等待超时;
1205 - Lock wait timeout exceeded; try restarting transaction
结论:
where条件对非索引字段进行范围写操作,事务期间会锁整张表。其他事务对该表的所有写操作,都会被阻塞。
乐观锁
乐观锁是一种思想,是一个策略,并不是一个真实存在的锁,乐观锁的概念是相对于悲观锁来说的。
MySQL的乐观锁可通过版本号实现,即每次更新数据通过主键和版本号联合更新:
update student set name = "Daming"
where
id = 2 and version = 10200;
版本号的维护:
每次新增数据,初始化一个版本号;每次修改数据,一并修改版本号(通常是递增版本号);版本号字段值不能重复,可以不设置索引。
死锁
死锁是指两个或多个事务争夺同一个资源,对方都持有自己想要的资源而又都不放锁。
比如下面两个事务同时处理:
-- 事务1
begin;
update students set name = 'jack' where id=3;
update students set name = 'brian' where id=4;
commit;
-- 事务2
begin;
update students set name = 'brian' where id=4;
update students set name = 'jack' where id=3;
commit;
在并发情况下,如果两个事务都同时执行完第一条update语句,持有了该行的排他锁,接着每个事物都去尝试执行第二条update 语句,却发现该行已经被其他事务锁定,于是两个事务都等待对方释放锁,同时都持有对方需要的锁,于是陷入死循环。
除非有外部因素介入才可能解除死锁。
处理死锁
InnoDB处理死锁的方式很简单:
将持有最少行级排他锁的事务进行回滚。
1213 - Deadlock found when trying to get lock; try restarting transaction
mysql>
查看死锁日志
方式一:
show engine innodb status;
在打印出来的信息中找到 “LATEST DETECTED DEADLOCK” 内容,就可以分析产生死锁的原因了。
方式二:
使用 INFORMATION_SCHEMA 数据库中的信息表。
INNODB_LOCK_WAITS-- 锁等待
INNODB_LOCKS-- 锁
INNODB_TRX-- InnoDB事务
---END---