记录些MySQL题集(5)

分库分表查询问题:异构索引表

面试官:分库分表有什么好的方案?

异构索引表,则是在实施分库分表过程中一个非常巧妙的设计,用来解决分库分表的查询问题

分库分表的查询问题

问题说明

在哈希分库分表时,为了避免分布不均匀造成的“数据倾斜”,通常会选择一些数据唯一的字段进行哈希操作,比如ID。但是现在要查询uid为1的记录,应该去哪个表或库去查询?这个时候就会发现,要想查询uid为1的记录,只能去所有的库或分表上进行查询,也就是所谓的“广播查询”。整个查询过程大概是这样的

性能问题

显然,整个查询过程需要进行全库扫描,涉及到多次的网络数据传输,一定会导致查询速度的降低和延迟的增加

数据聚合问题

另外,当这个用户有成千上万条数据时,不得已要在一个节点进行排序、分页、聚合等计算操作,需要消耗大量的计算资源和内存空间。对系统造成的负担也会影响查询性能。这是一个非常典型的“事务边界大”的案例,即“一条SQL到所有的数据库去执行”。

解决分库分表的查询问题

异构索引表”是可以解决这个问题的。

引入异构索引表

简单来说,“异构索引表”是一个拿空间换时间的设计。具体如下:添加订单数据时,除了根据订单ID进行哈希取模运算将订单数据维护到对应的表中,还要对uid进行哈希取模运算,将uid和订单id维护在另一张表中,如图所示:

引入“异构索引表”后,因为同一个uid经过哈希取模运算后得到的结果是一致的,所以,该uid所有的订单id也一定会被分布到同一张user_order表中。当查询uid为1的订单记录时,就可以有效地解决数据聚合存在的计算资源消耗全库扫描的低效问题了。

接下来,通过查询过程,看看这两个问题是怎么解决的。

引入后的查询过程

引入“异构索引表”后,查询uid为1的订单记录时,具体过程分为以下几步:

  1. 应用向中间件发送select * from order where uid = 1,请求查询uid为1的订单记录。

  2. 中间件根据uid路由到“异构索引表”:user_order,获得该uid相关的订单ID列表(排序、分页可以在此sql操作)。

  3. 中间件根据返回的订单ID,再次准确路由到对应的订单表:order

  4. 中间件将分散的订单数据进行聚合返回给应用。

看上去引入“异构索引表”之后,多了一个查询步骤,但换来的是:

  1. 根据订单ID准确路由到订单表,避免了全库扫描。

  2. user_order表进行了排序、分页等操作,避免大量数据回到中间件去计算。

异构索引表解决不了的场景

“异构索引表”只适合简单的分库分表查询场景,如果存在复杂的查询场景,还是需要借助搜索引擎来实现。

总结

异构索引表作为一种巧妙的设计,避免了分库分表查询存在的两个问题:全库扫描不必要的计算资源消耗。但是,异构索引表并不适用所有场景,对于复杂的查询场景可能需要结合其他技术或策略来解决问题。

事务ACID,底层是如何实现的?

什么是事务

事务(Transaction)是数据库管理系统执行过程中的一个逻辑单位,它由一个有限的数据库操作序列构成。事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个sql语句,这些语句要么都执行,要么都不执行。这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务的目的是确保数据的完整性和一致性,它通过一系列的操作,将数据库从一个一致性状态转换到另一个一致性状态。

MySQL逻辑架构和存储引擎

图片

如上图所示,MySQL服务器逻辑架构从上往下可以分为三层:

(1)第一层:处理客户端连接、授权认证等。

(2)第二层:服务器层,负责查询语句的解析、优化、缓存以及内置函数的实现、存储过程等。

(3)第三层:存储引擎,负责MySQL中数据的存储和提取。

MySQL 中服务器层不管理事务,事务是由存储引擎实现的。

MySQL支持事务的存储引擎有InnoDB、NDB Cluster等,其中InnoDB的使用最为广泛;其他存储引擎不支持事务,如MyIsam、Memory等。

数据库事务的执行过程

数据库事务( transaction) 是指作为一个逻辑工作单元执行的一系列数据库操作,这些操作要么全部成功,要么全部失败,是一个不可分割的工作单元。事务是在事务开始和事务结束之间执行的一系列数据库操作。事务的目的是确保数据库操作的一致性和完整性,同时提供对并发访问的隔离性和恢复能力。

简化版的执行过程

图片

详细版的执行过程

下面是一个详细版的一个数据库事务( transaction)的执行过程

图片

InnoDB的一次更新事务涉及到多个组件和步骤,包括Buffer Pool、BinLog、UndoLog、RedoLog以及物理磁盘。下面是一次完整的事务更新操作过程:

step1. 开始数据,InnoDB 收到更新请求

执行SQL,收到事务的更新请求

step2. 加载数据到Buffer Pool缓存中

在进行数据更新时,InnoDB首先在缓冲池(Buffer Pool)中查找待更新记录是否已经在内存中。若记录不在内存中,InnoDB会将记录从磁盘文件读取到缓冲池(Buffer Pool)中。

缓冲池是InnoDB存储引擎提供的临时存储区,用于提升数据读取和修改的速度。将数据加载到缓冲池后,后续的更新操作均在缓冲池内进行。这样可以减少磁盘I/O操作,从而提高事务处理速度。缓冲池在内存中存储数据,可以降低磁盘I/O的开销,提高数据读取和写入的速度,从而优化事务处理性能。

step3. 写入Undo Log

在更新数据之前,InnoDB会将原始数据的副本写入Undo Log(回滚日志)。Undo Log是保证事务回滚和并发控制的关键部分,也是确保事务原子性和一致性的重要机制。Undo Log记录了事务开始前的数据状态,以便在需要回滚时进行数据恢复。通过记录撤销日志,InnoDB能够实现事务的滚动回滚,提高事务处理的灵活性。撤销日志在事务处理过程中起到了关键作用,它记录了事务的修改过程,使得事务能够在需要时回滚到之前的状态,保证数据的一致性和完整性。

step4. 更新内存数据

