MySQL的锁与事务详解

https://www.cnblogs.com/volcano-liu/p/9890832.html

参考:锁与事务

【二】事务的四大特性ACID

(1)原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
(2)一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;一致性主要由msql的日志机制处理,它记录数据库的所有变化,为事务回复提供跟踪记录。一致性属性保证数据库从不返回一个未处理的事务。
(3)隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;说白了,就是当前事务修改数据,在事务没提交之前,其他所有线程和事务都无法看到它的修改结果,保证事务和事务之间不会发生冲突
(4)持久性(Durability): 一个事务被提交之后,数据库的日志已经被更新了,如果系统崩溃导致数据丢失,就可以使用日志里的记录和备份来恢复丢失的数据,例如我们自行导出的sql文件。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

那么事务的这几大特性是如何保证的呢?

  1. 原子性:undoLog日志保证,即失败了可以通过undolog日志回滚。
  2. 隔离性:由MVCC(多版本控制)、锁机制来保证,MVCC的实现也用到了undolog日志。
  3. 一致性:由两阶段提交机制来保证,用到了RedoLog和binlog日志。
  4. 持久性:也是由两阶段提交机制来保证,用到了RedoLog和binlog日志。

【三】事务的创建过程

(1)初始化事务
start transaction;
这条指令开启了事务的一个单独单元。后面跟的是事务的sql语句
(2)创建事务
事务单元的具体执行内容,也就是要执行的所有sql语句。
(3)select检查一下数据是否被录入
检查一下要操作的数据是否已经发生了变化
(4)提交事务
commit指令用提交事务,标志着事务单独单元的结束
(5)事务回滚
rollback指令可以撤销事务单独单元里的所有执行结果,单元里的所有的sql都会执行回滚操作。

事务的两阶段提交保证了事务的一致性、持久性

两阶段提交分为两个主要阶段:准备阶段(Prepare Phase)和提交阶段(Commit Phase)。

1. 准备阶段(Prepare Phase)
  1. 开始事务:事务开始时,所有的操作都在内存中进行,直到事务准备提交。
  2. 写入redo log:InnoDB 会将所有的修改记录到 redolog 中,并将其标记为“准备”状态,表示事务还未提交。事务执行中间过程的 redo log 也是直接写在 redo log buffer 中的,这些缓存在 redo log buffer 里的 redolog 也会被「后台线程」每隔一秒一起持久化到磁盘。

准备阶段结束

2. 提交阶段(Commit Phase)
  1. 提交事务:生成该事务的 Binlog(记录逻辑变更),并写入磁盘
  2. InnoDB 会将 redo log 的状态从“准备”变为“提交”,表示事务已经提交,这意味着事务可以被持久化。
  3. 完成提交:事务的所有修改才会对外可见。

在这里插入图片描述

两阶段提交的意义

  • 一致性:通过两阶段提交,MySQL 可以确保即使在系统崩溃的情况下,事务的修改也不会丢失或部分提交,从而保证数据的一致性。
  • 持久性:在事务提交后,所有的修改都被持久化到磁盘,确保数据不会因系统故障而丢失。
为什么需要两阶段提交
  1. 主从复制的数据一致性(依赖 Binlog)。
  2. 事务的崩溃恢复(依赖 Redo Log的状态校验)。
  3. 跨存储引擎事务的原子性(如 XA 事务)。

https://www.cnblogs.com/shoshana-kong/p/17471621.html

binlog、redolog、undolog分别是什么东西
binlog
  • 作用:用于记录所有对数据库执行更改的SQL语句,主要用于数据复制(主从同步)和数据恢复。
  • 本质:以二进制格式存储,可以通过工具(如mysqlbinlog)解析成SQL语句。是逻辑日志,记录的是SQL语句或行级别的变更。可以配置为不同的格式:STATEMENT(记录SQL语句)、ROW(记录行级别的变更)、MIXED(混合模式)。
  • 使用场景:主从复制:从库通过读取主库的binlog来同步数据。数据恢复:可以通过binlog恢复到某个时间点的数据状态。
