跳至主要內容

Weiser大约 8 分钟

事务并发访问同一数据资源的情况主要就分为读-读写-写读-写三种。

  1. 读-读 即并发事务同时访问同一行数据记录。由于两个事务都进行只读操作,不会对记录造成任何影响,因此并发读完全允许。
  2. 写-写 即并发事务同时修改同一行数据记录。这种情况下可能导致脏写问题,这是任何情况下都不允许发生的,因此只能通过加锁实现,也就是当一个事务需要对某行记录进行修改时,首先会先给这条记录加锁,如果加锁成功则继续执行,否则就排队等待,事务执行完成或回滚会自动释放锁。
  3. 读-写 即一个事务进行读取操作,另一个进行写入操作。这种情况下可能会产生脏读不可重复读幻读。最好的方案是读操作利用多版本并发控制(MVCC),写操作进行加锁

按锁作用的数据范围进行分类的话,锁可以分为行级锁表级锁

  1. 行级锁:作用在数据行上,锁的粒度比较小。
  2. 表级锁:作用在整张数据表上,锁的粒度比较大。

锁的分类

为了实现读-读之间不受影响,并且写-写读-写之间能够相互阻塞,Mysql使用了读写锁的思路进行实现,具体来说就是分为了共享锁排它锁

  1. 共享锁(Shared Locks):简称S锁,在事务要读取一条记录时,需要先获取该记录的S锁S锁可以在同一时刻被多个事务同时持有。我们可以用select ...... lock in share mode;的方式手工加上一把S锁
  2. 排他锁(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修饰的列递增赋值的原理主要是两个:

  1. AUTO-INC锁:在执行插入语句的时先加上表级别的AUTO-INC锁,插入执行完成后立即释放锁。如果我们的插入语句在执行前无法确定具体要插入多少条记录,比如INSERT ... SELECT这种插入语句,一般采用AUTO-INC锁的方式
  2. 轻量级锁:在插入语句生成AUTO_INCREMENT值时先才获取这个轻量级锁,然后在AUTO_INCREMENT值生成之后就释放轻量级锁如果我们的插入语句在执行前就可以确定具体要插入多少条记录,那么一般采用轻量级锁的方式对AUTO_INCREMENT修饰的列进行赋值。这种方式可以避免锁定表,可以提升插入性能。

mysql默认根据实际场景自动选择加锁方式,当然也可以通过innodb_autoinc_lock_mode强制指定只使用其中一种。

InnoDB中的行级锁

前面说过,通过MVCC可以解决脏读不可重复读幻读这些读一致性问题,但实际上这只是解决了普通select语句的数据读取问题。事务利用MVCC进行的读取操作称之为快照读,所有普通的SELECT语句在READ COMMITTEDREPEATABLE READ隔离级别下都算是快照读。除了快照读之外,还有一种是锁定读,即在读取的时候给记录加锁,在锁定读的情况下依然要解决脏读不可重复读幻读的问题。由于都是在记录上加锁,这些锁都属于行级锁

InnoDB的行锁,是通过锁住索引来实现的,如果加锁查询的时候没有使用过索引,会将整个聚簇索引都锁住,相当于锁表了。根据锁定范围的不同,行锁可以使用记录锁(Record Locks)间隙锁(Gap Locks)临键锁(Next-Key Locks)的方式实现。假设现在有一张表t,主键是id。我们插入了4行数据,主键值分别是 1、4、7、10。接下来我们就以聚簇索引为例,具体介绍三种形式的行锁。

  • 记录锁(Record Locks) 所谓记录,就是指聚簇索引中真实存放的数据,比如上面的1、4、7、10都是记录。 Record 显然,记录锁就是直接锁定某行记录。当我们使用唯一性的索引(包括唯一索引和聚簇索引)进行等值查询且精准匹配到一条记录时,此时就会直接将这条记录锁定。例如select * from t where id =4 for update;就会将id=4的记录锁定。
  • 间隙锁(Gap Locks) 间隙指的是两个记录之间逻辑上尚未填入数据的部分,比如上述的(1,4)、(4,7)等。 Gap 同理,间隙锁就是锁定某些间隙区间的。当我们使用用等值查询或者范围查询,并且没有命中任何一个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]等。 Next-Key 临键锁就是记录锁(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)

两阶段锁协议规定所有的事务应遵守的规则:

  1. 在对任何数据进行读写操作之前,首先要申请并获得对该数据的封锁
  2. 在释放一个封锁之后,事务不再申请和获得其它任何封锁

即事务的执行分为两个阶段:

  1. 第一阶段是获取封锁的阶段,称为扩展阶段
  2. 第二阶段是释放封锁的阶段,称为收缩阶段

在InnoDB事务中,行锁在需要的时候才加上,但是并不是不需要了就立马释放,而是要等到事务结束才会释放

如果一个事务需要锁多个行,要把最可能造成锁冲突,最可能影响并发的锁尽量往后放