跳至主要內容

MySQL-原理篇-③事务

holic-x2024年6月29日...大约 27 分钟JAVAMySQL

MySQL-原理篇-③事务

学习核心

学习资料

事务特性:ACID

1.ACID 概念核心

数据库事务有四大特性: ACID --> 原子性 一致性 隔离性 持久性

InnoDB引擎通过什么技术来保证事务的这四个特性?

2.ACID 案例拆解

案例准备:以t_account表操作为例(用于存储账户信息)

CREATE TABLE t_account (
    id INT NOT NULL AUTO_INCREMENT COMMENT '自增id',
    name VARCHAR(100) COMMENT '客户名称',
    balance INT COMMENT '余额',
    PRIMARY KEY (id)
) Engine=InnoDB CHARSET=utf8;

# 初始化数据
INSERT INTO `t_account` (`id`, `name`, `balance`) VALUES ('1', '小明', '15');
INSERT INTO `t_account` (`id`, `name`, `balance`) VALUES ('2', '小红', '10');

​ 当向账户中执行存钱、取钱、转账等操作时,账户中的余额会发生变动,每一个操作都相当于现实世界中账户的一次状态转换。假设有这么一种场景:小明给小红转账10元,那么正常来说会执行两条语句:而这两条语句操作可以视为同一个事务,要确保两条语句都执行成功才能确保事务的正确性(即确保小明转出10元,小红收到10元),基于这个场景设计展开对事务ACID特性的理解

UPDATE account SET balance = balance - 10 WHERE id = 1; # 小明账户金额-10
UPDATE account SET balance = balance + 10 WHERE id = 2; # 小红账户金额+10

原子性

​ 原子性:事务在逻辑上是不可分割的操作单元,所有的语句要么都执行成功,要么都执行失败并进行撤销。

​ 场景中"小明给小红转账10元"就是一个事务,这个事务可能会拆解成若干个步骤,而这些步骤要么都成功,要么都不成功,不能存在中间的状态。如果中间的任意一个节点发生错误(例如数据库本身错误、操作系统错误、断电等异常)就会导致意想不到的结果,因此数据库的设计者为了确保这一特性,一旦中间节点出现异常,就会将已经触发了的操作恢复成未执行前的状态。

一致性

​ 一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态一致状态的含义是数据库中的数据应满足完整性约束,对照现实世界来说,如果数据库中的数据全部符合现实世界中的约束(all defined rules),则这些数据就是一致的(符合一致性)

​ 所谓约束:回归到数据库核心概念,主键约束、唯一约束、外键约束、check约束、非空约束等等,而完整性约束则包括数据类型基本约束、实体完整性、参照完整性、用户自定义完整性的概念(实际上就是对标数据库约束,确保数据完整性)

如何确保数据库中数据的一致性?(可以从两个方面切入:数据库本身的约束、程序设计编写业务代码确保数据一致性)

​ ==如何理解原子性和一致性的区别?==结合业务场景分析其特性概念:

隔离性

​ 隔离性(Isolation):隔离性是针对并发而言,用于隔离并发运行的多个事务,以避免产生相互影响。所谓的并发是指数据库服务器同时执行多个事务,如果在执行的过程中不采取有效的专门控制机制,并发事务之间会发生相互干扰

​ 假设一种场景:在"小明给小红转账10元"这个事务发生的同时,"小明发工资了收到10元"。很明显这是两个不同的事务,正常逻辑来说小明转出10元,收到10元,其总余额应该是不变的。但是基于并发事务的场景,就有可能出现下列的情况

# 事务1
UPDATE account SET balance = balance - 10 WHERE id = 1; # 语句1 小明转出10
UPDATE account SET balance = balance + 10 WHERE id = 2; # 语句2 小红收到10
# 事务2
UPDATE account SET balance = balance + 10 WHERE id = 1; # 语句3 小明收到10

​ 正常逻辑下,事务1、事务2先后执行都是符合场景的。但是如果事务1和事务2是并发的场景下,则可能出现语句1和语句3同时执行的情况,那么此时语句执行时获取到的balance都为10,假设事务1中的语句1先执行完balance变为15-10=5,事务2中的语句3紧随其后执行完balance会变为10+10=20,事务1中的语句2正常执行完结整个事务。那么此时小明更新后的balance就会固化到数据库中(balance为20),此时就会发现执行结果与预期并不一致(账户无缘无故多了10),这便是由于并发事务未隔离导致相互影响,从而衍生异常情况。

