Administrator
发布于 2022-12-06 / 60 阅读
0
0

MySQL并发事务访问相同记录

1、读-读 情况

并发事务相继读取相同的记录 。读取操作本身不会对记录有任何影响,并不会引起什么问题,所以允许这种情况的发生。

2、写-写 情况

并发事务相继对相同的记录做出改动。在这种情况下会发生脏写的问题,这是不允许发生的情况,所以MySQL会通过加锁来实现让事务顺序运行,这个锁其实是一个内存中的结构。

1、事务执行之前,记录没有锁结构

image-1670310754377

2、当有事务要对此记录进行改动时,首先会看看内存中有没有与这条记录关联的锁结构 ,当没有的时候就会在内存中生成一个锁结构与之关联

image-1670310844537

trx信息 :代表这个锁结构是哪个事务生成的。
is_waiting :代表当前事务是否在等待。
is_waiting 属性值为 false ,则获取锁成功,为true,则是加锁失败

image-1670310967227

3、事务提交之后,会把该事务生成的锁结构释放掉,然后看看还有没有别的事务在等待获取锁,有就把等待事务 对应的锁结构的 is_waiting 属性设置为 false ,然后把该事务对应的线程唤醒,让它继续执行

image-1670311088923

3、读-写 或 写-读 情况

即一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生 脏读 、 不可重复读 、 幻读 的问题。

以上的问题在SQL标准下规定的不同隔离级别下可能发生的问题不一样;MySQL 在REPEATABLE READ 隔离级别实际上就已经解决了 幻读 问题。

解决 脏读 、 不可重复读 、 幻读 这些问题有两种可选方案:

1、读操作利用多版本并发控制( MVCC ),写操作进行 加锁 。

2、读、写操作都采用 加锁 的方式。

采用 MVCC 方式的话, 读-写 操作彼此并不冲突,性能更高,采用加锁方式的话,读-写 操
作彼此需要排队执行,影响性能。一般情况下采用 MVCC 来解决 读-写 操作并发执行的问题,
但是业务在某些特殊情况下,要采用 加锁 的方式执行,比如银行业务等

业务没有特殊需求的话,mysql的默认隔离级别就够用了,默认读取最新记录,而幻读在实际中是允许发生的

两种解决方式的扩充

一致性读(Consistent Reads)

事务利用 MVCC 进行的读取操作称之为 一致性读 ,或者 一致性无锁读 ,也称之为 快照读 。
一致性读 并不会对表中的任何记录做 加锁 操作,其他事务可以自由的对表中的记录做改动。

锁定读(Locking Reads)
共享锁和独占锁

对于 写-写 、 读-写 或 写-读 这些情况可能会引起一些问题,需要使用 MVCC 或者 加锁 的方式来解决。在使用 加锁 的方式解决问题时,由于既要允许 读-读 情况不受影响,又要使 写-写 、 读-写 或 写-读 情况中的操作相互阻塞,所以MySQL给锁分了个类:

共享锁 ,英文名: Shared Locks ,简称 S锁 。在事务要读取一条记录时,需要先获取该记录的 S锁 。

独占锁 ,也常称 排他锁 ,英文名: Exclusive Locks ,简称 X锁 。在事务要改动一条记录时,需要先获
取该记录的 X锁 。

S锁 和 S锁 是兼容的, S锁 和 X锁 是不兼容的, X锁 和 X锁 也是不兼容的

兼容性 X S
X 不兼容 不兼容
S 不兼容 兼容
写操作

平常所用到的写操作无非是 DELETE 、 UPDATE 、 INSERT 三种:

DELETE :
先在 B+ 树中定位到这条记录的位置,然后获取一下这条记录的 X锁 ,然后再执行 delete mark 操作。我们也可以把这个定位待删除记录在 B+ 树中位置的过程看成是一个获取 X锁 的 锁定读 。

UPDATE :

1、如果未修改该记录的键值并且被更新的列占用的存储空间在修改前后未发生变化(原地更新),则先在 B+ 树中定位到这条记录的位置,然后再获取一下记录的 X锁 ,最后在原记录的位置进行修改操作。其实也可以把这个定位待修改记录在 B+ 树中位置的过程看成是一个获取 X锁 的 锁定读 。

