目录
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多版本并发控制机制
当两个或多个事务选择同一行数据修改,有可能发生更新丢失问题,即最后的更新覆盖 了由其他事务所做的更新。
事务A读取到了事务B已经修改但尚未提交的数据。
事务A内部的相同查询语句在不同时刻读出的结果不一致。
事务A读取到了事务B提交的新增数据。
set tx_isolation='read‐uncommitted'; --读未提交
set tx_isolation='read‐committed'; -- 读已提交
set tx_isolation='repeatable‐read'; -- 可重复读
set tx_isolation='serializable'; -- 串行化
隔离级别
|
脏读 (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');
最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
将事务的隔离级别设置为读未提交。
set tx_isolation='read-uncommitted';
第一步:事务1,执行一下代码,开启事务,设置事务隔离级别为读为提交,查询lilei的数据,不提交。
set tx_isolation='read-uncommitted';
BEGIN;
SELECT * FROM account WHERE id = 1 ;
第二步:事务2,更新lilei的balance,并查询,但不提交。
set tx_isolation='read-uncommitted';
BEGIN;
UPDATE `test`.`account` SET `balance`='101' WHERE `id`=1;
SELECT * FROM account WHERE id = 1 ;
第三步:查询数据库里面的事务个数,应该有2个事务。
SELECT *
FROM information_schema.innodb_trx
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 1;
第四步:事务1,再次查询该记录,且提交。此时,事务1结束,事务1得到的最终lilei的数据为101。 数据库里面应该只有一个事务。
SELECT * FROM account WHERE id = 1 ;
COMMIT;
第五步:事务2 回滚数据。
第六步:再次查看事务。所有事务已结束。再次查看数据表中最终数据,得lilei得balance为0。
结论:上述演示中,事务1结束时,查得lilei的balance为101,而所有事务结束后,最终数据库lilei的balance持久的数据为0。 这就是读未提交的脏读现象。
第一步:事务1,第一次查询数据,事务未结束。
SELECT * FROM account WHERE id = 1 ;
第二步:事务2修改了数据。
UPDATE `test`.`account` SET `balance`='101' WHERE `id`=1;
第三步:事务1,再次查询数据,事务还未结束。
SELECT * FROM account WHERE id = 1 ;
结论:同一条sql,在一个事务中,不同时间查询同一条数据,结果不一样,原因是,这条记录被其他事务修改了。这就是出现了不可重复读。
第一步:事务1,查询account表,查询的3条数据,此时事务没有关闭。
第二步:事务2在account表中新增了一条数据。
第三步:还未提交的事务1,做了一些操作后,又查询了一次account表,查出了4条数据。
总结:事务1在事务未关闭前,多次读一个表,读到的数据不一样,读到了其他事务新增的数据,产生了幻读。
第一步:事务1,查询数据,得lilei得balance为101。事务未提交。
第二步:事务2修改lilei的balance为3000,提交事务。
第三步:事务1还没有执行完,还在用第一次读到的数据101做业务计算,其实数据已经被事务2修改为3000了,事务1做完业务计算后,更新且提交数据,此时事务1将事务2已经修改的3000更改为了10101。
第四步:查询最终数据。
总结:事务1的修改覆盖了事务2已经修改的数据,导致事务2修改的数据丢失了,造成了脏写,即更新丢失。
允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
第一步:事务1读取id为1的数据,balance 为0,此时事务1未关闭。
第二步:事务2将id为1的balance修改为100,但没有提交。
第三步:事务1再次读取id为1的数据,此时balance 还是0,没有读取到事务2修改但没有提交的100。说明此隔离级别解决了脏读。
第四步:将事务2提交数据。
第五步:事务1再次读取id为1的数据,读到balance为100,是事务2提交的数据。此时事务2还没有结束,但是多次读取的id为1的数据确不一样,这就出现了不可重复读。
读已提交的隔离级别下使用了MVCC(multi-version concurrency control)机制。
对同一字段的多次读取结果都是一致 的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
第一步:事务1第一次查询id为1的数据,balance为0。此时事务1未提交。
第二步:事务2查询并修改id为1的balance为100,此时事务未提交。
第三步:事务1第二次查询id为1的数据,balance为0,没有读取到事务2未提交的100。解决了脏读。
第四步:事务2提交数据。
第五步:查询此时数据库id为1的数据,此时balance为100。
第六步:事务1第三次查询id为1的数据,balance还是0。 三次查询,balance的数据一样, 解决了不可重复读。
undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链。
Read View 就是一个保存事务ID的list列表,记录是的本事务执行时,MySQL还有哪些事务在执行。在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束之前永,远都不会变化;如果是读已提交隔离级别在每次执行查询sql时都会重新生成read-view;这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。
Read View 会将当前版本链中的事务分为三段,如下图所示:
对照上图理解
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),那就包括两种情况 下图是一个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第三次查询,同理。 readview和可见性算法其实就是记录了sql查询那个时刻数据库里提交和未提交所有事务的状态。 要实现RR隔离级别,事务里每次执行查询操作readview都是使用第一次查询时生成的readview,也就是都是以第一次查询 时当时数据库里所有事务提交状态来比对数据是否可见,当然可以实现每次查询的可重复读的效果了。 要实现RC隔离级别,事务里每次执行查询操readview都会按照数据库当前状态重新生成readview,也就是每次查询都是 跟数据库里当前所有事务提交状态来比对数据是否可见,当然实现的就是每次都能查到已提交的最新数据效果了。
6.1.4readview和可见性算法的原理解释
6.1.5 注意点