接下来,InnoDB会在缓冲池中更新数据。包括 Buffer Pool和 Redo Log Buffer。

这意味着,当执行update语句时,InnoDB会先更新已经读取到Buffer Pool中的数据,修改操作会直接在内存中进行,而不是立即写入磁盘。此时,缓冲池中的数据被标记为"脏页",表示与磁盘上的数据不一致。脏页是缓冲池中已经被修改但尚未写入磁盘的数据页,它需要后续的处理才能将修改同步到磁盘,保证数据的持久性。

更新Buffer Pool和Redo Log Buffer通常是数据库系统内部的操作,这些操作可以作为事务处理的一部分。更新Buffer Pool和Redo Log Buffer的一般步骤:

第一步:Buffer Pool更新: 缓冲池 (Buffer Pool) 是数据库系统中用于存储数据页的内存区域。当需要读取或写入数据时,数据库系统首先会检查缓冲池,如果数据页已经在缓冲池中,就可以直接进行访问,而不必去访问磁盘。 要更新Buffer Pool,首先需要确定要读取或写入的数据页。如果数据页已经在缓冲池中,可以直接进行读取或写入操作;如果数据页不在缓冲池中,则需要将其从磁盘加载到缓冲池中。 更新Buffer Pool的过程通常是由数据库系统自动管理的,但也可以通过合适的API或查询语句手动进行。

第二步:Redo Log Buffer更新: 重做日志(Redo Log)是数据库系统用于持久化事务操作的一种技术。当事务进行数据更新时,数据库系统会首先将更新操作记录到Redo Log中,以确保即使在系统崩溃时也能够恢复到事务之前的状态。 Redo Log Buffer是一个内存区域,用于暂时存储事务更新操作的日志记录。在事务进行数据更新时,数据库系统会将更新操作记录到Redo Log Buffer中,然后定期将其写入到磁盘的Redo Log文件中。 更新Redo Log Buffer通常是在事务进行提交(Commit)时完成的。

当事务提交时,数据库系统会确保将Redo Log Buffer中的所有日志记录写入到磁盘中,以保证事务的持久性。在更新Buffer Pool和Redo Log Buffer时,数据库系统通常会采取一些优化措施,例如异步写入、批量提交等,以提高性能和减少IO开销。同时,数据库管理员也可以通过调整参数和配置来优化Buffer Pool和Redo Log Buffer的性能。

step5. 写入Redo Log 文件

为了保证事务的持久性,InnoDB在Buffer Pool中记录修改操作的同时,InnoDB会先将更新操作写入Redo Log(重做日志)。Redo Log是一种物理日志,它记录了事务对数据库的修改操作。通过Redo Log,即使系统发生故障,也可以通过重做日志来恢复事务修改后的状态。这一机制保证了事务的可靠性,降低了系统故障带来的风险。重做日志是保证数据持久性和恢复性的关键,它记录了事务的修改过程,使得事务的修改能够在故障恢复后得到恢复。

step6. 提交事务

当事务完成所有的更新操作后,事务被提交。在提交事务时,InnoDB会将事务标记为"准备提交"状态。此时,事务的修改操作仍然在缓冲池中,尚未写入磁盘。事务提交是事务处理的重要环节,它标志着事务处理完毕,可以进行后续的提交操作。在提交之前,事务的修改操作需要得到处理,保证数据的完整性和一致性。

step7. 写入BinLog

在事务提交之后,InnoDB会将事务的修改操作写入BinLog(归档日志)。BinLog是MySQL的二进制日志,用于记录数据库的所有修改操作。在归档日志中记录的信息包括:事务开始的时间、数据库名、表名、事务ID、SQL语句等。它可以用于数据恢复、主从复制、数据分析和同步等场景。归档日志在数据库中起到了关键作用,它记录了数据库的修改过程,使得数据库的修改能够在故障恢复后得到恢复。

step8. 刷新脏页到磁盘

最后,在提交过程完成后,InnoDB会将缓冲池(Buffer Pool)中的脏页刷新到物理磁盘上的数据文件中。这个过程称为"刷脏"。通过刷脏操作,将缓冲池中的修改操作同步到磁盘,确保数据的持久性。然而,这个写入过程并非立即执行,而是由后台线程异步执行的,因此可能会有一定的延迟。总而言之,MySQL会在适当的时机选择将数据写入磁盘以进行持久化。

典型的 MySQL 事务通常包括以下步骤:

  1. 开始事务:使用 START TRANSACTION 或 BEGIN 命令来开始一个新的事务。这将确保接下来的操作将被视为一个事务单元,并且要么全部成功提交,要么全部失败回滚。

  2. 执行SQL操作:在事务中执行各种SQL操作,例如插入、更新或删除数据,查询等。这些操作可能会涉及一个或多个数据库表。

  3. 数据处理与验证:在执行SQL操作之前或之后,进行数据处理和验证。这可能包括检查约束条件、验证输入数据的有效性等。

  4. 提交或回滚事务:如果所有的数据库操作都成功,并且通过了数据验证步骤,那么事务可以被提交(COMMIT)。这将导致所有的更改永久地应用到数据库中。如果在任何时候出现了错误或者违反了事务的约束条件,那么整个事务将会被回滚(ROLLBACK),所有的更新将被撤销,数据库将恢复到事务开始之前的状态。

  5. 结束事务:一旦事务被提交或者回滚,事务就结束了。可以使用 COMMIT 或 ROLLBACK 命令来结束事务。此时数据库会释放任何由事务占用的资源。

其中start transaction标识事务开始,commit提交事务,将执行结果写入到数据库。如果sql语句执行出现问题,会调用rollback,回滚所有已经执行成功的sql语句。当然,也可以在事务中直接使用rollback语句进行回滚。

MySQL自动提交

在 MySQL 中,默认情况下是开启了自动提交(Auto-Commit)模式的。如下所示:

图片

在自动提交模式下,如果没有 start transaction 显式地开始一个事务,那么每个sql语句都会被当做一个事务执行提交操作。这意味着每个单独的 SQL 语句都会被作为一个事务并立即提交。这种模式的好处是简化了对数据库的操作,但也可能会导致性能问题或数据不一致。通过如下方式,可以关闭autocommit;需要注意的是,autocommit参数是针对连接的,在一个连接中修改了参数,不会对其他连接产生影响。

