写点什么

探讨 MySQL 事务特性和实现原理

作者:小小怪下士
  • 2023-02-10
    湖南
  • 本文字数:3914 字

    阅读完需:约 13 分钟

一、概念

事务 一般指的是逻辑上的一组操作,或者作为单个逻辑单元执行的一系列操作,一个事务中的所有操作会被封装成一个不可分割的执行单元,这个单元的所有操作要么全部执行成功,要么全部执行失败,只要其中任意一个操作执行失败,整个事务就会执行回滚操作。

二、事务的特性以及类型介绍

2.1 事务特性

原子性(atomicity)

事务的原子性指的是构成事务的所有操作要么全部执行成功,要么全部执行是失败。

一致性(consistency)

事务的一致性指的是事务执行之前和执行之后,数据始终处于一致的状态。

隔离性(isolation)

事务的隔离性指的是并发执行的两个事务之间互不干扰,也就是说,一个事务执行的过程中是无法看到其他事务运行过程的中间状态的。


👨‍注意:MySQL是通过锁个MVCC机制来保证事务的隔离性。

持久性(duration)

事务的持久性指的是一旦事务被提交后,此事务对数据的更改操作会被持久化到数据库中,并且不会被回滚。

2.2 两种事务类型介绍

  • 本地事务

  • 分布式事务

本地事务

通常基于关系型数据库控制的事务可以称作为传统事务或者本地事务。

本地事务的执行流程


  1. 客户端开始事务操作之前,需要开启一个连接回话;

  2. 开始回话之后,客户端发起开启事务的指令;

  3. 事务开始后,客户端发送各种 SQL 语句处理数据;

  4. 正常情况下,客户端会发起提交事务的指令,如果发生异常情况,客户端会发起回滚事务命令;

  5. 上述流程完成后,关闭会话。


✔本地事务是有资源管理器在本地进行管理的。


本地事务的缺点在于:


  • 不具备分布式事务的处理能力

  • 一次事务过程只能连接一个支持事务的数据库,既不能用于多个事务性数据库。

三、并发事务会带来的哪些问题?

更新丢失(脏写)

当两个会在两个以上的事务同时操作同一行数据,并给予最初选定的值更新该行数据时,视为事务之间是无法感知彼此的存在的,所以会出现最后的更新操作会覆盖之前其他事务完成的更新操作。


举个例子:


张三的账户为 100 元,当前有两个事务:事务 1 和事务 2,事务 1 是将张三账户余额增加 100 元,事务 2 是将张三余额增加 200,起初事务 1 和事务 2 同时读到张三的账户余额都是 100 元,然后事务 1 和事务 2 分别更新张三月,假设事务 1 先于事务 2 提交,但是最近两个事务都提交后张三的余额为 300 元(正常情况应该是 400 元),也就是说:后提交的事务 2 覆盖了事务 1 的更新操作,这就是所谓的更新丢失,更新丢失(脏写)本质上是写操作的冲突,然而解决脏写的方式是让每个事务串行方式执行,保证事务按照一定的顺序执行写操作。


脏读

一个事务读取到另一个事务未提交的数据。比如:事务 1 正在对张三的余额增加 100 元,在这个事务没提交之前,另一个事务 2 读取了正在修改的这条数据,如果没有事务的控制下,第二条事务就会读取到没有被提交的脏数据,并且对脏数据丛下一步的处理,此时就会产生未提交数据的依赖关系,通常这种现象被称为 脏读,也就是说:脏读是一个事务读取了另一个事务没提交的数据。



🤔脏读本质上是读写操作的冲突,解决方法是先写后读,也就是写完之后再读。

不可重复读

一个事务读取了某些数据,在一段时间后,这个事务再次读取之前读过的数据,此时发现读取的数据发生了变化,或者其中某些数据记录已经被删除,这种现象被称为:不可重复读,即同一个事务,使用同样的查询语句,在不同时刻读取到的结果不一致。


不可重复读的本质上也是读写操作冲突,解决方法是先读后写,也就是读完之后再写。

幻读

一个事务按照相同的查询条件重新读取之前读过的数据,此时发现其他事务插入了满足当前查询条件的新数据,数据结果集变多,这种现象称为幻读,即一个事务两次读取一个范围的数据记录,两次读到的结果不同。


幻读的本质上也是读写操作冲突,解决方法是先读后写,也就是读完之后再写。


那到这里,很多小伙伴就有疑问,同样本质都是读写操作冲突,解决方法是先读后写,不可重复读和幻读到底有何区别?,我下面见简单解释一下:


1️⃣ 不可重复读的重点在于更新和删除操作,而幻读的重点在于插入操作。2️⃣ MySQL 使用锁机制实现事务的隔离级别时,在可重复读隔离级别中,SQL 语句第一个读取到数据后,会将相应的数据加锁,使得其他事务无法修改和删除这些数据,通过锁的方式实现可重复读。但是这种方法无法对新数据的插入加锁,如果事务 1 读取了数据,或者修改和删除了数据,事务 2 还可以进行插入操作,导致事务 1 莫名其妙地多了一条之前没有的数据,这就是幻读。3️⃣ 所以,幻读是无法通过锁机制来避免,需要使用串行化的事务隔离级别,但是这种事务隔离级别会大大降低数据库的并发能力。


✔MySQL 是通过 MVCC(多版本并发控制)机制来避免不可重复读和幻读的。

四、MySQL 事务的隔离级别


使用下面的命令可以查询全局级别和会话级别的事务隔离级别:


