关于java:MySQL-是如何实现-ACID-的

1次阅读

共计 4936 个字符,预计需要花费 13 分钟才能阅读完成。

起源:https://llc687.top/131.html

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

ACID

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

  • (Atomicity原子性: 事务是最小的执行单位,不容许宰割。原子性确保动作要么全副实现,要么齐全不起作用;
  • (Consistency)一致性: 执行事务前后,数据保持一致;
  • (Isolation)隔离性: 并发拜访数据库时,一个事务不被其余事务所烦扰。
  • (Durability)持久性: 一个事务被提交之后。对数据库中数据的扭转是长久的,即便数据库产生故障。

隔离性

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

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

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

隔离级别 脏读 不可反复读 幻读
读未提交 能够呈现 能够呈现 能够呈现
读提交 不容许呈现 能够呈现 能够呈现
可反复读 不容许呈现 不容许呈现 能够呈现
序列化 不容许呈现 不容许呈现 不容许呈现

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

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

粒度

从粒度上来说就是表锁、页锁、行锁。
表锁有动向共享锁、动向排他锁、自增锁等。
行锁是在引擎层由各个引擎本人实现的。但并不是所有的引擎都反对行锁,比方 MyISAM 引擎就不反对行锁。

行锁的品种

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

  • 共享锁:读锁,容许其余事务再加 S 锁,不容许其余事务再加 X 锁,即其余事务只读不可写。select...lock in share mode 加锁。
  • 排它锁:写锁,不容许其余事务再加 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 系列面试题和答案全副整顿好了,微信搜寻​Java 技术栈,在后盾发送:面试,​能够在线浏览。

ReadView

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

  • trx_ids: 以后零碎沉闷 (未提交) 事务版本号汇合。
  • low_limit_id: 创立以后 read view 时“以后零碎最大 事务版本号+1”。
  • up_limit_id: 创立以后 read view 时“零碎正处于 沉闷事务 最小版本号”
  • creator_trx_id: 创立以后 read view 的事务版本号;

开始查问

当初开始查问,一个 select 过去了,找到了一行数据。

  • DATA_TRX_ID <up_limit_id:阐明数据在以后事务之前就存在了,显示。
  • DATA_TRX_ID >= low_limit_id:

    阐明该数据是在以后 read view 创立后才产生的,数据不显示。

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

RR 级别的幻读

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

事物 1 事物 2
begin begin
select * from dept
insert into dept(name) values(“A”)
commit
update dept set name=”B”
commit

咱们冀望是

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 但可能并不太分明为什么有,当晓得了设计的目标,理解起来就会更加清晰了。另外,关注公众号 Java 技术栈,在后盾回复:面试,能够获取我整顿的 Java 系列面试题和答案,十分齐全。

参考:

https://zhuanlan.zhihu.com/p/…

https://learnku.com/articles/…

https://www.cnblogs.com/rjzhe…

https://www.cnblogs.com/kisme…

近期热文举荐:

1.1,000+ 道 Java 面试题及答案整顿(2021 最新版)

2. 别在再满屏的 if/ else 了,试试策略模式,真香!!

3. 卧槽!Java 中的 xx ≠ null 是什么新语法?

4.Spring Boot 2.5 重磅公布,光明模式太炸了!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0