要在 MySQL 中禁用自动提交,可以使用以下 SQL 语句:

SET autocommit = 0;

这样设置之后,直到显式执行了 COMMIT 或 ROLLBACK 语句,MySQL 将不会自动提交事务。在事务执行完成后,你可以使用 COMMIT 来提交事务并将更改保存到数据库中,或者使用 ROLLBACK 来撤销事务中的所有更改。

特殊操作

在MySQL中,存在一些特殊的命令,如果在事务中执行了这些命令,会马上强制执行commit提交事务;如DDL语句(create table/drop table/alter/table)、lock tables语句等等。不过,常用的select、insert、update和delete命令,都不会强制提交事务。

事务的ACID特性

ACID,是指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的。事务通常具有以下四个特性,也被称为ACID属性:

  1. 原子性(Atomicity):事务作为一个整体执行,包含在其中的对数据库的操作要么全部执行,要么全部不执行。

  2. 一致性(Consistency):事务必须使数据库从一个一致性状态变换到另一个一致性状态。也就是说,一个事务的执行不能破坏数据库数据的完整性和一致性。

  3. 隔离性(Isolation):事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务是不可见的。

  4. 持久性(Durability):一旦事务提交,则其结果就是永久性的,即使系统崩溃也不会丢失。

事务的这些特性确保了即使在高并发的环境中,数据库也能保持数据的完整性和一致性。在数据库系统中,事务是通过一系列的操作来完成的,包括数据的插入、更新、删除等。如果事务中的任何操作失败,或者因为某种原因被中断,那么整个事务都会回滚(Rollback),即撤销所有已经执行的操作,使数据库回到事务开始之前的状态。如果事务中的所有操作都成功完成,那么事务会提交(Commit),所做的更改会永久保存到数据库中。

4种事务隔离级别

事务隔离级别主要定义了事务在并发执行时的行为,特别是它们如何与其他事务交互以及它们如何看到数据库中的更改。ANSI/ISO SQL标准定义了4中事务隔离级别:未提交读(read uncommitted),提交读(read committed),重复读(repeatable read),串行读(serializable)。

  • Oracle中默认的事务隔离级别是提交读 (read committed)。

  • 对于MySQL的Innodb的默认事务隔离级别是重复读(repeated read)。

MySQL支持四种不同的事务隔离级别,每种级别都有其特定的行为和适用场景。以下是MySQL的四种事务隔离级别及其描述:

  1. READ UNCOMMITTED(读取未提交)

    • 允许读取尚未提交的数据变更。

    • 这是最低的隔离级别,它可能导致脏读、不可重复读和幻读。

    • 在这个级别,一个事务可以读取到另一个尚未提交事务的修改,这可能导致数据的不一致性。

  2. READ COMMITTED(读取已提交)

    • 只允许读取并发事务已经提交的数据。

    • 这个级别可以防止脏读,但仍可能导致不可重复读和幻读。

    • 在这个级别,每个事务只能看到它开始时的数据状态以及它提交时其他事务所做的提交。

  3. REPEATABLE READ(可重复读取)

    • 这是MySQL的默认隔离级别

    • 它确保在同一事务中多次读取同一数据时,看到的是相同的数据版本,即使其他事务在此期间修改了这些数据。

    • 尽管可以避免脏读和不可重复读,但在这个级别下仍可能出现幻读(即在一个事务中,两次相同的查询可能会返回不同的结果集,因为其他事务在此期间插入了新的记录)。

  4. SERIALIZABLE(可串行化)

选择适当的事务隔离级别需要根据应用的需求和性能考虑进行权衡。在某些情况下,可能需要更高的隔离级别来确保数据的一致性,而在其他情况下,可能需要降低隔离级别以提高性能。同时,也需要注意不同隔离级别可能带来的并发问题,如脏读、不可重复读和幻读等。

  • 这是最高的隔离级别。

  • 它通过强制事务串行执行来避免脏读、不可重复读和幻读。

  • 在这个级别,每个事务在执行时都会完全锁定它所访问的数据,从而确保数据的一致性。但这也可能导致性能下降,因为并发事务必须等待其他事务完成才能执行。

图片

脏读(Dirty Read)
一个事务读取到另一个尚未提交事务的修改。

不可重复读(Non-repeatable Read)
在同一个事务内,多次读取同一数据返回的结果有所不同。

幻读(Phantom Read)
一个事务在执行两次相同的查询时,因为另一个并发事务的插入或删除操作,导致两次查询返回的结果集不同。

ACID的 A 原子性如何实现?

原子性定义

原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。原子性是ACID(原子性、一致性、隔离性和持久性)的四个基本特征之一,它确保数据库操作要么全部成功,要么全部失败,不存在部分完成的情况。

原子性的实现机制

  1. 事务:事务是一系列数据库操作的逻辑单位,它们要么全部成功执行,要么全部失败回滚。事务可以通过SQL语句或编程接口来启动、提交或回滚。在一个事务内执行的所有操作都视为一个原子操作。

  2. redo log日志:数据库系统使用重做日志 来记录事务执行过程中的所有数据库操作。这些日志记录包括在事务开始之前的数据状态,以及事务执行过程中对数据的任何修改。这些日志记录可以用于在系统故障后恢复数据库到一致状态。

  3. Undo日志:除了记录事务执行的操作之外,数据库系统还会记录撤销操作(undo),即使在事务提交之前也会记录。这些undo日志记录用于在事务回滚时撤销对数据的修改,以确保原子性。如果事务失败,系统可以使用undo日志来还原到事务开始之前的状态。

  4. 锁机制:数据库系统使用锁来控制并发访问,确保在一个事务执行过程中,其他事务无法修改被当前事务使用的数据。这样可以防止其他事务对当前事务的操作造成干扰,从而维护原子性。