2、如果未修改该记录的键值并且至少有一个被更新的列占用的存储空间在修改前后发生变化,则先在
B+ 树中定位到这条记录的位置,然后获取一下记录的 X锁 ,将该记录彻底删除掉(就是把记录彻底移
入垃圾链表),最后再插入一条新记录。这个定位待修改记录在 B+ 树中位置的过程看成是一个获取 X
锁 的 锁定读 ,新插入的记录由 INSERT 操作提供的 隐式锁 进行保护。

3、如果修改了该记录的键值,则相当于在原记录上做 DELETE 操作之后再来一次 INSERT 操作,加锁操作就
需要按照 DELETE 和 INSERT 的规则进行了。

INSERT :

一般情况下,新插入一条记录的操作并不加锁,InnoDB 通过 隐式锁 的来保护这条新插入的记录在本事务提交前不被别的事务访问。

多粒度锁

从数据操作的类型对锁进行划分,可以分为读锁、写锁。

从数据操作的粒度进行划分则可以划分为:表级锁、页级锁、行锁

锁如果是针对记录的,被称之为 行级锁 或者 行锁 ,对一条记录加锁影响的也只是这条记录,那么这个锁的粒度就比较细;一个事务在表级别进行加锁,就被称之为 表级锁 或者 表锁 ,对一个表加锁影响整个表中的记录,这个锁的粒度比较粗。

给表加的锁也可以分为 共享锁( S锁 )和 独占锁 ( X锁 )

在对表加锁的时候会有两个问题:

如果是加S锁,但如果表内的某一行在修改记录加了X锁,那么是不能加锁的。
可以简单的理解为如果加锁成功了,但其实某条记录是无法访问的,那就不能算是对整表上了锁。

如果是加X锁,但如果表内有记录加了S锁或者X锁,那也是不能加锁的。
理解为除S锁和S锁之外,都不兼容,如果加锁成功,那就不算是对整表加锁。

如何要在对表加锁的时候,知道有哪些记录是加了锁的呢?

对此InnoDB 提出了一种锁称为意向锁 (英文名: Intention Locks )

意向共享锁,英文名: Intention Shared Lock ,简称 IS锁 。当事务准备在某条记录上加 S锁 时,需要先
在表级别加一个 IS锁 。

意向独占锁,英文名: Intention Exclusive Lock ,简称 IX锁 。当事务准备在某条记录上加 X锁 时,需
要先在表级别加一个 IX锁 。

事务在对表加意向锁的时候是不关注表上是否有别的意向锁的,意向锁只是判断表内的行是否存在锁,只有在对整表加锁的时候才会用到意向锁进行判断

表级别的各种锁的兼容性:

兼容性 X IX S IS
X 不兼容 不兼容 不兼容 不兼容
IX 不兼容 兼容 不兼容 兼容
S 不兼容 不兼容 兼容 兼容
IS 不兼容 兼容 兼容 兼容

表锁(Table Lock)

1、表级别的S锁、X锁

一般情况下,不会使用InnoDB存储引擎提供的表级别的 S锁 和 X锁 。只会在一些特殊情况下,比方说崩溃恢复 过程中用到。

2、表级别的 IS锁 、 IX锁

对使用 InnoDB 存储引擎的表的某些记录加 S锁 之前,那就需要先在表级别加一个 IS锁 ,当我们在对使用 InnoDB 存储引擎的表的某些记录加 X锁 之前,那就需要先在表级别加一个 IX锁 。 IS锁 和 IX锁的使命只是为了后续在加表级别的 S锁 和 X锁 时判断表中是否有已经被加锁的记录,以避免用遍历的方式来查看表中有没有上锁的记录

3、表级别的 AUTO-INC锁

在使用 MySQL 过程中,我们可以为表的某个列添加 AUTO_INCREMENT 属性,之后在插入记录时,系统会自动为它赋上递增的值。

系统实现这种自动给 AUTO_INCREMENT 修饰的列递增赋值的原理主要是两个:

1、采用 AUTO-INC 锁,也就是在执行插入语句时就在表级别加一个 AUTO-INC 锁,然后为每条待插入记录
的 AUTO_INCREMENT 修饰的列分配递增的值,在该语句执行结束后,再把 AUTO-INC 锁释放掉。这样一个
事务在持有 AUTO-INC 锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增
值是连续的。

