事务是什么
在 MySQL 中,事务是一组原子性的 SQL 操作,这些操作要么全部成功执行,要么全部不执行。就好像是一个不可分割的工作单元,保证了数据的一致性和完整性。也就是说MySQL服务同样会被多个客户端同时访问(MySQL内部采用多线程架构),所以此时多个执行流同时访问MySQL数据库中的内容,为了保障数据一致性与完整性就有了事务的概念。所以事务就要满足以下特性:原子性、一致性、隔离性、持久性(ACID)。
事务版本支持
在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务, MyISAM 不支持。查看数据库引擎 show engines \G指令:
事务提交方式
- 手动提交
- 自动提交
查看事务提交方式:
设置提交方式:
SET AUTOCOMMIT = 0; // 关闭自动提交
SET AUTOCOMMIT = 1; // 打开自动提交
开启事务后,数据库暂时停止自动提交功能,将后续的操作视为一个事务单元。而对于事务是否自动提交的设置只会对单SQL语句起作用,表明单SQL语句是否被当作一个事务进行提交。所以当设置自动提交以后,单SQL语句进行数据的CURD操作时,都会自动将该操作视作一个事务自动提交到数据库当中,使得修改立即生效,从而达到持久化的目的。
事务启动与关闭
启动:
begin;/start transaction;
关闭:
commit;/rollback;
其中可以在启动事务执行中添加保存点 savepoint s1;然后回滚的时候可以rollback to s1;就直接回滚到保存点s1的位置,但此时并不会关闭事务。
事务的原子性与隔离性
- 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
- 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
事务隔离级别的认识
MySQL中的一个事物可能会有多条语句构成,所以在一个事务就会有三个阶段:执行前、执行中、执行后。而包保证事务得原子性就不运行事务执行中的过程被干扰,也就是用户层只能看到事务执行前和执行后的结果。而要保障事务的执行过程是原子性的就间接的有了隔离性特征,而对与不同的事务存在不同的干扰程度,所以就有了事务的隔离级别。
查看设置隔离性
查看:
select @@global.transaction_isolation; --查看全局隔级别
select @@session.transaction_isolation;--查看会话(当前)全局隔离级别
select @@transaction_isolation;--查看会话(当前)全局隔离级别
设置:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ
COMMITTED | REPEATABLE READ | SERIALIZABLE}// |表示在所有项中选择其一
还有就是全局隔离级别一旦更改,所有的会话隔离级别也跟着更改。但是需要重新登录才能生效。
读未提交【Read Uncommitted】
该隔离级别表示:所有的事务都可以看到其他事务没有提交的执行结果。相当于没有任何隔离性,也会有很多并发问题,如脏读(一个事物执行过程中,读到了另一个事务还未提交的数据)等。演示:
读提交【Read Committed】
该隔离级表示:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读(当前启动的事务在不同时间段读到了不同的查询结果)。演示:
可重复读【Repeatable Read】
这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行,也就是当前事务执行结束并提交,另一个事务也无法看到当前数据的改变,知道另一个事务也结束才行。但是一般的数据库在可重复读情况的时候会出现幻读的情况,无法屏蔽其他事务insert的数据(因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题)导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。而MySQL解决了该问题。演示:
串行化【Serializabl】
这是事务的最高隔离级别,它通过强制事务排序(使所有的事务串行化),使之不可能相互冲突,从而解决了幻读的问题。也就是当前事务执行完了以后其他事务才能开始执行CURD操作。演示:
如果左侧事务一直不结束,右侧事务就会超时退出;
所以综上阐述可以得出:
事务的一致性
事务的一致性表示在事务开始之前和事务结束以后,数据库的完整性没有被破坏。也就是说写入的数据必须完全符合预期。所以一致性其实就是依靠AID(原子性、隔离性、持久性)和用户正确的SQL语句和业务逻辑来保障的。
多版本并发控制( MVCC )
MVCC 是一种并发控制的方法,它主要用于数据库管理系统中,目的是在多个事务并发访问数据库时,能够在保证数据一致性的前提下,提高系统的并发性能。MVCC 允许事务在读取数据时看到一个数据在某个时间点的快照,而不是被其他并发事务修改后的最新数据。
存在事务ID:
为了便于针对不同的事务与表数据版本进行关联,所以MySQL为每一个事务分配了单向增长(事务到来顺序)的事务ID。而且MySQL服务器时长会面临多个多个事务的到来并进行处理,所以MySQL服务器要将事务管理起来,也就是构建每一个事务结构体对象,然后通过数组将结构体对象进行管理。
表中的隐藏列字段
- DB_TRX_ID :6 byte,最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事务ID。
- DB_ROLL_PTR : 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在 undo log 中)。
- DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以DB_ROW_ID 产生一个聚簇索引,构建b+树结构。
- 还有一个删除标记字段flag,当数据被删除时并不是真的清除数据,而是先将flag标记更改即代表删除。
如下,如果一个表创建时只有name和age字段时,其实就MySQL本质是创建以下的表结构。这都是更好的实现事务并发特性。
undo日志
我们上一章中讲到MySQL实现数据交互的过程。我们MySQL服务启动的时候,会在内存中为我们申请一段BufferPool缓冲区。而undo日志其实就是在BufferPool中的。这其实就是MySQL在用用层维护的一段内存空间。
我们的当前表的行数据信息其实都是存储在BufferPool的b+树的叶子结点中,而对于表中的历史修改数据其实存在BufferPool的undo log中的。所以我们其实在事务中每一次对数据修改其实就是修改b+树的叶子结点信息,但是修改之前的数据会先拷贝一份放到BufferPool的undo log中记录起来。
更改数据的演示:
而对于delete删除数据其实就更容易了,直接将原数据拷贝到undo log中并修改一下flag字段即可。而对于insert插入操作时,因为插入数据是不存在历史版本的,但是依旧需要进入历史版本,不过是通过delete语句来协助回滚的。
而当进行commit操作时就是将该事务的历史数据版本全部清空(如果历史数据没有其他事务进行访问)。而对于rollback操作其实就是回到历史版本,清空undo log中的该事务历史版本之后的数据(如果当前数据没有其他事务访问)。所以说comm和rollback操作是否要清空undo log的历史数据是取决于历史数据清空是否会影响到其他事务的查看。
Read View
当前读与快照读的认识:
- 当前读:读取最新的记录,就是当前读。增删改,都叫做当前读(因为是读取当前数据进行增删改操作的),select也有可能当前读,比如:select * from table_name lock in share mode(共享锁)
- 快照读:读取历史版本,就叫做快照读。
所以再次加深理解,读写为什么通常不需要进行加锁控制的原因其实就是:增删改操作都是当前读,而我们一般基本的select xxx from table_name 操作其实是进行快照读,读取的是历史版本数据,本质上访问的其实就不是同一个位置的数据,那么也就不存在冲突安全的问题,所以也就不需要加锁保护,从而就可以实现并发的读写操作。 而对于隔离性的体现其实就是根据所读取不同的历史版本而形成的隔离级别。
而Read View就是事务进行 快照读 操作的时候产生的 读视图 (Read View),在该事务执行的快照读(select)的那一刻,会生成数据库系统当前的一个快照(类似拍照记录下来),记录并维护系统当前活跃事务的ID。而Read View在MySQL中其实就是一个类,也就是进行快照读的时候Read View类中的成员数据会被填充,以此来判断当前事务可以查看的版本情况。
ReadView 中主要成员变量:
m_ids; //一张列表,用来维护Read View生成时刻,系统运行中的事务ID
up_limit_id; //记录m_ids列表中事务的最小的ID
low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,所有出现过的最大事务ID+1
creator_trx_id //创建该ReadView的事务ID
一张图了解四个字段:
在此要理解一个概念,多个事务并发执行的过程中都有对应的事务ID编号,而编号意味着到来的先后顺序,但是并不是先到的事务先执行完,这取决于每个事物的所有SQL语句执行时间与实物执行顺序。
所以我们快照读的时候可以得到以上关键信息,而且在undo log当中每一个历史版本链都有自己对应的事务ID(DB_TRX_ID),那么对于当前事务能否看到版本链中的数据就通过对比当前事务的DB_TRX_ID和ReadView中的关键字段即可。
简单模拟过程图:
快照读理解RC和RR隔离级别
正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同,所以说快照读其实就很好的作为了RC与RR的评判标准。
- 在RR级别下的某个事务的对某条记录的第一次快照读(select)会创建一个Read View对象, 将当前系统活跃的其他事务记录起来。此后尽管再次调用快照读(select)的时候,还是使用的是同一个Read View,因此只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见。
- 而在RC级别下的,事务中,每一次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因。