通过这些机制的配合,数据库系统能够有效地实现原子性。当事务提交时,系统会将所有的数据库操作应用到数据库中,然后将这些操作记录写入日志,以确保这些操作在系统故障后仍然能够恢复。如果事务失败或者回滚,系统会使用undo日志来撤销事务执行过程中对数据的任何修改,以保证数据库的一致性和完整性。

通常认为:

  • 其中redo log用于保证事务持久性;

  • undo log 则是事务原子性和隔离性实现的基础。

所以原子性实现的核心,是利用 Innodb 的 undo log 重做日志。

MySQL的七种日志

错误日志(error log)

error log主要记录MySQL在启动、关闭或者运行过程中的错误信息,在MySQL的配置文件my.cnf中,可以通过log-error=/var/log/mysqld.log 执行mysql错误日志的位置。

慢查询日志(slow query log)

  • MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。

  • 具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。long_query_time的默认值为10,意思是运行10秒以上的语句。

  • 由他来查看哪些SQL超出了我们的最大忍耐时间值,比如一条sql执行超过5秒钟,我们就算慢SQL,希望能收集超过5秒的sql,结合之前explain进行全面分析

  • 在生产环境中,如果要手工分析日志,查找、分析SQL,显然是个体力活,MySQL提供了日志分析工具mysqldumpslow。

一般查询日志(general log)

general log 记录了客户端连接信息以及执行的SQL语句信息,包括客户端何时连接了服务器、客户端发送的所有SQL以及其他事件,比如 MySQL 服务启动和关闭等等。

重写日志(redo log)

redo log 属于MySQL存储引擎InnoDB的事务日志。

回滚日志(undo log)

undo log属于逻辑日志,如其名主要起到回滚的作用,它是保证事务原子性的关键。

二进制日志(bin log)

bin log是一种数据库Server层(和什么引擎无关),以二进制形式存储在磁盘中的逻辑日志。

美团一面:聊聊MySQL的七种日志

七大日志中,其中redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础。

原子性的关键undo log

undo log名为回滚日志,是实现原子性的关键,当事务回滚时能够撤销所有已经成功执行的sql语句,他需要记录你要回滚的相应日志信息。undo log是实现原子性的关键,是当事务回滚时,能够撤销所有已经成功执行的sql语句。InnoDB实现回滚,靠的是undo log:

  • 当事务对数据库进行修改时,InnoDB会生成对应的undo log;

  • 如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。

undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:

  • (1)当你delete一条数据的时候,就需要记录这条数据的信息,回滚的时候,insert这条旧数据

  • (2)当你update一条数据的时候,就需要记录之前的旧值,回滚的时候,根据旧值执行update操作

  • (3)当年insert一条数据的时候,就需要这条记录的主键,回滚的时候,根据主键执行delete操作

以update操作为例:当事务执行update时,其生成的undo log中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到update之前的状态。undo log记录了这些回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。

ACID的 D 持久性如何实现?

ACID的持久性定义

ACID的持久性是指一旦事务提交,其所做的更改将永久保存在数据库中,即使系统发生故障也不会丢失。ACID的持久性包括两个方面:

  • 事务一旦提交,它对数据库的改变就应该是永久性的。

  • 接下来的其他操作或故障不应该对其有任何影响。

持久性的实现机制

  1. 事务日志(Redo Log):数据库系统将事务操作记录到事务日志中。这些日志包含了事务执行的所有更新操作,包括对数据页的修改。在事务提交时,数据库系统会将事务日志中的操作应用到数据库中的数据文件,确保事务的更新被永久保存下来。

  2. 数据文件持久性:数据库系统确保在将事务操作应用到数据文件之前,先将其记录到事务日志中。只有在事务日志中的操作被成功地写入到磁盘之后,数据库系统才会将这些操作应用到数据文件中。这样可以确保即使在系统崩溃时,数据库系统也可以通过重新应用事务日志来恢复到事务提交之后的状态。

  3. 写前日志(Write-Ahead Logging, WAL):一种常见的持久性实现方法是使用写前日志。在执行更新操作之前,数据库系统首先将更新操作写入到事务日志中,然后再将更新操作应用到数据库中。这样可以确保在事务提交之前,更新操作已经被记录到日志中,即使系统发生故障,也可以通过重新应用日志来恢复数据。

  4. 数据库备份:定期对数据库进行备份也是确保数据持久性的一种重要手段。通过备份,即使发生严重的系统故障,也可以通过恢复备份数据来重新构建数据库,确保数据不会永久丢失。

通过以上机制的配合,数据库系统可以确保事务提交后的更改是持久的,即使在系统崩溃或故障的情况下,也可以通过事务日志来恢复数据,从而实现持久性。通常认为,实现持久性的核心组件 是 redo log。

核心组件:redo log

redo log和undo log都属于InnoDB的事务日志。InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。这里也采用了缓存的架构, 类似CacheAside的模式。

