阿里面试:事务ACID,底层是如何实现的?

文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :

免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领

免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取


阿里面试:事务ACID,底层是如何实现的?

尼恩特别说明: 尼恩的文章,都会在 《技术自由圈》 公号 发布, 并且维护最新版本。 如果发现图片 不可见, 请去 《技术自由圈》 公号 查找

尼恩说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,很多小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试机会,遇到很多很重要的面试题:

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

ACID靠什么保证的?

ACID的一致性是指什么?是如何实现的?

ACID中的持久性是指什么?是如何实现的?

ACID中的隔离性是指什么?是如何实现的?

最近有小伙伴在面试阿里,又遇到了MVCC相关的面试题。小伙伴 支支吾吾的说了几句,没说清楚,面试挂了。

所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。

这里,尼恩团队把MVCC/ 事务 进行了全面的梳理,穿透式的梳理,

梳理为一个PDF文档 《MVCC 学习圣经:一次穿透MYSQL MVCC 》, 并且持续迭代。

这个文档将成为大家 面试的杀手锏, 此文当最新PDF版本,可以找40岁老架构师尼恩获取。

当然,上面的面试题以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V171版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,回复:领电子书

本文作者:

  • 第一作者 Moen (负责写初稿 )
  • 第二作者 尼恩 (40岁老架构师, 负责提升此文的 技术高度,让大家有一种 俯视 技术的感觉)

什么是事务

事务(Transaction)是数据库管理系统执行过程中的一个逻辑单位,它由一个有限的数据库操作序列构成。

事务(Transaction)是访问和更新数据库的程序执行单元;

事务中可能包含一个或多个sql语句,这些语句要么都执行,要么都不执行。

这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。

事务的目的是确保数据的完整性和一致性,它通过一系列的操作,将数据库从一个一致性状态转换到另一个一致性状态。

首先回顾一下MySQL事务的基础知识。

MySQL逻辑架构和存储引擎

img

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

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

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

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

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

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

如无特殊说明,后文中描述的内容都是基于InnoDB。

innodb 一个数据库事务( transaction)的执行过程

数据库事务( transaction) 是指作为一个逻辑工作单元执行的一系列数据库操作,这些操作要么全部成功,要么全部失败,是一个不可分割的工作单元。

事务是在事务开始和事务结束之间执行的一系列数据库操作。

事务的目的是确保数据库操作的一致性和完整性,同时提供对并发访问的隔离性和恢复能力。

图解:一个简化版的执行过程

下面是一个简化版的执行过程

图片

注意:请点击图像以查看清晰的视图!