2、采用一个轻量级的锁,在为插入语句生成 AUTO_INCREMENT 修饰的列的值时获取一下这个轻量级锁,然
后生成本次插入语句需要用到的 AUTO_INCREMENT 列的值之后,就把该轻量级锁释放掉,并不需要等到整个插入语句执行完才释放锁。

行级锁

1、 记录锁(Record Locks)

记录锁就是仅仅把一条记录锁上,官方的类型名称为: LOCK_REC_NOT_GAP。

记录锁是有S锁和X锁之分的,称之为 S型记录锁 和 X型记录锁 。

当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可
以继续获取X型记录锁;

当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不
可以继续获取X型记录锁。

2、间隙锁(Gap Locks)

MySQL 在 REPEATABLE READ 隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用 MVCC 方案解决,也可以采用 加锁 方案解决。但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些 幻影记录 加上 记录锁。InnoDB提出了一种称之为Gap Locks 的锁,官方的类型名称为: LOCK_GAP ,我们可以简称为 gap锁

eg:图中id值为8的记录加了gap锁,意味着 不允许别的事务在id值为8的记录前边的间隙插入新记录。如果有事务想在(3,8)之间执行插入操作,那么该锁阻塞插入操作,直到拥有这个gap锁的事务提交了之后,才继续执行

image-1670315039857

gap锁的提出仅仅是为了防止插入幻影记录而提出的。

3、临键锁(Next-Key Locks)

有时候既想锁住某条记录 ,又想阻止其他事务在该记录前边的 间隙插入新记录 ,所以InnoDB就提出了一种称之为 Next-Key Locks 的锁,官方的类型名称为: LOCK_ORDINARY ,我们也可以简称为next-key锁 。

Next-Key Locks是在存储引擎 innodb 、事务级别在 可重复读 的情况下使用的数据库锁,innodb默认的锁就是Next-Key locks。

4、插入意向锁(Insert Intention Locks)

一个事务在 插入 一条记录时需要判断一下插入位置是不是被别的事务加了 gap锁 ( next-key锁也包含 gap锁 ),如果有的话,插入操作需要等待,直到拥有 gap锁 的那个事务提交。但是InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新记录,但是现在在等待。这种类型的锁命名为 Insert Intention Locks ,官方的类型名称为: LOCK_INSERT_INTENTION ,我们也可以称为 插入意向锁 。

其实就是在某条记录上加了一个锁结构,这个锁结构多了一个type属性(标识为插入意向锁),等到加了gap锁的事务提交后,就将等待事务的意向锁里的is_waiting属性赋值为false,将线程唤醒。和锁结构差不多。

插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁。

页锁

页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。

从对待锁的态度划分:乐观锁、悲观锁

这部分得看看多线程或者并发的书籍

  1. 乐观锁 适合 读操作多 的场景,相对来说写的操作比较少。它的优点在于 程序实现 , 不存在死锁
    问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。

  2. 悲观锁 适合 写操作多 的场景,因为写的操作具有 排它性 。采用悲观锁的方式,可以在数据库层
    面阻止其他事务对该数据的操作权限,防止 读 - 写 和 写 - 写 的冲突。

按加锁的方式划分:显式锁、隐式锁

隐式锁

1、对于聚簇索引记录来说,有一个 trx_id 隐藏列,该隐藏列记录着最后改动该记录的 事务id 。那么如果在当前事务中新插入一条聚簇索引记录后,该记录的 trx_id 隐藏列代表的的就是当前事务的 事务id ,如果其他事务此时想对该记录添加 S锁 或者 X锁 时,首先会看一下该记录的trx_id 隐藏列代表的事务是否是当前的活跃事务,如果是的话,那么就帮助当前事务创建一个 X锁 (也就是为当前事务创建一个锁结构, is_waiting 属性是 false ),然后自己进入等待状态(也就是为自己也创建一个锁结构, is_waiting 属性是 true )。

2、对于二级索引记录来说,本身并没有 trx_id 隐藏列,但是在二级索引页面的 Page Header 部分有一个 PAGE_MAX_TRX_ID 属性,该属性代表对该页面做改动的最大的 事务id ,如果 PAGE_MAX_TRX_ID 属性值小于当前最小的活跃 事务id ,那么说明对该页面做修改的事务都已经提交了,否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记录,然后再重复1 的做法。


评论