文章目录
- 什么是事务
- 为什么会出现事务
- 事务的版本支持
- 事务的提交方式
- 事务常见操作方式
- 正常演示 - 证明事务的开始与回滚
- 非正常演示1 - 证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)
- 非正常演示2 - 证明commit了,客户端崩溃,MySQL数据不会在受影响,已经持久化
- 非正常演示3 - 对比试验。begin会自动更改提交方式
- 非正常演示4 - 证明单条 SQL 与事务的关系
- 事务的隔离级别
- 查看与设置隔离性
- 读未提交(Read Uncommitted)
- 读提交(Read Committed)
- 可重复读(Repeatable Read)
- 串行化(Serializable)
- 一致性
- 多版本并发控制MVCC
- 3个记录隐藏列字段
- undo日志
- 快照
- Read View
什么是事务
首先,我们知道MySQL是一定会在同时被多个用户访问的,那么就会发送下面这种情况
上述情况就会导致,同一张票被出售两次,显然不合理;
那么我们应该如何做,才可以避免上述情况呢?当满足下列条件时,即可避免
- 买票的过程是原子的
我买票的时候别人不能影响我
- 买票的过程不能互相影响
我正在买的时候,你来买了
- 买完票应该是永久的
我买完票,交完钱了,你不能不给我票,直接扣我的钱,而什么也不给我,即不给票也不还钱
- 买前,买后状态是确定的
买前、买后、买成功、买失败,不能模棱两可,一定是确定的,成不成功并不重要
那么到底什么是事务呢???
事务是一组逻辑上相关的数据库操作(DML语句),这些操作要么全部成功,要么全部失败。事务的主要目的是确保数据的完整性和一致性。例如,在一个银行转账系统中,转账操作需要同时更新两个账户的余额,这两个操作必须作为一个整体执行,否则会导致数据不一致。
- 我对我的账号
+100
; - 我对你的账号
-100
;
单独的一条SQL并没有实际意义
一个 MySQL 数据库,可不止你一个事务在运行,同一时刻,甚至有大量的请求被包
装成事务,在向 MySQL 服务器发起事务处理请求。而每条事务至少一条 SQL ,最多很多 SQL ,这样如果大家都访问同样的表数据,在不加保护的情况,就绝对会出现问题
所以,一个完整的事务,绝对不是简单的 sql 集合,还需要满足如下四个属性,通常称为ACID属性:
-
原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败。如果事务在执行过程中发生错误,系统会回滚到事务开始前的状态
-
一致性(Consistency):事务执行前后,数据库必须保持一致状态。这意味着事务执行后,数据库必须满足所有的完整性约束
-
隔离性(Isolation):多个事务并发执行时,每个事务的操作应该与其他事务隔离,防止数据不一致。MySQL提供了不同的隔离级别来控制事务的隔离程度,包括读未提交( Readuncommitted )、读提交( read committed )、可重复读( repeatable read )和串行化( Serializable )
-
持久性(Durability):一旦事务提交,对数据的修改就是永久的,即使系统发生故障也不会丢失
综上,事务就是在ACID
四大属性的加持下,由一条、多条SQL构成的;
事务的本质(白话)是站在MySQL使用者角度,我要完成一个功能(转账),这个功能由多条SQL语句组成;
为什么会出现事务
首先,事务被 MySQL 编写者设计出来;
本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,
不需要我们去考虑各种各样的潜在错误和并发问题。
因此事务本质上是为了应用层服务的
,而不是伴随着数据库系统天生就有的。
事务的版本支持
首先,在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务, MyISAM 不支持。
我们可以通过以下SQL来查询
- Engine: 表示存储引擎的名称
- Support: 表示服务器对存储引擎的支持级别,YES表示支持,NO表示不支持,DEFAULT表示数据库默认使用的存储引擎,DISABLED表示支持引擎但已将其禁用
- Comment: 表示存储引擎的简要说明
- Transactions: 表示存储引擎是否支持事务,可以看到InnoDB存储引擎支持事务,而MyISAM存储引擎不支持事务
- XA: 表示存储引擎是否支持XA事务
- Savepoints: 表示存储引擎是否支持保存点
事务的提交方式
事务的提交方式常见的有两种:
- 自动提交
- 手动提交
我们可以用下面SQL来查询当前的提交方式
此时Value的值为ON表示自动提交
用 SET 来改变 MySQL 的自动提交模式:
将autocommit的值设置为1表示打开自动提交,设置为0表示关闭自动提交
事务常见操作方式
正常演示 - 证明事务的开始与回滚
提前准备
为了便于演示,我们将MySQL的隔离级别设置的比较低,即成读未提交
现象如下
创建测试表,简单银行用户表
启动一个事务,一旦启动后,之后的所有SQL都属于同一个事务
两种启动事务的方式
设置savapoint s1
并插入一条数据
设置savapoint s2
并插入一条数据
设置savapoint s3
并插入一条数据
回滚事务
回滚到s3时,s3后面的SQL就没有了;
回滚到s1时,s1后面的SQL就没有了;
提交事务
提交事务后就不能回滚了
丢弃整个SQL,直接
rollback;
非正常演示1 - 证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)
事务在提交之前因为某些原因与MySQL断开连接,那么MySQL会自动让事务回滚到最开始,此时事务内容无效;
这也就是ACID中的A属性,原子性
非正常演示2 - 证明commit了,客户端崩溃,MySQL数据不会在受影响,已经持久化
非正常演示3 - 对比试验。begin会自动更改提交方式
查看autocommit的值为ON,表示事务的提交方式是自动提交
事务在提交之前与MySQL断开连接,那么MySQL依旧会自动让事务回滚到最开始;
证明begin操作会自动更改提交方式,不会受MySQL是否自动提交影响
非正常演示4 - 证明单条 SQL 与事务的关系
将autocommit设置为ON,表示事务执行后自动提交
- 查看autocommit的值为ON,表示事务的提交方式是自动提交
- 左终端中直接向表中新插入一条记录,由于隔离级别是读未提交,因此在右终端中肯定能够查询到新插入的这条记录
- 执行单条SQL后不使用commit进行提交,MySQL异常退出,这时右终端仍然可以看到之前新插入的记录了,因为单条SQL在执行后被自动提交持久化了
将autocommit设置为OFF,表示事务执行后需要手动提交
- 设置autocommit的值为OFF,表示事务执行后需要手动提交
- 左终端中直接向表中新插入一条记录,由于隔离级别是读未提交,因此在右终端中肯定能够查询到新插入的这条记录
- 执行单条SQL后不使用commit进行提交,MySQL异常退出,这时右终端看不到之前新插入的记录了,因为单条SQL在执行后异常退出,MySQL断开连接则会自动进行回滚操作
结论:我们之前一直都在使用单SQL事务,只不过autocommit默认是打开的,因此单SQL事务执行后自动就被提交了
事务的隔离级别
举个例子:
你妈妈给你说:你要么别学,要学就学到最好。至于你怎么学,中间有什么困难,你妈妈不关心。那么你的学习,对你妈妈来讲,就是原子的。那么你学习过程中,很容易受别人干扰,此时,就需要将你的学习隔离开,保证你的学习环境是健康的。
- 数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性
- 数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别
隔离级别
-
读未提交(Read Uncommitted):事务可以读取其他事务未提交的数据,可能会导致脏读、不可重复读和幻读。
-
读已提交(Read Committed):事务只能读取其他事务已提交的数据,避免了脏读,但可能会导致不可重复读和幻读。
-
可重复读(Repeatable Read):MySQL默认的隔离级别,确保同一事务中多次读取同一数据时,结果一致。避免了脏读和不可重复读,但可能会有幻读。
-
串行化(Serializable):最高的隔离级别,强制事务串行执行,避免了脏读、不可重复读和幻读,但性能最差。
查看与设置隔离性
查看隔离级别
- 查看全局的隔离级别
SELECT @@global.transaction_isolation;
- 查看会话隔离级别
SELECT @@session.transaction_isolation;
也可以省略掉session
SELECT @@transaction_isolation;
设置隔离级别
语法:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED
| READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
- 设置会话隔离级别
只会影响当前会话的隔离级别,不会影响全局的,即使新起会话也不会影响
- 设置全局隔离级别
设置全局隔离级别会影响后续的新会话,但当前会话的隔离级别没有发生变化,如果要让当前会话的隔离级别也改变,则需要重启会话
读未提交(Read Uncommitted)
事务A所作的修改在没有提交之前,事务B就已经能够看到了
一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读
读提交(Read Committed)
事务A所作的修改在没有提交之前,事务B不能看到
只有当事务A提交后,事务B才能看到修改后的数据
在B事务没有commit之前,执行过程中,两个相同的select查询得到了不同的数据,这种现象叫做不可重复读
不可重复读是个问题吗??
可重复读(Repeatable Read)
A事务修改数据,B事务并不能查到
只有A事务commit后,B事务也查不到
只有当双方都commit后,B事务才可以查到
- 在可重复读隔离级别下,一个事务在执行过程中,相同的select查询得到的是相同的数据,这就是可重复读
- 一个事务在执行过程中,相同的select查询得到了新的数据,如同出现了幻觉,这种现象叫做幻读
串行化(Serializable)
将隔离级别都设置为串行化,双方查询互不影响
此时事务A要删除一个数据,但是阻塞在了这里
但是事务B的查询并不受影响
当事务Bcommit后,离开事务A就恢复,立马执行SQL对表进行修改
- 串行化是事务的最高隔离级别,多个事务同时进行读操作时加的是共享锁,因此可以并发执行读操作,但一旦需要进行写操作,就会进行串行化,效率很低,几乎不会使用
结论:
一致性
多版本并发控制MVCC
数据库的并发场景
- 读-读并发:不存在任何问题,也不需要并发控
- 读-写并发:有线程安全问题,可能会存在事务隔离性问题,可能遇到脏读、幻读、不可重复读
- 写-写并发:有线程安全问题,可能会存在两类更新丢失问题
- 每个事务都有自己的事务ID,ID的大小决定着,事务到来的顺序;
- mysqld可能会在一个时间范围中处理多个事务,事务也有自己的声明周期,mysqld要对多个事务进行管理;
3个记录隐藏列字段
当我们创建下面一个表
该记录不仅包含name和age字段,还包含上述三个隐藏字段
undo日志
MySQL 中的一段内存缓冲区,用来保存日志数据的
- redo log:重做日志,用于MySQL崩溃后进行数据恢复,保证数据的持久性
- bin log:逻辑日志,用于主从数据备份时进行数据同步,保证数据的一致性
- undo log:回滚日志,用于对已经执行的操作进行回滚,保证事务的原子性
快照
现在有一个事务ID为10的事务,要将刚才插入学生表中的记录的学生姓名“张三”改为“李四”
- 事务10,因为要修改,所以要先给该记录加行锁
- 修改前,现将改行记录拷贝到undo log中,所以,undo log中就有了一行副本数据。(原理就是写时拷贝)
- 所以现在 MySQL 中有两行同样的记录。现在修改原始记录中的name,改成 ‘李四’。并且修改原始记录的隐藏字段
DB_TRX_ID
为当前 事务10 的ID, 我们默认从 10 开始,之后递增。而原始记录的回滚指针DB_ROLL_PTR
列,里面写入undo log
中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它 - 事务10提交,释放锁
又有一个事务11,对student表中记录进行修改(update):将age(28)改成age(38)
- 先给该记录加行锁
- 修改前,现将改行记录拷贝到undo log中,所以,undo log中就又有了一行副本数据。此时,新的副本,我们采用头插方式,插入undo log
- 现在修改原始记录中的age,改成 38。并且修改原始记录的隐藏字段
DB_TRX_ID
为当前 事务11 的ID。而原始记录的回滚指针DB_ROLL_PTR
列,里面写入undo log中副本数据的地址,从而指向副本记录,既表示我的上一个版本就是它 - 事务11提交,释放锁
这样就形成了一个基于历史版本的链表
上面的每个版本,我们可以称之为快照
当前读 、快照读
当前读:读取最新的记录,就叫做当前读。
快照读:读取历史版本,就叫做快照读。
事务在进行增删查改的时候,并不是都需要进行加锁保护:
-
事务对数据进行增删改的时候,操作的都是最新记录,即当前读,需要进行加锁保护
-
事务在进行select查询的时候,既可能是当前读也可能是快照读,如果是当前读,那也需要进行加锁保护,但如果是快照读,那就不需要加锁,因为历史版本不会被修改,也就是可以并发执行,提高了效率,这也就是MVCC的意义
Read View
Read View
就是事务进行 快照读 操作的时候生产的读视图
(Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID
其源码
class ReadView {// 省略...
private:/** 高水位:大于等于这个ID的事务均不可见*/trx_id_t m_low_limit_id;/** 低水位:小于这个ID的事务均可见 */trx_id_t m_up_limit_id;/** 创建该 Read View 的事务ID*/trx_id_t m_creator_trx_id;/** 创建视图时的活跃事务id列表*/ids_t m_ids;/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/trx_id_t m_low_limit_no;/** 标记视图是否被关闭*/bool m_closed;// 省略...
};
也就是
-
id < m_up_limit_id || id == m_creator_trx_id
事务id小于m_up_limit_id(已提交)或事务id为创建该Read View的事务的id,则可见 -
id >= m_low_limit_id
事务id大于等于m_low_limit_id,则不可见 -
m_ids.empty()
事务id位于m_up_limit_id和m_low_limit_id之间,并且活跃事务id列表为空(即不在活跃列表中),则可见