关联博文:
【1】 事务的四个关键属性(ACID)
原子性(atomicity): 事务是一个原子操作, 由一系列动作组成。事务的原子性确保动作要么全部完成要么完全不起作用。原子性强调事务的不可分割。
一致性(consistency): 事务执行的前后,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性。如小明转给小红100块,那么这个事务前后小明和小红的总额应该保持不变。事务结束时,所有的内部数据结构也都必须是正确的。
隔离性(isolation): 数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
持久性(durability): 事务完成之后,它对于数据的修改是持久的,即使出现系统故障也能够保持。通常情况下, 事务的结果被写到持久化存储器中。
① 原子性(atomicity)
原子性是指事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚,不存在中间的状态。如果无法保证原子性就会出现数据不一致的情形。
事务是一个原子操作, 由一系列动作组成。 组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有的操作执行成功,整个事务才提交。
事务中的任何一个数据库操作失败,已经执行的任何操作都必须被撤销,让数据库返回初始状态。
② 一致性(consistency)
根据定义,一致性是指事务执行前后,数据从一个合法状态变换到另外一个合法状态。这种状态是语义上的而不是语法上的,跟具体的业务有关。
那什么是合法的数据状态呢?满足预定的约束的状态就叫做合法的状态。通俗一点,这状态是由你自己来定义的(比如满足现实世界中的约束)。满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的!如果事务中的某个操作失败了,系统就会自动撤销当前正在执行的事务,返回到事务操作之前的状态。
举例1: A账户有200元,转账300元出去,此时A账户余额为-100元。你自然就 发现了此时数据时不一致的,为什么呢?因为你定义了一个状态,余额这列必须>=0 。
举例2: A账户200元,转账50元给B账户,A账户的钱扣了,但是B账户因为各种意外,余额并没有增加。你也指定此时数据时不一致的,为什么呢?因为你定义了一个状态,要求A+B的总余额必须不变。
举例3: 在数据表中我们将姓名字段设置为唯一约束,这时当事务进行提交或者事务发生回滚的时候,如果数据表中的姓名不唯一,就破坏了事务的一致性要求。
MySQL如何保证强一致性呢?
InnoDB支持事务,同Oracle类似,事务提交需要写redo、undo。采用日志先行的策略,将数据的变更在内存中完成,并且将事务记录成redo,顺序的写入redo日志中,即表示该事务已经完成,就可以返回给客户已提交的信息。
但是实际上被更改的数据还在内存中,并没有刷新到磁盘,即还没有落地,当达到一定的条件,会触发checkpoint,将内存中的数据(page)合并写入到磁盘,这样就减少了离散写、IOPS,提高性能。
在这个过程中,如果服务器宕机了,内存中的数据丢失,当重启后,会通过redo日志进行recovery重做。确保不会丢失数据。因此只要redo能够实时的写入到磁盘,InnoDB就不会丢数据。
③ 隔离性(isolation)
事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
在并发数据操作时,不同的事务拥有各自的数据空间,它们的操作不会对对方产生干扰。准确地说,并非要求做到完全无干扰。
数据库规定了多种事务隔离界别,不同的隔离级别对应不用的干扰程度。隔离级别越高,数据一致性越好,但并发行越弱。比如对于A对B进行转账,A没把这个交易完成的时候,B是不知道A要给他转钱。如果无法保证隔离性会怎么样呢?假设A账户有200元,B账户0元。A账户往B账户转账两次,每次金额为50元,分表在两个事务中执行。如果无法保证隔离性,会出现下面的情形:
update accounts set moenty=money-50 where name ='AA'; update accounts set money=money+50 where name ='BB' ;
④ 持久性(durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。
持久性是通过事务日志来保证的。日志包括重做日志和回滚日志。当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到重做日志中,然后再对数据库中对应的行进行修改。这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的重做日志,重新执行,从而使事务具有持久性。
总结,ACID是事务的四大特性,在这四个特性中,原子性是基础,隔离性是手段,一致性是约束条件,而持久性是我们的目的。数据库事务,其实就是数据库设计者为了方便起见,把需要保证原子性、隔离性、一致性和持久性的一个或多个数据库操作称为一个事务。
【2】Spring中的事务传播行为
当事务方法被另一个事务方法调用时, 必须指定事务应该如何传播。例如: 方法可能继续在现有事务中运行, 也可能开启一个新事务, 并在自己的事务中运行。事务的传播行为可以由传播属性指定,Spring 定义了 7 种类传播行为。
事务传播行为 | 说明 |
REQUIRED | 如果有事务在运行,当前的方法就在这个事务内运行;否则,就启动一个新的事务,并在自己的事务内运行; |
REQUIRES_NEW | 当前的方法必须启动新事务,并在它自己的事务内运行;如果有事务正在运行,应该将它挂起。 |
SUPPORTS | 如果有事务在运行,当前的方法就在这个事务内运行;否则它可以不运行在事务中。 |
NOT_SUPPORTED | 当前的方法不应该运行在事务中,如果有运行的事务,将它挂起。 |
MANDATORY | 当前的方法必须运行在事务内部,如果没有正在运行的事务,将抛出异常。 |
NEVER | 当前的方法不应该运行在事务中,如果有运行的事务,就抛出异常。 |
NESTED | 如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行。否则,就启动一个新的事务,并在它自己的事务内运行。 |
MANDATORY表示强制性的,就是必须运行在事务内部。 |
保证同一个事务中
PROPAGATION_REQUIRED 支持当前事务,如果不存在 就新建一个(默认)
PROPAGATION_SUPPORTS 支持当前事务,如果不存在,就不使用事务
PROPAGATION_MANDATORY 支持当前事务,如果不存在,抛出异常
保证没有在同一个事务中
PROPAGATION_REQUIRES_NEW 如果有事务存在,挂起当前事务,创建一个新的事务
PROPAGATION_NOT_SUPPORTED 以非事务方式运行,如果有事务存在,挂起当前事务
PROPAGATION_NEVER 以非事务方式运行,如果有事务存在,抛出异常
PROPAGATION_NESTED 如果当前事务存在,则嵌套事务执行其中,最常使用的是 REQUIRED 、REQUIRES_NEW。
前六个策略类似于EJB CMT,第七个(PROPAGATION_NESTED)是Spring所提供的一个特殊变量。 它要求事务管理器或者使用JDBC 3.0 Savepoint API提供嵌套事务行为(如Spring的DataSourceTransactionManager)
REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务。这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等。 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。
① PROPAGATION_REQUIRED
如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
//事务属性 PROPAGATION_REQUIRED methodA{ …… methodB(); …… } //事务属性 PROPAGATION_REQUIRED methodB{ …… }
使用spring声明式事务,spring使用AOP来支持声明式事务,
会根据事务属性,自动在方法调用之前决定是否开启一个事务,并在方法执行之后决定事务提交或回滚事务。
单独调用methodB方法:
main{ metodB(); }
相当于
Main{ Connection con=null; try{ con = getConnection(); con.setAutoCommit(false); //方法调用 methodB(); //提交事务 con.commit(); } Catch(RuntimeException ex) { //回滚事务 con.rollback(); } finally { //释放资源 closeCon(); } }
Spring保证在methodB方法中所有的调用都获得到一个相同的连接。在调用methodB时,没有一个存在的事务,所以获得一个新的连接,开启了一个新的事务。
单独调用MethodA时,在MethodA内又会调用MethodB。执行效果相当于:
main{ Connection con = null; try{ con = getConnection(); methodA(); con.commit(); } catch(RuntimeException ex) { con.rollback(); } finally { closeCon(); } }
调用MethodA时,环境中没有事务,所以开启一个新的事务.当在MethodA中调用MethodB时,环境中已经有了一个事务,所以methodB就加入当前事务。
② PROPAGATION_SUPPORTS
如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同。
//事务属性 PROPAGATION_REQUIRED methodA(){ methodB(); } //事务属性 PROPAGATION_SUPPORTS methodB(){ …… }
单纯的调用methodB时,methodB方法是非事务的执行的。当调用methdA时,methodB则加入了methodA的事务中,事务地执行。
③ PROPAGATION_MANDATORY
如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。换句话说则是必须有一个存在事务能够让当前方法事务的执行,且自己不会开启事务。
//事务属性 PROPAGATION_REQUIRED methodA(){ methodB(); } //事务属性 PROPAGATION_MANDATORY methodB(){ …… }
当单独调用methodB时,因为当前没有一个活动的事务,则会抛出异常throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transactionfound”);当调用methodA时,methodB则加入到methodA的事务中,事务地执行。
④ PROPAGATION_REQUIRES_NEW
总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。
ROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。
//事务属性 PROPAGATION_REQUIRED methodA(){ doSomeThingA(); methodB(); doSomeThingB(); } //事务属性 PROPAGATION_REQUIRES_NEW methodB(){ …… }
调用A方法:
main(){ methodA(); }
相当于
main(){ TransactionManager tm = null; try{ //获得一个JTA事务管理器 tm = getTransactionManager(); tm.begin();//开启一个新的事务 Transaction ts1 = tm.getTransaction(); doSomeThing(); tm.suspend();//挂起当前事务 try{ tm.begin();//重新开启第二个事务 Transaction ts2 = tm.getTransaction(); methodB(); ts2.commit();//提交第二个事务 } Catch(RunTimeException ex) { ts2.rollback();//回滚第二个事务 } finally { //释放资源 } //methodB执行完后,恢复第一个事务 tm.resume(ts1); doSomeThingB(); ts1.commit();//提交第一个事务 } catch(RunTimeException ex) { ts1.rollback();//回滚第一个事务 } finally { //释放资源 } }
在这里,我把ts1称为外层事务,ts2称为内层事务。从上面的代码可以看出,ts2与ts1是两个独立的事务,互不相干。Ts2是否成功并不依赖于 ts1。如果methodA方法在调用methodB方法后的doSomeThingB方法失败了,而methodB方法所做的结果依然被提交。而除了 methodB之外的其它代码导致的结果却被回滚了。使用PROPAGATION_REQUIRES_NEW,需要使用 JtaTransactionManager作为事务管理器。
⑤ PROPAGATION_NOT_SUPPORTED
总是非事务地执行,并挂起任何存在的事务。使用PROPAGATION_NOT_SUPPORTED
,也需要使用JtaTransactionManager作为事务管理器。(代码示例同上,可同理推出)
⑥ PROPAGATION_NEVER
总是非事务地执行,如果存在一个活动事务,则抛出异常。其与PROPAGATION_MANDATORY
是相反的。
⑦ PROPAGATION_NESTED
如果一个活动的事务存在,则运行在一个嵌套的事务中。如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED
属性执行。
这是一个嵌套事务,使用JDBC 3.0驱动时,仅仅支持DataSourceTransactionManager作为事务管理器。需要JDBC 驱动的java.sql.Savepoint类。
有一些JTA的事务管理器实现可能也提供了同样的功能。使用PROPAGATION_NESTED,还需要把PlatformTransactionManager的nestedTransactionAllowed属性设为true;而 nestedTransactionAllowed属性值默认为false。
另一方面, PROPAGATION_NESTED开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务。嵌套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint。 嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于:
PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back。
//事务属性 PROPAGATION_REQUIRED methodA(){ doSomeThingA(); methodB(); doSomeThingB(); } //事务属性 PROPAGATION_NESTED methodB(){ …… }
如果单独调用methodB方法,则按REQUIRED属性执行。如果调用methodA方法,相当于下面的效果:
main(){ Connection con = null; Savepoint savepoint = null; try{ con = getConnection(); con.setAutoCommit(false); doSomeThingA(); savepoint = con2.setSavepoint(); try{ methodB(); } catch(RuntimeException ex) { con.rollback(savepoint); } finally { //释放资源 } doSomeThingB(); con.commit(); } catch(RuntimeException ex) { con.rollback(); } finally { //释放资源 } }
当methodB方法调用之前,调用setSavepoint方法,保存当前的状态到savepoint。如果methodB方法调用失败,则恢复到之前保存的状态。但是需要注意的是,这时的事务并没有进行提交,如果后续的代码(doSomeThingB()方法)调用失败,则回滚包括methodB方法的所有操作。
嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。
PROPAGATION_NESTED 与PROPAGATION_REQUIRES_NEW的区别
:它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。
使用 PROPAGATION_REQUIRES_NEW时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要JTA事务管理器的支持。
使用PROPAGATION_NESTED时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务DataSourceTransactionManager使用savepoint支持PROPAGATION_NESTED时,需要JDBC 3.0以上驱动及1.4以上的JDK版本支持。其它的JTA TrasactionManager实现可能有不同的支持方式。
PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务。 这个事务将被完全commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等。 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。
另一方面, PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务。 嵌套事务开始执行时, 它将取得一个 savepoint。如果这个嵌套事务失败, 我们将回滚到此 savepoint。 嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。
由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于,
PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back。
PROPAGATION_REQUIRED应该是我们首先的事务传播行为。它能够满足我们大多数的事务需求。
【3】事务并发调度的问题
先看一下访问相同数据的事务在不保证串行执行的情况下可能出现哪些问题呢?
3类数据读问题(脏读,不可重复读,幻读),2类数据更新问题(第一类丢失更新,第二类丢失更新)。
第一类丢失更新:A事务撤销时,把已提交的B事务的数据覆盖掉。
第二类丢失更新:A事务提交时,把已提交的B事务的数据覆盖掉。
① 脏写(Dirty Write)
对于两个事务SessionA、SessionB,如果事务SessionA修改了另一个未提交事务SessionB修改过的数据
,那就意味着发生了脏写,示意图如下:
SessionA和SessionB各开启了一个事务,SessionB中的事务先将studentno列为1的记录的name列更新为’李四’,然后SessionA中的事务又把这条数据更新为’张三’。如果之后SessionB中的事务进行了回滚,那么SessionA中的更新也将不复存在,这种现象就称之为脏写。
这时SessionA中的事务就乜有效果了,明明把数据更新了,最后也提交事务了,但是看到的数据什么变化也没有。这里如果大家对事务的隔离级别比较了解的话,会发现默认隔离级别下,SessionA中的更新语句会处于等待状态。
② 脏读(Dirty Read)
对于两个事务SessionA、SessionB,SessionA读取了已经被SessionB更新但还没有提交的字段
。之后若SessionB回滚,SessionA读取的内容就是临时且无效的。
SessionA和SessionB各开启了一个事务,SessionB中的事务先将studentno列为1的记录name列更新为“张三”,然后SessionA中的事务再去查询这条studentno为1的记录。如果读到列name的值为“张三”,而SessionB中的事务稍后进行了回滚,那么SessionA中的事务相当于读到了一个不存在的数据,这种现象就称之为“脏读”。
————————————————
③ 不可重复读(Non-Repeatable Read)
对于两个事务SessionA、SessionB,SessionA读取了一个字段,然后SessionB更新了该字段。之后SessionA再次读取同一个字段,值就不同了。那就意味着发生了不可重复读。
我们在SessionB中提交了几个隐式事务(注意是隐式事务,意味着语句结束事务就提交了),这些事务都修改了studentno列为1的记录的列的name值。每次事务提交之后,如果SessionA中的事务都可以查看到最新的值,这种现象也被称之为不可重复读。
④ 幻读(Phantom Read)
对于两个事务SessionA、SessionB,SessionA从一个表中读取了一个字段,然后SessionB在该表中插入了一些新的行。之后,如果SessionA再次读取同一个表,就会多出几行。那就意味着发生了幻读。
SessionA中的事务先根据条件studentno>0这个条件查询表student,得到了name列值为张三的记录。之后SessionB中提交了一个隐式事务,该事务向表student中插入了一条新记录。之后SessionA中的事务再根据相同的条件studentno>0查询表student,得到的结果集中包含SessionB中的事务新插入的那条记录,这种现象被称之为幻读。
有同学可能在验证的时候发现第二次select并没有查询到数据,那是否没有发生幻读? 错,当你插入id为2的数据时候会提示主键冲突!所以这里要灵活的理解读取的意思。select是读取,insert其实也属于隐式的读取。只不过是在MySQL的机制中读取的,插入数据也是要先读取一下有没有主键冲突才能决定是否执行插入。
Session A | Session B |
begin | |
select * from sys_user 不存在id为2的数据 |
begin |
insert into sys_user(id,name) values (2,‘jane’); |
insert into sys_user(id,name) values (2,‘jane’); Duplicate entry ‘2’ for key ‘sys_user.PRIMARY’ |
幻读,并不是说两次读取获取的结果集不同。幻读侧重的方面是某一次的select操作得到的结果所表征的数据状态无法支撑后续的业务操作。更为具体一些:select某记录是否存在,不存在,准备插入此记录,但执行insert时发现此记录已存在,无法插入,此时就发生了幻读。
注意和不可重复读的区别,这里是新增,不可重复读是更改。这两种情况对策是不一样的,对于不可重复读,只需要采取行级锁防止该记录数据被更改,然而对于幻读必须加表级锁,防止在这个表中新增一条数据。注意1:
那如果SessionB中删除了一些符合studentno>0的记录而不是插入新记录,那SessionA之后再根据studentno>0的条件读取的记录变少了,这种现象算不算幻读呢?这种现象不属于幻读,幻读强调的时候一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录。
注意2:
那对于先前已经读到的记录,之后又读取不到的情况,算啥呢?这相当于对每一条记录都发生了不可重复读的现象,幻读只是重点强调了读取到了之前读取没有获取到的记录。
如何解决幻读问题?
其实RR也是可以避免幻读的,通过对select操作手动加行X锁(独占锁)(select …for update这也正是serializable隔离级别下会隐式为你做的事情)。同时即便当前记录不存在,当前事务也会获得一把记录锁(因为InnoDB的行锁锁定的是索引,故记录实体存在与否没关系,存在就加行X锁,不存在就加间隙锁),其他事务则无法插入此索引的记录,故杜绝了幻读。
在serializable隔离级别下,SessionA执行时时会隐式的添加行(X)锁/gap(X)锁
的,从而SessionB会被阻塞,SessionA会正常往下执行。待SessionA提交后,SessionB才能继续执行(主键冲突失败)。对于SessionA来说业务是正确的,成功的阻塞扼杀了扰乱业务的SessionB,对于SessionA来说他前期读取的结果是可以支撑其后续业务的。
所以MySQL的幻读并非读取两次返回的结果集不同,而是事务在插入实现检测不存在的记录时,惊奇的发现这些数据已经存在了,之前的检测读取到的数据如同鬼影一般。
⑤ 三级封锁协议
数据库想要在“合适”的时机阻塞住数据库操作,那么首先要定义好怎么样的时机算是“合适”,因为各个系统支持的业务千差万别,对数据的实时性和有效性的要求也不同。于是数据库理论中就提出了封锁级别的概念,对不同的同步要求采用不同的封锁级别。
一级封锁协议
事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。 一级封锁协议可以防止丢失修改,并保证事务T是可恢复的。使用一级封锁协议可以解决丢失修改问题。在一级封锁协议中,如果仅仅是读数据不对其进行修改,是不需要加锁的,它不能保证可重复读和不读“脏”数据.
二级封锁协议
一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,读完后方可释放S锁。 二级封锁协议除防止了丢失修改,还可以进一步防止读“脏”数据。但在二级封锁协议中,由于读完数据后即可释放S锁,所以它不能保证可重复读。
三级封锁协议
一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放。 三级封锁协议除防止了丢失修改和不读“脏”数据外,还进一步防止了不可重复读。
可见,三级锁操作一个比一个厉害(满足高级锁则一定满足低级锁)。但有个非常致命的地方,一级锁协议就要在第一次读加x锁,直到事务结束。几乎就要在整个事务加写锁了,效率非常低。三级封锁协议只是一个理论上的东西,实际数据库常用另一套方法来解决事务并发问题。
三级封锁协议反映在实际的数据库系统上,就是四级事务隔离机制。总的来说,四种事务隔离机制就是在逐渐的限制事务的自由度,以满足对不同并发控制程度的要求。
两段锁协议
数据库在调度并发事务时遵循“两段锁”协议,“两段锁”协议是指所有事务必须分两个阶段对数据项进行加锁和解锁。
扩展阶段:在对任何数据项的读、写之前,要申请并获得该数据项的封锁。
收缩阶段:每个事务中,所有的封锁请求必须先于解锁请求。
在数学上可以证明,遵循两段锁的调度可以保证调度结果与串行化调度相同。这样的机制保证了数据库并发调度与串行调度的等价。
【3】事务的隔离级别
① 基础介绍
上面介绍了几种并发事务执行过程中可能遇到的一些问题,这些问题有轻重缓急之分,我们给这些问题按照严重性来排一下序:
脏写 > 脏读 > 不可重复读 > 幻读 >
我们愿意舍弃一部分隔离性来换取一部分性能在这里就体现在:设立一些隔离级别,隔离级别越低,并发问题发生的就越多。
mysql用意向锁(另一种机制)来解决事务并发问题,为了区别封锁协议,弄了一个新概念隔离性级别:包括Read Uncommitted、Read Committed、Repeatable Read、Serializable。
Read uncommitted:读未提交,在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免脏读、不可重复读和幻读。
Read committed:读已提交,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。可以避免脏读,但不可重复读、幻读问题仍然存在。
Repeatable read:可重复读,事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交。那么事务A再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍然存在。这是MySQL的默认隔离级别。
Serializable:可串行化,确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。能避免脏读、不可重复读和幻读。
一个事务与其他事务隔离的程度称为隔离级别。数据库规定了多种事务隔离级别, 不同隔离级别对应不同的干扰程度, 隔离级别越高, 数据一致性就越好, 但并发性越弱。SQL标准中规定,针对不同的隔离级别,并发事务可以发生不同严重程度的问题,具体情况如下:
脏写怎么没涉及到?因为脏写这个问题太严重了,不论是哪种隔离级别,都不允许脏写的情况发生。
不同的隔离级别有不同的现象,并有不同的锁和并发机制,隔离级别越高,数据库的并发性能就越差,4种事务隔离级别与并发性能的关系如下:
事务隔离机制的实现基于锁机制和并发调度
其中并发调度使用的是MVVC(多版本并发控制),通过保存修改的旧版本信息来支持并发一致性读和回滚等特性。因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容)。MySQL的InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读)。
InnoDB 存储引擎在 分布式事务 的情况下一般会用到SERIALIZABLE(可串行化)隔离级别。
② MySQL支持的四种隔离级别
不同的数据库厂商对SQL标准中规定的四种隔离级别支持不一样:
Oracle 支持的2 种事务隔离级别:READ_COMMITTED, SERIALIZABLE。Oracle 默认的事务隔离级别为: READ_COMMITTED。
MySQL支持4 种事务隔离级别:READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ和SERIALIZABLE。MySQL 默认的事务隔离级别为: REPEATABLE_READ 。
5.7之前
# 1.查看当前会话隔离级别 select @@tx_isolation; # 2.查看系统当前隔离级别 select @@global.tx_isolation; # 3.设置当前会话事务隔离级别 set session transaction isolation level read committed; # 4.设置系统当前隔离级别 set global transaction isolation level repeatable read;
MySQL5.7.20版本之后,引入transaction_isolation来替换tx_isolation。
show variables like 'transaction_isolation'
③ 如何设置事务的隔离级别
通过下面的语句修改事务的隔离级别:
set [global|session] transaction isolation level 隔离级别; # 其中,隔离级别格式: READ UNCOMMITTED READ COMMITTED REPEATABLE READ SERIALIZABLE
或者:
set [global|session] transaction_isolation ='隔离级别'; # 其中,隔离级别格式: READ-UNCOMMITTED READ-COMMITTED REPEATABLE-READ SERIALIZABLE