目录
事务的引入
事务的特点
事务的提交方式
验证原子性和持久性
验证事务的隔离性和一致性
查看并设置隔离性
读未提交
读提交
可重复读
串行化
事务的一致性
上期我们学习了MySQL数据库中第一个比较重要的知识点------索引,索引可以提高sql语句中select语句查询时的效率,本期我们将学习MySQL中第二个比较重要的知识点------事务。
事务的引入
注意:因为myisam存储引擎不支持事务,而innodb存储引擎支持事务,所以事务的知识点我们是基于innodb存储引擎进行学习的。
引入一个场景,张三和李四是一对非常要好的朋友,某天,张三遇到了一些急事,需要一大笔钱,所以就去找李四借,李四停了张三的阐述后,果断的答应,然后从某某银行的账户中给张三转了一大笔钱。
上述就是一个简单的转账场景,转账意味着什么,当李四给张三进行转账时,势必有两个现象。
- 李四向张三转账,所以李四的账户中的余额会减少对应的转账金额。
- 李四接受了张三的转账,所以张三的账户中会增加对应的转账金额。
当然,上述现象是业务逻辑层的现象。
在数据库中,李四账户金额减少和张三账户金额增加,其实就对应了对数据库中 银行账户表 的两个update语句,一个update语句用于减少表中李四对应的账户余额字段,一个update语句用于增加表中张三对应的记录的账户余额字段。
基于此,我们给出MySQL事务的概念。
MySQL事务是基于上层一个业务逻辑的一组sql语句的集合。上述情景的两个update语句 组合起来 就构成了一个事务。
-
事务的特点
事务有 原子性,持久性,隔离性,一致性 这四个特点。
先来谈原子性。
- 原子性:一个事务中的所有sql操作,要么就成功的执行完毕,要么就不执行。如果执行的过程中出现了错误,我们就会回滚到事务最开始的状态,也就是在没有执行事务中的sql语句之前,表中原有的记录的状态。
然后谈持久性。
- 持久性:当事务中的所有sql语句执行完之后,通过commit提交,此时sql语句对表记录的影响,就被持久化在了数据库表中,永久保存。
再来谈隔离性。
- 隔离性: 隔离性又分为四个级别,读未提交,读提交,可重复读,可串行化。隔离性简单来说就是两个事务在访问同一张表时,一个事务对表的操作并不会影响到另一个事务,从而保证另一个事务的sql语句执行的结果符合上层的业务逻辑,最终维护数据的一致性。
最后谈一致性。
- 一致性:一致性简单来说,就是在事务执行前后,数据库中的数据的前后对比,是符合业务逻辑的, 且必须是有效的数据。就比如,李四给张三转账了100,李四接受了100,那么李四的账户表中的余额必须减少100,张三的账户表中的余额必须增加100。
为什么MySQL要引入事务?
之所以MySQL要引入事务,这是为应用层的业务逻辑考虑,比如业务层有多个用户访问同一张数据库中的表,就得有原子性和隔离性进行保证多用户访问数据的安全性和一致性,以及最终将用户修改的数据持久化到数据库表中。
有了事务之后,MySQL可以根据事务的四个特点,实现多用户场景下对数据库访问的安全性,一致性,持久化等要求。
事务的提交方式
事务的提交方式有两种,一种是手动提交,一种是自动提交(mysql的默认方式)。
一般情况下,mysql的自动提交是打开的。
关闭mysql自动提交。
重新设置mysql自动提交。
验证原子性和持久性
设置mysql默认隔离级别为读未提交。
重新设置隔离级别之后,重启客户端才会生效。
创建account表。
情景1:验证事务的开始与回滚(打开mysql的默认提交)。
开始事务,并设置回滚点save。begin开始事务,savepoint设置回滚点。
插入一条记录,设置回滚点save1。
再插入一条记录,并查询表中的所有记录。
回滚到save1,再次查看account表中的记录,此时应该只有张三这条记录。
运行结果符合预期。
再次回滚到save,此时表中的数据应该为空。
运行结果符合预期。
情景二:验证在事务崩溃时,mysql的自动回滚。
查看mysql的自动提交是否打开,并且查询account表的数据。
我们发现,mysql的自动提交是打开的,并且account表中无数据。
开始一个事务,并插入一条记录,然后输入aborted + ctrl + \,异常终止该事务,因为是读未提交,所以可以通过另一个中断随时观察事务1是否回滚。
上图为事务1对应的操作。
另一个终端,查询事务1还没有回滚之前的accout表中的数据。
此时我们发现此时accout表中存在张三这一条数据。
另一个终端再去查询事务1回滚之后的数据。
此时因为事务1发生了回滚,所以account表中没有了记录。
综上,我们发现,当事务中出现了异常时,mysql会自动帮我们将表记录回滚到事务还没开始之前的样子,而mysql的自动回滚与自动提交是否开启是无关的,无论自动提交是否开启,mysql都会在事务崩溃时,将数据回滚到事务开始前的状态。
情景三:事务commit之后,数据会持久化在表中,即使事务崩溃,操作系统也不会自动回滚。
开始事务1,插入一条记录,并commit,然后使事务1崩溃。
通过终端2查看,崩溃前后的数据。
不难发现,事务1崩溃前后,account表中的记录没有发生变化,这是因为在崩溃之前,事务1已经将记录commit提交,使得记录持久化在了数据库表中。
情景四:begin开始的事务,事务的提交方式就变成了手动,此时与mysql的自动提交方式无关,此时事务的提交方式必须手动使用commit提交。
通过情景二,我们可以得出一个结论,当mysql的自动提交打开时,使用begin开始一个事务,在这个事务内插入了记录之后,如果mysql发生了崩溃,mysql不会自动提交事务插入的记录,而是会将事务回滚。
关闭mysql自动提交,begin开始一个事务,插入多条记录,然后使得mysql崩溃。
通过另一个终端,查看mysql崩溃之后,表中的记录。
我们发现,此时我们关闭了mysql自动提交之后,begin开始一个事务,插入了多条记录之后,在mysql崩溃之后,mysql也不会自动提交事务中插入的记录,而是会事务自动回滚。
综上,我们可以得出一个结论,begin开始的事务,事务的提交与mysql自动提交无关,mysql只会在崩溃之后对事务进行回滚,不会在mysql崩溃之后,提交事务中插入的记录。
所以begin开始的事务只能在begin之后,使用commit自动提交。
将mysql事务自动提交关闭。
开始事务,插入数据之后,使用commit手动提交。
通过另一终端,查询发现,插入的数据已经持久化在了数据库表中。
综上,begin之后的事务,只能使用commit手动提交。
那么, mysql事务的自动提交,既然不会对begin之后的事务提交,那么自动提交会给哪些事务自动提交呢?
情景五:验证mysql的自动提交对哪些事务有效。
关闭mysql事务自动提交,没有使用begin开始事务,直接在mysql中使用insert往account表中插入数据,然后使得mysql崩溃。
在另一个终端中,查看崩溃之后表中的数据。
我们发现,此时表中是没有数据的。
打开mysql的事务自动提交。
不使用begin开始事务,使用insert子句插入几条记录, 使得mysql崩溃。
在另一个终端中,查询account表中的数据。
我们惊奇的发现,在mysql自动提交开启的情况下,没有使用begin开始事务,而是使用insert插入记录,此时即使mysql崩溃,最终也会在崩溃之后在数据库表中查询到崩溃前插入的记录。
所以,通过上述mysql自动提交开启和关闭所对应的两个现象,这就进一步说明了,单条的mysql也是一个事务,且mysql的自动提交是对于单条sql语句的事务是有效的。
通过以上五个情景,我们其实已经验证了事务的原子性和持久性的特点。
事务的原子性通过事务的回滚和commit实现。比如当事务插入的数据无误,使用commit提交,提交之后,在于其它事务看来,当前的事务已经成功的插入了对应的记录。如果插入的数据有误,或者崩溃,要么是手动使用rollback关键字进行回滚,回滚到最初事务还没开始的状态,要么是mysql帮助回滚,回滚到最初事务还没开始的状态,对于其它事务看来,就像当前事务没有进行任何sql操作一般。
事务的持久性,则是通过在begin的事务中通过手动commit实现,或者是单条的sql记录,mysql自动帮助提交,实现持久性。
验证事务的隔离性和一致性
如何理解隔离性?
隔离性,其实就是为了防止,事务和事务之间的相互干扰,而引出的一个性质,为了防止干扰而又引入了一些防止干扰的措施,不同的措施就对应了不同的隔离级别。
比如我们如今的降噪耳机,有普通降噪和深度降噪,降噪耳机就实现了自己与外界嘈杂环境的隔离,而降噪耳机的模式,就是自己与外界环境的隔离级别,一个模式降噪级别低,一个模式降噪级别高。
事务的隔离性,分为四个级别,读未提交,读提交,可重复读,串行化。我们依次进行验证。
查看并设置隔离性
查询全局mysql全局隔离级别。
当前mysql全局隔离级别为 READ_UNCOMMITTED。
使用tx_isolation 默认查看的是当前会话的隔离级别。
那么全局隔离级别和会话隔离级别有什么区别呢?
全局隔离级别,一旦一设置,影响的是之后所有的的新建的会话,之后新建的会话的隔离级别都默认是全局隔离级别,全局隔离级别在当前会话设置之后,不会影响当前会话的隔离级别,只影响之后的新建的会话的隔离级别。
会话隔离级别,影响的是当前的会话的隔离级别,不会影响除过当前会话之外的其它正在运行的会话的隔离级别,且设置好当前会话的隔离级别之后,当前会话的隔离级别立即生效。
一般情况下,我们以全局隔离级别为主。
读未提交
使用begin开始两个事务,设置两个事务的隔离级别为读未提交。
在左边的事务插入一条记录之后,不提交,在右边的事务中进行查看。
我们发现此时即使左边的事务在插入记录时没有进行提交,此时右边的事务也可以看到插入的记录。
上述的这种现象我们称之为读未提交。
那么读未提交会造成什么问题呢?
我们依次分析,比如说,左边的事务插入了一个用户的记录,即在账户表中插入了一个用户,那么插入之后,右边的事务也读取到了插入的用户,此时如果这个事务刚好将读取的新插入的用户交给了上层进行业务逻辑,比如说银行给新插入的用户开了钻卡,但是如果此时,左边的事务发现自己插入的数据插入错了,所以进行了回滚,此时就相当于没有插入记录,那么此时上层进行业务逻辑就出了大bug,就相当于是对于一个不存在的账户开了钻卡。
所以可重复读会造成,左边的事务插入的记录,在右边进行读取时,读取到的这个新插入的记录可能是一个无效的记录。比如右边的事务在进行读取之后,左边的事务进行了回滚,那么右边的事务读取到的新插入的记录就是一个无效的记录。我们把右边读取到的这一个无效的记录就称作脏数据,把右边这种读取到脏数据的读的方式称作脏读。
综上,读未提交 会造成 脏读 的现象,导致读取到的记录是一个无效的记录。
读提交
使用begin开始左右两个事物,设置全局隔离级别为读提交。
查看左右两个事务的会话隔离级别。
都为读提交。
在左边事务插入记录之前,左右两个事务查询数据库表中的记录。
此时数据库表中的记录都是空的。
往左边事务中插入一条记录,不commit,在左边事务中继续查询表中记录。
此时左边事务查询到的记录仍为空,和读未提交不同。
此时左边事务手动commit,提交事务。
在左边事务提交之后,我们在右边中查询到了左边事务插入的记录。
读提交会造成什么问题呢?
通过读提交,我们发现,左边的事务在不同的时间段对同一个表进行查询,但是两次查询,表中的记录不同。我们把在一个事务内,不同时间段查询同一张表,但是两次查询表中的数据记录不同的现象成为不可重复读。
不可重复读会造成什么问题呢?
还是针对于account这个表,比如说在业务逻辑层,某某银行根据account表中的用户记录,如果用户的账户余额>10亿,则送辆su7ultra。10亿>账户余额>=5个亿,则送一辆su7pro。5亿>账户余额>=1亿,则送一辆su7。
有这样一个用户名为马云,他的账户中有10个亿,所以当右边的用户在第一次进行查询时,查询到了大于10亿的记录中,有一个记录的账户名称为马云,所以银行给马云送了一两su7ultra。马云突然又从账户中转走了2个亿,此时左边来了一个事务,此时将马云的账户中的余额转走了2亿,然后提交,此时马云的账户中剩下了8亿。然后右边的事务又来查询 10亿>账户余额>=5个亿的记录,此时因为右边事务已经提交,所以又在右边事务查询的 10亿>账户余额>=5个亿的记录中查询到了马云的记录,所以此时银行又为马云送了一辆小米su7pro。此时对于银行而言,本来一个用户最多送一个赠品,但是此时相当于对马云这一个用户送了两次赠品,所一就出现了事故。所以不可重复读也会造成不好的影响。
可重复读
使用begin开始左右两个事物,设置全局隔离级别为可重复读。
左右两个事务的隔离均为可重复读。
往左边的事务插入一条记录,不提交,在左边的事务中进行数据的查询。
此时,左边插入的记录因为左边的事务没有提交,所以在右边的事务中无法看到新插入的记录。
在左边的记录插入之后,提交左边的事务,在右边事务中再次查询。
此时,即使左边的事务进行了提交,也无法在右边的事务中查询到左边事务插入的数据。
在右边的事务提交之后,就可以看到左边的事务插入的数据。
所以即使此时左边的事务进行任何的插入操作,修改删除操作,此时对于右边的事务是没有任何影响的,所以就保证了右边的事务的数据库表中数据的一致性。
那么可重复读对于事务而言有什么问题吗?
对于,MySQL数据库而言,可重复读是没有任何问题的,但是对于其他数据库而言,可重复读,可能会导致左边事务在插入记录并且提交后,在右边的事务中可以查询到新插入的数据。所以对于右边的事务而言,不同时间段查询数据库中的同一张表,但是在后续查询的过程中,出现了数据库中表记录新增的现象,这种现象我们称之为幻读。
串行化
使用begin开始左右两个事物,设置全局隔离级别为串行化。
查看左右两个事务所在会话的隔离级别。
往左边的事务中插入一条记录。
如上图,此时出现了一个比较奇怪的现象,我们发现,左边事务在插入记录时卡住了,这是为什么呢?
读未提交,读提交,可重复读,当事务处于这三种隔离级别下的时候,多个事务是可以同时执行的。但是但是但是,对于串行化隔离级别下的事务,只能先后去执行,也就是一个事务执行完之后另一个事务再去执行,但是select操作除外,select操作可以同时执行。所以此时当左边的事务插入记录时就插入就阻塞住了,必须等待右边的事务提交之后,左边的事务才能进行插入操作。
将右边的事务提交,观察左边事务的现象。
通过上图不难发现,一旦右边事务提交,左边事务会立即插入成功。
串行化会造成什么问题吗?
串行化除过事务的执行是串行的,效率比较慢之外,是没有什么问题的。
四种隔离级别总结。
隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
读未提交 | 是 | 是 | 是 | 否 |
读已提交 | 否 | 是 | 是 | 否 |
可重复读 | 否 | 否 | 否 | 否 |
可串行化 | 否 | 否 | 否 | 是 |
读未提交,读提交,可重复读都是并发的,也就是允许多个事务同时在这些隔离级别下并发运行,并发运行就势必有着事务安全问题,那么mysql是如何解决事务安全问题的呢?在mysql中mvcc(多版本控制)+行锁来解决事务安全的问题,这个在事务进阶章节我们进一步讲述。
串行化是不是并发的,是一个事务执行完另一个事务再开始执行。所以串行化不会有事务安全问题,但是,效率是很低的。
mysql基于效率和安全性考虑,最终选取了可重复读作为了默认的隔离级别,因为既兼顾了多事务并发的高效率,又通过mysql内部的mvcc和行锁机制,解决了多事务并发的问题和脏读,不可重复读,幻读等问题。
事务的一致性
事务的一致性其实我们已经验证了。事务的安全性是依靠上层应用和mysql事务本身的原子性,隔离性,持久性共同决定的。
比如原子性,当李四转账给张三,那么就一定有两个操作,对李四和张三账户余额的修改,原子性保证了,在李四账户余额减少操作成功之后,如果李四的账户余额增加操作没有成功,就会通过回滚机制去阻止当前的事务操作,只有对张三和李四的表中账户余额的操作都成功时,进行commit提交持久化,这样就实现了使用原子性和一致性,保障了李四账户余额减少多少和张三余额增加多少的一致性。
再比如隔离性,还是上述的可重复读,因为可重复读这个隔离级别,解决了不可重复读的问题,即不会造成上述的读提交隔离级别中,银行给马云发送了两量车的类似事件。
大家在想想,如果上层的应用层代码,在李四给张三转账的这个程序中,如果只有李四余额减少的代码而没有张三余额增加的代码,那么就算底层有原子性和持久性作为保障,但是仍然会出现转账时李四余额减少而张三余额没有增加的漏洞,这是事务造成的吗,当然不是,这是上层应用代码的漏洞,所以事务的一致性,一定是由上层的应用逻辑代码和事务的原子性,隔离性和持久性共同保障的。
以上便是事务初阶的所有内容。
本期内容到此结束^_^