InnoDB提供了缓存(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。

图片

Buffer Pool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。于是,redo log被引入来解决这个问题:当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。

redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。在 MySQL 中,Redo Log(重做日志)实际上是一种实现 WAL(写前日志)机制的方式。MySQL 中的 Redo Log 是用于保证事务持久性和崩溃恢复的重要组成部分。Redo Log 的基本原理如下:

  1. 记录数据变化:在执行数据更新操作时,MySQL 将这些更新操作记录到 Redo Log 中。这些记录包括对数据页的修改、索引更新等。

  2. 持久化到磁盘:提交事务之前,Redo Log 中的记录会被 写入到磁盘上的 Redo Log 文件中。这样即使在将更新操作应用到数据文件之前,相应的 Redo Log 记录已经持久化到磁盘上。

  3. 崩溃恢复:如果系统在将更新操作应用到数据文件之前发生崩溃,MySQL 可以通过重新应用 Redo Log 来恢复数据。MySQL 在启动时会检查 Redo Log 文件,如果发现未完成的事务,则会尝试回滚或者重新应用这些事务,以确保数据库状态的一致性。

Redo Log 实际上是 WAL 机制的具体实现,在 MySQL 中它用于记录事务执行过程中的数据更新操作,并确保这些操作的持久性。通过 Redo Log,MySQL 可以在系统崩溃或者故障的情况下,通过重新应用 Redo Log 来恢复数据库到事务提交之后的状态,从而保证了数据的一致性和持久性。提交事务之前,Redo Log 中的记录会被 写入到磁盘上的 Redo Log 文件中。

图片

既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:

(1)刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。

(2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。

为什么不是binlog 保证持久性?

我们知道,在MySQL中还存在binlog(二进制日志)也可以记录写操作并用于数据的恢复,但二者是有着根本的不同的:

(1)作用不同:redo log是用于crash recovery的,保证MySQL宕机也不会影响持久性;binlog是用于point-in-time recovery的,保证服务器可以基于时间点恢复数据,此外binlog还用于主从复制。

(2)层次不同:redo log是InnoDB存储引擎实现的,而binlog是MySQL的服务器层实现的,同时支持InnoDB和其他存储引擎。

(3)内容不同:redo log是物理日志,内容基于磁盘的Page;binlog的内容是二进制的,根据binlog_format参数的不同,可能基于sql语句、基于数据本身或者二者的混合。

(4)写入时机不同:binlog在事务提交时写入;redo log的写入时机相对多元:

  • 前面曾提到:当事务提交时会调用fsync对redo log进行刷盘;这是默认情况下的策略,修改innodb_flush_log_at_trx_commit参数可以改变该策略,但事务的持久性将无法保证。

  • 除了事务提交时,还有其他刷盘时机:如master thread每秒刷盘一次redo log等,这样的好处是不一定要等到commit时刷盘,commit速度大大加快。

ACID的 I 隔离性(Isolation)如何实现?

隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。与原子性、持久性等侧重于研究事务本身不同,隔离性呢,隔离性研究的是不同事务之间的相互影。

图片

隔离性可分为4不同的等级,隔离级别最高的是“可串行化”,但是性能最低。实际应用中,可串行化在现实生活中用得并不多。如何既能尽可能提高隔离性,又提高并发性能?这里分场景进行 优化,可以分为两个场景:

  • 写写隔离:(一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性

  • 读写隔离:(一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性

图片

事务并发处理的四大场景

MVCC的根本目标:提升并发能力。首先, 这里讲 事务的并发处理分为四大场景,分别是

  • 读-读

  • 写-写

  • 读-写

  • 写-读

这四种情况分别对应并发事务执行时的四种场景。

读-读场景

读-读场景即是指多个事务/线程在并发读取一个相同的数据,比如事务T1正在读取ID=16的行记录,事务T2也在读取这条记录,两个事务之间是并发执行的。MySQL执行查询语句,绝对不会对引起数据的任何变化,因此对于这种情况而言,不需要做任何操作,因为不改变数据就不会引起任何并发问题。

写-写场景

写-写场景也比较简单,也就是指多个事务之间一起对同一数据进行写操作,比如事务T1ID=16的行记录做修改操作,事务T2则对这条数据做删除操作,事务T1提交事务后想查询看一下,结果连这条数据都不见了,这也是所谓的脏写问题,也被称为更新覆盖问题,对于这个问题在所有数据库、所有隔离级别中都是零容忍的存在,最低的隔离级别也要解决这个问题。

读-写、写-读场景

读-写、写-读实际上从宏观角度来看,可以理解成同一种类型的操作,但从微观角度而言则是两种不同的情况,

  • 读-写是指一个事务先开始读,然后另一个事务则过来执行写操作,

  • 写-读则相反,主要是读、写发生的前后顺序的区别。

并发事务中同时存在读、写两类操作时,这是最容易出问题的场景,脏读、不可重复读、幻读都出自于这种场景中,当有一个事务在做写操作时,读的事务中就有可能出现这一系列问题,因此数据库才会引入各种机制解决。

各并发事务场景的解决方案

对于写-写、读-写、写-读这三类存在线程安全问题的场景,最为简单粗暴的方式,通过 加锁 的方案确保线程安全。但是,直接加锁 会导致部分的串行化、整体串行化,因此效率会下降,而MVCC机制的诞生则解决了这个问题。因此MySQL推出了MVCC机制,在读-写并存(读-写、写-读)的场景,使用局部无锁架构,提升性能。

MVCC 机制 在线程安全问题和加锁串行化之间做了一定取舍,让两者之间达到了很好的平衡,即防止了脏读、不可重复读及幻读问题的出现又无需对并发读-写事务加锁处理。

ACID的 C 一致性如何实现?

一致性基本概念

一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。

ACID的一致性是指在数据库事务执行过程中,数据库从一个一致的状态转移到另一个一致的状态。这意味着事务的执行不会破坏数据库的完整性约束,数据库的约束和规则在事务开始和结束时都保持一致。

一致性实现

可以说,一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。实现一致性的实现主要依赖于以下几个方面:

  1. 约束和规则:数据库中通常定义了一系列的约束和规则,如主键约束、外键约束、唯一性约束、默认值约束等。在事务执行过程中,数据库系统会确保所有的更新操作都符合这些约束和规则,以维护数据库的一致性。

  2. 事务的原子性:在数据库事务中,原子性确保了事务内的所有操作要么全部成功执行,要么全部失败回滚。这确保了数据库在事务执行期间保持一致状态,不会出现部分完成的情况。

  3. 并发控制:数据库系统通过并发控制机制来管理多个并发事务对数据库的访问。这确保了在并发执行的多个事务之间保持数据的一致性。例如,通过锁机制、隔离级别等方式来确保事务的隔离性和一致性。

  4. 事务日志:事务日志记录了事务执行过程中的所有操作,包括更新操作和撤销操作。事务日志可以用于在系统故障后恢复数据库到一致状态,确保事务的一致性。

  5. 数据备份和恢复:定期对数据库进行备份,并确保备份数据的完整性和一致性。在系统发生故障或者数据损坏时,可以通过备份数据来恢复数据库,确保数据的一致性。

通过以上机制的配合,数据库系统可以确保事务的执行过程中维护数据库的一致性。这些机制保证了在任何情况下,数据库都能从一个一致的状态转移到另一个一致的状态,不会破坏数据库的完整性约束和规则。

ACID特性实现的总结

  • 原子性:语句要么全执行,要么全不执行,是事务最核心的特性,事务本身就是以原子性来定义的;实现主要基于undo log

  • 持久性:保证事务提交后不会因为宕机等原因导致数据丢失;实现主要基于redo log

  • 隔离性:保证事务执行尽可能不受其他事务影响;InnoDB默认的隔离级别是RR,RR的实现主要基于锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)

  • 一致性:事务追求的最终目标,一致性的实现既需要数据库层面的保障,也需要应用层面的保障

锁机制实现写-写隔离

首先来看两个事务的写操作之间的相互影响。隔离性要求同一时刻只能有一个事务对数据进行写操作,InnoDB通过锁机制来保证这一点。锁机制的基本原理可以概括为:

  • 事务在修改数据之前,需要先获得相应的锁;

  • 获得锁之后,事务便可以修改数据;

  • 该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。

从操作的粒度进行的MySQL锁的分类

从操作的粒度可分为表级锁、行级锁和页级锁。

表级锁

每次操作锁住整张表锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在MyISAM、InnoDB、BDB 等存储引擎中。表锁的特点:

  • 开销小,加锁快

  • 不会出现死锁

  • 锁定粒度大,发生锁冲突的概率最高,并发度最低

行级锁

每次操作锁住一行数据锁定粒度最小,发生锁冲突的概率最低,并发度最高。应用在InnoDB 存储引擎中。行锁的特点:

  • 开销大,加锁慢

  • 会出现死锁

  • 锁定粒度小,发生锁冲突的概率最低,并发度最高

页级锁

每次锁定相邻的一组记录,锁定粒度界于表锁和行锁之间,开销和加锁时间界于表锁和行锁之间,并发度一般。页锁的特点:

  • 开销和加锁时间介于表锁和行锁之间

  • 会出现死锁

  • 锁定粒度介于表锁和行锁之间,并发度一般

InnoDB存储引擎三种行锁模式

InnoDB引擎行锁是通过对索引数据页上的记录加锁实现的,主要实现算法有 3 种:Record Lock、Gap Lock 和 Next-key Lock,也就是InnoDB的三种行锁模式。

  • RecordLock锁(行锁):锁定单个行记录的锁。(RecordLock锁 是记录锁,RC、RR隔离级别都支持)

  • GapLock锁:间隙锁,锁定索引记录间隙(不包括记录本身),确保索引记录的间隙不变。(GapLock是范围锁,RR隔离级别支持。RC隔离级别不支持)

  • Next-key Lock 锁(临键锁):记录锁和间隙锁组合,同时锁住数据,并且锁住数据前后范围。(记录锁+范围锁,RR隔离级别支持。RC隔离级别不支持)

记录锁(Record Locks)

记录锁, 仅仅锁住索引记录的一行,在单条索引记录上加锁。 (2)record lock锁住的永远是索引,而非记录本身,即使该表上没有任何索引,那么innodb会在后台创建一个隐藏的聚集主键索引,那么锁住的就是这个隐藏的聚集主键索引。

所以说当一条sql没有走任何索引时,那么将会在每一条聚合索引后面加X锁,这个类似于表锁,但原理上和表锁应该是完全不同的。

间隙锁(Gap Locks)

(1)区间锁, 仅仅锁住一个索引区间(开区间,不包括双端端点)。

(2)在索引记录之间的间隙中加锁,或者是在某一条索引记录之前或者之后加锁,并不包括该索引记录本身。

(3)间隙锁可用于防止幻读,保证索引间的不会被插入数据

比如在 100、10000中,间隙锁的可能值有 (∞, 100),(100, 10000),(10000, ∞),

图片

临键锁(Next-Key Locks)

(1)record lock + gap lock, 左开右闭区间。

(2)默认情况下,innodb使用next-key locks来锁定记录。select … for update

(3)但当查询的索引含有唯一属性的时候,Next-Key Lock 会进行优化,将其降级为Record Lock,即仅锁住索引本身,不是范围。

(4)Next-Key Lock在不同的场景中会退化:

图片

比如在 100、10000中,临键锁(Next-Key Locks)的可能有 (∞, 100],(100, 10000] , 这里的关键是左开右闭。

图片

读-写场景实现事务的隔离

介绍完写操作之间(写-写)的相互影响,下面,来看看读-写场景 ,如何通过MVCC实现事务隔离。

读-写场景三类问题

首先来看并发情况下,读操作可能存在的三类问题:

(1)脏读:当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据),这种现象是脏读。

(2)不可重复读:在事务A中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。

脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。

(3)幻读:在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。

不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。

事务隔离级别

SQL标准中定义了四种隔离级别,并规定了每种隔离级别下上述几个问题是否存在。目前数据库事务的隔离级别一共有 4 种。

事务的四个隔离级别:

  • 未提交读(READ UNCOMMITTED):所有事务都可以看到其他事务未提交的修改。一般很少使用;

  • 读已提交(READ COMMITTED):Oracle默认隔离级别,事务之间只能看到彼此已提交的变更修改;

  • 可重复读(REPEATABLE READ):MySQL默认隔离级别,同一事务中的多次查询会看到相同的数据行;可以解决不可重复读,但可能出现幻读;

  • 可串行化(SERIALIZABLE):最高的隔离级别,事务串行的执行,前一个事务执行完,后面的事务会执行。读取每条数据都会加锁,会导致大量的超时和锁争用问题;

隔离级别与读问题的关系如下:

图片

在实际应用中,第一种和第四种很少用:

  • 读未提交在并发时会导致很多问题,而性能相对于其他隔离级别提高却很有限,因此使用较少。

  • 可串行化强制事务串行,并发效率很低,只有当对数据一致性要求极高且可以接受没有并发时使用,因此使用也较少。

因此在大多数数据库系统中,默认的隔离级别是读已提交 或可重复读(后文简称RR)。数据库一般默认的隔离级别为 读已提交 RC ,比如 Oracle,也有一些数据的默认隔离级别为 可重复读 RR,比如 Mysql。"可重复读"(Repeatable Read)这个级别确保了对同一字段的多次读取结果是一致的,除非数据是被本身事务自己所修改。"可重复读" RR它能够防止脏读、不可重复读,但可能会遇到幻读的情况。 不过,mysql用自己的方案解决了RR 幻读的情况。

Mysql如何实现RR级隔离时,不会幻读?

一般而言,数据库的读已提交(READ COMMITTED)能够满足业务绝大部分场景了。所以, 大厂建议将 MySQL默认的Repeatable Read隔离级别,改成了RC隔离级别 , 主要是为了提升性能。可以通过如下两个命令 查看全局隔离级别 :

图片

可以通过如下两个命令 查看 本次会话的隔离级别:

图片

MVCC 无锁架构,COW思想的实现

MVCC是一种无锁架构。是COW思想的一种实现。MVCC最大的优点是读不加锁,因此读写不冲突,并发性能好。Copy-On-Write(COW,写时复制)是一种常见的并发编程思想, 主要解决 读写并发的问题。Copy-On-Write基本思想是,当多个线程需要对共享数据进行修改时,不直接在原始数据上进行操作,而是先将原始数据复制一份(即写时复制),然后在副本上进行Write。Copy-On-Write 通过操作写操作副本,引入局部无锁架构,解决并且处理之间的数据冲突,提高了并发性能。

MVCC学习圣经:一文穿透MySQL MVCC,吊打面试官

图片

MVCC机制的三个核心组件

  • 隐藏字段

  • Undo-log日志

  • ReadView

图片

核心组件1. 隐藏字段

在Innodb存储引擎中,每一行记录中都有隐藏字段

  • 在有聚簇索引的情况下每一行记录中都会隐藏3个字段,

  • 如果没有聚簇索引的情况下每一行记录中都会隐藏4个字段。

在有聚簇索引的情况下每一行记录中都会隐藏3个字段为DB_TRX_ID,DB_ROLL_PTR、deleted_bit,

  • DB_TRX_ID:记录创建这条数据上次修改它的事务 ID,

  • DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本

  • deleted_bit字段,即记录被更新或删除,这里的删除并不代表真的删除,而是将这条记录的delete flag改为true

除了上面的3个隐藏字段,没有聚簇索引还会有DB_ROW_ID这个字段。

核心组件2. undo log

在事务的ACID特性中,undo log(回滚日志)主要用于实现事务的原子性、隔离性、一致性的关键组件之一。它的主要作用包括:

  1. 事务的回滚操作

    当一个事务执行过程中发生错误或者被用户显式回滚时,数据库系统需要能够撤销该事务已经执行的操作,将数据库恢复到事务开始之前的状态。这就是回滚操作。undo log记录了事务执行过程中所做的所有修改操作的逆操作,通过undo log可以快速回滚事务所做的修改,从而保证事务的原子性。

  2. 恢复和崩溃恢复

    当数据库系统发生崩溃或者异常关闭时,可能会导致部分事务未提交的修改操作丢失或者部分已提交的修改操作未持久化到磁盘。通过undo log,数据库系统可以在恢复过程中, 将未提交的修改操作回滚,并将已提交但未持久化的修改操作重新应用到数据库中,从而保证数据库的一致性和完整性。

总的来说,undo log在数据库系统中扮演着非常重要的角色,它不仅用于实现事务的回滚操作和并发控制,还用于数据库系统的恢复和崩溃恢复。通过记录事务的修改操作和逆操作,undo log确保了数据库的原子性、隔离性和一致性,是数据库系统的关键组件之一。

MVCC 实现了自己 Copy-On-Write思想提升并发能力的时候, 也需要数据的副本,这里既然undo-log 有了那么多副本,MVCC 就借鸡生蛋, 复用 这些数据副本。所以,undo log 中的副本,可以用于实现多版本并发控制(MVCC),提升事务的并发性能。

核心组件3. read-view

那么多的数据副本,通过对比时间戳或者版本号,看到自己能看的版本?undo log保存的是一个版本链,也就是使用DB_ROLL_PTR这个字段来连接的。多个事务的 undo-log 日志副本 (数据快照),组成了一个 副本链,如下图:

图片

那么,如果多个事务并行的读写操作,每一个事务应该使用那个版本呢?MVCC 使用 一个新的组件,read-view + 一组对比规则,来计算 可见版本。read-view 有一些列的对比规则,这些规则用于确定一个事务在读取数据时,如何与数据库中的其他事务的版本号(这里其实就是事务ID)进行比较,以确定它所能看到的数据版本。

当 执行一个select语句时MVCC 会产生一致性视图read view。那么这个read view 没有记录事务的开始时间,和截止时间 , 而是换成另一种方式去记录开始时间和截止时间,换成什么方式呢:

  • read view 记录当前活跃事务 id,组成活跃事务id数组 ,这个属性的作用,哪些事务是当前事务,也是不可见的

  • read view 记录当前最小活跃事务 id,这个属性的作用,用于判断哪些事务是已经提交了的

  • read view 记录当前的下一个事务 id,这个属性的作用,用于判断哪些事务是未来事务,也是不可见的

图片

下面是mysql 的MVCC 的read view 版本对比规则, 确实也是一个非常复杂的对比逻辑。

图片

通过 上面的这个复杂的对比流程, read-view 终于确定一个事务在执行时所能看到的数据视图。

图片

快照读和当前读

  • 快照读(也称普通读,英文名:Consistent Read)

  • 当前读(也称锁定读,Locking Read)

快照读,就是读取快照数据,即快照生成的那一刻的数据。在不加锁的情况下,我们使常用的 普通的SELECT语句 就是快照读,如下:

SELECT * FROM USER WHERE ......

当前读,就是读取最新的数据,要读取最新提交的数据版本。我们在加锁SELECT语句,或者对数据进行增、删、改都会进行当前读。如下:

SELECT * FROM USER LOCK IN SHARE MODE;

SELECT * FROM USER FOR UPDATE;

INSERT INTO USER VALUES ......

DELETE FROM USER WHERE ......

UPDATE USER SET ......

在MySQL中只有在 RR 和 RC 这两个事务隔离级别下才会使用 快照读

在RR中,快照会在事务中第一次SELECT语句执行时生成,只有在本事务中对数据进行更改 才会更新快照。在RC中,每次SELECT都会重新生成一个快照,总是读取最新版本数据。

RR解决脏读、不可重复读、幻读等问题,使用的是MVCC:MVCC。快照读,就是读取快照数据,即快照生成的那一刻的数据。在同一时刻,不同的事务读取到的数据可能是不同的(即多版本 数据快照 )——在T5时刻,事务A和事务C可以读取到不同版本的数据。

图片

RR隔离级别的非加锁读

下面以RR隔离级别为例,结合前文提到的几个问题分别说明。

(1)脏读

图片

当事务A在T3时刻读取zhangsan的余额前,会生成ReadView,由于此时事务B没有提交仍然活跃,因此其事务id一定在ReadView的rw_trx_ids中,因此根据前面介绍的规则,事务B的修改对ReadView不可见。

接下来,事务A根据指针指向的undo log查询上一版本的数据,得到zhangsan的余额为100。

这样事务A就避免了脏读。

(2)不可重复读

图片

当事务A在T2时刻读取zhangsan的余额前,会生成ReadView。

此时事务B分两种情况讨论,一种是如图中所示,事务已经开始但没有提交,此时其事务id在ReadView的rw_trx_ids中;一种是事务B还没有开始,此时其事务id大于等于ReadView的low_limit_id。

无论是哪种情况,根据前面介绍的规则,事务B的修改对ReadView都不可见。

当事务A在T5时刻再次读取zhangsan的余额时,会根据T2时刻生成的ReadView对数据的可见性进行判断,从而判断出事务B的修改不可见;因此事务A根据指针指向的undo log查询上一版本的数据,得到zhangsan的余额为100,从而避免了不可重复读。

(3)幻读

图片

MVCC避免幻读的机制与避免不可重复读非常类似。

当事务A在T2时刻读取0<id<5的用户余额前,会生成ReadView。

此时事务B分两种情况讨论,一种是如图中所示,事务已经开始但没有提交,此时其事务id在ReadView的rw_trx_ids中;一种是事务B还没有开始,此时其事务id大于等于ReadView的low_limit_id。

无论是哪种情况,根据前面介绍的规则,事务B的修改对ReadView都不可见。

当事务A在T5时刻再次读取0<id<5的用户余额时,会根据T2时刻生成的ReadView对数据的可见性进行判断,从而判断出事务B的修改不可见。因此对于新插入的数据lisi(id=2),事务A根据其指针指向的undo log查询上一版本的数据,发现该数据并不存在,从而避免了幻读。

读已提交(RC)隔离级别下的非加锁读

前面介绍的MVCC,是RR隔离级别下“非加锁读”实现隔离性的方式。

(1)读已提交(RC)隔离级别下的非加锁读

RC与RR一样,都使用了MVCC,其主要区别在于:

RR是在事务开始后第一次执行select前创建ReadView,直到事务提交都不会再创建。根据前面的介绍,RR可以避免脏读、不可重复读和幻读。

RC每次执行select前都会重新建立一个新的ReadView,因此如果事务A第一次select之后,事务B对数据进行了修改并提交,那么事务A第二次select时会重新建立新的ReadView,因此事务B的修改对事务A是可见的。因此RC隔离级别可以避免脏读,但是无法避免不可重复读和幻读。

(2)加锁读与next-key lock

按照是否加锁,MySQL的读可以分为两种:

一种是非加锁读,就是快照读、一致性读,使用普通的select语句,这种情况下使用MVCC避免了脏读、不可重复读、幻读,保证了隔离性。另一种是加锁读,查询语句有所不同,如下所示:

SELECT * FROM USER LOCK IN SHARE MODE;
SELECT * FROM USER FOR UPDATE;

加锁读在查询时会对查询的数据加锁(共享锁或排它锁)。

由于锁的特性,当某事务对数据进行加锁读后,其他事务无法对数据进行写操作,因此可以避免脏读和不可重复读。而避免幻读,则需要通过next-key lock。next-key lock 是行锁的一种,实现相当于record lock(记录锁) + gap lock(间隙锁);其特点是不仅会锁住记录本身(record lock的功能),还会锁定一个范围(gap lock的功能)。

因此,加锁读同样可以避免脏读、不可重复读和幻读,保证隔离性。

隔离级别、并发性、数据一致性的三角之间关系

图片

事务隔离级别和并发性和数据一致性密切相关。不同的隔离级别提供了不同的并发性和数据一致性保证。

  1. 并发性

    • 并发性指的是数据库系统同时处理多个事务的能力。隔离级别越低,允许的并发操作越多,系统的并发性能越高。

    • 但是,过高的并发操作可能会导致事务之间的相互干扰,产生一些并发问题,如脏读、不可重复读和幻读。

  2. 数据一致性

    • 数据一致性指的是事务执行后,数据库中的数据是否保持一致性。隔离级别越高,数据一致性越好,但对并发操作的限制也越严格。

    • 高隔离级别可以防止一些并发问题的产生,如脏读、不可重复读和幻读,但会降低系统的并发性能。

Mysql如何实现RR级隔离时,不会幻读?

阿里面试:Seata 如何实现 RC ?保证事务的隔离性?

隔离性的实现总结

概括来说,InnoDB实现的RR,通过锁机制(包含next-key lock)、MVCC(隐藏列+undo log的版本链+ReadView)等,实现了一定程度的隔离性,可以满足大多数场景的需要。不过需要说明的是,RR虽然避免了幻读问题,但是毕竟不是Serializable,不能保证完全的隔离,下面是两个例子:

第一个例子,如果在事务中第一次读取采用非加锁读,第二次读取采用加锁读,则如果在两次读取之间数据发生了变化,两次读取到的结果不一样,因为加锁读时不会采用MVCC。

第二个例子,如下所示,大家可以自己验证一下。

图片

参考文献

MVCC学习圣经:一文穿透MySQL MVCC,吊打面试官

Mysql如何实现RR级隔离时,不会幻读?

美团一面:聊聊MySQL的七种日志

  • 17
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值