图解:一个详细版的一个数据库事务( 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. 更新内存数据,包括 Buffer Pool和 Redo Log Buffer

接下来,InnoDB会在缓冲池中更新数据。

这意味着,当执行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事务

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

  1. 开始事务:使用 START TRANSACTIONBEGIN 命令来开始一个新的事务。这将确保接下来的操作将被视为一个事务单元,并且要么全部成功提交,要么全部失败回滚。
  2. 执行SQL操作:在事务中执行各种SQL操作,例如插入、更新或删除数据,查询等。这些操作可能会涉及一个或多个数据库表。
  3. 数据处理与验证:在执行SQL操作之前或之后,进行数据处理和验证。这可能包括检查约束条件、验证输入数据的有效性等。
  4. 提交或回滚事务:如果所有的数据库操作都成功,并且通过了数据验证步骤,那么事务可以被提交(COMMIT)。这将导致所有的更改永久地应用到数据库中。如果在任何时候出现了错误或者违反了事务的约束条件,那么整个事务将会被回滚(ROLLBACK),所有的更新将被撤销,数据库将恢复到事务开始之前的状态。
  5. 结束事务:一旦事务被提交或者回滚,事务就结束了。可以使用 COMMITROLLBACK 命令来结束事务。此时数据库会释放任何由事务占用的资源。

以下是一个典型的MySQL事务的示例:

START TRANSACTION;

-- 执行SQL操作
INSERT INTO orders (customer_id, total_amount) VALUES (123, 100);
UPDATE customers SET points = points + 10 WHERE id = 123;

-- 数据处理与验证
IF (SELECT total_amount FROM orders WHERE id = LAST_INSERT_ID()) > 50 THEN
    COMMIT;
ELSE
    ROLLBACK;
END IF;

-- 提交或回滚事务
COMMIT;

在这个示例中,事务首先通过 START TRANSACTION 开始,然后执行一系列SQL操作,包括向 orders 表中插入一条新的订单记录和更新 customers 表中客户的积分。接着进行了数据处理和验证,如果订单的总金额大于50,则提交事务,否则回滚事务。最后使用 COMMIT 命令提交事务。

典型的MySQL事务是如下操作的:

START TRANSACTION;

-- 执行SQL操作

-- 提交
COMMIT;

其中start transaction标识事务开始,commit提交事务,将执行结果写入到数据库。

如果sql语句执行出现问题,会调用rollback,回滚所有已经执行成功的sql语句。

START TRANSACTION;

-- 执行SQL操作

-- 回滚
ROLLBACK;

当然,也可以在事务中直接使用rollback语句进行回滚。

MySQL自动提交

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

img

在自动提交模式下,如果没有 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是衡量事务的四个特性:

  • 原子性(Atomicity,或称不可分割性)
  • 一致性(Consistency)
  • 隔离性(Isolation)
  • 持久性(Durability)

事务通常具有以下四个特性,也被称为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的事务日志。

下面看看一下redo log存在的背景。

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的服务器层(可以参考文章前面对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 机制 在线程安全问题和加锁串行化之间做了一定取舍,让两者之间达到了很好的平衡,即防止了脏读、不可重复读及幻读问题的出现又无需对并发读-写事务加锁处理。

首先,来看看写-写场景 ,如何通过锁机制实现事务隔离。

锁机制实现写-写隔离

首先来看两个事务的写操作之间的相互影响。

隔离性要求同一时刻只能有一个事务对数据进行写操作,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)

(1)记录锁, 仅仅锁住索引记录的一行,在单条索引记录上加锁。
(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] , 40岁老架构师尼恩提示,这里的关键是左开右闭

具体的讲解,请参见《尼恩Java面试宝典》配套视频。

读-写场景,如何实现事务的隔离?

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

读-写场景三类问题: 脏读、不可重复读和幻读

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

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

脏读举例如下(以账户余额表为例):

img

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

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

不可重复读举例如下:

img

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

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

幻读举例如下:

img

以上内容如果不懂,请参考后面的《尼恩Java面试宝典》 配套视频

事务隔离级别

SQL标准中定义了四种隔离级别,并规定了每种隔离级别下上述几个问题是否存在。

先来回顾一下,数据库事务的隔离级别,目前数据库事务的隔离级别一共有 4 种,由低到高分别为:

事务的四个隔离级别:

  • 未提交读(READ UNCOMMITTED):所有事务都可以看到其他事务未提交的修改。一般很少使用;
  • 读已提交(READ COMMITTED):Oracle默认隔离级别,事务之间只能看到彼此已提交的变更修改;
  • 可重复读(REPEATABLE READ):MySQL默认隔离级别,同一事务中的多次查询会看到相同的数据行;可以解决不可重复读,但可能出现幻读;
  • 可串行化(SERIALIZABLE):最高的隔离级别,事务串行的执行,前一个事务执行完,后面的事务会执行。读取每条数据都会加锁,会导致大量的超时和锁争用问题;

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

img

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

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

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

因此在大多数数据库系统中,默认的隔离级别是读已提交可重复读(后文简称RR)

数据库一般默认的隔离级别为 读已提交 RC ,比如 Oracle,

也有一些数据的默认隔离级别为 可重复读 RR,比如 Mysql。

"可重复读"(Repeatable Read)这个级别确保了对同一字段的多次读取结果是一致的,除非数据是被本身事务自己所修改。

"可重复读" RR它能够防止脏读、不可重复读,但可能会遇到幻读的情况。 不过,mysql用自己的方案解决了RR 幻读的情况 , 参考 文章:

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

一般而言,数据库的读已提交(READ COMMITTED)能够满足业务绝大部分场景了。

所以, 大厂建议将 MySQL默认的Repeatable Read隔离级别,改成了RC隔离级别 , 主要是为了提升性能, 具体请参考 《尼恩Java 面试宝典》 MYSQL 专题:

在这里插入图片描述

可以通过如下两个命令 查看全局隔离级别 :

img

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

img

MVCC是 无锁架构:是COW思想的一种实现

MVCC是一种无锁架构。

MVCC最大的优点是读不加锁,因此读写不冲突,并发性能好。

Copy-On-Write(COW,写时复制)是一种常见的并发编程思想, 主要解决 读写并发的问题。

Copy-On-Write基本思想是,当多个线程需要对共享数据进行修改时,不直接在原始数据上进行操作,而是先将原始数据复制一份(即写时复制),然后在副本上进行Write。

Copy-On-Write 通过操作写操作副本,引入局部无锁架构,解决并且处理之间的数据冲突,提高了并发性能。

关于 COW思想 的介绍,请参见 尼恩的 另一篇文章:

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

以上内容如果不懂,请参考后面的《尼恩Java面试宝典》 配套视频

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这个字段。

40岁老架构师尼恩提是:隐藏字段的细节,稍后详细介绍。

核心组件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,这个属性的作用,用于判断哪些事务是未来事务,也是不可见的

以上内容如果不懂,请参考后面的《尼恩Java面试宝典》 配套视频

注意,上面是尼恩为大家总结和归纳的,比较清晰好记, mysql 的MVCC 版本的对比规则, 看上去非常、非常复杂。

下面是mysql 的MVCC 的read view 版本对比规则, 确实也是一个非常复杂的对比逻辑, 很多小伙伴傻傻看不懂, 并且背诵了半天还记不住,非常痛苦。

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

但是上面这种图很复杂,网上有上万篇文章 抄来抄去, 没有一篇文章做了总结和简化。

这个对比规则 逻辑复杂,导致尽管大家看了那些文章,甚至看了很多视频,还是不能理解透彻, 迷迷糊糊的,面试的时候 说不清楚,也很容易忘了。

尼恩团队看不下去,用咱们的雄厚技术实力(洪荒之力), 给大家来总结和简化。 具体如下:

此图,是全网的第一张彻底穿透式的解读 MVCC的对比规则的图。

通过此文,尼恩团队 第一次,帮助大家搞清楚复杂的 MVCC的底层原理。

此图很容易理解,很容易记忆。

大家可收藏起来, 面试之前复习一下,一定能吊打面试官。不对,是吊死面试官。

以上内容如果不懂,请参考后面的《尼恩Java面试宝典》 配套视频

和上面的图类似, 尼恩团队用 深厚的架构功力 把一些其他的复杂的问题做了穿透式、起底式的分析, 比如:

  • 比如Netty的内存池和对象池(那个超级难,很多人穷其一生都搞不懂),
  • 比如DDD的建模和落地,
  • 比如Caffeine的底层架构,
  • 比如高性能葵花宝典
  • 比如 Thread Local 学习圣经
  • 等等等等。

这个技术难题一旦掌握,大家内力猛涨。 所以,建议大家去看看尼恩的这些核心内容。

扯远了,言归正传。

快照读和当前读

  • 快照读(也称普通读,英文名: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可以读取到不同版本的数据。

img

RR隔离级别的非加锁读

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

(1)脏读

img

当事务A在T3时刻读取zhangsan的余额前,会生成ReadView,由于此时事务B没有提交仍然活跃,因此其事务id一定在ReadView的rw_trx_ids中,

因此根据前面介绍的规则,事务B的修改对ReadView不可见。

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

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

(2)不可重复读

img

当事务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)幻读

img

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的功能)。

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

