事务的概念
事务是站在用户角度的。比如说,用户完成转账,对应的sql语句就是张三的账户扣除钱,李四的账户加上钱,完全这一任务(即钱的加和减少)需要俩个SQL语句。对应的这俩个updata语句就能完成转账这一“事务”。
MySQL在同一时间会同时存在大量的事务,大量的事务同时对一张表做CURD,如果不加控制,就会导致数据不一致的问题,常见的幻读、脏读、不可重复读等。
本文就会介绍事务的由来,如何开启事务,事务的四种特性:原子性、统一性、永久性、隔离性。介绍MVCC无锁并发版本以及实现原理。
当然一个事务并不是简单的SQL指令集合,还必须满足下列的特点:
- 原子性:要么完成,要么没做。事务的执行过程,如果出错了,就必须进行数据的回滚(RallBack)。
- 一致性:事务的前后是可预期的。
- 隔离性:数据库允许多个并发的事务同时对数据的读取和修改(CURD)。隔离性是可以防止多个事务并发时的数据不一致。不同程度的隔离级别对事物的并发程度是不同的容忍的。
- 持久性:在事务结束后,数据会被永久保存,不可回退。
为什么存在事务?
事务的存在是为了方便程序员的编写。事务的存在,我们只需要开启事务,如果编写错了,就回滚事务 ,事务结束后,只需要提交事务。
简单的三步操作就能将事务的性质全部做到。我们不在关系,网络问题,宕机了,数据如何恢复,事务帮我们做好了这一操作。
事务本质就是为了应用层服务的,是数据库与生俱来的。
事务的提交方式
事务的提交方式有俩种,手动提交和自动提交。一般来说,默认的是自动提交。
通过show variables like "autocommit"查看事务提交的方式。
show variables like 'autocommit';
修改默认提交的方式
set autocommit = 1;
事务的实验
下面就来演示事务的性质,为了有利于测试,将隔离级别调到最低(读未提交级别)。以id为主键,创建InnoDB测试表。
修改全局的事务隔离级别为读未提交。隔离级别分为全局和当前会话,当前会话会在登录的时候,继承全局的隔离级别,所以修改全局后,要修改会话的隔离级别,就要重启mysql。
set global transaction isolation level read uncommitted;
为什么要修改隔离级别呢?为什么修改成读未提交?后面会详细介绍。
创建测试表:账户表,id为主键,姓名,薪资。
1)正常演示 - 证明事务的开始与回滚
操作1:启动俩个终端,分别开始事务(begin)。事务1和事务2.
左边终端设置保存点save1,右边事务查询表account ,得到结果为空。
操作2:左边的终端插入一条数据(1,张三,100),并且设置回滚点save2。
现象:右边终端查询表account,看到刚刚插入的消息(1,张三,100)
由于隔离级别是最低的,读为提交,只要事务1操作,事务2就会读取到最新的数据,在事务1没有提交commit 时候,事务2就能读取到更改的数据。
操作3 :事务1插入消息(2,李四,100)并且,保存回滚点3 .事务2立马select查询。
现象:能够查询到俩条数据,张三、李四。逻辑和操作2完全一致。
操作 4:回滚数据:rollback to +保存点
事务1回滚(rolbackl)保存点2 ,事务2进行查询。
现象:事务2中查询的结果和操作2中一致,看到了历史的版本。
从这个实验,就能发现事务的机制,提供数据的回滚。
非正常演示 - 为commit,客户端崩溃
同样开启双事务,事务1和事物2,事务1正常插入消息。之后将事物1退出,模拟mysql崩溃。
左边终端输入ctr +\ 终止mysql ,事务2查询结果
观察到的现象,即使在事务1插入数据的情况下,事务2也能查询到数据。
但是事务1没有提交,一旦发生奔溃,数据就会自动回滚。
符合事务的原子性,即要么完成了,要么就是没做。
非正常演示 - 证明commit 后,客户端奔溃,也不影响数据
同样开启双事务,事务一插入俩条数据后,commit提交。
终止客户端,模拟mysql奔溃,事务2查询结果。
在事务1提交之后,关闭mysql1,观察到事物2 依旧可以查询到数据。
符合事物的永久性,即一个事物提交commit 之后,数据就是被永久保存的,即使是mysql崩溃也不会回滚数据。
演示 - 事物会开启自动提交
事物是在开始之后,会开启自动提交,不论是否提前将自动提交关闭。
mysql客户端1,关闭自动提交后,开启事务。事务1更新某一行的数据,事务2读取。
模拟mysql崩溃,事务2查询,观察结果。
演示:单条SQL实际上就是事务
实际上,autocommit的设置影响的是单条SQL 指令。
如果autocoomit 被设置为OFF,单条SQL需要commit才能持久化。
操作:
1)设置为自动提交,然后更新account的数据,最后关闭mysql1,观察结果。
2)设置为手动提交,然后更新account的数据,最后关闭mysql1,观察结果。
执行完单条sql后,将数据库终止,在另一个终端select 表,可以看到新增的数据。
将自动提交设置为手动提交。继续观察结果。
如果设置的是手动提交,单条sql语句被执行后,如果没有提交,因为设置了最低的隔离级别,另一个mysql客户端 也能看到。但是如果客户端出现崩溃,由于事务的原子性,数据会发生回滚,另一个客户端就不能看到更新的数据,看到的是历史版本。
总结一下:
- 事务是为程序员服务的,事务有四个性质:隔离性,持久性,原子性,一致性。上述的例子实际上就算原子性和持久性。
- 事务的begin和commit,在此期间,将提交方式修改为自动提交。如果发生错误会发生回滚数据。
- 单条SQL实际上也是事务,收到autocommit的影响。
- 如果一个事务没有设置回滚点(savepoint)如果发生回滚,会回滚到事务的起始位置。
- 一个事务被提交,就无法在回滚。
事务的隔离级别
- 事务的隔离性是保证事务的执行过程,尽量不受到干扰。
- 比如有俩个事务同时对一张表进行CURD,不加保护就会造成数据不一致。
- 事务是由多条sql语句组合的,就会决定事务有三个过程:执行前,执行后,执行中。事务的原子性决定,在事务执行中发生错误,会发生数据回滚。但是俩个事务,事务1和事务2,事务1先开始执行,事务2后到来,事务2要么看到执行前,要么看到执行后。
- 不同程度的隔离级别,就代表事务看到不同程度的干扰。
事务的隔离级别
- 读未提交:事务隔离最低的级别,上述的实验都是按照读未提交来做的。假设有俩个sql客户端,一个客户端就数据表进行修改,没有commit,另一个客户端也没有commit,也能看到对应的修改记录。因为读未提交,隔离级别是最低的,会发生很多并发的问题:脏读、幻读、不可重复读等。
- 读提交 :读提交是大部分数据库默认的隔离级别,它的定义是:俩个事务之间,另一个事务只能看到已经commit的事务操作。隔离级别比读未提交高一点,但是它也存在很多并发问题:幻读、不可重复读等。
- 可重复读:MySQL默认的隔离级别,读取的前后会看到相同的结果,但是会产生幻读。不过MySQL的InnoDB存储引擎中,通过索引锁定(next-key locking)策略来减少幻读的发生,严格意义上不可避免。
- 串行化:最高的隔离级别,强制排序,使之不发生幻读。读的时候使用共享锁保护,但是会导致锁的竞争,效率非常慢,不推荐使用。
查看与设置隔离级别
查看/修改事务的隔离级别主要有俩种:全局隔离级别、当前会话隔离级别。
当前会话隔离级别会在登录的时候,继承全局的隔离级别。
修改全局的隔离级别,不会影响当前会话隔离级别,需要将mysql重启。
修改当前会话的隔离级别,在下一次登录mysql之后,会发生重置。
查看和修改全局隔离级别
查看全局隔离级别:
select @@global.tx_isolation;
修改全局隔离级别:
set global transaction isolation level read committed;
查看/修改当前会话隔离级别
查看当前会话隔离级别
select @@session.tx_isolation;
select @@tx.isolation; //上面的简写
修改会话隔离级别
//修改隔离级别为串行化
set session transaction isolation level serializable;
读未提交
读未提交是最低的隔离级别,事务更新什么,但是没有提交,其它事务立马就能读到。
不建议使用。
演示:修改隔离级别读未提交。开启双事务,事务1和事务2。实验读未提交。
事务1对数据表进行修改,事务2立马可见,双方没有提交。这是隔离级别最低的事务隔离性。
在这一隔离级别通常都会发生脏读:一个事务进行中,读到另外事务更新但是没有提交的数据。
读提交
修改隔离级别。创建双事务,事务1和事务。正常插入数据。
修改隔离级别为读为提交
事务1插入数据,事务2查询。
实验的结果:在事务1新增数据后,事务2不能到读取到数据,只有当事务1commit提交数据记录之后,事务2才能查询到结果。这就是读提交的现象。
读提交会发生一个常见的问题----不可重复读:在不同时间,查询出来的结果不一致。
不可重复读是一个问题:
假如有个成绩等级表,张三在80分段。这时候开启俩个事务,事务1发现张三的成绩分段错误把张三移到90分段,之后提交。事务2,在之前查询过一下,在修改过后也查询过发现张三即在80分段,也在90分段。这个表就是错误的!
可重复读
将当前会话的隔离级别设为可重复读。
事务1插入数据,事务2读取数据。最后分别提交,观察结果。
查看当前会话默认隔离级别。并且开启事务
往事务1中插入一条数据,事务2读取数据,发现无法读取到新增的数据。
事务1更新id=1的姓名为赵六,事务2继续查询,查询不到更新的数据
事务1提交,事务2读取,发现依旧无法读取,只有当事务2也提交后,才能读取到结果。
- 可重复读只有当只有提交了,才能读取到修改的结果。避免了不可重复读问题,在任意一个时间段读取到的结果是一致的。
- 幻读问题:可重复读针对insert操作会产生幻读,insert的数据,在多次读取中被读取出来了,导致前后读取的结果不一致。
为什么会产生幻读?
因为隔离性是对数据加锁实现的,但是insert前,这一条数据是不存在的,因此是无法被互斥锁保护。但是在InnoDB中通过GAP解决了幻读。
串行化
串行化是最高的隔离级别,是依靠锁来实现的。
开启双事务,事务1和事务2。事务1是新增数据,事务2查询。
查看隔离级别
事务1新增一条数据,事务2进行查询。
事务1提交后 ,事务2立马出现结果。
就像线程的阻塞等待。
串行化隔离级别是最高的,代价就是效率过低,不推荐使用。
事务隔离级别的总结
- 隔离级别越高,安全性就越高,效率就会越低。
- 一般来说,不修改mysql默认的隔离级别(可重复读)。
- 脏读是读取到了未提交的数据。不可重复读是在不同时间段读取到结果不一致。幻读是针对新增问题,有时候新增之后,就算没有提交也会看到数据记录。
关于一致性
一致性是可预期的,如果成功commit,结果就是确定的。如果不成功,数据就会回滚。
一致性既有用户提供,也有mysql的原子性保证。只有用户输入正确了,才能达到预期的结果,只有mysql提供原子性,才能保证正确的结果。
下文就会介绍MVCC无所并发版本的实现机制。