一、从转账到事务解说
1、事务的概念:
我们先说个小故事:
平行世界里,某一对异地情侣boy和girl,boy有100块钱,girl有10块钱,现在boy想在给girl 手机转账50块钱,对应的数据库操作为:
UPDATE account SET balance = balance - 50 WHERE id = boy; # 1
UPDATE account SET balance = balance + 50 WHERE id = girl; # 2
只有操作1和2都共同完成的时候,boy的转账才算成功,可是,如何在执行了操作1之后,服务器突然断电了,操作2还没有执行。。。
这种情况就相当于从boy的账户里减了钱,但是girl的账户却没收到钱,这对情侣的总资金就变少了50,在他们看来那50块钱就像是不翼而飞了一样。
那数据库是怎么解决这种问题的呢?这就是我们即将要讲到的事务了。
“事务"不是某种客观存在的实体,而是一种逻辑操作的组合,这种组合的操作需符合ACID特性。
(1)、原子性(Atomicity) A
原子性取义于物理原子,原子是物质的最小单位,不可分割;原子性操作要么成功进入下一个状态,要么失败退回最初状态,不存在中间状态。
在我们现实生活中的转账,只有两种情况:要么转账成功,要么转账失败;无论是这里面的哪种情况转账双方的总资金是不变的;不存在上面提到平行世界中的那个例子——只转到一半。
很多在用户看来是单一操作的,在机器或者服务器看来就是很多条操作的组合(就像高级语句int i =0;对应的多条的二进制语句一样),原子性就是保证这些组合要么全执行,要么全不执行(退回执行前的状态)。
(2)、一致性(Consistency) C
这里的一致性是指数据库世界和现实世界规则的一致性,当然数据库里的规则当然没能和现实世界一一对应,毕竟现实世界的规则很多很多,比如身份证号不能重复,性别只有男和女,房价不能为负,高考分数最高是750等等。
也正因为这个的存在,数据库层面也出现了各种约束:主键约束、唯一约束、非空约束等。
一致性,比如一个高考生的成绩表中出现了751分,这种情况不用去核查都知道是数据错了,因为现实世界中高考成绩是不可能超过750的(最起码现在是这样)
(3)、隔离性(Isolation) I
假设前面提到的boy给girl转50块,分两次操作:第一次(T1)转30,第二次(T2)转20;但每一次转账都细分为6个步骤。
一般情况下,无论是先进行T1,还是T2,boy都可以成功转账50给girl的,毕竟30+20=50,20+30=50;但是实际上却没有这么简单,真实数据库中,T1和T2的操作可交替进行的(类比操作系统中的进程并发)。
经过上图的流程,最后我们惊讶地发现boy的账户由100变到了70,girl的账户由10变成了60;原来两人的总资金100+10=110,而现在70+60=130,现总资金比原总资金多了20。
这很明显就存在了问题,不然如果一直这样下去的话,所有人都去转账了,钱不少反而变多。
所以在数据库中,我们不仅要保持事务的原子性,还要隔离其他事务对某种事务的影响,使得其他的状态转换并不能影响该状态转换的结果,这就是隔离性。
(4)、持久性(Durability) D
持久性是指一旦状态转换成功后,该状态就被记录下来,无法回滚。
就好像boy给girl转30,一旦转成功之后,转账成功的状态就被永久记录下来了,不可逆,boy不可能单方面把30要回来的,当然如果是girl把30给回boy,这已经是另外一个过程了。
说了这么多,终于到了事务概念的总结了:
事务就是需要保持以上四种特性(ACID)的一系列数据库操作!
二、事务的状态及其转换
事务是一个抽象的概念,是一系列的数据库操作的集合,在它的执行过程中分为一下几种状态:
- 活动的:事务在正常执行。
- 部分提交的:事务操作已经完成,但是操作的结果只停留在内存中,还没有刷盘保存。
- 提交的:事务操作被刷盘之后。
- 失败的:只要事务无法刷新回磁盘都是失败的,数据库不允许不具有持久性的事务存在,又因为原子性,既然无法成功,那只能失败。
- 中止的:回滚,由中间状态回到初始状态。
三、MySQL中事务的基本操作
1、开启事务
BEGIN;
START TRANSACTION;
事务可以由以上两个语句开启,BEGIN和START TRANSACTION有相同的功效,都代表着一个事务的开启,但是START TRANSACTION后边可以加几个修饰符:
- READ ONLY ,只读,标志着当前开启的事务是一个只读事务,只能读取数据,不能修改其实事务也能访问到的数据;但是可以修改临时表中的数据,临时表只存在当前会话,其他事务无法访问。
START TRANSACTION,READ ONLY ;
- READ WRITE ,读写,标志着当前开启的事务是一个读写事务,既可以读取数据,也可以修改数据。
START TRANSACTION,READ WRITE ;
- WITH CONSISTENT SNAPSHOT ,启用一致性读。
START TRANSACTION,WITH CONSISTENT SNAPSHOT;
若没显式指明事务的访问模式,则默认是READ WRITE,读写模式!
2、提交事务
BEGIN或START TRANSACTION开始的事务,一直到COMMIT结束。
COMMIT;
(1)自动提交
MySQL中有一个系统变量 autocommit ,默认是on开启的状态的,当我们不显式使用BEGIN或START TRANSACTION开始事务,则默认每一个语句都是一个事务,执行完毕后自动提交(受autocommit的状态影响)
(2)隐式提交
有些时候,在autocommit =off关闭的状态下,事务还是悄悄的被提交了,这种就是隐式提交。
下面列举几种常见的隐式提交的情况:
- 定义或修改数据库对象的数据定义语言(Data definition language,缩写为: DDL )。
数据库对象是指数据库 、 表 、 视图 、 存储过程 等
当我们使用CREATE 、ALTER 、 DROP 等DDl语句 去修改数据库对象的时候,这些语句就会被隐式提交(这时候是提交由事务开始到该语句的所有,并不是单单提交该语句;可以理解为这种情况下,该DDl语句等效与COMMIT)。 - 隐式使用或修改 mysql 数据库中的表
ALTER USER 、 CREATE USER 、 DROP USER 、 GRANT 、 RENAME USER 、 REVOKE 、 SET PASSWORD 等
- 两个开启事务语句之间
BEGIN;
SELECT … # 事务中的一条语句
UPDATE … # 事务中的一条语句
… # 事务中的其它语句
BEGIN; # 此语句会隐式的提交前边语句所属于的事务
3、手动中止事务
事务是具有ACID特性的一系列数据库操作,当我们发现其中某条语句写错了的时候,可以手动中止事务,让事务回滚(回到某一状态)。
ROLLBACK; # 回滚到上一次提交后的状态(初始状态)
在事务未提交之前,我们都可以使用ROLLBACK进行回滚,复原。
ROLLBACK是手动回滚的指令,但是在事务遇到错误而无法继续执行的时候,会自动回滚的。
在不支持事务的存储引擎中无法进行回滚。
保存点(savepoint):
当我们在一个事务中执行了很多语句,进行了很多操作,突然发现写错了一个,那这时候该怎么办?
使用ROLLBACK回滚吗?可是ROLLBACK只能回到上次事务提交的时候(初始状态),也就是说如果使用ROLLBACK的话,之前的操作全部被推翻,只能重到再来,有种“一夜回到解放前的感觉”。
为了解决这个问题,保存点(savepoint)就诞生了。
在事务开始之后,我们可以在确认前面的操作没问题的前提下,为当前位置设置一个保存点,这样就算后面出错了也能回到当前位置,而不是最初位置。
保存点定义语句:
SAVEPOINT 自定义保存点名称
回滚到某保存点的回滚语句:
ROLLBACK TO 自定义保存点
四、事务的隔离级别
事务隔离级别的划分源于事务隔离性和服务器性能之间的矛盾,由两者间的平衡力度不同而划分。
我们的服务器可以为多个事务提供服务,当多个事务都需要访问同一数据的时候,就需要体现隔离性了,是让它们排队进行呢(串行访问,性能差),还是怎样,这就涉及到了隔离级别。
1、数据并发产生的问题
串行访问,性能差,并发访问,由涉及到隔离性,两者间如何取舍呢?
为此,我们需要先知道数据并发时所可能产生的问题都有哪些,对结果的影响程度如何。
(1)脏写( Dirty Write )
事务A修改了事务B修改而未提交的数据,此时若事务B回滚的话,事务A就会看到一个奇怪的值(和它改的不一样)
(2)脏读( Dirty Read )
脏读和脏写的情况有些类似,事务A查看了事务B修改而未提交的数据,若事务B回滚的话,事务A相当于读到一个不存在的数据,这就是脏读。
(3)不可重复读( Non-Repeatable Read )
不可重复读,顾名思义就是不能重复读取数据,因为重复读取的数据不一致,一次是一个样,另一次又是另外一个样。
强调的是在一个事务A开启的过程中,别的事务B可以更改事务A中的数据并进行修改提交,造成事务A前后查询结果不一致!!
**注意和脏读区别:**脏读是指可以访问到未提交的数据,不可重复读是指在事务A查询的时候,其他事务可以对它所查询的数据进行修改。
(4)幻读( Phantom )
事务A查询一次后,事务B增加了某些行并提交,当事务A再次查询时,发现了幻影记录(事务B增加的行)。
强调的是第二次读取比第一次的多!
2、SQL中的四种隔离级别
针对上述提到的并发产生的问题,根据严重性大小进行排序:
脏写 > 脏读 > 不可重复读 > 幻读
并发会引发上述问题,并发度越高,隔离性越低,当串行时,隔离性最高,但性能却最低。
为此,SQL标准中制定了四个隔离级别:
- READ UNCOMMITTED :读未提交, 事务A可以读到别的事务B未提交的执行结果(中间数据),因此,可能导致脏读、不可重复读、幻读 。
- READ COMMITTED :读已提交, 事务A只能读到别的事务B提交后的结果,可以避免产生脏读,但是不可重复读,幻读的问题仍然存在。
- REPEATABLE READ :可重复读, 通过一定的机制使得事务A在别的事务B修改前后读到的数据一致,即实现了多次读取,结果一致。(这个具体的机制,我们后面再慢慢聊,不着急~)
- SERIALIZABLE :可串行化 ,事务对同一数据的访问只能串行访问,只有先来的访问结束了,其他事务才可访问,这个隔离级别可以解决上述所有并发产生的问题,但是服务器的性能却极差。
因为脏写的问题是最严重的(事务修改了数据,实际上却没有修改),所以SQL中制定的四个隔离级别都解决了脏写问题,所以此处就不谈脏写了。
脏写的严重性体会:
某老板开了家网店可以销售价值100的物件,为了优惠顾客,最初价格是10,后来优惠期过了,他的管理员启动事务B想把价格改回100的,可却手抖写少了两个0,成了1,但现在事务B还没提交;此时,老板启动事务A发现显示的价格是1,因此他赶紧骂骂咧咧地来修改为100并提交,可是另一边他的管理员发现错误了,他执行ROLLBACK回滚。。
此时,若老板和管理员说:“你不用管了,我已经修改好了,就用这个价格吧”
然而此时的真实价格是1.并不是100,如果继续下去的话,那老板岂不是亏惨了??
五、MySQL中支持的四种隔离级别
MySQL中默认的隔离级别是REPEATABLE READ ,但是我们可以通过以下语句修改:
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL SERIALIZABLE;` `\#或` `SET [GLOBAL|SESSION] TRANSACTION_ISOLATION = 'SERIALIZABLE';
使用GLOBAL或SESSION的区别:
GLOBAL针对所有事务会话有效,SESSION只针对当前事务会话有效。
两者都不影响之前或当前的事务,只会影响后续事务。
是我们可以通过以下语句修改:
SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL SERIALIZABLE;` `\#或` `SET [GLOBAL|SESSION] TRANSACTION_ISOLATION = 'SERIALIZABLE';
使用GLOBAL或SESSION的区别:
GLOBAL针对所有事务会话有效,SESSION只针对当前事务会话有效。
两者都不影响之前或当前的事务,只会影响后续事务。