MySQL之事务详解

目录

1. 事务

1.1 定义

1.2 事务四大特性

2. 多事务并发问题

2.1 更新丢失(Lost Update)或脏写

2.2 脏读(Dirty Reads)

2.3 不可重读(Non-Repeatable Reads)

2.4 幻读(Phantom Reads

3. 事务隔离级别

3.1 读未提交(Read uncommitted)

3.1.1 定义

3.1.2 示例分析

3.1.2.1 case1:脏读

3.1.2.2 case2:不可重复读

3.1.2.3 case3:幻读

3.1.2.4 case3:脏写

3.2 读已提交(Read committed)

3.2.1 定义

3.2.1 示例分析

3.3.1.1 验证是否解决了脏读

3.2.1 读已提交解决脏读原理

3.3 可重复读 (Repeatableread)

3.3.1 定义

3.3.2 示例分析

3.3.2.1 验证是否解决了脏读和不可重复读

3.3.3 可重复读解决脏读和不可重复读原理。

3.4 串行化(Serializable)

4. 大事务的影响

5.事务优化

6. MVCC多版本并发控制机制


1. 事务

1.1 定义

  • 事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态
  • 事务是逻辑上 的一组操作,要么都执行成功,要么都执行失败。

1.2 事务四大特性

关系性数据库需要遵循ACID规则,具体内容如下:

MySQL之事务详解_第1张图片

  • 原子性(Atomicity) :事务是最小的执行单位,不允许分割。当前事务的操作要么同时成功,要么同时失败。原子性由 undo log日志来实现
  • 一致性(Consistent) 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;它由其它3个特性以及业务代码正确逻 辑来实现。
  • 隔离性(Isolation) :在事务并发执行时,他们内部的操作不能互相干扰。隔离性由 MySQL的各种锁以及MVCC机制来实现。
  • 持久性(Durable) :一旦提交了事务,它对数据库的改变应该是永久性的。持久 性由redo log日志来实现。

2. 多事务并发问题

2.1 更新丢失(Lost Update)或脏写

        当两个或多个事务选择同一行数据修改,有可能发生更新丢失问题,即最后的更新覆盖 了由其他事务所做的更新。

2.2 脏读(Dirty Reads)

        事务A读取到了事务B已经修改但尚未提交的数据。

2.3 不可重读(Non-Repeatable Reads)

        事务A内部的相同查询语句在不同时刻读出的结果不一致。

2.4 幻读(Phantom Reads

        事务A读取到了事务B提交的新增数据。

3. 事务隔离级别

  • 为了达到事务的四大特性,数据库定义了4种不同的事务隔离级别,由低到高依 次为Read uncommitted、Read committed、Repeatable read、 Serializable,这四个级别可以逐个解决脏读、不可重复读、幻读这几类问题。事务的隔离级别越高,并发作用越小
  • 查看当前数据库的事务隔离级别: show variables like 'tx_isolation';
  • 设置事务隔离级别:
 set tx_isolation='read‐uncommitted'; --读未提交

 set tx_isolation='read‐committed'; -- 读已提交

 set tx_isolation='repeatable‐read'; -- 可重复读

 set tx_isolation='serializable'; -- 串行化
  • Mysql默认的事务隔离级别是可重复读( REPEATABLE_READ), Oracle 默认采用的 READ_COMMITTED隔离级别。用Spring开发程序时,如果不设置隔离级别默认用 Mysql设置的隔离级别,如果Spring设置了就用已经设置的隔离级别。
  • 事务隔离机制的实现基于锁机制和并发调度。其中并发调度使用的是MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。
  • 事务隔离级别与事务并发出现的问题关系如以下表格:
隔离级别

脏读

(Dirty Read)

不可重复读
(NonRepeatable Read)

幻读

(Phantom Read)

读未提交(Read
uncommitted)
可能 可能 可能
读已提交(Read
committed)
不可能 可能 可能
可重复读
(Repeatableread)
不可能 不可能 可能
可串行化
(Serializable)
不可能 不可能 不可能

接下来会详解各个事务隔离级别,以及其存在的并发问题,准备一下表,进行案例分析。

CREATE TABLE `account` (
	`id` INT(11) NOT NULL AUTO_INCREMENT,
	`name` VARCHAR(255) NULL DEFAULT NULL COLLATE 'utf8_general_ci',
	`balance` INT(11) NULL DEFAULT NULL,
	PRIMARY KEY (`id`) USING BTREE
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
AUTO_INCREMENT=4
;
INSERT INTO `test`.`account` (`name`, `balance`) VALUES ('lilei', '450');
INSERT INTO `test`.`account` (`name`, `balance`) VALUES ('hanmei', '16000');
INSERT INTO `test`.`account` (`name`, `balance`) VALUES ('lucy', '2400');

3.1 读未提交(Read uncommitted)

3.1.1 定义

最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读

3.1.2 示例分析

将事务的隔离级别设置为读未提交。

set tx_isolation='read-uncommitted';

3.1.2.1 case1:脏读

第一步:事务1,执行一下代码,开启事务,设置事务隔离级别为读为提交,查询lilei的数据,不提交

set tx_isolation='read-uncommitted';

BEGIN;

SELECT * FROM account WHERE id = 1 ;

MySQL之事务详解_第2张图片

第二步:事务2,更新lilei的balance,并查询,但不提交。

set tx_isolation='read-uncommitted';
BEGIN;

UPDATE `test`.`account` SET `balance`='101' WHERE  `id`=1;

SELECT * FROM account WHERE id = 1 ;

MySQL之事务详解_第3张图片

第三步:查询数据库里面的事务个数,应该有2个事务。


SELECT  * 
FROM  information_schema.innodb_trx 
WHERE  TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 1;

MySQL之事务详解_第4张图片

第四步:事务1,再次查询该记录,且提交。此时,事务1结束,事务1得到的最终lilei的数据为101。 数据库里面应该只有一个事务。

SELECT * FROM account WHERE id = 1 ;

COMMIT;

MySQL之事务详解_第5张图片

MySQL之事务详解_第6张图片

第五步:事务2 回滚数据。

MySQL之事务详解_第7张图片

第六步:再次查看事务。所有事务已结束。再次查看数据表中最终数据,得lilei得balance为0。

MySQL之事务详解_第8张图片

MySQL之事务详解_第9张图片

结论:上述演示中,事务1结束时,查得lilei的balance为101,而所有事务结束后,最终数据库lilei的balance持久的数据为0。 这就是读未提交的脏读现象。

3.1.2.2 case2:不可重复读

第一步:事务1,第一次查询数据,事务未结束。

SELECT * FROM account WHERE id = 1 ;

MySQL之事务详解_第10张图片

第二步:事务2修改了数据。

UPDATE `test`.`account` SET `balance`='101' WHERE  `id`=1;

MySQL之事务详解_第11张图片

第三步:事务1,再次查询数据,事务还未结束。

SELECT * FROM account WHERE id = 1 ;

MySQL之事务详解_第12张图片

结论:同一条sql,在一个事务中,不同时间查询同一条数据,结果不一样,原因是,这条记录被其他事务修改了。这就是出现了不可重复读。

3.1.2.3 case3:幻读

第一步:事务1,查询account表,查询的3条数据,此时事务没有关闭。

MySQL之事务详解_第13张图片

第二步:事务2在account表中新增了一条数据。

MySQL之事务详解_第14张图片

第三步:还未提交的事务1,做了一些操作后,又查询了一次account表,查出了4条数据。

MySQL之事务详解_第15张图片

总结:事务1在事务未关闭前,多次读一个表,读到的数据不一样,读到了其他事务新增的数据,产生了幻读。

3.1.2.4 case3:脏写

第一步:事务1,查询数据,得lilei得balance为101。事务未提交。

MySQL之事务详解_第16张图片

第二步:事务2修改lilei的balance为3000,提交事务。

MySQL之事务详解_第17张图片

第三步:事务1还没有执行完,还在用第一次读到的数据101做业务计算,其实数据已经被事务2修改为3000了,事务1做完业务计算后,更新且提交数据,此时事务1将事务2已经修改的3000更改为了10101。

MySQL之事务详解_第18张图片

第四步:查询最终数据。

MySQL之事务详解_第19张图片

总结:事务1的修改覆盖了事务2已经修改的数据,导致事务2修改的数据丢失了,造成了脏写,即更新丢失。

3.2 读已提交(Read committed)

3.2.1 定义

允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生

3.2.1 示例分析

  • 将事务的隔离级别设置为读未提交:set tx_isolation='read-committed';
  • 读已提交,解决了脏读,该章节示例验证是否还存在脏读现象,幻读和不可重复读验证,参考3.1读未提交的示例分析。
3.3.1.1 验证是否解决了脏读

第一步:事务1读取id为1的数据,balance 为0,此时事务1未关闭。

MySQL之事务详解_第20张图片

第二步:事务2将id为1的balance修改为100,但没有提交。

MySQL之事务详解_第21张图片

第三步:事务1再次读取id为1的数据,此时balance 还是0,没有读取到事务2修改但没有提交的100。说明此隔离级别解决了脏读

MySQL之事务详解_第22张图片

第四步:将事务2提交数据。

MySQL之事务详解_第23张图片

第五步:事务1再次读取id为1的数据,读到balance为100,是事务2提交的数据。此时事务2还没有结束,但是多次读取的id为1的数据确不一样,这就出现了不可重复读。

MySQL之事务详解_第24张图片

3.2.1 读已提交解决脏读原理

读已提交的隔离级别下使用了MVCC(multi-version concurrency control)机制。

3.3 可重复读 (Repeatableread)

3.3.1 定义

 对同一字段的多次读取结果都是一致 的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。

3.3.2 示例分析

  • 将事务的隔离级别设置为读未提交:set tx_isolation='repeatable‐read';
  • 读已提交,解决了脏读,该章节示例验证是否还存在脏读和不可重复读现象,幻读验证,参考3.1读未提交的示例分析。
3.3.2.1 验证是否解决了脏读和不可重复读

第一步:事务1第一次查询id为1的数据,balance为0。此时事务1未提交。

MySQL之事务详解_第25张图片

第二步:事务2查询并修改id为1的balance为100,此时事务未提交。

MySQL之事务详解_第26张图片

第三步:事务1第二次查询id为1的数据,balance为0,没有读取到事务2未提交的100。解决了脏读。

MySQL之事务详解_第27张图片

第四步:事务2提交数据。

MySQL之事务详解_第28张图片

第五步:查询此时数据库id为1的数据,此时balance为100。

MySQL之事务详解_第29张图片

第六步:事务1第三次查询id为1的数据,balance还是0。 三次查询,balance的数据一样, 解决了不可重复读。MySQL之事务详解_第30张图片

3.3.3 可重复读解决脏读和不可重复读原理。

可重复读的隔离级别下使用了MVCC(multi-version concurrency control)机制,select操作是快照读(历史版本);insert、update和delete 是当前读(读的是当前版本)。

3.4 串行化(Serializable)

  • 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读
  • 串行模式下innodb的查询也会被加上行锁,如果查询的记录不存在会给这条不存在的记录加上锁(这种是间隙锁)。 如果客户端A执行的是一个范围查询,那么该范围内的所有行包括每行记录所在的间隙区间 范围都会被加锁。此时如果客户端B在该范围内插入数据都会被阻塞,所以就避免了幻读。
  • 这种隔离级别并发性极低,开发中很少会用
  • 具体验证示例可参考上面章节示例

4. 大事务的影响

  • 并发情况下,数据库连接池容易被撑爆。
  • 锁定太多的数据,造成大量的阻塞和锁超时。
  • 执行时间长,容易造成主从延迟 回滚所需要的时间比较长。
  • undo log膨胀。
  • 容易导致死锁。

5.事务优化

  • 将查询等数据准备操作放到事务外。
  • 事务中避免远程调用,远程调用要设置超时,防止事务等待时间太久。
  • 事务中避免一次性处理太多数据,可以拆分成多个事务分次处理。
  • 更新等涉及加锁的操作尽可能放在事务靠后的位置。
  • 能异步处理的尽量异步处理。
  • 应用侧(业务代码)保证数据一致性,非事务执行。

6. MVCC多版本并发控制机制

  •  Mysql在可重复读隔离级别下如何保证事务较高的隔离性,这个隔离性就是靠MVCC(Multi-Version Concurrency Control)机制来保证的,对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免了频繁加锁互斥,而在串行化隔离级别为了保证较高的隔离性是通过将所有操作加锁互斥来实现的。
  • Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制
  • MVCC机制的实现就是通过read-view机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据。

6.1 undo日志版本链与read view机制详解

6.1.1 undo日志理解

undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链。

6.1.2 read View理解

        Read View 就是一个保存事务ID的list列表,记录是的本事务执行时,MySQL还有哪些事务在执行。在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束之前永,远都不会变化;如果是读已提交隔离级别在每次执行查询sql时都会重新生成read-view;这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。

        Read View 会将当前版本链中的事务分为三段,如下图所示:

MySQL之事务详解_第31张图片

6.1.3版本链比对规则

对照上图理解

 1. 如果 row 的 trx_id 落在绿色部分( trx_id

2. 如果 row 的 trx_id 落在红色部分( trx_id>max_id ),表示这个版本是由将来启动的事务生成的,是不可见的(若 row 的 trx_id 就是当前自己的事务是可见的);

3. 如果 row 的 trx_id 落在黄色部分(min_id <=trx_id<= max_id),那就包括两种情况

  • a. 若 row 的 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见(若 row 的 trx_id 就是当前自己的 事务是可见的);
  • b. 若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,可见。

下图是一个RR级别下事务,undo日志与查询事务生成的readView和结果,可根据上述规则进行分析理解。

        此处分析一下查询事务1的readView和几次查询的结果。查询事务1,开启事务查询时, 有未提交的事务100,事务200,和已提交的事务300,则它的readView 则为[100,200] 300;则此时已提交事务则在tx_id<100,未开始事务区间则为tx_id>300; 未提交与已提交事务区间则为100<=tx_id<=300。查询事务1,第一次查询时,已产生的版本列有tx_id为80,300。300在100<=tx_id<=300这个区间但不在活跃数据[100,200]中,所以这条数据对事务查询1可见,balance为500。经过事务200提交数据,事务查询1第二次查询数据,此时日志中的版本链tx_id为200,200,300,80。此时查询1的readView 仍然为[100,200] 300。版本链中的200在100<=tx_id<=300并且在数组[100,200]中,则此时版本链中tx_id中为200的两条数据对事务查询1都不可见,直到对比到tx_id为300时,可见,所以查得balance仍然为500。查询事务1第三次查询,同理。

MySQL之事务详解_第32张图片

6.1.4readview和可见性算法的原理解释

        readview和可见性算法其实就是记录了sql查询那个时刻数据库里提交和未提交所有事务的状态。 要实现RR隔离级别,事务里每次执行查询操作readview都是使用第一次查询时生成的readview,也就是都是以第一次查询 时当时数据库里所有事务提交状态来比对数据是否可见,当然可以实现每次查询的可重复读的效果了。 要实现RC隔离级别,事务里每次执行查询操readview都会按照数据库当前状态重新生成readview,也就是每次查询都是 跟数据库里当前所有事务提交状态来比对数据是否可见,当然实现的就是每次都能查到已提交的最新数据效果了。

6.1.5 注意点

  • 对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的 trx_id,同时在该条记录的头信息(record header)里的(deleted_flag)标记位写上true,来表示当前记录已经被删除, 在查询时按照上面的规则查到对应的记录如delete_flag标记位为true,意味着记录已被删除,则不返回数据。
  • begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改操作或加排它锁操作(比如 select...for update)的语句,事务才真正启动,才会向mysql申请真正的事务id,mysql内部是严格按照事务的启动顺序来分 配事务id的。

你可能感兴趣的:(mysql,数据库)