Redo Log(重做日志)
  • 作用:用于保证事务的持久性(Durability),确保事务提交后,即使系统崩溃,数据也不会丢失。
  • 本质:记录的是物理日志,即对数据页的修改操作。
  • 使用场景:事务提交时,会先将修改操作写入redolog,然后再写入磁盘。崩溃恢复:MySQL重启时,会通过redolog恢复未写入磁盘的数据。
Undo Log(回滚日志)
  • 作用:用于保证事务的原子性和一致性,支持事务回滚和多版本并发控制(MVCC)。
  • 本质:记录的是逻辑日志,保存事务执行前的数据状态。
  • 使用场景:事务回滚:如果事务执行失败,可以通过undolog回滚到事务开始前的状态。MVCC:在并发事务中,提供一致性读视图,避免读写冲突。

【四】并发事务带来的数据问题

多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题:

【1】脏读(Dirty read)

当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的,比如这个未提交数据的事务执行失败回滚了。

【2】丢失修改(Lost to modify)

指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。

【3】不可重复读(Unrepeatableread)

指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。

【4】幻读(Phantom read)

幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

【5】不可重复读和幻读区别:

不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。

【五】事务隔离级别(MySQL应对高并发事务是如何给出解决方案)

SQL 标准定义了四个隔离级别:

【1】READ-UNCOMMITTED(读取未提交)

最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。

【2】READ-COMMITTED(读取已提交)

允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。

实现方式:MVCC(多版本控制),在事务里每次读操作时判断版本,即这个事务里单次读操作能用的版本有哪些。

【3】REPEATABLE-READ(可重复读)

对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。

实现方式:MVCC(多版本控制),对整个事务判断版本,即这个事务每次读能用的版本有哪些。

但是MySQL通过 Next-Key Lock 的机制,使得这个隔离级别下,也可以防止幻读。但是在极端场景下,比如有多张表的join,RR级别依旧没有办法保证幻读不会发生。

【4】SERIALIZABLE(可串行化)

最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

实现方式:读锁和写锁。

【六】锁机制与InnoDB锁算法

【1】MyISAM和InnoDB存储引擎使用的锁:

MyISAM采用表级锁(table-level locking)
InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁

【2】表级锁和行级锁对比:

表级锁: MySQL中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM和 InnoDB引擎都支持表级锁。
行级锁: MySQL中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。

【3】InnoDB存储引擎的锁的算法有三种:

Record lock:单个行记录上的锁
Gap lock:间隙锁,锁定一个范围,不包括记录本身
Next-key lock:record+gap 锁定一个范围,包含记录本身

【4】对于MyISAM引擎

使用表级锁的锁模式。

MySQL的表级锁有两种模式:

  1. 表共享读锁(Table Read Lock)
  2. 表独占写锁(Table Write Lock)。

锁模式的兼容性:

  1. 对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;
  2. 对MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;
  3. MyISAM表的读操作与写操作之间,以及写操作之间是串行的。当一个线程获得对一个表的写锁后,只有持有锁的线程可以对表进行更新操作。其他线程的读、写操作都会等待,直到锁被释放为止。
【5】对于InnoDB引擎

InnoDB行锁实现方式

InnoDB行锁是通过给索引上的索引项加锁来实现的只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。下面通过一些实际例子来加以说明。

  1. 在不通过索引条件查询的时候,InnoDB确实使用的是表锁,而不是行锁
  2. 由于MySQL的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
  3. 当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论是使用主键索引、唯一索引或普通索引,InnoDB都会使用行锁来对数据加锁。

所以MySQL行级锁,锁住是什么东西 ?锁住的是索引上的索引记录

【七】锁分类

MySQL的锁机制与索引机制类似,都是由存储引擎负责实现的,这也就意味着不同的存储引擎,支持的锁也并不同,这里是指不同的引擎实现的锁粒度不同。但除开从锁粒度来划分锁之外,其实锁也可以从其他的维度来划分,因此也会造出很多关于锁的名词,下面先简单梳理一下MySQL的锁体系。

总归说来说去其实就共享锁、排他锁两种,只是加的方式不同、加的地方不同,因此就演化出了这么多锁的称呼。

