蛋疼的mysql之旅:事务事务的特性事务的隔离级别
Posted deepdarkfantasy019
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了蛋疼的mysql之旅:事务事务的特性事务的隔离级别相关的知识,希望对你有一定的参考价值。
关于事务,我很不能理解,什么事务隔离级别、事务回滚、锁机制等。而且很让我困惑的是,查了那么多的博客,我依旧没看懂,信心备受打击,决心就算要花很多时间,都要把这些东西弄懂。
DROP TABLE IF EXISTS `city`;
CREATE TABLE `city` (
`ID` int(0) NOT NULL AUTO_INCREMENT,
`Name` char(35) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT ‘‘,
`CountryCode` char(3) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT ‘‘,
`District` char(20) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL DEFAULT ‘‘,
`Population` int(0) NOT NULL DEFAULT 0,
PRIMARY KEY (`ID`) USING BTREE,
INDEX `CountryCode`(`CountryCode`) USING BTREE,
CONSTRAINT `city_ibfk_1` FOREIGN KEY (`CountryCode`) REFERENCES `country` (`Code`) ON DELETE RESTRICT ON UPDATE RESTRICT
) ENGINE = InnoDB AUTO_INCREMENT = 4079 CHARACTER SET = latin1 COLLATE = latin1_swedish_ci ROW_FORMAT = Dynamic;
INSERT INTO `city` VALUES (1, ‘Kabul‘, ‘AFG‘, ‘Kabol‘, 1779600);
INSERT INTO `city` VALUES (2, ‘Qandahar‘, ‘AFG‘, ‘Qandahar‘, 237500);
INSERT INTO `city` VALUES (3, ‘Herat‘, ‘AFG‘, ‘Herat‘, 186800);
INSERT INTO `city` VALUES (4, ‘Mazar-e-Sharif‘, ‘AFG‘, ‘Balkh‘, 127800);
INSERT INTO `city` VALUES (5, ‘Amsterdam‘, ‘NLD‘, ‘Noord-Holland‘, 731200);
INSERT INTO `city` VALUES (6, ‘Rotterdam‘, ‘NLD‘, ‘Zuid-Holland‘, 593321);
INSERT INTO `city` VALUES (7, ‘Haag‘, ‘NLD‘, ‘Zuid-Holland‘, 440900);
INSERT INTO `city` VALUES (8, ‘Utrecht‘, ‘NLD‘, ‘Utrecht‘, 234323);
INSERT INTO `city` VALUES (9, ‘Eindhoven‘, ‘NLD‘, ‘Noord-Brabant‘, 201843);
INSERT INTO `city` VALUES (10, ‘Tilburg‘, ‘NLD‘, ‘Noord-Brabant‘, 193238);
SET FOREIGN_KEY_CHECKS = 1;
1、存储引擎
mysql 有一个重要的特征,被称为 Pluggable Storage Engine Arehitecture (可替换存储引擎构架).这里需要解释一下 MySQL 功能的实现方式。 MysQL 功能可以分为两个部分,外层部分主要完成与客户端的连接以及事前调查 sQL 语句的内容的功能,而内层部分就是所谓的存储引擎部分,它负责接收外层的数据操作指示,完成实际的数据输入输出以及文件操作工作。
mysql提供了多种存储引擎( storage Ensine ) , 还可以给不同的表选择不同的存储引擎。不同于其他关系数据库而独有的特征。用户可以根据自己的目的或喜好来选择存储像这样用户可以任意选择存储引擎的功能,现在 MySQL 提供了如表 6 一 l 所示的主要存储。
到这里就可以了,作为菜鸡的我觉得知道有哪些引擎就OK。我们使用InnoDB引擎,因为它支持事务,不指定的话,默认都是InnoDB。
使用select create table 表名 来确认当前使用的引擎是哪一个。
2、为什么需要事务:
《mysql高效编程》以银行转账为例子。
这个解释太复杂,我给一个简单的解释:如何把大象装进冰箱?
1.打开冰箱。
2.把大象放进去。
3.关上冰箱。
这三个步骤依次做完,我们才完成把大象装进冰箱 这件事。但是如果执行完步骤2,还没关上冰箱,这时候别人叫我去吃饭,那么这件事显然是没有完成的。
那么怎么办呢?我有两个选择。
第一,吃完饭后回来接着做步骤3,关上冰箱。(前滚操作)
第二,恢复原状(回滚操作),那么两步
1.拿出大象。
2.关上冰箱。
注意这个例子,它已经包含了一些mysql事务的知识点,包括事务的工作机制(redo日志和undo日志)。
这在实际业务中是很常见的,我们平时的业务,往往一次请求到服务器,同时会访问、修改多个数据。这个依次完成,只有所有操作全部按顺序,正确执行完,此次的业务才算正确处理完。只要遇到异常情况中途中断,那么数据就失去完整性。这显然是不被允许的。(因为数据之间是相互关联的,牵一发动全身。)
3、事务的特性
事务具有的ACID属性:
原子性(Atomicity):事务中所有的操作视为一个原子单元,即对于事务所进行的修改、删除等操作只能是全部提交或者全部回滚。
一致性(Consistency):事务在完成操作后,必须使所用的数据从一种一致性状态变为另外一种一致性状态,所有的变更都必须应用于事务的修改,以确保数据的完整性。
隔离性(Isolation):一个事务中的操作语句所做的修改必须与其他事务相隔离,在进行事务查看数据时数据所处的状态,要么是被另一个事务并发修改之前的状态,要么是修改之后的状态,即当前事务不查看由另一个并发事务正在修改的数据。这种特性通过锁机制实现。
持久性(Durability):事务完成之后,所做的修改对数据的影响是永久的,即使系统重启或者是出现故障数据仍可以恢复。
关于这个我并不想太深究,查阅了一些资料后,以下是一些我自己的理解:
首先事务并不是mysql独有的,它甚至也不是数据库独有的。它更像是一种规范:正如前面大象的例子,当我把“把大象装进冰箱”看作一个事务,那么它应该遵守ACID属性:
1.原子性:不可分割,组成事务的步骤是一个整体,它们要么全部执行,要么全部不执行。如果中途遇到问题中断了,必须有方法能够保证它继续执行(前滚)或者撤销之前的操作(回滚)。这一块mysql主要通过redo日志和undo日志实现。
2.一致性:???谁来给我解释一下?
3.隔离性:事务之间不能互相影响。比如此时我哥们正在做一个“把苹果放进冰箱”,我这刚把冰箱门打开,他就把苹果放进去了,导致我大象放进去的时候屁股会很疼。这显然是不行的。这一块mysql主要通过锁机制来保证并发执行的事务之间不会相互影响。这一块mysql主要MVCC(多版本并发控制)实现。
4.持久性:同样是redo日志来保证,记录了事务的每一步骤,确保即使宕机,重启之后也能够继续执行之前的事务。
4、一个简单的事务演示
我们有一个city表:
4.1、回滚事务
执行一下几行命令:
(1)BEGIN;开始事务
(2)DELETE FROM city; 删除表中的全部数据
(3)select * from city; 确认city表的数据
(4)rollback;回滚事务
(5)select * from city; 再次确认city表的数据
这里我们没有commit提交事务,而是rollback,因此在第二次select * 的时候,可以看到之前的操作都已经被回滚了。如果是commit的话,city表的数据就真正地被删除。
4.2 自动提交功能
你可能要说,我平时写sql都不需要这么麻烦,直接delete就可以了,为什么还要开始事务再提交事务呢?那是因为在MySQL中执行命令时,通常都直接被确定了。也就是说用户不用意识此事,所有的命令都会被自动COMMIT。特别是当存储引擎为MylSAM的情况下,本身它是不支持事务处理的,只要执行了命令,所有的命令都会被提交。
如果存储引擎为InnoDB时,当执行了[STARTTRANSACTION](或BEGIN)命令后,将不会自动提交了,只有明确执行了 COMMIT命令后才会被提交,在这之前可以执行ROLLBACK命令回滚更新操作。
可以手动关闭/开启自动提交功能:
-- INNODB默认开启自动提交,你也可以关闭它。
查看:select @@AUTOCOMMIT
关闭:SET AUTOCOMMIT=0
开启:SET AUTOCOMMIT=1
4.3 部分回滚(SAVEPOINT)
事务执行过程中,我们可以标记节点,之后我们可以回滚到指定节点。也就是说回滚到SAVEPOINT的位置就停止回滚。继续刚才的例子,删除id为1,2的数据,标记节点sq,再删除id为3的数据。
三条数据都被删除了,接下来回滚到sq节点,再次确认数据:
id=3的数据又回来了,也就是说回滚到sq节点就停下了。
4.4 几个例外
DROP DATABASE;
DROP TABLE;
DROP;
ALTER TABLE.
这几个命令是不走事务的,每次执行都会直接提交,要注意。
5.事务的隔离级别
刚才演示的是“一个”事务,如果有多个事务同时执行呢?
5.1读未提交(READ UNCOMMITTED)
再开一个命令窗口,演示两个事务同时操作city表:
1.A事务将id为1的数据的population字段数值-100,
2.B事务查询id为1的数据;
3.A事务回滚;
4.B事务查询id为1的数据;
5.查看city表确认数据。
像这种,一个事务读取到其它事务未提交的数据的情况,就属于读未提交(脏读)。如何理解读未提交?事务在未commit之前,可以把它看成“将要发生但还未发生的事情”。举个例子,我是一个警察,每天坐在办公室查看街道的监控,有一天我发现监控里面张三和李四发生口角,张三说要拿刀砍死李四,我立马出去把张三抓了起来。可张三说他只是吓唬吓唬李四。这就是“脏读”。因为犯罪并未实质上发生。
5.2读已提交(READ COMMITTED)
继续上面的演示,解决脏读的方式就是提高隔离级别,从READ UNCOMMITTED;------>READ COMMITTED;
1.A事务将id为1的数据的population字段数值-100,
2.B事务查询id为1的数据;
3.A事务提交;
4.B事务查询id为1的数据;
这里已经解决了脏读,A事务还未提交时,B事务一直读取到的是原来的数据。也就是说我不管张三说了什么,只有等张三真的拿到砍死了李四(提交事务),我才开始行动。
但是这同样存在问题,A事务提交后,B事务再次查看id=1的数据,发现数据改变了。就是说,在事务B下多次查看相同的数据,由于其他事务更新并提交了数据,导致查询结果不同。这就是不可重复读。
5.3可重复读(REPEATABLE READ)
要避免这种情况,再把隔离水平提高一级 变成REPEATABLE READ。重复上面的步骤
1.A事务将id为1的数据的population字段数值-100,
2.B事务查询id为1的数据;
3.A事务提交;
4.B事务查询id为1的数据;
5.B事务提交
6.再次确认数据
可以看到,A事务提交后,B事务再次查询数据依旧没有变化,这样就保证一个事务中多次查询相同数据,得到的结果是一致的。
但是!!你应该跟我有一样的疑惑:A事务提交了啊!id=1的数据确实改变了,你这样不就是和事实不符?查阅了很多资料,只找到这个勉强能够说服我的。
也就是说,应该从“隔离”二字入手,设置隔离级别的目的,是为确保事务之间不能相互影响,或者说,调整事务之间互相影响的程度。REPEATABLE READ有点类似“快照”,也就是说,在事务并发场景下,一个数据对象可能不断的变换模样,那么一个事务第一次获取数据后,相当于照了一张相片下来,之后它再怎么变我都不关心了,我只认我第一次看到的模样。
5.3.1MVCC(多版本并发控制)实现读写分离。
先继续上面的例子,再多试一种情况:
1.B事务查询id为1的数据;
2.A事务将id=1的数据,id改为5555,
3.A事务提交;
4.B事务查询id=1的数据;
5.B事务提交
6.再次确认id=1的数据;
没毛病,即使是作为条件的字段被更改,依旧符合可重复读。
但是,你应该想到了,id=1的数据,如果A事务对Population +100然后提交。B事务对Population -100会怎么样?按理说Population原本是1778700,A事务+100=1778800,B事务-100=1778700,数据应该不变。但是如果A事务提交,B事务还是当成1778700,那么-100=1778600。Population到底是以事务A提交前的为准还是以事务A提交后的为准?这种重复读会不会导致修改数据出错呢?
1.B事务查询id为1的数据;
2.A事务将id=1的数据,Population +100
3.A事务提交;
4.b事务将id=1的数据,Population -100
5.B事务查询id=1的数据;
6.B事务提交
7.再次确认id=1的数据
因此可以证明,MySQL的可重复读,对事务B进行查询时,事务A提交的更新不会影响到事务B。但是对事务B进行更新时,事务A提交的更新会影响到事务B。
我们刚才说,可重复的的意思是:我不管其他事务做了什么,事务开始后,第一次读到某个数据,即使之后其他事务修改并提交,我还是只认第一次读到的数据。但是“update”语句好像有不是这样了。大哥,你不按套路出牌啊!怎么跟刚才自相矛盾呢?注意:谈到事务的隔离级别,我们总是会说到所谓的虚读、幻读、不可重复读。这都是基于“读”操作。而“写”操作,依旧是根据实际的数据来,因此并不会出现混乱的现象。
一句话来总结就是:在并发事务的场景下,不可避免的会出现多个事务操作同一个对象导致互相干扰的问题。(只要一谈到并发,肯定会出现这个问题)为了确保数据完整性。采用“读写分离”的方式,“读”主要通过基于MVCC(多版本并发控制)实现的“调整事务隔离级别”来确保并发事务的独立性。“写”操作主要通过锁机制来实现。
你可能又要问了,我JAVA出身,JAVA中的并发咱还是略有研究的,都是通过“锁机制”来保证线程安全。为什么mysql的“读”操作要搞什么MVCC、事务隔离级别。这么花里胡哨的呢?这样的好处很明显:MVCC最大的优点是读不加锁,因此读写不冲突,并发性能好。
5.3.2 MVCC(多版本并发控制)原理:
InnoDB实现MVCC,多个版本的数据可以共存,主要是依靠数据的隐藏列(也可以称之为标记位)和undo log。其中数据的隐藏列包括了该行数据的版本号、删除时间、指向undo log的指针等等;当读取数据时,MySQL可以通过隐藏列判断是否需要回滚并找到回滚需要的undo log,从而实现MVCC;
MVCC(Multi-Version Concurrency Control多版本并发控制):
MVCC每次更新操作都会复制一条新的记录,新纪录的创建时间为当前事务id
优势为读不加锁,读写不冲突
InnoDb存储引擎中,每行数据包含了一些隐藏字段 DATA_TRX_ID,DATA_ROLL_PTR,DB_ROW_ID,DELETE BIT
DATA_TRX_ID 字段记录了数据的创建和删除时间,这个时间指的是对数据进行操作的事务的id
DATA_ROLL_PTR 指向当前数据的undo log记录,回滚数据就是通过这个指针
DELETE BIT位用于标识该记录是否被删除,这里的不是真正的删除数据,而是标志出来的删除。真正意义的删除是在mysql进行数据的GC,清理历史版本数据的时候。
具体的DML:
INSERT:创建一条新数据,DB_TRX_ID中的创建时间为当前事务id,DB_ROLL_PT为NULL
UPDATE:将当前行的DB_TRX_ID中的删除时间设置为当前事务id,DELETE BIT设置为1
DELETE:复制了一行,新行的DB_TRX_ID中的创建时间为当前事务id,删除时间为空,DB_ROLL_PT指向了上一个版本的记录,事务提交后DB_ROLL_PT置为NULL
可知,为了提高并发度,InnoDb提供了这个「非锁定读」,即不需要等待访问行上的锁释放,读取行的一个快照即可。 既然是多版本读,那么肯定读不到隔壁事务的新插入数据了,所以解决了幻读。
作者:ElseF
链接:https://www.jianshu.com/p/47e6b959a66e
5.4 串行化(SERIALIZABLE)
1.B事务查询city表。
2.A事务删除2条数据。
3.A事务提交。
4.B事务再次查询city表。
这里是想演示“幻读”,即
但是我发现并不是说隔离级别到串行化(SERIALIZABLE)才能解决“幻读”,事实上,只要读已提交(READ COMMITTED),就已经不存在“幻读”现象了。而且,当我想试验一下《Mysql高效编程》中的示例时,发现跟书本中的描述又不一样了。所以,串行化我们单独讨论:
Serializable会对所有读操作都加锁,读写发生冲突,不会使用MVCC。在每个读的数据行上,加上锁,使之不可能相互冲突,因此,会导致大量的超时现象。
1.A/B事务修改隔离级别为串行化
2.B事务查询city表;
3.A事务删除id=4的数据;
4.A事务提交;
5.B事务再次查询city表;
此时发现,当B事务查询了city表后,A事务的delete语句进入了等待状态。说明city表已经加锁了。只能先commit事务B,释放锁之后才能执行delete语句。
写操作被加锁,那如果读操作呢?
注意,这里是行级锁,我们刚才事务B查的是全表,每一行都被加了锁。如果用条件限制一下,只查询id<=7的数据,而事务A删除id=8的数据呢?会不会被锁?
这一次正常执行,并没有进入等待状态。
接下来反过来,A事务先删除id=5,B事务先查id>5,再查全部;
暂时先这样,下次继续讨论锁机制、redo日志和undo日志。
以上是关于蛋疼的mysql之旅:事务事务的特性事务的隔离级别的主要内容,如果未能解决你的问题,请参考以下文章
MySQL 基础 事务 -- 事务简介事务操作事务四大特性(原子性一致性隔离性持久性)并发事务问题(不可重复读脏读幻读)事务隔离级别(解决并发事务问题)