目录
粒度
行锁的种类
行锁的实现算法
Record Lock
Gap Lock
Next-Key Lock
锁之于隔离性
MVCC
版本链
ReadView
开始查询
RR 级别的幻读
一条SQL更新语句怎么运行
redo log
Buffer Pool
binlog
binlog 和 redo log
首页 Java Java面试题 面试官:MySQL 是如何实现 ACID 的?

面试官:MySQL 是如何实现 ACID 的?

Aug 17, 2023 pm 02:39 PM
java java面试题

在面试中,面试官只要问MySQL的ACID,然后可以立马背出来八股文来(还有部分人估计都还回答不上来)。更可恶的是,有些面试官不按套路出牌,会继续问了,MySQL到底是如何实现ACID的呢?

蒙圈了吧,实话实说,这道题能劝退95%的人。

今天,本文主要探讨MySQL InnoDB 引擎下ACID的实现原理,对于诸如什么是事务,隔离级别的含义等基础知识不做过多阐述。

ACID

MySQL 作为一个关系型数据库,以最常见的 InnoDB 引擎来说,是如何保证 ACID 的。

  • (Atomicity原子性: 事务是最小的执行单位,不允许分割。原子性确保动作要么全部完成,要么完全不起作用;
  • (Consistency)一致性: 执行事务前后,数据保持一致;
  • (Isolation)隔离性: 并发访问数据库时,一个事务不被其他事务所干扰。
  • (Durability)持久性: 一个事务被提交之后。对数据库中数据的改变是持久的,即使数据库发生故障。

隔离性

先说说隔离性,首先是四种隔离级别。

隔离级别 说明
读未提交 一个事务还没提交时,它做的变更就能被别的事务看到
读提交 一个事务提交之后,它做的变更才会被其他事务看到
可重复读 一个事务中,对同一份数据的读取结果总是相同的,无论是否有其他事务对这份数据进行操作,以及这个事务是否提交。InnoDB默认级别
串行化 事务串行化执行,每次读都需要获得表级共享锁,读写相互都会阻塞,隔离级别最高,牺牲系统并发性。

不同的隔离级别是为了解决不同的问题。也就是脏读、幻读、不可重复读。

隔离级别 脏读 不可重复读 幻读
读未提交 可以出现 可以出现 可以出现
读提交 不允许出现 可以出现 可以出现
可重复读 不允许出现 不允许出现 可以出现
序列化 不允许出现 不允许出现 不允许出现

那么不同的隔离级别,隔离性是如何实现的,为什么不同事物间能够互不干扰?答案是 锁 和 MVCC。

先来说说锁, MySQL 有多少锁。

粒度

从粒度上来说就是表锁、页锁、行锁。表锁有意向共享锁、意向排他锁、自增锁等。行锁是在引擎层由各个引擎自己实现的。但并不是所有的引擎都支持行锁,比如 MyISAM 引擎就不支持行锁。

行锁的种类

在 InnoDB 事务中,行锁通过给索引上的索引项加锁来实现。这意味着只有通过索引条件检索数据,InnoDB才使用行级锁,否则将使用表锁。行级锁定同样分为两种类型:共享锁和排他锁,以及加锁前需要先获得的意向共享锁和意向排他锁。

  • 共享锁:读锁,允许其他事务再加S锁,不允许其他事务再加X锁,即其他事务只读不可写。select...lock in share mode 加锁。select...lock in share mode 加锁。
  • 排它锁:写锁,不允许其他事务再加S锁或者X锁。insert、update、delete、for update
排它锁:写锁,不允许其他事务再加S锁或者X锁。insert、update、delete、for update加锁。

行锁是在需要的时候才加

上🎜的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。🎜🎜

行锁的实现算法

Record Lock

单个行记录上的锁,总是会去锁住索引记录。

Gap Lock

间隙锁,想一下幻读的原因,其实就是行锁只能锁住行,但新插入记录这个动作,要更新的是记录之间的“间隙”。所以加入间隙锁来解决幻读。

Next-Key Lock

Gap Lock + Record Lock, 左开又闭。

锁之于隔离性

大致介绍了下锁,可以看到。有了锁,当某事务正在写数据时,其他事务获取不到写锁,就无法写数据,一定程度上保证了事务间的隔离。但前面说,加了写锁,为什么其他事务也能读数据呢,不是获取不到读锁吗

MVCC

前面说到,有了锁,当前事务没有写锁就不能修改数据,但还是能读的,而且读的时候,即使该行数据其他事务已修改且提交,还是可以重复读到同样的值。这就是MVCC,多版本的并发控制,Multi-Version Concurrency Control。

版本链

Innodb 中行记录的存储格式,有一些额外的字段:DATA_TRX_ID和DATA_ROLL_PTR

  • DATA_TRX_ID:数据行版本号。用来标识最近对本行记录做修改的事务 id。
  • DATA_ROLL_PTR:指向该行回滚段的指针。该行记录上所有旧版本,在 undo log 中都通过链表的形式组织。

undo log : 记录数据被修改之前的日志,后面会详细说。

面试官:MySQL 是如何实现 ACID 的?

ReadView

在每一条 SQL 开始的时候被创建,有几个重要属性:

  • trx_ids: 当前系统活跃(未提交)事务版本号集合。
  • low_limit_id: 创建当前 read view 时“当前系统最大事务版本号+1”。
  • up_limit_id: 创建当前read view 时“系统正处于活跃事务最小版本号”
  • creator_trx_id: 创建当前read view的事务版本号;
面试官:MySQL 是如何实现 ACID 的?

开始查询

现在开始查询,一个 select 过来了,找到了一行数据。

  • DATA_TRX_ID

  • DATA_TRX_ID >= low_limit_id:

    说明该数据是在当前read view 创建后才产生的,数据不显示。


    • 不显示怎么办,根据 DATA_ROLL_PTR 从 undo log 中找到历史版本,找不到就空。
  • up_limit_id  <low_limit_id :就要看隔离级别了。

面试官:MySQL 是如何实现 ACID 的?

RR 级别的幻读

有了锁和 MVCC , 事务的隔离性得到解决。这里要引申一下,默认的 RR 的级别,解决了幻读吗?幻读通常针对的是 INSERT, 不可重复度则针对 UPDATE 。

事物 1 事物 2
begin begin
select * from dept
- 插入部门(名称)值(“A”)
- 提交
更新部门集名称=“B”
提交

我们期望是

id  name
1   A
2   B

实际却是

id  name
1   B
2   B

其实在 MySQL 可重复读的隔离级别中并不是完全解决了幻读的问题,而是解决了读数据情况下的幻读问题。而对于修改的操作依旧存在幻读问题,就是说 MVCC 对于幻读的解决时不彻底的。

原子性

接着说说原子性。前文有提到 undo log ,回滚日志。隔离性的MVCC其实就是依靠它来实现的,原子性也是。实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。

当事务对数据库进行修改时,InnoDB会生成对应的 undo log;如果事务执行失败或调用了 rollback,导致事务需要回滚,便可以利用 undo log 中的信息将数据回滚到修改之前的样子。undo log 属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB 会根据 undo log 的内容做与之前相反的工作:

  • 对于每个 insert,回滚时会执行 delete;
  • 对于每个 delete,回滚时会执行insert;
  • 对于每个 update,回滚时会执行一个相反的 update,把数据改回去。

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

持久性

Innnodb有很多 log,持久性靠的是 redo log。

一条SQL更新语句怎么运行

持久性肯定和写有关,MySQL 里经常说到的 WAL 技术,WAL 的全称是 Write-Ahead Logging,它的关键点就是先写日志,再写磁盘。就像小店做生意,有个粉板,有个账本,来客了先写粉板,等不忙的时候再写账本。

redo log

redo log 就是这个粉板,当有一条记录要更新时,InnoDB 引擎就会先把记录写到 redo log(并更新内存),这个时候更新就算完成了。在适当的时候,将这个操作记录更新到磁盘里面,而这个更新往往是在系统比较空闲的时候做,这就像打烊以后掌柜做的事。

redo log 有两个特点:

  • 大小固定,循环写
  • crash-safe

对于redo log 是有两阶段的:commit 和 prepare 如果不使用“两阶段提交”,数据库的状态就有可能和用它的日志恢复出来的库的状态不一致. 好了,先到这里,看看另一个。

Buffer Pool

InnoDB还提供了缓存,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宕机而丢失,从而满足了持久性要求。而且这样做还有两个优点:

  • 刷脏页是随机 IO,redo log 顺序 IO
  • 刷脏页以Page为单位,一个Page上的修改整页都要写;而redo log 只包含真正需要写入的,无效 IO 减少。

binlog

说到这,可能会疑问还有个 bin log 也是写操作并用于数据的恢复,有啥区别呢。

  • 层次:redo log 是 innoDB 引擎特有的,server 层的叫 binlog(归档日志)
  • 内容:redolog 是物理日志,记录“在某个数据页上做了什么修改”;binlog 是逻辑日志,是语句的原始逻辑,如“给 ID=2 这一行的 c 字段加 1 ”
  • 写入:redolog 循环写且写入时机较多,binlog 追加且在事务提交时写入
binlog 和 redo log

对于语句 update T set c=c+1 where ID=2;

  1. 执行器先找引擎取 ID=2 这一行。ID 是主键,直接用树搜索找到。如果 ID = 2 这一行所在数据页就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,再返回。
  2. 执行器拿到引擎给的行数据,把这个值加上 1,N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
  3. 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
  4. 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
  5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成

为什么先写 redo log 呢 ?

  • 先 redo 后 bin : binlog 丢失,少了一次更新,恢复后仍是0。
  • 先 bin 后 redo : 多了一次事务,恢复后是1。

一致性

一致性是事务追求的最终目标,前问所诉的原子性、持久性和隔离性,其实都是为了保证数据库状态的一致性。当然,上文都是数据库层面的保障,一致性的实现也需要应用层面进行保障。

也就是你的业务,比如购买操作只扣除用户的余额,不减库存,肯定无法保证状态的一致。

总结

MySQL 都很熟, ACID 也知道是个啥,但 MySQL 的 ACID 怎么实现的?

有时候,就像你知道了有 undo log、redo log 但可能并不太清楚为什么有,当知道了设计的目的,了解起来就会更加清晰了。

以上是面试官:MySQL 是如何实现 ACID 的?的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

热门话题

Laravel 教程
1602
29
PHP教程
1505
276
键盘上的音量键无法正常工作 键盘上的音量键无法正常工作 Aug 05, 2025 pm 01:54 PM

First,checkiftheFnkeysettingisinterferingbytryingboththevolumekeyaloneandFn volumekey,thentoggleFnLockwithFn Escifavailable.2.EnterBIOS/UEFIduringbootandenablefunctionkeysordisableHotkeyModetoensurevolumekeysarerecognized.3.Updateorreinstallaudiodriv

Edge PDF查看器不起作用 Edge PDF查看器不起作用 Aug 07, 2025 pm 04:36 PM

testthepdfinanotherapptoderineiftheissueiswiththefileoredge.2.enablethebuilt inpdfviewerbyTurningOff“ eflblyopenpenpenpenpenpdffilesexternally”和“ downloadpdffiles” inedgesettings.3.clearbrowsingdatainclorwearbrowsingdataincludingcookiesandcachedcachedfileresteroresoreloresorelorsolesoresolesoresolvereresoreorsolvereresoreolversorelesoresolvererverenn

Python记录到文件示例 Python记录到文件示例 Aug 04, 2025 pm 01:37 PM

Python的logging模块可通过FileHandler将日志写入文件,首先调用basicConfig配置文件处理器和格式,如设置level为INFO、使用FileHandler写入app.log;其次可添加StreamHandler实现同时输出到控制台;进阶场景可用TimedRotatingFileHandler按时间分割日志,例如设置when='midnight'实现每日生成新文件并保留7天备份,需确保日志目录存在;建议使用getLogger(__name__)创建命名logger,生产

如何在Java加入一系列字符串? 如何在Java加入一系列字符串? Aug 04, 2025 pm 12:55 PM

使用String.join()(Java8 )是连接字符串数组最简单推荐的方法,直接指定分隔符即可;2.对于旧版本Java或需要更多控制时,可使用StringBuilder手动遍历并拼接;3.StringJoiner适用于需要前缀、后缀等更灵活格式的场景;4.使用Arrays.stream()结合Collectors.joining()适合在连接前对数组进行过滤或转换等操作;综上所述,若使用Java8及以上版本,大多数情况下应首选String.join()方法,语法简洁易读,而对于复杂逻辑则推荐

python pandas造型数据框架示例 python pandas造型数据框架示例 Aug 04, 2025 pm 01:43 PM

在JupyterNotebook中使用PandasStyling可实现DataFrame的美观展示,1.使用highlight_max和highlight_min高亮每列最大值(绿色)和最小值(红色);2.通过background_gradient为数值列添加渐变背景色(如Blues或Reds)以直观显示数据大小;3.自定义函数color_score结合applymap为不同分数区间设置文字颜色(≥90绿色,80~89橙色,60~79红色,

计算的属性与VUE中的方法 计算的属性与VUE中的方法 Aug 05, 2025 am 05:21 AM

computed有缓存,依赖不变时多次访问不重新计算,而methods每次调用都执行;2.computed适用于基于响应式数据的计算,methods适合需要参数或频繁调用但结果不依赖响应式数据的场景;3.computed支持getter和setter,可实现数据的双向同步,methods不支持;4.总结:优先使用computed以提升性能,当需要传参、执行操作或避免缓存时使用methods,遵循“能用computed就不用methods”的原则。

以示例运行子过程 以示例运行子过程 Aug 06, 2025 am 09:05 AM

使用os/exec包运行子进程,通过exec.Command创建命令但不立即执行;2.使用.Output()运行命令并捕获stdout,若退出码非零则返回exec.ExitError;3.使用.Start()非阻塞启动进程,结合.StdoutPipe()实时流式输出;4.通过.StdinPipe()向进程输入数据,写入后需关闭管道并调用.Wait()等待结束;5.必须处理exec.ExitError以获取失败命令的退出码和stderr,避免僵尸进程。

Python检查路径是否为目录示例 Python检查路径是否为目录示例 Aug 04, 2025 pm 02:12 PM

最常用的方法是使用os.path.isdir()或pathlib.Path.is_dir()。1.使用os.path.isdir():importos,path="/path/to/your/directory",ifos.path.isdir(path):print("这是一个目录")else:print("这不是一个目录或路径不存在")。2.使用pathlib.Path.is_dir()(推荐):frompathlibimportP

See all articles