vlambda博客
学习文章列表

​MySQL完全笔记【数据库锁篇】


数据库锁(Lock)

数据库的锁机制是由存储引擎提供的,而每个存储引擎的实现策略不相同,这里重点对比下MyIsAM 和 InnoDB 存储引擎。

对比项 InnoDB MyIsAM
事务 支持 不支持
行锁、表锁 表锁
外键 支持 不支持


隐式和显式锁定

InnoDB采用两阶段锁定协议:隐式锁和显式锁。

隐式锁:

在事务执行过程中,随时都可以执行锁定,锁只有在commit 或 rollback 时才会释放,并且所有的锁是在同一时刻被释放。

这就是隐式锁,InnoDB 会根据隔离级别在需要的时候自动加锁。

显式锁:

  1. 同样,InnoDB 也支持通过特定语句进行显式锁定。(这些语句不属于SQL规范)

SELECT ... LOCK IN SHARE MODE;      -- 显式上共享锁
SELECT ... FOR UPDATE;                     -- 显式上排他锁
  1. MySQL也支持 LOCK TABLESUNLOCK TABLES 语句,这是在服务层实现的,和存储引擎无关。它们有自己的用途,但并不能代替事务处理。


注:

上面显式的通过SQL上共享锁和排他锁,锁住的资源是整张表还是某几行,得视情况分析。

LOCK TABLESUNLOCK 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会根据以下两个条件检查每行记录:

  1. InnoDB只查找创建版本号 小于等于 当前事务版本号的数据行。

    这样可以确保事务读取到的行,要么是在事务开始之前就存在的,要么是事务自身插入或修改过的。

  2. 行的删除版本要么未定义,要么 大于 当前事务版本号。

    这样可以确保事务读取到的行,要么没被删除,要么是在当前事务开启之后被删除的。

只有符合上述两个条件的记录,才能返回作为查询结果。


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:

mysql> begin;
mysql> update student_signset is_sign = 1where id >=4 and id <=8;Query OK, 4 rows affected (0.00 sec)

事务A在没有提交的情况下,开启事务B:(模拟并发)

mysql> 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:

mysql> 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:

mysql> begin;mysql> update student_signset is_sign = 1where age>18 and age < 60 ;Query OK, 1 row affected (0.00 sec)Rows matched: 7 Changed: 1 Warnings: 0mysql>

事务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" whereid = 2 and version = 10200;

版本号的维护:

每次新增数据,初始化一个版本号;每次修改数据,一并修改版本号(通常是递增版本号);版本号字段值不能重复,可以不设置索引。


死锁

    死锁是指两个或多个事务争夺同一个资源,对方都持有自己想要的资源而又都不放锁。

比如下面两个事务同时处理:

-- 事务1begin;update students set name = 'jack' where id=3;update students set name = 'brian' where id=4;commit;
-- 事务2begin;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 transactionmysql>

查看死锁日志

方式一:

mysql> show engine innodb status;

在打印出来的信息中找到 “LATEST DETECTED DEADLOCK” 内容,就可以分析产生死锁的原因了。

方式二:

使用 INFORMATION_SCHEMA 数据库中的信息表。

INNODB_LOCK_WAITS-- 锁等待INNODB_LOCKS-- 锁INNODB_TRX-- InnoDB事务




---END---