(一)按照锁对数据操作的粒度分类

Mysql为了解决并发、数据安全的问题,使用了锁机制。可以按照锁的粒度把数据库锁分为表级锁和行级锁。

全局锁:锁定数据库中的所有表。加上全局锁之后,整个数据库只能允许读,不允许做任何写操作

(1)表级锁:

Mysql中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单 ,资源消耗也比较少,加锁快,不会出现死锁 。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM和 InnoDB引擎都支持表级锁。

  1. 表锁(分为表共享读锁 read lock、表独占写锁 write lock)
  2. 元数据锁(meta data lock,MDL):基于表的元数据加锁,加锁后整张表不允许其他事务操作。这里的元数据可以简单理解为一张表的表结构
  3. 意向锁(分为意向共享锁、意向排他锁):这个是InnoDB中为了支持多粒度的锁,为了兼容行锁、表锁而设计的,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查
(2)行级锁:

Mysql中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。 InnoDB支持的行级锁,包括如下几种;

  1. 记录锁(Record Lock):对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项;防止其他事务对此行进行update和delete,在 RC、RR隔离级别下都支持
  2. 间隙锁(Gap Lock):对索引项之间的“间隙”加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁),不包含索引项本身。其他事务不能在锁范围内插入数据,这样就防止了别的事务新增数据产生幻读。
  3. 临键锁(Next-key Lock):锁定索引项本身和索引范围。间隙锁的升级版,同时具备记录锁+间隙锁的功能,在RR隔离级别下支持,可解决幻读问题。

虽然使用行级索具有粒度小、并发度高等特点,但是表级锁有时候也是非常必要的:

  1. 事务更新大表中的大部分数据直接使用表级锁效率更高;
  2. 事务比较复杂,使用行级索很可能引起死锁导致回滚。

