一、什么是事务,为什么需要使用事务?
1、什么是事务
事务是数据库运行的基本单位,是指对数据库进行读或写的一个操作序列,这些操作要么全部成功,要么全部失败,是一个不可分割的工作单位.
2、为什么要使用事务
可以保证数据的一致性和完整性.
二、MySQL中的事务
1、MySQL中默认采用的是自动提交(autocommit)模式,如下所示:
在自动模式下,每条sql语句都会被当做一个事务执行提交操作.
2、在MySQL中可以通过如下方法关闭autocommit(也就是不再使用MySQL默认的事务,显式开启事务),需要注意的是,autocommit针对的是当前连接,在当前连接中修改了参数,不会对其它的连接产生任何影响.
如果显示的开启事务,具体的操作步骤如下:
1 start transaction;
2 一条或者多条SQL语句;
3 commit;
4 [rollback;]
其中start transaction标识事务的开始,commit标识提交事务,将执行结果写入数据库,如果SQL语句执行出现问题,会调用rollback,回滚所有已经执行成功的SQL语句,当然也可以在事务中直接使用rollback语句进行回滚.
特别注意:在MySQL中存在一些特殊的命令,如果在事务中执行了这些命令,会马上强制执行commit提交事务;例如DDL语句(create|drop|alter table)、lock tables等.不过DQL(select)、DML(insert、delete、update)命令都不会强制执行commit提交事务.
三、ACID特性
1、原子性
原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做;如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。
实现原理(Undo Log)
在说明原子性原理之前,首先介绍一下MySQL的事务日志。MySQL的日志有很多种,如二进制日志、错误日志、查询日志、慢查询日志等,此外InnoDB存储引擎还提供了两种事务日志:redo log(重做日志)和undo log(回滚日志)。其中redo log用于保证事务持久性;undo log则是事务原子性和隔离性实现的基础。
下面说回undo log。实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句.InnoDB实现回滚,靠的是undo log:当事务对数据库进行修改时,InnoDB会生成对应的undo log;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB会根据undo log的内容做与之前相反的工作:对于每个insert,回滚时会执行delete;对于每个delete,回滚时会执行insert;对于每个update,回滚时会执行一个相反的update,把数据改回去。
以update操作为例:当事务执行update时,其生成的undo log中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到update之前的状态。
2、一致性
事务执行前与执行后数据内在的逻辑始终是成立的。比如转账前与转账后两人存款的总和始终不变。
事务必须是使数据库从一个一致性状态变到另一个一致性状态。在事务T开始时,此时数据库有一种状态,这个状态是所有的MySQL对象处于一致的状态,例如数据库完整性约
束正确,日志状态一致等,当事务T提交后,这时数据库又有了一个新的状态,不同的数据,不同的索引,不同的日志等,但此时,约束,数据,索引,日志等MySQL各种对象还是要
保持一致性(正确性)。 这就是 从一个一致性的状态,变到另一个一致性的状态。也就是事务执行后,并没有破坏数据库的完整性约束(一切都是对的)。
这里我们举个大家都在说的财务系统的例子.
A要向B支付100元,而A的账户中只有90元,并且我们给定账户余额这一列的约束是,不能小于0.那么很明显这条事务执行会失败,因为90-100=-10,小于我们给定的约束了.
这个例子里,支付之前我们数据库里的数据都是符合约束的,但是如果事务执行成功了,我们的数据库数据就破坏约束了,因此事务不能成功,这里我们说事务提供了一致性的保证.
然后我们再看个例子
A要向B支付100元,而A的账户中只有90元,我们的账户余额列没有任何约束.但是我们业务上不允许账户余额小于0.因此支付完成后我们会检查A的账户余额,发现余额小于0了,
于是我们进行了事务的回滚.
这个例子里,如果事务执行成功,虽然没有破坏数据库的约束,但是破坏了我们应用层的约束.而事务的回滚保证了我们的约束,因此也可以说事务提供了一致性保证(ps:事实上,是我们应用层利用事务回滚保证了我们的约束不被破坏).
最后我们再看个例子
A要向B支付100元,而A的账户中只有90元,我们的账户余额列没有任何约束.然后支付成功了.
这里,如果按照很多人的理解,事务不是保证一致性么?直观上账户余额为什么能为负呢.但这里事务执行前和执行后,我们的系统没有任何的约束被破坏.一直都是保持正确的状态.
所以,综上.你可以理解一致性就是:应用系统从一个正确的状态到另一个正确的状态.而ACID就是说事务能够通过AID来保证这个C的过程.C是目的,AID都是手段.
3、隔离性
隔离性是指,事务内部的操作和其它事务是隔离的,并发执行的各个事务之间不能互相干扰,严格的隔离性对应的是事务隔离级别中的Serializable(串行化),但是实际应用中出于对性能方面的考虑,基本不会使用串行化.
3.1、在讨论隔离性之前我们首先来看一下并发情况下读操作可能会出现的问题.
(1)、脏读:事务A读到了事务B中未提交的数据.
时间 | 事务A | 事务B |
T1 | 开启事务 | 开启事务 |
T2 | 将Venus的余额由100修改为200 | |
T3 | 查询Venus的余额,显示为200 | |
T4 |
事务B开启了事务,执行了修改操作,但是并没有执行commit操作,而这个时候事务A中切读取到了事务B未提交的数据,这种现象称为脏读.
(2)、不可重复读:事务A两次读取到的数据不一样.脏读和不可重复读的区别是,脏读读取的数据是未提交的数据,而不可重复读读取到的数据是已经提交的数据.
时间 | 事务A | 事务B |
T1 | 开启事务 | 开启事务 |
T2 | 查询Venus的余额为100 | |
T3 | 修改Venus的余额,将100改成200 | |
T4 | 提交事务 | |
T5 | 查询到Venus的余额为200 |
事务A第一次读取到的余额为100,而第二次读取到的余额为200,我们把这种两次读取到的数据不一致的现象称为不可重复读.
(3)、虚读(幻读):事务A两次按照一定的条件查询数据库,两次查询的结果行数不一致.不可重复读和虚读的区别是,不可重复读是两次查询到的结果内容不一致而虚读是两次查询的结果行数不一致.
时间 | 事务A | 事务B |
T1 | 开启事务 | 开启事务 |
T2 | 查询结果 0<id<5的余额 Venus:100 [id=1] | |
T3 | 添加用户Mercury:200 [id=2] | |
T4 | 提交事务 | |
T5 |
查询结果 0<id<5的余额 Venus:100 [id=1] Mercury:200 [id=2] |
事务A第一次读取到的数据只有Venus一条记录,而第二次读取到数据有Venus和Mercury两条记录,第一次查询和第二次查询得到的数据条数不同,这种现象称为虚读或者也称为幻读.
3.2、隔离级别
数据库事务的隔离级别有四种,由低到高分别是 Read Uncommitted、Read Committed、Repeatable Read、Serializable.其中MySQL的默认隔离级别是 Repeatable Read,可以通过下面查询语句查询出(MySQL5.x和MySQL8.x查询的语句有变化,这里以8.x演示).
1、Read Uncommitted(读未提交)
一个事务读取另外一个未提交的事务的数据.
2、Read Committed(读已提交)
一个事务读取另外一个事务提交之后的数据.
3、Repeatable Read(可重复读)
一个事务开启之后就不允许其它事务进行任何的修改操作.
4、Serializable(串行化)
各个事务串行顺序执行,B事务必须等待A事务执行完毕后才能执行.
3.3、不同事务隔离级别在并发下可能出现的问题
事务隔离级别 | 是否会出现脏读 | 是否会出现不可重复读 | 是否会出现虚读(幻读) |
Read Uncommitted | 会 | 会 | 会 |
Read Committed | 不会 | 会 | 会 |
Repeatable Read | 不会 | 不会 | MySQL8.0以后不会 |
Serializable | 不会 | 不会 | 不会 |
3.4、案例演示
<一>、当MySQL的事务隔离级别是READ UNCOMMITTED时会出现脏读、不可重复读、幻读吗?
由于MySQL默认的事务隔离级别是REPEATABLE-READ,所以案例演示之前我们要将MySQL的事务隔离级别修改为Read Uncommitted,使用SELECT @@GLOBAL.TRANSACTION_ISOLATION; 可以查询出全局事务隔离级别,可以看出,MySQL默认的事务隔离级别是REPEATABLE-READ,使用SET GLOBAL TRANSACTION ISOLATION LEVEL (事务隔离级别);来设置全局事务的隔离级别,可以看出两个客户端的事务隔离级别都变成了READ_UNCOMMITTED.
1、左边客户端开启事务--->右边客户端开启事务--->右边客户端第一次查询Venus余额,显示为100--->左边客户端将Venus的余额由100修改为200--->右边客户端第二次查询Venus余额,显示为200(由于左边客户端还未commit事务,而右边客户端读取到了左边客户端未提交的数据,出现了脏读).
2、左边客户端开启事务--->右边客户端开启事务--->右边客户端第一次查询Venus余额,显示为100--->左边客户端将Venus的余额由100修改为200--->左边客户端提交事务--->右边客户端第二次查询Venus余额,显示为200(由于左边客户端修改了数据,并且提交了事务,而右边客户端第一次读取的数据和第二次读取的数据不一致,出现了不可重复读).
3、左边客户端开启事务--->右边客户端开启事务--->右边客户端第一次查询到一条记录Venus--->左边客户端插入一条记录Mercury--->左边客户端提交事务--->右边客户端第二次查询,显示两条记录Venus和Mercury(由于右边客户端两次查询到的记录数不同,出现了虚读(幻读)).
经过测试,我们发现当MySQL的事务隔离级别为READ UNCOMMITTED时,会出现脏读、不可重复读、幻读(虚读).
<二>、当MySQL的事务隔离级别是READ UNCOMMITTED时会出现脏读、不可重复读、幻读吗?
在此之前我们将M有SQL的事务隔离级别修改为READ COMMITTED;
1、左边客户端开启事务--->右边客户端开启事务--->右边客户端第一次查询Venus余额,显示为100--->左边客户端将Venus的余额由100修改为200--->右边客户端第二次查询余额,显示为100(由于左边客户端还未commit事务,而右边客户端两次读取到的数据是一致的,未出现脏读).
2、左边客户端开启事务--->右边客户端开启事务--->右边客户端第一次查询Venus余额,显示为100--->左边客户端将Venus的余额由100修改为200--->左边客户端提交事务--->右边客户端第二次查询Venus余额,显示为200(由于左边客户端修改了数据,并且提交了事务,而右边客户端第一次读取的数据和第二次读取的数据不一致,出现了不可重复读).
3、左边客户端开启事务--->右边客户端开启事务--->右边客户端查询到两条记录Venus和Mercury--->左边客户端插入一条记录Mars--->左边客户端提交事务--->右边客户端第二次查询,显示三条记录Venus、Mercury和Mars(由于右边客户端两次查询到的记录数不同,出现了虚读(幻读)).
<三>、当MySQL的事务隔离级别是REPEATABLE READ时会出现脏读、不可重复读、幻读吗?
在此之前我们将M有SQL的事务隔离级别修改为REPEATABLE READ;
1、左边客户端开启事务--->右边客户端开启事务--->右边客户端第一次查询Venus余额,显示为200--->左边客户端将Venus的余额由200修改为300--->右边客户端第二次查询余额,显示为200(左边的客户端虽然没有提交事务,但是右边客户端两次读取的数据一致,未出现脏读).
2、左边客户端开启事务--->右边客户端开启事务--->右边客户端第一次查询Venus余额,显示为200--->左边客户端将Venus的余额由200修改为100--->左边客户端提交事务--->右边客户端第二次查询余额,显示为200(由于左边客户端修改了数据,并且提交了事务,但是右边客户端第一次读取的数据和第二次读取的数据一致,未出现不可重复读).
3、左边客户端开启事务--->右边客户端开启事务--->右边客户端查询到三条记录Venus、Mercury和Mars--->左边客户端插入一条记录Sature--->左边客户端提交事务--->右边客户端第二次查询,显示三条记录Venus、Mercury和Mars(由于右边客户端两次查询到的记录数相同,并且数据一致,未出现了虚读(幻读)).
4、持久性
持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
实现原理(Redo Log)
redo log和undo log都属于InnoDB的事务日志。下面先聊一下redo log存在的背景。
InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存(Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。
Buffer Pool的使用大大提高了读写数据的效率,但是也带了新的问题:如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。于是,redo log被引入来解决这个问题:当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。
既然redo log也需要在事务提交时将日志写入磁盘,为什么它比直接将Buffer Pool中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:
(1)刷脏是随机IO,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序IO。
(2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效IO大大减少。