ACID概念
在传统的E-R数据库事务中,我们经常提到ACID的概念,这里我们再简单的重温一下。
- A(原子性):在数据库事务操作中,该操作是一个整体,要么整体成功或者失败。
- C(一致性):一致性强调的是数据库中事务前后的数据是一致性,而事务中不做要求。
- I(隔离性):隔离性是指不同的事务不会相互影响各自的执行。
- D(持久性):事务操作一旦完成,则不会因为任何因素发生数据的丢失。
在ACID中,隔离性我们还可以进一步细分成不同的隔离级别,不同的隔离主要是数据库的隔离性与高性能中选均衡。
隔离级别 | 描述 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
读未提交 | 事务过程中可以读取其他事务未提交的数据 | ✔️ | ✔️ | ✔️ |
读已提交 | 事务过程中可以读取其他已经提交的数据 | ❌ | ✔️ | ✔️ |
可重复读 | 事务过程中多次读取相同的记录数据不变(Mysql默认级别) | ❌ | ❌ | ✔️ |
串行化 | 最高的事务级别,串行化读取 | ❌ | ❌ | ❌ |
而对于不同隔离实现的关键点,就引入了本文的主题:锁
Mysql innodb的锁机制与其他存储引擎相比,我认为其中最大的一个区别在与innodb实现了不同粒度的锁机制。
针对不同的粒度,Mysql实现了行锁和表锁。关于Mysql的行锁我们需要注意:行锁是基于索引实现的,如果查询记录中无法使用索引,则默认是表锁。
提到行锁和表锁之前,我们先普及一个基本概念:共享锁和排他锁。
共享锁和排他锁
- 共享锁(S)
共享锁允许多个事务同时加锁,又称为读锁
- 排他锁(X):
排他锁不允许多个事务同时加锁,又称为写锁
下面我们在描述一下共享锁和排他锁的兼容情况。
S锁 | X锁 | |
---|---|---|
S锁 | 兼容 | 不兼容 |
X锁 | 不兼容 | 不兼容 |
共享锁和排他锁是一个非常基本的概念,该概念适用于行锁和表锁。
针对表锁和行锁的不同粒度,我们考虑下面的问题:何时我们可以加表锁?如何判断表中是否有行锁?
为了解决这个问题,innodb引入了意向锁:意向共享锁和意向排他锁
意向锁
首先意向锁属于表锁,意向锁是当我们在对数据库记录进行加行锁的时候会同步给数据库表所加的锁。意向锁也可细分为共享意向锁和排他意向锁。
下面我们看一下共享意向锁、排他意向锁、共享锁和排他锁的兼容性。
IS锁 | IX锁 | S锁 | X锁 | |
---|---|---|---|---|
IS锁 | 兼容 | 兼容 | 兼容 | 不兼容 |
IX锁 | 兼容 | 兼容 | 不兼容 | 不兼容 |
S锁 | 兼容 | 不兼容 | 兼容 | 不兼容 |
X锁 | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
从上图可知,意向锁之间完全兼容,因为只有意向锁的时候真正的锁冲突需要延迟到行锁的维度上进行判断和检查。
一致性非锁定读
从锁之间的兼容可知,一旦记录或者表上加了X锁,会造成我们无法去读取数据库的数据,造成数据库读取性能的下降。
为了提高读取的性能,Innodb一般默认的读取采用的一致性非锁定读。
一致性非锁定读指InnoDB存储引擎通过多版本控制(MVCC)的方式来读取当前执行时间数据库中行的数据,如果读取的行正在执行DELETE或UPDATE操作,读取操作不会等待行上锁的释放。相反的,InnoDB会去读取行的一个快照数据 。
每一个数据行可能会存在多个版本,Innodb的MVCC在不同的隔离级别对数据的快照存在不同:
- 已提交读:事务能读取到的数据永远是最新的数据快照,所以存在不可重复读的问题。
- 可重复读:事务能读取到的数据永远是事务开始时候的版本,故而解决了不可重复读的问题,但存在幻读的问题。
一致性锁定读
一致性非锁定读的反面就是锁定读,锁定读会对记录加S锁,它带来的最大的好处是可以锁定读取的数据行,从而避免在读取之后🈶事务对其进行更新。
下面我们介绍一种非常典型的应用场景:
/*
* 第一步:从数据库中读取用户的好友计数
* 第二部:从改计数的值+count来更新好友计数
1. select follow from Music_UserInfo where userId=1 lock in share mode;
2. update Music_UserInfo set follow=a+b
延伸说明: lock in share mode与select for update的区别
lock in share mode: 对数据库行记录加S锁,容易造成死锁
select for update:对数据库行记录加X锁
锁的算法
下面我们会主要介绍行锁、间隙锁和Next-key锁,这些不同的锁算法,都是针对数据库的行记录的排他锁来说的,而表锁不在这里讨论。
行锁(Record Lock)
锁直接加在索引的记录上面,锁住的是索引Key。
间隙锁(Gap Lock)
锁定索引的一个间隙范围,确保这个间隙范围的一致性,只有在可重复读的隔离级别中有效。
Next-key Lock
行锁和间隙锁组合在一起,形成Next-key Lock。
假设有表如下,同时Innodb的隔离级别是可重复读。
create table Students(
sid int,
name varchar(256),
age int,
primary key(id),
key `idx_age`(`age`)) Engine=InnoDB DEFAULT CHARSET=UTF8;
假设存在如下的索引:
uk(sid) - 唯一的主键索引
index_age(age) - 非唯一索引
假设表存在如下的数据:
(1, "Lucy", 1)
(2, "Kate", 3)
(3, "John", 4)
(4, "Keith", 6)
(6, "Messy", 6)
(7, "Lili", 8)
(8, "Nancy", 13)
(9, "Smith", 15)
(10, "April", 16)
假设有如下SQL语句:
1. update Students set name='Lucy H' where id=1;(行级锁)
2. update Students set name='Kate W' where age=5;(行级锁,间隙锁,Next Key Lock)
3. update Students set name="Macy" where age>7;(间隙锁)
问题1:如何保证不能插入age=6的记录?
控制age<6的记录、age>6的记录、age=6的记录的记录间不插入数据,这里引入的是间隙锁
问题2:如何理解间隙锁?
根据age字段,可以划分为如下的区间(-∞,1)(1,3)(3,4)(4,6)(6,8)(8,13)(13,15)(15,16)(16,+∞)
比如区间(6,8)的记录之间可以插入更多的记录,则认为存在间隙。
问题3:间隙锁解决了什么问题?
间隙锁解决了缓存问题,所以Mysql可以在可重复读的隔离级别下解决幻读问题。
问题4:间隙锁在什么条件下生效?
a. 必须是在可重复隔离级别下
b. 检索条件中必须包含有索引,这个与行锁的实现方式有关
实际案例:
session 1:
start transaction;
select * from Students where age=6 for update; //加next-key, 锁age=6的记录和(4,8)的区间
session 2:
start transaction;
insert into Students(14,'johnson',5); //阻塞
insert into Students(15,'johnson',4); //阻塞
insert into Students(5,'johnson',10); //成功
这里的案例还是比较简答的,这里我们再稍微扩展一下。
问题1:对于联合索引的情况,间隙应该怎么计算?
问题2:当查询出的记录是唯一索引时,是否还存在间隙锁?
死锁问题
在介绍mysql死锁之前,先来温习一下死锁产生是需要4个必要条件:
- 互斥条件
- 不可剥夺条件
- 请求和保持条件
- 循环等待条件
在mysql中,所谓的死锁我们可以简单的理解是多个事务在执行过程中,因争夺锁资源而造成的互相等待的情况。而mysql中解决死锁的常见方式有:
- 超时:超时需要其中的部分事务进行回滚,另外一部分事务则顺利执行。超时后mysql会按照FIFO的方式进行回滚,可能会导致大事务进行回滚,影响性能。
- 等待图:等待图可以进行死锁的检测,死锁检测是一种主动的发现死锁的方式,当发现有死锁产生时,可以选择主动回滚,mysql一般会回滚undo最少的事务。