加间隙锁的规则

  1. 索引上的等值查询(唯一索引),给不存在的记录加锁时, 优化为间隙锁
  2. 索引上的等值查询(非唯一普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁
  3. 索引上的范围查询(唯一索引) – 会访问到不满足条件的第一个值为止

注意:间隙锁唯一目的是防止其他事务插入间隙。间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用间隙锁

临建锁(Next-Key Lock)

临键锁是间隙锁的Plus版本,或者可以说成是一种由记录锁+间隙锁组成的锁:
(1)记录锁:锁定的范围是表中具体的一条行数据。
(2)间隙锁:锁定的范围是左右开区间,但不包含当前这一条真实数据,只锁间隙区域。

而临键锁则是两者的结合体,加锁后,即锁定左开右闭的区间(每个临键锁是左开右闭区间),也会锁定当前行数据。

实际上在InnoDB中,除开一些特殊情况外,当尝试对一条数据加锁时,默认加的是临键锁,而并非记录锁、间隙锁。也就是说,在前面举例幻读问题中,当T1要对ID>2的用户做修改余额,锁定3、9这两条行数据时,默认会加的是临键锁,也就是当事务T2尝试插入ID=6的数据时,因为有临建锁存在,因此无法再插入这条“幻影数据”,也就至少保障了T1事务执行过程中,不会碰到幻读问题。

间隙锁和临建锁的目的都是用来解决可重复读的问题,如果在读提交级别,间隙锁和临建锁都会失效。

临键锁(Next-Key锁)

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;
对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的临键锁(Next-Key锁)。

例:
假如emp表中只有101条记录,其empid的值分别是 1,2,…,100,101,下面的SQL:

mysql> select * from emp where empid > 100 for update;

是一个范围条件的检索,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
InnoDB使用临键锁的目的:

  1. 防止幻读,以满足相关隔离级别的要求。对于上面的例子,要是不使用临键锁,如果其他事务插入了empid大于100的任何记录,那么本事务如果再次执行上述语句,就会发生幻读;
  2. 为了满足其恢复和复制的需要。

innodb自动使用临键锁的条件:

  1. 必须在RR级别(可重复读)下
  2. 检索条件必须有索引(没有索引的话,mysql会全表扫描,那样会锁定整张表所有的记录,包括不存在的记录,此时其他事务不能修改不能删除不能添加)
间隙锁、临键锁会产生死锁吗

会的,例如交叉间隙锁定

假如说users 表只有到age=10的记录,没有age=15和age=20的。

-- 事务A
BEGIN;
SELECT * FROM users WHERE age = 20 FOR UPDATE; -- 获取记录20的锁和(10,20)间隙锁

-- 事务B
BEGIN;
SELECT * FROM users WHERE age = 15 FOR UPDATE; -- 获取记录15的锁和(10,15)间隙锁

-- 事务A尝试锁定事务B已锁定的范围
SELECT * FROM users WHERE age = 15 FOR UPDATE; -- 等待事务B

-- 事务B尝试锁定事务A已锁定的范围
SELECT * FROM users WHERE age = 20 FOR UPDATE; -- 等待事务A
-- 此时形成死锁

(二)按照是否可写分类

表级锁和行级锁可以进一步划分为共享锁(s)和排他锁(X)

(1)读锁:共享锁(s)

共享锁(Share Locks,简记为S)又被称为读锁,其他用户可以并发读取数据,但任何事务都不能获取数据上的排他锁,直到已释放所有共享锁。

一个事务已获取共享锁,当另一个事务尝试对具备共享锁的数据进行读操作时,可以加共享锁,可正常读;进行写操作时,会被共享锁排斥。不能加X锁,直到释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

共享锁的意思很简单,也就是不同事务之间不会排斥,可以同时获取锁并执行。但这里所谓的不会排斥,仅仅只是指不会排斥。

(2)写锁:排他锁(X)

排它锁((Exclusive lock,简记为X锁))又称为写锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。

(3)两者之间的区别

1-共享锁(S锁):如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不 能加排他锁。获取共享锁的事务只能读数据,不能修改数据。
2-排他锁(X锁):如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获取排他锁的事务既能读数据,又能修改数据。

(4)锁的兼容性

共享锁与共享锁:兼容。多个事务可以同时持有同一数据资源的共享锁。
共享锁与排他锁:不兼容。持有共享锁的事务不能获取排他锁,反之亦然。
排他锁与排他锁:不兼容。同一数据资源上不能同时存在多个排他锁。
共享锁与间隙锁:兼容。
排他锁与间隙锁:不兼容。
间隙锁和间隙锁:兼容。
间隙锁和意向锁:不兼容。

(三)另外两个表级锁:IS和IX

当一个事务需要给自己需要的某个资源加锁的时候,如果遇到一个共享锁正锁定着自己需要的资源的时候,自己可以再加一个共享锁,不过不能加排他锁。但是,如果遇到自己需要锁定的资源已经被一个排他锁占有之后,则只能等待该锁定释放资源之后自己才能获取锁定资源并添加自己的锁定。而意向锁的作用就是当一个事务在需要获取资源锁定的时候,如果遇到自己需要的资源已经被排他锁占用的时候,该事务可以需要锁定行的表上面添加一个合适的意向锁。如果自己需要一个共享锁,那么就在表上面添加一个意向共享锁。而如果自己需要的是某行(或者某些行)上面添加一个排他锁的话,则先在表上面添加一个意向排他锁。意向共享锁可以同时并存多个,但是意向排他锁同时只能有一个存在。

InnoDB另外的两个表级锁:

意向共享锁(IS): 表示事务准备给数据行记入共享锁,事务在一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(IX): 表示事务准备给数据行加入排他锁,事务在一个数据行加排他锁前必须先取得该表的IX锁。
注意:

这里的意向锁是表级锁,表示的是一种意向,仅仅表示事务正在读或写某一行记录,在真正加行锁时才会判断是否冲突。意向锁是InnoDB自动加的,不需要用户干预。
IX,IS是表级锁,不会和行级的X,S锁发生冲突,只会和表级的X,S发生冲突。

【四】触发锁的时机

MySQL中的锁定机制通常发生在以下几个关键时机:

(1)事务开始:当一个事务开启时,它会自动获得对正在操作的数据行的锁,这被称为意向锁(Intention Lock)。如果需要锁定数据表,还会获取表级共享锁(Shared锁)。

(2)串行化隔离级别的事务读、写操作:读取操作(SELECT)会获取行级共享锁(S锁);而所有的写入操作(INSERT、UPDATE、DELETE)则会获取行级排他锁(X锁)。如果是对多行进行操作,可能会获取到意向键(Next-Key Locks)。

(3)RR和RC隔离级别的写操作:RR级别的写操作,如果是多行操作,会加意向键(Next-Key Locks)的写锁。RC级别的写操作,会对单独这一个数据记录加写锁。

(4)索引扫描:在索引上进行范围查询时,MySQL会在索引级别上锁定,这称为索引快照锁(Index Locks)。

(5)等待锁释放:当一个事务持有锁并且阻塞其他事务时,只有当该事务完成提交或者回滚才会释放锁。

(6)死锁检测:MySQL会定期检查是否存在死锁,并在检测到死锁时通过回滚其中一个事务来解除。

表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高;

【五】防止死锁的方式:

  1. 尽早提交事务:避免在事务中执行长时间的计算、网络操作或其他耗时操作,以减少锁的持有时间。
  2. 拆分大事务:将大事务拆分为多个较小的事务,以减少事务的持有锁时间。
  3. 使用合适的索引:确保查询语句使用合适的索引,以减少锁定的数据量。使用索引可以提高查询效率,并减少事务持有锁的时间。
  4. 尽量使用更精确的条件来限制查询范围,避免长时间持有锁定的行。
  5. 尽量约定以相同的顺序来访问表:在应用中,如果不同的程序会并发存取多个表,应尽量约定以相同的顺序来访问表,这样可以大大降低产生死锁的机会。
  6. 批量数据先进行排序:在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能。

怎么检测死锁呢,mysql自己就能检测出死锁并报错。

【八】多版本并发控制MVCC

参考:https://blog.youkuaiyun.com/qq_46312987/article/details/123941459
参考:https://zhuanlan.zhihu.com/p/394923053

1.1 什么是多版本并发控制(MVCC)?

多版本并发控制(MVCC,Multiversion Concurrency Control)是一种数据库并发控制方法,它通过保留数据的多个版本来管理事务并发。与传统的锁机制不同,MVCC 允许多个事务同时读取和写入数据,而不会相互干扰,从而提高数据库的并发性和性能。

在 MVCC 中,每当数据发生变化时,数据库会创建一个新的版本,而不是直接修改原始数据。这意味着多个事务可以同时读取数据,而不必等待其他事务的完成,从而实现高并发读写。

MVCC 在数据库系统中广泛应用,特别是在涉及高并发读操作的场景中,能够有效地减少锁竞争和死锁的发生。

1.2 MVCC 在数据库管理系统中的作用

MVCC 主要用于实现事务的并发控制,它通过以下几个方面提升数据库的并发性:

  1. 避免锁冲突:传统的并发控制方法通过加锁来保证数据一致性,这可能导致事务的阻塞。而 MVCC 允许多个事务在没有直接锁冲突的情况下并发执行,从而提高数据库的吞吐量。
  2. 支持高并发读操作:MVCC 使得数据库能够在多个事务中同时提供一致的快照,从而避免了锁对读取操作的阻塞,提升了系统的并发性能。
  3. 事务隔离性:MVCC 能够确保每个事务都看到一个一致的数据视图,从而保持数据库的一致性和隔离性。
1.3 MVCC 与传统锁机制的区别

传统的并发控制方法一般通过锁来确保事务的隔离性,常见的锁包括行锁、表锁等。然而,锁机制有时会导致如下问题:

死锁:多个事务互相等待对方释放锁,导致系统无法继续执行。
锁竞争:多个事务同时请求相同的资源时,事务会被阻塞,从而影响系统的吞吐量。
性能瓶颈:特别是在高并发的情况下,锁的争用会严重影响数据库的性能。
而 MVCC 不依赖于显式的锁机制来保证事务的隔离性。每个事务看到的数据是某一时刻数据的一个“快照”,因此,即使多个事务同时修改同一数据,它们也可以独立进行。MVCC 更注重通过版本控制来实现数据的一致性,减少了对锁的依赖,从而提高了数据库的性能和并发能力。

1.4 为什么需要 MVCC?

MVCC 解决了传统数据库管理系统在高并发环境下常见的一些问题。具体而言,它的优势体现在以下几个方面:

  1. 提高并发性能:通过避免锁冲突,多个事务可以并发执行,特别是对于读操作,几乎没有任何阻塞。
  2. 提供一致的数据视图:事务在执行时总是看到一致的数据,确保了事务的隔离性和一致性。
  3. 减少死锁的发生:由于不需要显式锁定资源,MVCC 可以减少锁竞争,从而降低死锁发生的几率。
  4. 提高系统响应速度:由于读操作不会被写操作阻塞,系统的响应时间通常较低,特别是在读多写少的场景中。

然而,MVCC 并不是没有代价的,它也带来了一定的性能开销,尤其是在写操作频繁的场景下。接下来我们将讨论 MVCC 的工作原理,以及它如何在实际应用中实现这些优势。

1.5 MVCC原理

MVCC基于三个隐式字段实现,数据库中的每行记录,除了我们自定义的字段外,还有数据库隐式定义的一些字段:

  • 隐式主键 DB_ROW_ID:如果数据表没有主键,InnoDB会自动根据 DB_ROW_ID 产生一个聚簇索引
  • 事务ID DB_TRX_ID:记录 创建这条记录 或 最后一次修改这条记录 的事务ID(即最新的事务ID)
  • 回滚指针 DB_ROLL_PTR:指向这条记录的上一个版本(存储于rollback segment里)

“可重复读”的隔离级别下,事务启动时对整库“拍了个快照”。主要基于以下3点来实现:

①row trx_id:InnoDB里每个事务都有一个唯一的事务ID,被叫做transaction id。它是在事务开始时被申请,按申请顺序严格递增。所以,每行数据的多个版本以事务ID来标识。也就是说,每次事务更新数据时,都会生成一个由transaction id赋值地新的数据版本,记为row trx_id。同时,旧的数据版本要被保留,并在新的数据版本中,有信息可以直接获取它。即,数据表中的一行记录,它可能有的多个版本号就是多个曾经更新过它的row trx_id。

②undo log(回滚日志):任何语句的更新都会生成undo log,主要用于事物的回滚。但在这里,在主要用于计算获取同一行数据在不同row trx_id下的实际值。即,不同事物在各自快照里能看到的数据,就是在每次被需要时,根据当前版本和一系列undo log“回滚”实现。

当我们执行了一条更新的SQL语句时update user set name = ‘niuniu where id = 1’,那么undo log的记录就会发生变化,会把之前的原有数据拷贝到undo log日志中。同时你可以看见最新的一条记录在末尾处有一个隐藏字段 DB_ROLL_PTR,它存放了undo log日志的指针地址。最终有可能需要通过指针来找到历史数据。

③可见性规则:可重复读的隔离级别下,InnoDB事务在启动时会声明说:“以我启动时刻为准,①如果一个数据的row trx_id比我的小,就认;②row trx_id比我的大,就必须通过undo log’回滚’到比我小的row trx_id为止;③我自己在事务里更新的,以最新值更新后的值为准。”

实现上为了做到这一点,InnoDB为每个事务构造了一个数组,用来保存事务在启动瞬间,当前活跃的,启动但尚未提交的,所有更新事务的row trx_id。所以,对事务启动瞬间,它构造出的“整库快照”就由下图表示:大致包括3个部分(过去事务,进行事务,未来事务)和2个水位(低水位以前可见,高水位以后不可见),它们共同构成了当前事务的一致性视图(read-view)。

在这里插入图片描述

下图分别开启了3个事务(在默认autocommit=1的正常情况下,begin/start transaction命令并不是事务的起点,而是在第一个语句执行时事务才真正启动,构建快照。此处通过with consistent snapshot命令在一开始就启动事务,方便理解),请问它们各自select语句的返回值是多少?
在这里插入图片描述
①事务启动:

假设数据(1,1)这一行的row trx_id是90,且当前只有一个活跃事务ID是99;

事务A:事务ID是100,在一个只读的事务中查询,时间上在事务B查询之后,视图数组是[99,100],低水位99,高水位100;

事务B:事务ID是101,在事务A后开始,在更新行之后查询,视图数组是[99,100,101],低水位99,高水位101;

事务C:事务ID是102,在事务B后开始,update语句本身就是一个事务,更新完成后自动提交,视图数组是[99,100,101,102],低水位99,高水位102;

②事务提交:

事务C:首先提交,将数据从(1,1)改成(1,2),行的最新版本号是102;

事务B:其次提交,所有的更新操作都是先读后写,因为要更新数据,所以只能读当前的最新值(被称作“当前读”),并在当前最新值上提交(否则就丢失了数据更新),将数据从(1,2)改成(1,3),行的最新版本号是101。所以,先更新再查询的事务B,看到的数据是(1,3);

事务A:最后提交,首先读到(1,3),但其版本号row trx_id=101,大于事务A的高水位,红色区域不可见;接着通过undo log回滚到(1,2),其版本号row trx_id=102,大于事务A的高水位,红色区域不可见;最后再通过undo log回滚到(1,1),其版本号row trx_id=90,绿色区域可见。所以,只读事务A看到的数据始终是(1,1)。

1.6 RC/RR级别下的InnoDB快照读有什么不同

正是由于生成Read View的时机不同,从而造成RC RR级别下快照读的结果的不同。

  • 在RR级别下的某个事务对某条记录进行的第一次快照读会创建一个快照Read View,此后在调用快照读的时候,使用的还是同一个ReadView,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见。
  • 而在RC隔离级别下,事务中每次快照都会生成一个快照和ReadView,这就是我们在RC级别下的事务中可以额看到别的事务提交更新的原因。
1.7 我们平时用RC还是RR,为什么?

在 MySQL 中,RR 是默认的隔离级别。

但是使用哪种隔离级别需要看具体的业务场景。

RR的特点:

  • 一致性保证:在一个事务中,多次读取同一数据时,结果是一致的(即使其他事务修改了数据)。通过MVCC机制保证。
  • 幻读问题:RR 隔离级别下,MySQL 通过 Next-Key Lock 机制避免了幻读问题。
  • 锁机制:使用更多的锁来保证一致性,可能会导致锁争用和性能下降,因为使用了 Next-Key Lock 机制,所以会需要锁住更大范围的数据间隙。

RR级别的使用场景:

  • 需要强一致性的场景,如金融系统、订单系统等。因为MySQL的RR级别可以防止可重复读和幻读,有强一致性的保证。
  • 事务中需要多次读取同一数据,且要求结果一致。
  • 需要避免幻读问题。

RC的特点:

  • 一致性保证:在一个事务中,每次读取的数据都是已提交的最新数据。通过 MVCC(多版本并发控制) 实现。不能处理不可重复读的问题。
  • 幻读问题:RC 隔离级别下,可能会出现幻读问题。
  • 锁机制:使用较少的锁,性能较高。

RC级别的使用场景:

  • 对一致性要求不高的场景,如日志系统、消息队列等。
  • 高并发场景,需要更高的性能。
  • 事务中不需要多次读取同一数据,或者可以接受读取到不同的结果。

为什么RC的性能更好?

  • 锁的范围更小:RC 只对当前读取的数据加锁,而 RR 需要对一个范围加锁(Next-Key Lock)。
  • Read View 的创建和释放更频繁:RC 每次执行 SQL 语句时都会创建和释放 Read View,虽然这会增加一些开销,但可以更快地释放资源。RR 在整个事务期间保持一个 Read View,虽然减少了 Read View 的创建开销,但会导致更长的锁持有时间。
  • RR 需要维护更长时间的 Undo Log,因为事务可能会访问历史版本的数据。RC 只需要维护较短时间的 Undo Log,因为每次读取的都是最新的已提交数据。

https://www.cnblogs.com/luyucheng/p/6297752.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值