MySQL -- 深入剖析事务底层原理

目录

[1. 什么是事务](#1. 什么是事务)

[1.1 事务的概念](#1.1 事务的概念)

[1.2 事务的特性](#1.2 事务的特性)

[2. 为什么需要事务](#2. 为什么需要事务)

[3. 事物的版本支持](#3. 事物的版本支持)

[4. 事务的提交方式](#4. 事务的提交方式)

[5. 事务常见操作方式](#5. 事务常见操作方式)

[6. 事务异常情况验证与结论](#6. 事务异常情况验证与结论)

[6.1 验证](#6.1 验证)

[6.2 结论](#6.2 结论)

[7. 事务隔离级别](#7. 事务隔离级别)

[7.1 理解隔离性](#7.1 理解隔离性)

[7.2 隔离级别](#7.2 隔离级别)

[7.3 查看与设置隔离性](#7.3 查看与设置隔离性)

[7.4 读未提交](#7.4 读未提交)

[7.5 读提交](#7.5 读提交)

[7.6 可重复读](#7.6 可重复读)

[7.7 串行化](#7.7 串行化)

[7.8 总结](#7.8 总结)

[7.9 幻读是怎么解决的](#7.9 幻读是怎么解决的)


1. 什么是事务

1.1 事务的概念

事务是一组逻辑操作单元,这组操作要么全部成功,要么全部失败。它通常由一条或者多条sql语句组成,在数据库看来,它们是一个整体。

什么叫有业务逻辑相关性?举个例子:向别人转账的时候,首先需要将发起人的账号减少一百块,然后在接收人的账户中增加一百块。这两句update的sql语句就是事务。

1.2 事务的特性

由于MsSQL服务器可能会被多个服务器同时访问,也就是说会并发进行多个事务,所以要求事务能够满足以下四大特性:

1)原子性 :当一个事务开始以后,只有成功和失败,不会处于中间态。案例: 银行转账。A 账户扣款和 B 账户入账,必须同时成功,或者如果中途失败,数据必须完全回滚到转账前的状态。

**Undolog(回滚日志)**保证了事务的原子性。

2)隔离性 :数据库允许多个事务同事对数据进行读写和修改的能力,隔离性可以防止多个事务在并发执行的时候由于交叉进行而导致的数据不一致。隔离性分为不同的等级,包括读未提交(Read Uncommit),读提交(Read Comit),可重复读(Repeatable Read),串行化((Serializable)

锁和MVCC(多版本并发控制)保证了隔离性。

3)持久性:事务处理结束以后,对数据的修改就是永久的,即使发生故障也不会丢失。

**Redo Log (重做日志)**保证了持久性。即使数据库宕机,重启后也能根据 Redo Log 恢复数据。

4)一致性:事务执行前后必须保持合法性和完整性。

如何更加深层次的理解?

从数据库层面:字段约束没有失效,例如外键约束依然成立;字段定义了是NOT NULL的,执行完不能是NULL。

从业务层面:比如转账,A减了100,B加了100,整个系统的总金额在事务前后应该是不变的。如果A减了100,B没收到100,虽然原子性保证了回滚,但是某种逻辑错误导致钱凭空消失就破坏了一致性。一致性确保了逻辑上的守恒(如A-100必须对应B+100),如果业务逻辑写错导致总额不对,那就是破坏了一致性。

一致性是事务的最终目标,原子性,隔离性,持久性都是为了实现一致性而存在的。

上面的四个属性可以简称为

ACID原子性(Atomicity,或称不可分割性)

一致性(Consistency)

隔离性(Isolation,又称独立性)

持久性(Durability)。

2. 为什么需要事务

事务实际上在MySQL数据库使用了一段时间以后由于生产需要而产生的。事务可以使上层的编程模型简化,不需要程序员再去考虑潜在的错误和一些并发问题。事务要么做完,要么不做,即使发生网络错误,服务器宕机这也不需要程序员去考虑了。事务本质上就是为了应用层服务的,不是伴随服务器产生的。

3. 事物的版本支持

可以使用 show engins 查询引擎是否支持事务。

在MySQL中只有InnoDB支持事务,MyISAM不支持。

4. 事务的提交方式

事务的提交方式分为手动提交和自动提交。

可以使用语句 **show variables like 'autocommit'**来查询是否开启了自动提交。

MySQL默认是开启自动提交的,所以我们平时的单句的sql语句都是自动提交的。

可以使用SET来改变事务的自动提交模式。

设置autocommit=0 禁止自动提交

设置autocommit=1 开启自动提交

5. 事务常见操作方式

首先创建一个表格方便操作:

事务的开启、回滚、与提交操作

在读未提交的隔离级别中,当前事务A对数据的操作事务B都是可见的,在事务中begin代表一个事务的开启,commit提交为一个事务的结束。在事务期间,可以使用rollback进行回滚操作,如果没有设置回滚点就会回滚到begin,刚开始这个事务时数据的状态,这里回滚事务B也能看到数据的变化。

6. 事务异常情况验证与结论

6.1 验证

这里验证在并发事务同时运行的时候,当一个事务在执行过程中如果遇到异常情况,此时会自动回滚,刚刚对数据做出的修改全部复原,这是事务的原子性保证的。

这里演示了当事务A插入了两条数据并且提交以后此时数据库崩溃异常退出,在事务B中查询的时候能看到这两个数据已经成功插入了,这是事务的持久性保证的,一旦事务提交以后,无论发生什么异常,数据都不会改变。

这里验证了单条sql语句也是事务,平时的单条事务是自动提交的,此时把自动提交关闭,当插入数据以后数据库奔溃,事务A还没有提交,进行回滚操作,在事务B中刚插入的数据被删除了。

这里验证了当自动提交打开以后,单条事务在执行以后就会自动被提交,奔溃了数据也不会回滚,数据已经持久化。

6.2 结论

1)事务在进行begin开启以后必须要commit提交数据才会持久化。

2)事务执行过程中可以手动回滚,如果遇到异常MySQL会自动回滚,保证原子性。

3)默认打开自动提交时影响的是单条sql语句组成的事务,begin开启的多条语句的事务的体积与是否打开自动提交无关。

4)事务被提交以后无法进行回滚,因为数据已经持久化了。

5)InnoDB支持事务,MyISAM不支持。

7. 事务隔离级别

7.1 理解隔离性

在MySQL服务器中可能有多个事务在并发的运行,每个事务可能是由多个sql语句组成的,此时执行中的各个事务就可能互相影响,比如事务A需要修改数据,事务B要删除数据,此时就会出现冲突,有了隔离性 将它们能看到的数据通过隔离级别分开来解决这样互相影响的情况。

隔离性:保证事务在执行中尽量不会受影响。

隔离级别:允许事务受不同程度的干扰。

7.2 隔离级别

读未提交【Read Uncommitted】:在这个隔离级别中,所有事务都可以看到其他事务没有提交的执行结果,实际生产中不会使用这种级别,相当于没有隔离性。这种隔离级别会造成脏读,幻读,不可重复读等并发问题。

**读提交【Read Committed】:**不允许事务看到其他事务还没有提交的更改,但是还是会有幻读现象。

**可重复读【Repeatable Read】:**当前事务没有提交时不允许看到其他没有提交事务的更改。它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行,解决了幻读问题。

**串行化【Serializable】:**这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突, 从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,。但是可能会导致超时和锁竞争。

7.3 查看与设置隔离性

使用以下语句可以查询全局隔离级别和会话的隔离级别。会话隔离级别只在当前会话生效,默认与全局隔离级别一致。

设置会话A的会话隔离级别为串行化,会话B不影响

设置会话A的隔离级别,会话B会受影响,注意,会话B此处已经重启了,全局隔离级别需要另外一个客户端重启才会受影响。

7.4 读未提交

此处事务A进行更新id1为的数据,此时没有提交,事务B也可以看到对应的更新。

一个事务在执行中可以看到另外一个事务未commit的数据,这种现象叫做脏读。

7.5 读提交

此处事务A插入数据以后事务B第一时间没有看到更新,当事务A提交以后事务B再去查找就看到了。

一个事务在另外一个事务提交前后看到两种不同状态的数据,这叫不可重复读。

7.6 可重复读

此处无论事务B在什么时候都看不到事务A的更新,只有提交事务B以后再去查才看到数据更新。

还有一种特殊情况,此时如果事务B进来什么动作也没有做,当事务A提交以后再去查看,此时是能看到事务A更新的数据。

这种情况与Read View有关,下文会进行讲解。

7.7 串行化

串行化第一个执行的事务会持有锁,其他的事务此时如果进行CURD都会被阻塞住,直到第一个事务提交。这人加锁保证了事务之间绝对不会相互干扰,但是极大降低了效率。

事务B提交,事务A进行插入

7.8 总结

1)上述的隔离级别越高,事务之间互相影响就越小,但是数据库的并发性能就越低,MySQL采取了较为合适的隔离级别 -- 可重读读。

2)不可重复读 (Non-Repeatable Read) 关注的是数据的修改 (UPDATE) 或 **删除 (DELETE),**会导致一个事务在多次查找出来的数据不一致,在某些场景下是不允许的。

3)幻读 (Phantom Read) 关注的是数据的**新增 (INSERT),**同样的查询,第一次和第二次查询出来的结果不一样。幻读在MySQL的可重复读中不存在,通过间隙锁解决了。

4)事务有长短且执行时间不相同,我们一般不需要更改MySQL默认的隔离级别,可重复读可以在大多数场景下保证数据安全性。

幻读和不可重复读的区别:

|----------|-----------------------------|--------------------------------------------------|
| 特性 | 不可重复读 | 幻读 |
| 侧重点 | 修改 (Update)、删除 (Delete) | 新增 (Insert) |
| 锁定对象 | 行锁 (Row Lock) | 间隙锁 (Gap Lock) / 范围锁 |
| 解决方法 | 只需要锁住满足条件的那几行记录,防止别人修改。 | 仅仅锁住行是不够的(因为新插入的数据还没有行),需要锁住行与行之间的间隙,防止别人插入。 |

7.9 幻读是怎么解决的

为了防止幻读(防止别人插入数据),MySQL 必须锁住"行与行之间的空隙 ",使用Next-Key Lock锁

MySQL中有三种锁:

  • Record Lock(记录锁): 锁住具体的索引项(比如锁住 id=10 这一行)。

  • Gap Lock(间隙锁): 锁住两个索引项之间的"空隙"(不包含记录本身),专门防插入。

  • Next-Key Lock(临键锁): Gap Lock + Record Lock 。即锁住记录本身,同时锁住它前面的间隙。

    • 数学表示为:左开右闭区间(1,10]

案例:

目前有一个表,其中只有一列id为主键,且当前数据为10,20,30。

当前间隙为(负无穷,10],(10,20],(20,30],(30,正无穷)。

此时事务A进行当前读:SELECT ID FROM T WHERE ID > 12 AND ID < 25 FOR UPDATE;

MySQL此时找到第一个满足条件的数据20,然后锁住(10,20],继续向右扫描找符号<25的数据,但是此时找到了30,后续不会有满足条件的了,停止查找,为了防止有人在21-25进行插入,所以此时会对(20,30]也进行加锁,25-30是被误伤的!

总结一个"万能公式"

MySQL 的加锁规则其实很简单,只有一条核心逻辑:

"扫描到哪里,锁就加到哪里。"

  • 什么时候停止扫描? 直到遇见第一条不满足条件的记录为止。
相关推荐
汗流浃背了吧,老弟!3 小时前
ChatGLM-整合数据库
数据库·langchain
JavaBoy_XJ3 小时前
Mysql在 Spring Boot 项目中的完整配置指南
数据库·spring boot·mysql·mysql配置
paopaokaka_luck3 小时前
基于SpringBoot+Vue的高校心理健康服务平台(websocket实时聊天、AI分析、Echarts图形化分析)
vue.js·spring boot·websocket·mysql·echarts
云和数据.ChenGuang3 小时前
openEuler 上安装与部署 Redis 运维教程
运维·数据库·redis·运维工程师·运维技术
元气满满-樱3 小时前
MySql源码安装
数据库·mysql·adb
妮妮喔妮3 小时前
Redis Cluster故障处理机制
java·数据库·redis
Elastic 中国社区官方博客3 小时前
AutoOps 实际应用:调查 ECK 上的 Elasticsearch 集群性能
大数据·数据库·elasticsearch·搜索引擎·全文检索
欧克小奥3 小时前
Redis单节点分片集群实现
数据库·redis·缓存
霑潇雨3 小时前
题解 | 统计每个产品的销售情况
数据库·sql·笔试·牛客