@
目录
- 前言
- 一、事务的概念
- 二、事务的常见操作
- 三、事务隔离级别详解
- (一)查看与设置隔离性
- [(二)读未提交【Read Uncommitted】](#(二)读未提交【Read Uncommitted】)
- [(三)读提交【Read Committed】](#(三)读提交【Read Committed】)
- [(四)可重复读【Repeatable Read】](#(四)可重复读【Repeatable Read】)
- (五)串行化【serializable】
- (六)隔离级别总结
- 四、隔离性再理解
- [(一)MVCC解决 读-写冲突](#(一)MVCC解决 读-写冲突)
- 1、三个记录隐藏列字段
- [2、undo 日志](#2、undo 日志)
- [3、Read View](#3、Read View)
- [(二)MVCC 整体流程](#(二)MVCC 整体流程)
- [(一)MVCC解决 读-写冲突](#(一)MVCC解决 读-写冲突)
- 结语
前言
在介绍本期内容前,我们先思考一个问题,我们都知道CURD是在开发过程中常见的操作,但是如果这些操作不加以控制就会出在同时操作的过程存在重复操作与操作间彼此矛盾的问题。在MySQL中如何解决这些问题?就有了本期的内容。
一、事务的概念
(一)什么是事务
MySQL的事务就是一组DML语句。这些语句就是要做的或所做的事情,且在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败。且MySQL提供一种机制,让事务规定不同的客户端看到的数据是不相同的。
其被设计出来本质是为了当应用程序访问数据库的时候,能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题。
而在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务, MyISAM 不支持。
(二)事务的属性
在MySQL中会出现存在着许多事务同时并行的情况。为了保证这些事务彼此间不会出错,一个完整的事务还需要满足如下四个属性ACID:
- 原子性: 一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚到事务开始前的状态,全过程不应受到打扰。
- 一致性: 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。
其要求事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的。 - 隔离性: 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。而事务隔离又分为不同级别。隔离性将是我们介绍的重点。
- 持久性: 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
二、事务的常见操作
(一)事务的提交
事务的提交方式常见的有两种:自动提交、手动提交。
以下为有关的常见操作。
sql
# 查看事务提交方式
show variables like 'autocommit';
# 手动进行提交
commit;
用 SET 来改变 MySQL 的提交模式
sql
# 禁止自动提交
SET AUTOCOMMIT=0;
# 开启自动提交
SET AUTOCOMMIT=1;
(二)事务的开始与回滚
开始一个事务的操作
sql
start transaction;
# or
begin;
设置保存点与回滚操作
sql
savepoint save1;
rollback to save1;
rollback;
为了便于进行演示,我们将mysql的默认隔离级别设置成读未提交,后面会进行说明。
sql
set global transaction isolation level READ UNCOMMITTED;
quit;
# 之后重启终端
# 查看隔离级别
select @@transaction_isolation;
演示所准备测试用例。
sql
create table if not exists account(
id int primary key,
name varchar(50) not null default '',
blance decimal(10,2) not null default 0.0
)ENGINE=InnoDB;
演示过程:
可见回滚到相应保存点的后,后面的操作结果都消失了。以上为正常情况下的演示过程,非正常情况,可不用start transaction或begin,并用abort来操作进行结论的验证。
同时我们关于事务的操作有以下结论:
- 只要输入begin或者start transaction,事务便必须要通过commit提交,才会持久化,与其是否设置set autocommit无关。
- 事务可以手动回滚,同时,当操作异常,MySQL会自动回滚。
- 对于 InnoDB 而言,每一条 SQL 语言都默认封装成事务,自动提交。(select有特殊情况,因为MySQL 有 MVCC )。
- 可以选择回退到哪个保存点,如果没有设置保存点,也可以回滚,只能回滚到事务的开始。直接使用 rollback (前提是事务还没有提交)。
三、事务隔离级别详解
根据前面的介绍,我们了解在MySQL中什么是隔离性,现在来进一步学习什么其关于隔离级别的内容。数据库中,允许事务受不同程度的干扰,就是隔离级别。
隔离级别主要有:读未提交【Read Uncommitted】、读提交【Read Committed】、可重复读【Repeatable Read】、串行化【Serializable】
接下我们都会进行介绍。
(一)查看与设置隔离性
1、查看隔离性
sql
# 全局
SELECT @@GLOBAL.transaction_isolation;
# 当前会话(局部)
SELECT @@SESSION.transaction_isolation;
SELECT @@transaction_isolation;
2、设置隔离性
sql
SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL {
REPEATABLE READ |
READ COMMITTED |
READ UNCOMMITTED |
SERIALIZABLE
};
其一样分为了全局与局部,以及具体去设置隔离级别。
且MySQL 默认的事务隔离级别是 REPEATABLE READ 可重复读。
(二)读未提交【Read Uncommitted】
在Read Uncommitted隔离级别下,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等。
sql
set global transaction isolation level read uncommitted;
在改模式对之前的案例进行操作:
此时未进行commit ,我们在另一个终端上对同一数据库进行操作。
发现此时在 Read Uncommitted 下我们却能读到先前终端更新但是未 commit 的数据。而这种一个事务在执行中,读到另一个执行中事务的更新 (或其他操作) 但是未commit的数据,的现象叫做脏读 (dirty read) ,在一些场景下是不合理的。
(三)读提交【Read Committed】
其满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。但这种隔离级别会引起不可重复读,即一个事务执行时,如果多 select,可能得到不同的结果。
sql
set global transaction isolation level read committed;
在Read Committed下用begin开启事务,同时另一个终端也begin,在前一个终端修改数据后后未commit有:
可见是修改前的数据。在前一终端commit后有:
此时才能看到另一终端更新后的数据。此时在同一个事务内,依旧还在事务操作中,同样的读取,却在不同的时间段读取到了不同的值,这种现象叫做不可重复读(non reapeatable read)。
(四)可重复读【Repeatable Read】
其确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。
sql
set global transaction isolation level repeatable read;
同上一个演示一样,在该模式下查看原先的数据,两个终端同时开启事务,
其中一个终端修改数据但未commit下查看另一终端的数据。
此时一样看不到数据的改变,让前一个终端commit 再看。
发现在Repeatable Read下,事务无论什么时候进行查找,看到的结果都是一致的,这就叫做可重复读。
结束事务后查看才能看到数据的更新。
但是如果在一般的数据库而不是MySQL,把之前修改数据的行为换为insert ,在前一终端commit后,在后一终端反复进行select查看时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读(phantom read)。而MySQL在Repeatable Read级别的时候,用Next-Key锁(GAP+行锁)着用方式解决了幻读问题。
(五)串行化【serializable】
这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,
从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,可能会导致超时和锁竞争,导致效率很低。
(六)隔离级别总结
四、隔离性再理解
为了更好理解隔离性,我们来了解数据库并发过程中的MVCC机制。
首先数据库并发的场景有三种:
- 读-读 :不存在任何问题,也不需要并发控制
- 读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
- 写-写:有线程安全问题,可能会存在更新丢失问题
其中我们要进行介绍学习的是读-写。
(一)MVCC解决 读-写冲突
多版本并发控制( MVCC )是一种用来解决 读-写冲突 的无锁并发控制。
为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。
为了了解MVCC机制,我们还需介绍三样东西。
1、三个记录隐藏列字段
- DB_TRX_ID :6 byte,最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事务ID
- DB_ROLL_PTR : 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在 undo log 中)
- DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以DB_ROW_ID 产生一个聚簇索引
2、undo 日志
我们知道MySQL 是以服务进程的方式,在内存中运行。我们之前所讲的所有机制:索引,事务,隔离性等,都是在内存中完成的,即在MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的。
此时undo log就扮演了 MySQL 中的一段内存缓冲区的作用,用来保存日志数据。
3、Read View
Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View)。
在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。
而当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据。
(二)MVCC 整体流程
-
开启版本链(开始写入数据)。
当我们更新数据时,MVCC不会直接覆盖原数据,而是先对当前数据执行加锁 ;再将修改前的数据拷贝到 undo log 中,生成Undo日志 ,生成一个旧版本记录;然后再修改原数据,更新内部的隐藏字段:DB_TRX_ID更新为当前执行修改操作的事务ID、DB_ROLL_PTR指向刚才在 undo log 中存放的旧版本记录。此时最新数据与Undo日志中的旧数据通过 DB_ROLL_PTR 串联,形成了一条完整的版本链。
-
读取数据(生成Read View)。
当执行 SELECT 查询时,InnoDB会为当前查询(或事务)生成一个 Read View,可以把它理解为当前时刻所有活跃事务(未提交事务)的清单。
-
决定读哪个版本(可见性判断)。
这是MVCC最核心的环节。拿着当前行的 DB_TRX_ID,去跟 Read View 做对比,判断这条记录对当前查询是否可见。如果 DB_TRX_ID < min_trx_id 说明该事务早已提交或者 等于当前查询自身的事务ID(读自己改过的数据)为可见;如果 DB_TRX_ID >= max_trx_id(未来事务) 或者 在 m_ids 活跃列表中(即为未提交的脏数据),为不可见。此时,当前查询不会读这条最新记录。而是顺着 DB_ROLL_PTR 指针,回退到Undo日志中的上一个旧版本。拿旧版本的 DB_TRX_ID 再次进行上述可见性判断,直到找到一个"对当前查询可见的版本为止。
-
此时会依照两种不同模式(先前所讲的两种隔离性级别),Read View的生成时机也会不同,导致有不同结果。
RC(读提交 ):每次执行SELECT,都重新生成一个全新的Read View。因此能读到其他事务最新已提交的数据(不可重复读)。
RR(可重复读):事务中第一次SELECT时生成Read View,并一直复用到最后。因此即使其他事务修改并提交了新数据,由于Read View不变,依然会去Undo链中找"旧版本",保证了事务内数据的一致性(可重复读)。
结语
以上便是MySQL事务的内容了,通过本期的学习我们对MySQL的事务尤其隔离性的内容有了更深刻的理解。如果喜欢我的内容,请点赞、收藏加关注,多多支持我这个编程小白啊,以及欢迎各位在评论区讨论交流,并指出我的不足,谢谢大家。