# 默认数据库的事务隔离级别为:可重复读(REPEATABLE-READ)SELECT @@global.tx_isolation;SELECT @@session.tx_isolation;SELECT @@tx_isolation;
复制代码


五、多版本并发控制 MVCC

Multi-Version Concurrency Control多版本并发控制,MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。

MVCC 比锁的优势

可以将MVCC看成行级别锁的一种妥协,它在许多情况下避免了使用锁,同时可以提供更小的开销。根据实现的不同,它可以允许非阻塞式读,在写操作进行时只锁定必要的记录。

MVCC 的实现原理

MVCC的是通过保存数据澡某个时间点的快照来实现的,也就是说,不管事务执行多长的时间,每个事务看到的数据都是一致的。根据事务的开始时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。InnonDB主要通过为每一行记录添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。但是InnoDB并不存储这些事件发生时的 实际时间 ,相反它只存储这些事件发生时的系统 版本号(version) 。这是一个随着事务的创建而不断增长的数字。每个事务在事务开始时会记录它自己的系统版本号。每个查询必须去检查每行数据的版本号与事务的版本号是否相同。


《高性能 MySQL》书籍中介绍到:

MVCC只在REPEATABLE READREAD COMMITTIED两个隔离级别下工作,其他两个隔离级别都和MVCC不兼容,因为READ UNCOMMITTED总是读取最新的数据行,而不符合当前事务版本数据行,而SERIALIZABLE串行化隔离级别则会对所有读取的行都加锁。


接下来举个例子说明在可重复读事务隔离级别下,MVCC机制是如何完成增删改查操作的。


  • 查询操作(SELECT)


在查询操作中,InnoDB 存储引擎跟根据以下两个条件查询对应的行记录,只有满足对应条件才会被返回:


1️⃣ 只查找不晚于当前事务版本的数据行,也就是说,InnoDB 存储引擎只会查找版本号小于或者等于当前事务版本的数据行,这些数据行要么在该事务开始前就存在,要么就是事务本身插入或者更新的行。2️⃣ 对于数据删除,数据行删除的版本要么还没有被定义,要么大于当前事务的版本号,只有这样才能确保事务读到的行,在事务开始前并没有被删除。


举个例子,存在 事务 A事务 B 两个事务,事务 A 中存在两套相同的SELECT语句,事务 B 存在一条UPDATE语句,事务 A 的第一条查询语句在事务 B 提交之前执行,第二条查询语句在 事务 B 提交之后执行,事务 A 如下所示:


-- 事务A操作START TRANSACTION;SELECT * FROM account WHERE id = 1;     //在事务B提交之前执行SELECT * FROM account WHERE id = 1;     //在事务B提交之后执行COMMIT;
复制代码


事务 B:


-- 事务B操作START TRANSACTION;UPDATE account SET balance = balance+100 WHERE id = 1;COMMIT;
复制代码


✔结论:如果没有使用 MVCC 机制,则事务 A 中的第一条 SELECT 语句读取的数据是修改前的数据,而第二条 SELECT 语句读取的是修改后的数据,两次读取的数据不一致,想想,那不就乱了吗😨? 如果使用了 MVCC 机制,无论事务 B 如何修改数据,事务 A 的两条查询语句的到的结果始终是一致的。


  • 插入操作(SELECT)


在插入操作中,InnoDB会将新插入的每一条行记录的当前系统版本包保存为行版本号。


比如:向account表插入一条数据,同时MVCC的两个版本号分别为create_versiondelete_versioncreate_version代表创建行的版本号,delete_version代表删除行的版本号,另外还有一个事务 ID 字段,如下面所示:


INSERT INTO account(id, name, balance) values(1001, 'austin', 100);复制代码
复制代码


对应的版本号信息如下表:



可以看出,当向数据表新增记录时,需要设置保存行的版本号,而删除行的版本号未定义。


  • 更新操作(SELECT)


在更新操作中,InnoDB存储引擎会插入一行新记录,并保存当前系统的版本号为新记录行的版本号,同时保存当前系统的版本号到原来数据行作为删除标识。比如:将 account 数据表中 id 为 1001 的用户账户月增加 100 元,对应 SQL 如下:


UPDATE account SET balance = balance+100 WHERE id = 1001;复制代码
复制代码


执行 SQL, 在 MVCC 机制下的更新操作如下表所示:



可以明显看出,执行更新操作时,MVCC机制是先将原来的数据复制一份,将balance字段增加 100 后,再讲create_version字段的值设置为当前系统的版本号,而delete_version字段的值未定义。


注意的是:原来的行会被复制到 Undo Log 中。


  • 删除操作(SELECT)


在删除操作中,InnoDB 存储引擎会保存删除的每一个行记录当前的系统版本号,作为删除标识。比如:删除 account 数据表中 id 为 1001 的数据,SQL 如下所示:


DELETE FROM account WHERE id = 1001;复制代码
复制代码


对应 MVCC 机制下的删除操作如下表所示:



可以看出,当删除数据表数据行时,MVCC 机制会将当前系统的版本号写入删除数据行版本字段 delete_version 中,以此来表示当前数据行已经被删除。

六、总结

本文主要讲述了事务的特性、类型和本地事务和分布式事务的区别,通过不同的示例解释并发事务带来的更新丢失、脏读、不可重复读、幻读的问题,同时介绍了多版本并发控制 MVCC 的实现原理。

用户头像

还未添加个人签名 2022-09-04 加入

热衷于分享java技术,一起交流学习,探讨技术。 需要Java相关资料的可以+v:xiaoyanya_1

评论

发布
暂无评论
探讨MySQL事务特性和实现原理_Java_小小怪下士_InfoQ写作社区