​ 这种设想只是泛化的场景,还可细化到将语句在数据库中的执行拆解为多个补充,并发事务下每个步骤都可能存在交替执行的情况(可以理解为CPU资源优先,线程通过调度抢占资源,谁抢到谁优先执行)

image-20240629174611661

持久性

​ 持久性(Durability):事务一旦提交对数据的修改就是持久性的,数据已经从内存转移到了外部服务器上,并执行了固化的步骤(之后无论数据库出什么幺蛾子都与本次转换无关,数据被永久固化到磁盘空间)

3.事务的控制

支持事务的存储引擎

​ MySQL中通过show engines命令查看存储引擎属性,确认其是否支持事务

image-20240629175204249

事务的控制

事务的隔离级别

1.并发事务导致的问题

​ 隔离性是针对并发事务而言的,通过隔离多个并发事务避免其相互影响。MySQL 服务端是允许多个客户端连接的,这意味着 MySQL 会出现同时处理多个事务的情况,那么在同时处理多个事务的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、虚读/幻读(phantom read)、丢失更新 的问题

脏读 :一个事务「读到」了另一个「已修改但未提交的数据」

​ 对于两个事务T1,T2 。 T1读取了已经被T2更新但是还没有提交的字段之后,如果T2发生了回滚,T1读取的内容就是无效的

image-20240629182054952

不可重复读:在一个事务内多次读取同一个数据,前后读取到的数据不一样

​ 对于两个事务T1,T2。T1读了一个字段,在过程中T2更新了该字段之后,T1再次读取同一个字段,两次读取的值就是不同的

image-20240629182243226

幻读(虚读):同一事务中,当同一个查询执行多次的时候,由于其他事务进行了插入操作并提交事务,导致每次返回不同的结果集

​ 对于两个事务T1,T2 T1从一个表中读取一个字段,然后T2在该表中插入一个新的行之后,如果T1再次读取这个表发现数据行数变多

image-20240629182333646

丢失更新:两个事务同时更新一行数据,一个事务对数据的更新把另一个事务对数据的更新覆盖了

2.针对并发问题设定的隔离级别

4种隔离级别

​ 并发问题严重性:脏读 > 不可重复读 > 虚读/幻读

​ 针对并发问题,SQL标准提出了四种隔离级别来规避这些现象,隔离级别越高,性能效率就越低,这四个隔离级别如下:

image-20240629183553906

隔离级别如何实现

这四种隔离级别是如何实现?

MySQL 的开启事务命令:两种命令的主要区别在于启动时机不同

(1)何为MVCC?

Read View 在MVCC中如何工作?

掌握Read View中 4 个字段的作用:creator_trx_idm_idsmin_trx_idmax_trx_id

事务ID是递增分配的(比如现在有id为1,2,3这三个事务,之后id为3的事务提交,那么一个新的读事务在生成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4)

聚簇索引记录中的两个隐藏列:

假设在账户余额表插入一条余额为 100 万的记录,然后把这两个隐藏列也画出来,该记录的整个示意图如下

image-20240629204038856

​ 在创建Read View后,可以将记录中的 trx_id 划分这三种情况:

image-20240629204127660

一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有几种情况

这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制),**所谓版本链控制是通过对比「事务的 Read View 里的字段」和「记录中的两个隐藏列」**来实现的

场景案例:设定一种场景来拆解不同隔离级别的工作原理,以及对应隔离级别可以或者不可以解决什么并发问题

​ 有一张账户余额表,里面有一条账户余额为 100 万的记录。然后有两个并发的事务,事务 A 只负责查询余额,事务 B 则会将余额改成 200 万,下面是按照时间顺序执行两个事务的行为

image-20240629205414796

​ 可重复读的实现原理是通过Read View实现的,在启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View

​ 假设事务 A (事务 id 为51)启动后,紧接着事务 B (事务 id 为52)也启动了,那这两个事务创建的 Read View 如下:

image-20240629204843485

​ 结合Read View的4个字段属性,分析Read View的具体内容:

​ 在可重复读隔离级别下,事务A、事务B按顺序执行了以下操作:

​ 拆解这一过程,分析如下:

image-20240629210404073

结合上述步骤分析,可重复读隔离级别通过启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View,搭配undo log构建MVCC,进而确保「可重复读」隔离级别下在事务期间读到的记录都是事务启动前的记录