以上内容如果不懂,请参考后面的《尼恩Java面试宝典》 配套视频

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

一图胜千言,40岁老架构师用一张图,给大家总结一下 事务隔离级别、并发性、数据一致性的三角之间关系:

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

  1. 并发性
    • 并发性指的是数据库系统同时处理多个事务的能力。隔离级别越低,允许的并发操作越多,系统的并发性能越高。
    • 但是,过高的并发操作可能会导致事务之间的相互干扰,产生一些并发问题,如脏读、不可重复读和幻读。
  2. 数据一致性
    • 数据一致性指的是事务执行后,数据库中的数据是否保持一致性。隔离级别越高,数据一致性越好,但对并发操作的限制也越严格。
    • 高隔离级别可以防止一些并发问题的产生,如脏读、不可重复读和幻读,但会降低系统的并发性能。

参考阅读一下 尼恩的几篇相关文章:

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

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

以上内容如果不懂,请参考后面的《尼恩Java面试宝典》 配套视频

隔离性的实现总结

概括来说,InnoDB实现的RR,通过锁机制(包含next-key lock)、MVCC(隐藏列+undo log的版本链+ReadView)等,实现了一定程度的隔离性,可以满足大多数场景的需要。

不过需要说明的是,RR虽然避免了幻读问题,但是毕竟不是Serializable,不能保证完全的隔离,下面是两个例子:

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

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

