并发事务访问相同记录的情况大致可以划分为3种:
- 读-读
并发事务读取相同的事务记录,读取操作本身不会对记录有任何影响,不会引起什么问题,所以读读不需要特别对待。 - 写-写
并发事务对相同的记录进行改动。 - 读-写 或者 写-读
一个事务进行读操作,另一个事务进行写操作。
写-写
在写-写情况会发生脏写的现象,任何一种隔离级别都不允许这种现象发生。所以在多个未提交事务相继对一条记录进行改动时,需要让它们排队执行。这个排队执行的过程就是通过为该记录加锁来实现的。
当一个事务相对这条记录进行改动时,首先会查看内存中有没有与这条记录相关联的锁结构。如果没有,就会在内存种生成一个锁结构与之关联。比如,事务T1需要对这条记录进行改动,就需要生成一个锁结构与之关联。
image.png锁结构中有很多信息,这里只列出2个比较重要的属性:
- trx 信息:表示这个锁结构是与哪个事务相关联。
- is_waiting:表示当前事务是否在等待。
在事务提交之前,另一个事务T2也相对该记录进行修改,那么T2先去看看有没有锁结构与之相关联:
image.png事务T1提交之后,就会把它生成的锁结构释放掉,然后检测以下还有没有与该记录关联的锁结构,发现了事务T2还在等待锁,于是把事务T2的锁结构is_wait
属性设置为fasle,然后把该事务对应的线程唤醒,让T2继续执行。
读-写 或 写-读
在读-写或者写-读的情况下,会出现脏写,不可重复读,幻读的现象。而怎么避免脏读,不可重复读,幻读这些现象有两种解决方案:
-
读操作使用多版本并发控制(MVCC),写操作进行加锁
-
读,写操作都采用加锁方式
脏读现象产生是因为当前事务读取了另一个未提交事务的一条记录,如果另一个事务在写记录的时候就给这条记录加锁,那么当前事务就无法在读取该记录时再获取到锁了,所以也就不会有脏读。不可重复读现象产生的原因是当前事务先读取一条记录,另外一个事务对该记录进行了改动,如果在当前事务读取记录时就给该记录加锁,那么另一个事务就无法修改该记录,所以也就不会出现不可重复读了。
幻读现象产生是因为某个事务读取了符合某些搜索条件的记录,之后别的事务又插入了符合相同搜索条件的新记录,导致该事务再次读取相同搜索条件的记录时,可以读到别的事务插入的新记录,这些新插入的记录就是幻影记录。简单采用加锁的方式避免幻读现象是不行的,因为在第一次读取记录时那些幻影记录并不存在,该给谁加锁呢?但是在InnodDB中还是通过另外的措施解决了幻读的问题。
上面两种方案中,如果采用了MVCC的方式,读-写操作彼此并不冲突,性能更高;如果采用加锁的方式,读-写操作需要排队执行,从而影响性能。一般情况下,我们愿意采用MVCC的方式,但是在某些特殊的业务场景中,要求必须采用加锁的方式来执行。
一致性无锁读
事务利用MVCC进行读取操作称为一致性读,或者一致性无锁读(也有称为快照读),所有普通的SELECT 语句在 READ COMMITED 和 REPEATABLE READ 隔离级别使用的都是MVCC。比如:
SELECT * FROM t;
SELECT * FROM t1 INNER JOIN t2 ON t1.col1 = t2.col2
锁定读
当我们采用锁来解决问题时,为了在 读-写 或者 写-读 优化锁的效率,我们又将锁进一步细分:
- 共享锁(shared Lock)
简称 S 锁,在事务读取一条记录时,需要先获取该记录的S锁。 - 独占锁 (Exclusi)
简称 X 锁,在事务需要修改一条记录时,需要先获取该记录的X锁。
S锁之间不阻塞,S与X锁之间阻塞,X锁与X锁之间阻塞。
前面提到普通的SELECT 语句在 READ COMMITED 和 REPEATABLE READ 隔离级别使用的都是MVCC,但是我们也可以显示的使用SQL语句来支持锁定读:
- 对读取的记录加S锁
SELECT ... LOCK IN SHARE MODE
- 读读取的记录加X锁
SELECT ... FOR UPDATE
平常我们使用的写操作包括
-
DELETE
对一条记录执行DELETE 操作的过程其实就是先在B+树中定位到这条记录的位置,然后获取这条记录的X锁,最后在执行delete mark 操作。 -
UPDATE
在一条记录进行UPDATE操作时分为下面3种情况:-
如果未修改该记录的键值并且被更新的列所占用的存储空间在修改前后未发生变化,则先在B+树中定位到这条记录的位置,然后再获取记录的X锁,
-
如果未修改该记录的键值并且被至少一个被更新的列所占用的存储空间在修改前后发生了变化,则先在B+树中定位到这条记录的位置,然后再获取记录的X锁,之后将该记录彻底删除掉,最后再插入一条新记录。
-
如果修改了该记录的键值,则相当于在原记录上执行DELETE操作之后再来一次INSERT操作,加锁操作如同上面所述。
-
-
INSERT
一般情况下,新插入的一条记录受到隐式锁的保护,不需要在内存中为其生成对应的锁结构。 但是某些特殊情况也会生成锁结构。
表级锁
上面提到的锁都是针对记录的,可以将其称为行锁或者行级锁。
其实一个事务也可以针对一个表来加锁,称为表级锁或者表锁,表锁也分为共享锁和独占锁。
MySQL 中不同存储引擎的锁机制也不一样,这里我们只介绍 InnoDB 引擎中的锁。
网友评论