透过“锁”事看InnoDB对并发的处理?

一. 并发场景下的问题

事务的特性:

  • A(Atomicity)原子性:操作时原子的,要么全部执行,要不全部不执行。
  • C(Consistentcy)一致性:数据库对外呈现出一致性的状态,如果呈现出了一个事务执行中的状态,那么就认为是不一致的。
  • I(Isolation)隔离型:对应事务的隔离级别。
  • D(Durability)持久性:事务对数据执行的操作,最终会通过redo log(以InnoDB引擎为例)持久化到磁盘上。

相对于串行处理方式,并发的事务处理可显著提升数据库的事务吞吐量、提高资源利用率。在MySQL实际应用中,根据场景的不同,可以分为以下几类:

  • 读读并发
  • 读写并发
  • 写写并发

在这些场景下,可能会出现更新丢失、脏读、不可重复度、幻读的问题。

  • 更新丢失:当多个事务同时更新某1/n行数据时,最后提交的事务会将之前提交的更新覆盖。
  • 脏读:一个事务正在插入/更新一行数据,在该事务提交之前,这条数据处于“不一致”状态。其他事务读取到这条“脏数据”并据此做进一步处理,就会产生对未提交数据的依赖关系,这种现象称为脏读。
  • 不可重复读:修改。一个事务在查询某条数据的一定时间后再次查询该数据,却发现该条数据已经发生了更新,这种现象称为不可重复读。
  • 幻读:增加或者删除。在同一个事务中,同一条完全相同的查询语句返回的结果集行数不同,这种情况称为幻读。(参考:https://stackoverflow.com/questions/11043712/what-is-the-difference-between-non-repeatable-read-and-phantom-read

其中,在各类并发场景下会出现的问题如下:

  • 读读并发场景不会导致数据不一致问题,因此无需特殊处理。
  • 读写并发场景可能会出现脏读、不可重复读、幻读的问题。
  • 写写并发场景可能会出现更新丢失的问题。

二.事务隔离级别

针对上述描述的问题,MySQL采用不同的事务隔离级别分别用于解决上述的部分问题,具体如下:

隔离级别 含义 读数据一致性 更新丢失 脏读 不可重复读 幻读
读未提交(Read Uncommitted) 事务中的修改,即使没有提交,对其他事务依然可见。 最低级别,只能保证不读取物理上损坏的数据。  否  是   是  是 
读已提交(Read Committed) 只有已提交事务所做的修改才对其他事务可见。 语句级  否
可重复读(Repeatable Read ) 同一事务中多次读取相同的记录结果时一致的。 事务级  否
串行化(Serializable) 在读取的每一行数据上加锁,强制事务串行执行,不支持并发。 事务级  否  否 

 

在上述隔离级别中,从上到下并发性能依次降低,安全性依次提高。InnoDB存储引擎下默认的事务隔离级别是RR,可通过如下SQL查询事务隔离级别。

select @@global.tx_isolation;

三.事务隔离级别的实现

 InnoDB对事务隔离级别的实现,基本可分为如下两种或两种的组合:

1.多版本并发控制(MVCC,Multiversion Currency Control)

MVCC可用来解决读写并发场景下的脏读、不可重复读问题。具体在不同隔离级别下的应用如下:

  • RC:可解决脏读问题。
  • RR:可解决脏读、不可重复读问题。

具体实现可参考这里,此处针对链接中的文章做简单补充:

相较于RC,RR之所以能够解决不可重复读问题,原因在于RR隔离级别下读取Read-View是事务级别的,RC是语句级别的。即:RR只在事务开始时读取一次Read-View,因此一个事务中的多次查询依赖同一个Read-View,能实现可重复读。RC级别下每次查询均读取     最新的Read-View,无法保证同一事务中先后读取到的Read-View是相同的,这也是先后读取到的数据有差异的原因。

2.锁

锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据了而言显得尤为重要。

锁可以解决上述描述读写并发场景下的幻读问题、写写并发场景下的更新丢失问题。下面针对锁、锁的类型及其应用场景展开详细描述。

2.1锁的基本原理

http://mysql.taobao.org/monthly/2016/01/01/

https://mp.weixin.qq.com/s/yq5Erdv5Dft3foJEVE2dxA

https://i6448038.github.io/2019/02/23/mysql-lock/

https://www.163.com/dy/article/G0S7FSGG05319WXB.html

duplicate key check检查时需要在二级唯一索引上加gap lock(版本:5.6之前,5.7.26-29之后加record lock)

https://juejin.cn/post/6844903597029720072

https://my.oschina.net/hebaodan/blog/3045246

只锁定二级索引不锁定主键索引的场景:

S锁+索引覆盖。

RR级别下通过MVCC无法解决幻读问题的场景:

事务1 事务2
select * from table where name='zhangsan';  
  insert into table (name) values('zhangsan');
  commit;
update table set  year = 11 where name='zhangsan';  
select * from table where name='zhangsan';  
commit;  

死锁发生场景:

  • 事务1,2均获取某一行数据的s锁后,均想获取x锁。
  • 事务1,2均获取到某一间隙锁后,希望进行插入操作,即增加插入意向锁。
  • 事务1,2操作同一条数据。由于检索条件不同,事务1,2分别锁定了聚簇索引和二级索引,然后事务1无法操作二级索引(先删除,再插入),事务2无法操作聚簇索引,发生死锁。(参考http://mysql.taobao.org/monthly/2016/01/01/)

关于插入意向锁:

InnoDb 在插入记录时,是不加锁的。如果事务 A 插入记录且未提交,这时事务 B 尝试对这条记录加锁,事务 B 会先去判断记录上保存的事务 id 是否活跃,如果活跃的话,那么就帮助事务 A 去建立一个锁对象,然后自身进入等待事务 A 状态,这就是所谓的隐式锁转换为显式锁。

  1. 执行 insert 语句,判断是否有和插入意向锁冲突的锁,如果有,加插入意向锁,进入锁等待;如果没有,直接写数据,不加任何锁;
  2. 执行 select ... lock in share mode 语句,判断记录上是否存在活跃的事务,如果存在,则为 insert 事务创建一个排他记录锁,并将自己加入到锁等待队列;

关于RR级别下MVCC和Gap Lock分别在解决幻读问题中充当了什么角色:

  • MVCC用于解决快照读产生的幻读问题。
  • Gap Lock用于解决当前读中产生的幻读问题。select …… for update/in share mode,不会走MVCC,都是读最新数据。
原文地址:https://www.cnblogs.com/jixiegongdi/p/14781500.html