锁
锁
事务并发访问同一数据资源的情况主要就分为读-读
、写-写
和读-写
三种。
读-读
即并发事务同时访问同一行数据记录。由于两个事务都进行只读操作,不会对记录造成任何影响,因此并发读完全允许。写-写
即并发事务同时修改同一行数据记录。这种情况下可能导致脏写
问题,这是任何情况下都不允许发生的,因此只能通过加锁
实现,也就是当一个事务需要对某行记录进行修改时,首先会先给这条记录加锁,如果加锁成功则继续执行,否则就排队等待,事务执行完成或回滚会自动释放锁。读-写
即一个事务进行读取操作,另一个进行写入操作。这种情况下可能会产生脏读
、不可重复读
、幻读
。最好的方案是读操作利用多版本并发控制(MVCC
),写操作进行加锁。
按锁作用的数据范围进行分类的话,锁可以分为行级锁
和表级锁
。
行级锁
:作用在数据行上,锁的粒度比较小。表级锁
:作用在整张数据表上,锁的粒度比较大。
锁的分类
为了实现读-读
之间不受影响,并且写-写
、读-写
之间能够相互阻塞,Mysql
使用了读写锁
的思路进行实现,具体来说就是分为了共享锁
和排它锁
:
共享锁(Shared Locks)
:简称S锁
,在事务要读取一条记录时,需要先获取该记录的S锁
。S锁
可以在同一时刻被多个事务同时持有。我们可以用select ...... lock in share mode;
的方式手工加上一把S锁
。排他锁(Exclusive Locks)
:简称X锁
,在事务要改动一条记录时,需要先获取该记录的X锁
。X锁
在同一时刻最多只能被一个事务持有。X锁
的加锁方式有两种,第一种是自动加锁,在对数据进行增删改的时候,都会默认加上一个X锁
。还有一种是手工加锁,我们用一个FOR UPDATE
给一行数据加上一个X锁
。
还需要注意的一点是,如果一个事务已经持有了某行记录的S锁
,另一个事务是无法为这行记录加上X锁
的,反之亦然。
除了共享锁(Shared Locks)
和排他锁(Exclusive Locks)
,Mysql
还有意向锁(Intention Locks)
。意向锁是由数据库自己维护的,一般来说,当我们给一行数据加上共享锁之前,数据库会自动在这张表上面加一个意向共享锁(IS锁)
;当我们给一行数据加上排他锁之前,数据库会自动在这张表上面加一个意向排他锁(IX锁)
。意向锁
可以认为是S锁
和X锁
在数据表上的标识,通过意向锁可以快速判断表中是否有记录被上锁,从而避免通过遍历的方式来查看表中有没有记录被上锁,提升加锁效率。例如,我们要加表级别的X锁
,这时候数据表里面如果存在行级别的X锁
或者S锁
的,加锁就会失败,此时直接根据意向锁
就能知道这张表是否有行级别的X锁
或者S锁
。
InnoDB中的表级锁
InnoDB
中的表级锁主要包括表级别的意向共享锁(IS锁)
和意向排他锁(IX锁)
以及自增锁(AUTO-INC锁)
。其中IS锁
和IX锁
在前面已经介绍过了,这里不再赘述,我们接下来重点了解一下AUTO-INC锁
。
大家都知道,如果我们给某列字段加了AUTO_INCREMENT
自增属性,插入的时候不需要为该字段指定值,系统会自动保证递增。系统实现这种自动给AUTO_INCREMENT
修饰的列递增赋值的原理主要是两个:
AUTO-INC锁
:在执行插入语句的时先加上表级别的AUTO-INC锁
,插入执行完成后立即释放锁。如果我们的插入语句在执行前无法确定具体要插入多少条记录,比如INSERT ... SELECT
这种插入语句,一般采用AUTO-INC锁
的方式。轻量级锁
:在插入语句生成AUTO_INCREMENT
值时先才获取这个轻量级锁
,然后在AUTO_INCREMENT
值生成之后就释放轻量级锁
。如果我们的插入语句在执行前就可以确定具体要插入多少条记录,那么一般采用轻量级锁的方式对AUTO_INCREMENT修饰的列进行赋值。这种方式可以避免锁定表,可以提升插入性能。
mysql默认根据实际场景自动选择加锁方式,当然也可以通过
innodb_autoinc_lock_mode
强制指定只使用其中一种。
InnoDB中的行级锁
前面说过,通过MVCC
可以解决脏读
、不可重复读
、幻读
这些读一致性问题,但实际上这只是解决了普通select
语句的数据读取问题。事务利用MVCC
进行的读取操作称之为快照读
,所有普通的SELECT
语句在READ COMMITTED
、REPEATABLE READ
隔离级别下都算是快照读
。除了快照读
之外,还有一种是锁定读
,即在读取的时候给记录加锁,在锁定读
的情况下依然要解决脏读
、不可重复读
、幻读
的问题。由于都是在记录上加锁,这些锁都属于行级锁
。
InnoDB
的行锁,是通过锁住索引来实现的,如果加锁查询的时候没有使用过索引,会将整个聚簇索引都锁住,相当于锁表了。根据锁定范围的不同,行锁可以使用记录锁(Record Locks)
、间隙锁(Gap Locks)
和临键锁(Next-Key Locks)
的方式实现。假设现在有一张表t
,主键是id
。我们插入了4行数据,主键值分别是 1、4、7、10。接下来我们就以聚簇索引为例,具体介绍三种形式的行锁。
- 记录锁(Record Locks) 所谓记录,就是指聚簇索引中真实存放的数据,比如上面的1、4、7、10都是记录。
显然,记录锁就是直接锁定某行记录。当我们使用唯一性的索引(包括唯一索引和聚簇索引)进行等值查询且精准匹配到一条记录时,此时就会直接将这条记录锁定。例如
select * from t where id =4 for update;
就会将id=4
的记录锁定。 - 间隙锁(Gap Locks) 间隙指的是两个记录之间逻辑上尚未填入数据的部分,比如上述的(1,4)、(4,7)等。
同理,间隙锁就是锁定某些间隙区间的。当我们使用用等值查询或者范围查询,并且没有命中任何一个
record
,此时就会将对应的间隙区间锁定。例如select * from t where id =3 for update;
或者select * from t where id > 1 and id < 4 for update;
就会将(1,4)区间锁定。 - 临键锁(Next-Key Locks) 临键指的是间隙加上它右边的记录组成的左开右闭区间。比如上述的(1,4]、(4,7]等。
临键锁就是记录锁(Record Locks)和间隙锁(Gap Locks)的结合,即除了锁住记录本身,还要再锁住索引之间的间隙。当我们使用范围查询,并且命中了部分
record
记录,此时锁住的就是临键区间。注意,临键锁锁住的区间会包含最后一个record的右边的临键区间。例如select * from t where id > 5 and id <= 7 for update;
会锁住(4,7]、(7,+∞)。mysql默认行锁类型就是临键锁(Next-Key Locks)
。当使用唯一性索引,等值查询匹配到一条记录的时候,临键锁(Next-Key Locks)会退化成记录锁;没有匹配到任何记录的时候,退化成间隙锁。
间隙锁(Gap Locks)
和临键锁(Next-Key Locks)
都是用来解决幻读问题的,在已提交读(READ COMMITTED)
隔离级别下,间隙锁(Gap Locks)
和临键锁(Next-Key Locks)
都会失效!
两阶段锁协议(Two-Pahse Locking -- 2PL)
两阶段锁协议规定所有的事务应遵守的规则:
- 在对任何数据进行读写操作之前,首先要申请并获得对该数据的封锁
- 在释放一个封锁之后,事务不再申请和获得其它任何封锁
即事务的执行分为两个阶段:
- 第一阶段是获取封锁的阶段,称为扩展阶段
- 第二阶段是释放封锁的阶段,称为收缩阶段
在InnoDB事务中,行锁在需要的时候才加上,但是并不是不需要了就立马释放,而是要等到事务结束才会释放
如果一个事务需要锁多个行,要把最可能造成锁冲突,最可能影响并发的锁尽量往后放