【MySQL-7】事务
目录
1. 整体学习思维导图
2. 什么是事务
2.1 事务的概念
2.2 事务的属性(ACID)
2.3 事务出现的原因
2.4 查看存储引擎对事务的支持
3. 事务的使用
3.1 事务的提交方式
3.1.1 手动提交
3.1.2 自动提交
结论:
3.2 事务的隔离级别
3.2.1 理解隔离
3.2.2 设置隔离级别
3.2.3 隔离级别
读未提交(Read Uncommitted)
读提交(Read Committed)
可重复读(Repeatable Read)
串行化(Serializable)
总结:
4. 隔离性和隔离级别的理解
4.1 数据库的并发场景
4.2 读-写场景并发(MVCC)理解
4.2.1 事务ID
4.2.2 三个隐藏记录字段
4.2.3 undo日志
模拟MVCC:
探讨:
4.3 当前读和快照读
4.3.1 Read View
4.4 RR与RC的区别:
1. 整体学习思维导图
2. 什么是事务
2.1 事务的概念
事务是从应用层面上来看待的,比如在一个同学退学了,我们需要在学校的教务系统的数据库中将该同学的信息,选课,成绩都要删除掉,这需要一系列的SQL语句,而我们就把这个处理的DML语句集合称作为一个事务!
2.2 事务的属性(ACID)
-
原子性(Atomicity):一个事务的所有操作要么全部执行,要么全部未执行,不能出现中间的状态。就拿前面的比方同学退学,我们的退学事务不能只说删除同学的信息,但不去删除选课和成绩!如果一个事务在执行过程中发生错误,那么该事务就会回滚(rollback)到没执行的状态!
-
一致性(Consistency):事务的执行结果是可以预期的,比如A向B转账50,那么A的账户会减少50,B的账户会增加50,A向B转账的过程就是一个事务,而这个事务的结果是可以预期的就称作为一致性。MySQL对于一致性并没有做过多的设计,MySQL是通过AID来保证C一致性的,简单说AID是因,C是果。
-
隔离性(Isolation):每个事务执行的期间是隔离的,数据库允许多个并发的事务对数据进行读写操作,隔离性可以防止多个事务在读写数据交叉导致数据的不一致。
-
隔离性的级别(根据隔离影响划分)
-
读未提交(Read UnCommitted)
-
读提交(Read Committed)
-
可重复读(repeatable read)
-
串行化(Serializable)
-
-
持久性(Durability):事务处理结束之后,对于数据的修改是永久性的,即使系统故障也不会丢失。
查看事务隔离级别:
查看全局:
mysql> select @@global.tx_isolation;
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ |
+-----------------------+
1 row in set, 1 warning (0.00 sec)
查看当前会话:
mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ |
+------------------------+
1 row in set, 1 warning (0.00 sec)
查看默认:
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)
2.3 事务出现的原因
事务的本质是一系列的DML语句,但是我们在应用层编写时不可能时常去注意细节,我们可以用面向对象的思想来解释,而事务就相当于一个对象,它将DML语句描述后组织起来。
我们还将MySQL中的一条信息称作为一条记录。
2.4 查看存储引擎对事务的支持
mysql> show engines \G;
*************************** 1. row ***************************
Engine: InnoDB
Support: DEFAULT
Comment: Supports transactions, row-level locking, and foreign keys
Transactions: YES -- 对事务支持
XA: YES
Savepoints: YES
*************************** 2. row ***************************
Engine: MRG_MYISAM
Support: YES
Comment: Collection of identical MyISAM tables
Transactions: NO -- 对事务不支持
XA: NO
Savepoints: NO
3. 事务的使用
3.1 事务的提交方式
-
手动提交
-
自动提交
3.1.1 手动提交
我们先创建一个测试表:
create table if not exists T_TB(
id int primary key comment'编号',
name varchar(20) default'',
age tinyint
);
手动提交和自动提交互不影响,下面是手动提交的开启/结束:
begin/start transaction
commit;
手动创建回滚点:
mysql> savepoint save1; Query OK, 0 rows affected (0.00 sec)-- 创建一个保存点save1
mysql> rollback to save1; -- 回滚到保存点save1
-
如果手动开始的事务没有提交,程序崩溃后会自动回滚;手动开始事务提交后即使程序崩溃也会持久化保存
-
自动提交的开启是否不影响手动提交!
3.1.2 自动提交
查看自动提交是否开启:OFF(没有开启)/ON(开启)
mysql> show variables like'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
自动提交开启后,我们正常进行一条条CURD操作都是一个个事务,即使程序异常崩溃也会持久化保存;如果关闭自动提交进行CURD操作,我们程序异常崩溃后会回滚!
所以操作能否对数据进行持久化的影响取决于是否提交了!
结论:
3.2 事务的隔离级别
3.2.1 理解隔离
-
隔离性:数据库中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征隔离性。
-
隔离级别:数据库中,允许事务受不同程度的干扰,就有了一种重要特征隔离级别 。
3.2.2 设置隔离级别
我们前面讲了如何查看隔离级别,这边我们来看如何设置隔离级别:
--设置
-- 设置当前会话 or 全局隔离级别语法
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED(读未提交) | READ
COMMITTED(读提交) | REPEATABLE READ(可重复读) | SERIALIZABLE(串行化)}
-
当前会话:只作用于这一次会话,一旦退出就消失!
-
全局:更新完全局隔离级别需要重启MySQL才会生效!
3.2.3 隔离级别
读未提交(Read Uncommitted)
在该隔离级别下,两个事务并发读写数据,其中一方事务进行读操作,另一方事务进行写操作,读操作的事务可以实时看到写操作事务对数据的修改(即使写操作事务并没有进行提交),我们将读操作事务读取到的数据称作为脏读(因为写操作事务没有提交cmmit,我们是不知道读取的数据是否会回滚rollback)!
读提交(Read Committed)
在该隔离级别下,两个事务并发读写数据时,任何一方想看到另外一方的修改需要等对方提交才可以,但是它会带来一个问题--不可重复度,不可重复读是指原本我们的数据内容只有三条记录,我们第一次读取也是三条,与此同时另一个事务进行了insert并且结束了事务,我们这边的事务又进行了一次查询发现表中的数据改变成了四条,与前面的三条不相符,我们将这种现象称作为不可重复读!
可重复读(Repeatable Read)
在该隔离级别下,两个事务并发读写数据时,任何一方在事务过程中都不会影响到另一个事务,即使第一个事务已经结束DML操作,但是第二个事务是无法看到进过操作后的表。只有双方都结束并且commit,才可以看到对方的修改。
-
多次查看,发现终端A在对应事务中insert的数据,在终端B的事务周期中,也没有什么影响,也符合可重复的特点。但是,一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert的数据(为什么?因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读(phantom read)。
串行化(Serializable)
在该隔离级别下,两个事务并发读写数据时,两个事务就需要进行排队执行,只有在对方提交完成后另一方才可以进行相应的操作(读取数据不受影响)。
我们可以看见更新SQL语句卡住了。
当我们右边事务提交后,卡住的SQL执行成功!
总结:
-
隔离级别越高,安全性越好,但是数据库的并发性能就会降低,我们需要根据需求找到一个平衡点。
-
不可重复读重点在于修改和删除
-
幻读的重点在于新增的数据
4. 隔离性和隔离级别的理解
4.1 数据库的并发场景
-
读-读:不存在问题
-
读-写:存在问题,可能会出现(脏读/幻读/不可重复读)
-
写-写:存在问题
4.2 读-写场景并发(MVCC)理解
我们先来理解数据库中出现并发场景最多的读-写场景。
多版本并发控制(MVCC)是一种解决读-写问题的无锁并发控制。
在理解MVCC之前我们需要来学习一些前置知识点:
-
三个隐藏记录字段
-
undo日志
-
Read View
4.2.1 事务ID
每个事务都要有自己的ID,ID决定他们到来的先后顺序。
MySQL可能有时并发的处理多个事务,事务也就有了自己的生命周期,那么我们就需要对事务进行先描述在组织,通过一个对象或者结构体将事务管理起来!
4.2.2 三个隐藏记录字段
-
DB_TRX_ID:称作为最近修改事务ID,记录创建这条记录/最后一次修改(修改/插入)该记录的事务ID 。
-
DB_ROLL_PTR: 称作为回滚指针,用于指向该条记录的上个版本。
-
DB_ROW_ID:隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以DB_ROW_ID 产生一个聚簇索引
补充:还有一个flag字段用于表示该条记录是否删除,比如说1表示删除,0表示存在。记录的更新和删除不一定是真的删除了!
-- 我们创建一个测试表,只有姓名和年龄
mysql> create table if not exists T_MVCC(
-> name varchar(15),
-> age int unsigned
-> );
Query OK, 0 rows affected (0.02 sec)
mysql> insert into T_MVCC values('张三', 18);
Query OK, 1 row affected (0.00 sec)
mysql> select * from T_MVCC limit 1;
+--------+------+
| name | age |
+--------+------+
| 张三 | 18 |
+--------+------+
1 row in set (0.00 sec)
我们将隐藏的字段也添加上,DB_TRX_ID默认null,DB_ROLL_PTR默认null,DB_ROW_ID默认1。
name | age | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
张三 | 18 | null | null | 1 |
4.2.3 undo日志
我们需要知道MySQL数据库在我们对数据进行的操作短时内都是在内存中执行的,根据算法过一定的时间或者满足一定的条件,数据才会被从内存向磁盘上写入!
而undo log就是存放在内存缓冲区用于日志信息!
模拟MVCC:
-
前提条件:我们需要将张三的age修改为20(update)。
在事务ID: 1修改前,给该条记录加上行锁,然后拷贝一份旧版本到undo log中,等待事务1提交后,解锁。修改后的事务ID: 1中的PTR指向上个版本,这样可以方便进行回滚操作!
而我们可以将undo log中的版本称作为一个个快照!
探讨:
-
对于删除delete,我们前面介绍过flag,所以删除也可以形成一个版本进行存放,在执行删除操作时,内部存放的是与之相反的添加的SQL,一旦回滚就执行该SQL。
-
对于插入insert,insert插入的是之前版本没有的数据,按前面来看insert是无法形成一个旧版本的,但是为了回滚操作,insert的数据也要被存放到undo log中,只有当commit后,insert存到的数据才会被清空!
-
对于select呢?我们什么时候读当前最新的数据(当前读)还是去读取旧数据(快照读)呢?我们下面来详细看看:
4.3 当前读和快照读
控制当前读还是快照读,取决于隔离级别!我们虽然说事务的属性是原子性的,但是在MySQL处理事务的过程中也是存在先后的顺序,而这个顺序将决定读取数据的版本和范围!
可以简单说当前读还是快照读,一个事务该读取那些版本的数据,也就是隔离级别实现的本质。
4.3.1 Read View
-
Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
-
Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。 我们创建一个事务的时候不会创建Read View, 只有当我们某个事务执行快照读(select)的时候,对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据。
Read View类的主要成员变量:
m_ids; // 相当于一张列表记录当前活跃的事务的ID
up_limit_id; // 用于记录m_ids表中最小的事务ID
low_limit_id; // 用于记录所有事务中(包括已经commit的)最大的ID值+1
creator_trx_id //创建该ReadView的事务ID
通过上图:
在m_ids表中的ID存在: 1 2 3
up_limit_id:1
low_limit_id: 5
creator_trx_id: 2
事务4修改操作后,id是不存在于m_ids,并且也不大于low_limit_id,快照读是可见事务4的版本。
4.4 RR与RC的区别:
-
select * from user lock in share mode ,以加共享锁方式进行读取,对应的就是当前读,该条语句可以读到最新的版本。
RR: 一个Read View用到底 RC: 每次快照读都会更新Read View
比方说我们现在存在着事务A,事务B:
-
我们先以RR做举例:事务B在快照读时生成的Read View中的m_ids中存放着事务A的ID,所以事务B是看不见事务A的修改版本,即使提交后,由于RR一个Read View用到底的原因,AID还在m_ids依旧不可见!
-
反观RC,每次更新Read View,虽然第一次事务A的ID在m_ids中不可见其版本,但是后续提交后再次快照读生成新的Read View事务A的ID不存在m_ids并且满足查看的条件,所以快照读的版本就是事务A的版本。