在上次MySQL事务详解(二):隔离级别的实现--MVCC的学习中,我们认识到了并发事务访问相同的数据时,
在读—写
情况下带来的脏读、不可重复读问题
,可以通过MySQL的MVCC
机制解决。
而对于读—写
情况下的幻读问题和写—写
情况下的脏写问题,我们提到可以通过MySQL的锁机制
解决。
锁的概念
锁的作用于现实生活中的锁的作用类似,都是控制对资源的访问。
而MySQL的锁更侧重于对于资源的并发访问,来保证访问数据的完整性于一致性
。
锁,其实就是与访问数据关联的一种数据结构。如下图:
当访问(多为修改)一条数据时,首先查询是否有与之关联的数据结构,没有就生成一个,并占有(即加锁成功),如果没有也生成一个并等待(加锁失败)。
当占有锁的事务释放锁之后,等待的事务就会被唤醒执行。
锁的分类
锁的提出是为了解决并发访问带来的问题,但是如果一味的对访问的数据进行加锁,将访问串行化,使其排队等待执行,同时又会降低并发量,降低性能。
因此对锁进行了不同级别的分类,来应对不同的操作场景,如下图:
操作类型
读锁(S锁、共享锁):读取数据用到的锁。给数据加S锁后,其他事务依然可以加S锁(即共享)
,但是不可以加X锁。
写锁(X锁、排他锁):更改数据用到的锁。给数据加X锁后,其他事务不可以加S锁和X锁(即排他)
。
态度
乐观锁:假设事务之间很少发生冲突,不主动对数据进行加锁
。通常使用版本号
和时间戳
在事务提交时检测是否冲突。
悲观锁:与乐观锁相反
,假设事务之间经常发生冲突,主动给数据加锁,获取事务所需资源。
粒度
全局锁:对整个数据库实例进行加锁。Flush tables with read lock
,常用于对整个库进行逻辑备份。全局锁对数据库影响较大,锁主库会导致不能更新,业务暂停,锁从库导致主从延迟。
表级锁:对整张表进行锁定,开销小,粒度大,发生冲突的概率高,并发低,可以避免死锁
。
行级锁:对某一行记录进行锁定,开销大,粒度小,发生冲突概率低,并发高,不能避免死锁
。
页级锁:对数据库的最小存储单元——页进行锁定,粒度介于表和行之间。应用在BDB存储引擎中,了解即可。
表级锁
表级别的S锁、X锁和元数据锁
# 上锁
lock table tableName read; # 读锁/共享锁
lock table tableName write;# 写锁/排他锁
#解锁
unlock tables; # 客户端断开的时候也会自动释放锁。
使用上面的语言可以为表加上S锁和X锁,以及解锁。
但是如果仅仅是对表进行select、insert、update、delete操作,存储引擎是不会默认为表加上表级锁
。
而实现这些操作与其他事务中的DDL语句冲突的是表级锁种的元数据锁(MDL)
。
元数据锁是系统自动控制的,每一条DML、DDL语句执行时都会申请MDL。
DML申请元数据读锁,DDL申请元数据写锁
。且DML与DML之间并不相互阻塞,因此DML可以并发执行。
所以表级别的S锁、X锁很少用到。
表级别的意向锁
意向读锁(IS):当我们想向表中的记录加上S锁时,首先给表加上IS锁。
意向写锁(IX):当我们想向表中的记录加上X锁时,首先给表加上IX锁。
IS和IX作用是为了后续给表加上S锁和X锁做的准备工作
,避免加上表级别S锁和X锁时,需要遍历所有的数据来判断是否有记录被锁上。
表级别自增锁
自增锁主要是用来给表中声明为AUTO_INCREMENT
的字段,在插入记录是递增而用。
- 在我们
不知道要插入的数据量时
,例如使用 insert...select;replace...select或者load data时,通常先给表加上一个自增锁,然后生成对应的值,插入完成之后,删除自增锁; - 在我们
知道具体的插入数据量时
,使用轻量级锁,未插入语句生成所需的值后即可释放锁,提高性能。
行级锁
行级锁才是使用最频繁的锁,在开发中遇到的死锁问题一般都是行级锁引起的。
根据不同的情况, 行级锁被分为不同的类型,解决不同的问题,幻读就是其中之一。
行级S锁、X锁
为我们要访问的某一条记录加上读锁或者写锁。S锁与X锁的关系之前已经学习过。
间隙锁
用来解决幻读的锁
。因为幻读是在其他事务插入数据后产生,当前事务在第一次select时,数据还不存在。如果使用行级的X锁,找不到上锁的对象。因此提出间隙锁,如下图:
给no=7的数据加上gap锁后,表明在这条数据之前(no为(3,7)间)不能插入新的数据。以此来解决幻读。
临键锁
当我们想锁住访问数据本身以及这条数据之前的间隙时,使用临建锁。即行级SX锁与gap锁的结合
。如下图:
插入意向锁
当我们想插入一条数据时,首先要判断插入的位置是有存在gap锁,如果存在就先生成一个插入意向锁,向系统表明有事务在等在插入当前位置。
两种数据读
快照读
MVCC使用的就是快照读,在读取事务是好像拍了一张照片,当前事务读取的是照片快照下的数据。
普通的select 语句都是快照读,不会给记录有加锁操作。
当前读
读取到的数据为最新的的数据。
select ..lock in share mode;
对记录加上S锁;
selelct ... for update;
对记录加上X锁;
以上两种select语句给记录加上了锁,并且读取到数据是最新的数据。