【可重复读】隔离级别解决了脏读、不可重复读问题

(3)读已提交是如何工作的

​ 读已提交的实现原理是通过Read View实现的,在每次读取数据时,都会生成一个新的 Read View

​ 也意味着,事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务

​ 假设假设事务 A (事务 id 为51)启动后,紧接着事务 B (事务 id 为52)也启动了,接着按顺序执行了以下操作:

image-20240629212522565

image-20240629213437379

结合上述步骤分析,在读提交隔离级别下,事务每次读数据时都重新创建 Read View,那么在事务期间的多次读取同一条数据,前后两次读的数据可能会出现不一致,因为可能这期间另外一个事务修改了该记录,并提交了事务

【读已提交】隔离级别解决了脏读问题

(4)MySQL的可重复读隔离级别是否完全解决幻读问题?

​ 要解决脏读现象,就要将隔离级别升级到读已提交以上的隔离级别;要解决不可重复读现象,就要将隔离级别升级到可重复读以上的隔离级别。而对于幻读现象,不建议将隔离级别升级为串行化,因为这会导致数据库并发时性能很差。MySQL InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它很大程度上避免幻读现象,其解决方案有两种:

​ 所谓幻读:当同一个查询在不同的时间产生不同的结果集时,事务中就会出现所谓的幻象问题。例如,如果 SELECT 执行了两次,但第二次返回了第一次没有返回的行,则该行是“幻像”行

​ 举个例子,假设一个事务在 T1 时刻和 T2 时刻分别执行了下面查询语句,途中没有执行其他任何语句:只要 T1 和 T2 时刻执行产生的结果集是不相同的,那就发生了幻读的问题

SELECT * FROM t_test WHERE id > 100;
- T1 时间执行的结果是有 5 条行记录,而 T2 时间执行的结果是有 6 条行记录,那就发生了幻读的问题
- T1 时间执行的结果是有 5 条行记录,而 T2 时间执行的结果是有 4 条行记录,也是发生了幻读的问题

快照读如何避免幻读?

​ 可重复读隔离级是由 MVCC(多版本并发控制)实现的,实现的方式是开始事务后(执行 begin 语句后),在执行第一个查询语句后,会创建一个 Read View,后续的查询语句利用这个 Read View,通过这个 Read View 就可以在 undo log 版本链找到事务开始时的数据,所以事务过程中每次查询的数据都是一样的,即使中途有其他事务插入了新纪录,是查询不出来这条数据的,所以就很好了避免幻读问题

当前读如何避免幻读?

​ MySQL 里除了普通查询是快照读,其他都是当前读,比如 update、insert、delete,这些语句执行前都会查询最新版本的数据,然后再做进一步的操作

​ 另外,select ... for update 这种查询语句是当前读,每次执行的时候都是读取最新的数据。针对加锁、不加锁的场景拆解当前读的执行流程

假设select ... for update不加锁,此时事务A在T2时刻是当前读,读取的是事务B插入的记录,就会出现T1、T2前后两次查询的结果集合不同,导致出现幻读

image-20240629220507405

​ 因此,Innodb 引擎为了解决「可重复读」隔离级别使用「当前读」而造成的幻读问题,就引出了间隙锁。假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生

image-20240629220930788

​ 以上述案例进行拆解,事务 A 执行了这面这条锁定读语句后,就在对表中的记录加上 id 范围为 (2, +∞] 的 next-key lock(next-key lock 是间隙锁+记录锁的组合)。事务 B 在执行插入语句的时候,判断到插入的位置被事务 A 加了 next-key lock,于是事物 B 会生成一个插入意向锁,同时进入等待状态,直到事务 A 提交了事务。这就避免了由于事务 B 插入新记录而导致事务 A 发生幻读的现象

幻读现象并没有完全解决可重复读隔离级别下虽然很大程度上避免了幻读,但是还是没有能完全解决幻读

​ 【幻读场景1】:在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成了一个 ReadView,之后事务 B 向表中新插入了一条 id = 5 的记录并提交。接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时刻,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id,之后事务 A 再使用普通 select 语句去查询这条记录时就可以看到这条记录了,于是就发生了幻读

image-20240629221336400

​ 【幻读场景2】:快照读、当前读组合

要避免这类特殊场景下发生幻读的现象的话,就是尽量在开启事务之后,马上执行 select ... for update 这类当前读的语句,因为它会对记录加 next-key lock,从而避免其他事务插入一条新记录

评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3