img

ACID的 C 一致性如何实现?

一致性基本概念

一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。

数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。

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

一致性实现

可以说,一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。

此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。

实现一致性的实现主要依赖于以下几个方面:

  1. 约束和规则:数据库中通常定义了一系列的约束和规则,如主键约束、外键约束、唯一性约束、默认值约束等。在事务执行过程中,数据库系统会确保所有的更新操作都符合这些约束和规则,以维护数据库的一致性。
  2. 事务的原子性:在数据库事务中,原子性确保了事务内的所有操作要么全部成功执行,要么全部失败回滚。这确保了数据库在事务执行期间保持一致状态,不会出现部分完成的情况。
  3. 并发控制:数据库系统通过并发控制机制来管理多个并发事务对数据库的访问。这确保了在并发执行的多个事务之间保持数据的一致性。例如,通过锁机制、隔离级别等方式来确保事务的隔离性和一致性。
  4. 事务日志:事务日志记录了事务执行过程中的所有操作,包括更新操作和撤销操作。事务日志可以用于在系统故障后恢复数据库到一致状态,确保事务的一致性。
  5. 数据备份和恢复:定期对数据库进行备份,并确保备份数据的完整性和一致性。在系统发生故障或者数据损坏时,可以通过备份数据来恢复数据库,确保数据的一致性。

通过以上机制的配合,数据库系统可以确保事务的执行过程中维护数据库的一致性。

这些机制保证了在任何情况下,数据库都能从一个一致的状态转移到另一个一致的状态,不会破坏数据库的完整性约束和规则。

ACID特性实现的总结

下面总结一下ACID特性及其实现原理:

  • 原子性:语句要么全执行,要么全不执行,是事务最核心的特性,事务本身就是以原子性来定义的;实现主要基于undo log
  • 持久性:保证事务提交后不会因为宕机等原因导致数据丢失;实现主要基于redo log
  • 隔离性:保证事务执行尽可能不受其他事务影响;InnoDB默认的隔离级别是RR,RR的实现主要基于锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)
  • 一致性:事务追求的最终目标,一致性的实现既需要数据库层面的保障,也需要应用层面的保障

以上内容如果不懂,请参考后面的《尼恩Java面试宝典》 配套视频

参考文献

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

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

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

说在最后:有问题找老架构取经

MVCC 、ACID如何实现相关的面试题,是非常常见的面试题。也是核心面试题。

以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。

在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典》V174,在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。

另外,如果没有面试机会,可以找尼恩来帮扶、领路。尼恩已经指导了大量的就业困难的小伙伴上岸.

前段时间,帮助一个40岁+就业困难小伙伴拿到了一个年薪100W的offer,小伙伴实现了 逆天改命

另外,尼恩也给一线企业提供 《DDD 的架构落地》企业内部培训,目前给不少企业做过内部的咨询和培训,效果非常好。

![img/10001.jpg)

技术自由的实现路径:

实现你的 架构自由:

吃透8图1模板,人人可以做架构

10Wqps评论中台,如何架构?B站是这么做的!!!

阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了

峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?

100亿级订单怎么调度,来一个大厂的极品方案

2个大厂 100亿级 超大流量 红包 架构方案

… 更多架构文章,正在添加中

实现你的 响应式 自由:

响应式圣经:10W字,实现Spring响应式编程自由

这是老版本 《Flux、Mono、Reactor 实战(史上最全)

实现你的 spring cloud 自由:

Spring cloud Alibaba 学习圣经》 PDF

分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)

一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)

实现你的 linux 自由:

Linux命令大全:2W多字,一次实现Linux自由

实现你的 网络 自由:

TCP协议详解 (史上最全)

网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!

实现你的 分布式锁 自由:

Redis分布式锁(图解 - 秒懂 - 史上最全)

Zookeeper 分布式锁 - 图解 - 秒懂

实现你的 王者组件 自由:

队列之王: Disruptor 原理、架构、源码 一文穿透

缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)

缓存之王:Caffeine 的使用(史上最全)

Java Agent 探针、字节码增强 ByteBuddy(史上最全)

实现你的 面试题 自由:

4800页《尼恩Java面试宝典 》 40个专题

免费获取11个技术圣经PDF:

posted @ 2024-05-06 03:16  疯狂创客圈  阅读(117)  评论(0编辑  收藏  举报