【JAVA】Spring事务管理详解

事物的特性(ACID)

原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
一致性: 执行事务前后,数据保持一致;
隔离性: 并发访问数据库时,一个用户的事物不被其他事物所干扰,各并发事务之间数据库是独立的;
持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

我们在使用JDBC或者Mybatis进行数据持久化操作时,我们的xml配置通常如下:

    <!-- 事务管理器 -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 数据源 -->
        <property name="dataSource" ref="dataSource" />
    </bean>

    <!-- 开启事务行为 -->
    <tx:annotation-driven transaction-manager="transactionManager" />

并发事务带来的问题

在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对统一数据进行操作)。并发虽然是必须的,但可能会导致一下的问题。

  • 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。

  • 丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
    例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。

  • 不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。

  • 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

不可重复度和幻读区别:
不可重复读的重点是修改,幻读的重点在于新增或者删除。

spring事务的传播性、隔离性

@Transactional
1.这里说明一下,有的把这个注解放在类名称上面了,这样你配置的这个@Transactional 对这个类中的所有public方法都起作用.
2.@Transactional 方法方法名上,只对这个方法有作用,同样必须是public的方法

我们在使用Spring声明式事务时,有一个非常重要的概念就是事务的属性。事务属性通常由事务的传播行为、事务的隔离级别、事务的超时值和事务的只读标识组成。我们在进行事务划分时,需要进行事务定义,也就是配置事务的属性。

屏幕快照 2018-08-30 下午2.15.21.png

Spring在TransactionDefinition接口中定义这些属性,以供PlatfromTransactionManager使用, PlatfromTransactionManager是spring事务管理的核心接口。

public interface TransactionDefinition {
    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;
    int ISOLATION_DEFAULT = -1; //默认的隔离级别
    int ISOLATION_READ_UNCOMMITTED = 1;
    int ISOLATION_READ_COMMITTED = 2;
    int ISOLATION_REPEATABLE_READ = 4;
    int ISOLATION_SERIALIZABLE = 8;
    int TIMEOUT_DEFAULT = -1;

    int getPropagationBehavior();

    int getIsolationLevel();

    int getTimeout();

    boolean isReadOnly();

    String getName();
}

getTimeout()方法,它返回事务必须在多少秒内完成。
isReadOnly(),事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的。
getIsolationLevel()方法返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据。

隔离级别

在TransactionDefinition接口中定义了五个不同的事务隔离级别 :
a) ISOLATION_DEFAULT:(PlatfromTransactionManager的)默认的隔离级别。使用数据库默认的事务隔离级别(MySql 默认为 REPEATABLE_READ;Oracle 默认为:READ_COMMITTED),另外四个与JDBC的隔离级别相对应 。
b)ISOLATION_READ_UNCOMMITTED:这是事务最低的隔离级别,它允许别外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读。 (未解决任何并发问题)

例如:
Mary的原工资为1000,财务人员将Mary的工资改为了8000,但未提交事务

Connection con1 = getConnection();  
con1.setAutoCommit(false);  
update employee set salary = 8000 where empId ="Mary"; 

与此同时,Mary正在读取自己的工资 。

Connection con2 = getConnection();  
select salary from employee where empId ="Mary";  
con2.commit();

Mary发现自己的工资变为了8000,欢天喜地!
而财务发现操作有误,而回滚了事务,Mary的工资又变为了1000 。

con1.rollback(); 

像这样,Mary记取的工资数8000是一个脏数据。

c)ISOLATION_READ_COMMITTED: 保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。
d)ISOLATION_REPEATABLE_READ : 这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)。

例如:
在事务1中,Mary 读取了自己的工资为1000,操作并没有完成 :

Connection con1 = getConnection();  
con1.setAutoCommit(false);  
select salary from employee empId ="Mary"; 

在事务2中,这时财务人员修改了Mary的工资为2000,并提交了事务.

Connection con2 = getConnection();  
update employee set salary = 2000;  
con2.commit();  

在事务1中,Mary 再次读取自己的工资时,工资变为了2000

//con1 
select salary from employee empId ="Mary";  

在一个事务中前后两次读取的结果并不致,导致了不可重复读。
使用ISOLATION_REPEATABLE_READ可以避免这种情况发生。

e)ISOLATION_SERIALIZABLE 这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读外,还避免了幻像读。

例如:
目前工资为1000的员工有10人。
事务1,读取所有工资为1000的员工。共读取10条记录

con1 = getConnection();  
Select * from employee where salary =1000; 

这时另一个事务向employee表插入了一条员工记录,工资也为1000

con2 = getConnection();  
Insert into employee(empId,salary) values("Lili",1000);  
con2.commit();  

事务1再次读取所有工资为1000的员工

//con1
select * from employee where salary =1000;  

共读取到了11条记录,这就产生了幻像读。
ISOLATION_SERIALIZABLE能避免这样的情况发生。但是这样也耗费了最大的资源。

事务的传播性

在TransactionDefinition接口中定义了七个事务传播行为。
假如我写了两个service类。名字分别为ServiceA和ServiceB。如下:

@Service("ServiceA")
public class ServiceA {

    @Resource(name="ServiceB")
    private ServiceB serviceB;

    @Transactional(propagation=Propagation.REQUIRED)
    public methodA(){
       //doSomething
       serviceB.methodB(); 
       //doSomething
    }
}
@Service("ServiceB")
public class ServiceB {

    @Transactional(propagation=Propagation.REQUIRED)
    public methodB(){
       //doSomething
    }
}

a)PROPAGATION_REQUIRED:如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。
如果单独调用serviceB.methodB方法:

public static void main(String[] args) {
   serviceB.methodB();
}

相当于:

public static void main(String[] args) {
  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.
执行效果相当于:

public static void main(String[] args) {
   Connection con = null;  
   try{  
      con = getConnection();  
      con.setAutoCommit(false);  
      methodA();  
      con.commit();  
   }cathc(RuntimeException ex){  
      con.rollback();  
   }finally{  
      closeCon();  
   }   
}  

调用MethodA时,环境中没有事务,所以开启一个新的事务.
当在MethodA中调用MethodB时,环境中已经有了一个事务,所以methodB就加入当前事务。

b)PROPAGATION_SUPPORTS :如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,PROPAGATION_SUPPORTS与不使用事务有少许不同。

//事务属性 PROPAGATION_REQUIRED   
methodA(){  
  serviceB.methodB();  
}  

//事务属性 PROPAGATION_SUPPORTS   
methodB(){  
  ……  
} 

单纯的调用methodB时,methodB方法是非事务的执行的。
当调用methdA时,methodB则加入了methodA的事务中,事务地执行。

c)PROPAGATION_MANDATORY :如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。

//事务属性 PROPAGATION_REQUIRED   
methodA(){  
  serviceB.methodB();  
}  

//事务属性 PROPAGATION_MANDATORY   
methodB(){  
  ……  
}  

当单独调用methodB时,因为当前没有一个活动的事务,则会抛出异常
当调用methodA时,methodB则加入到methodA的事务中,事务地执行。

d)PROPAGATION_REQUIRES_NEW :总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。

//事务属性 PROPAGATION_REQUIRED   
methodA(){  
   doSomeThingA();  
   serviceB.methodB();  
   doSomeThingB();  
}  

//事务属性 PROPAGATION_REQUIRES_NEW   
methodB(){  
  ……  
}  

当单独调用methodB时,相当于把methodB声明为REQUIRED。开启一个新的事务,事务地执行。
当调用methodA时,情况就大不一样了,相当于下面的效果。

public static void main(String[] args) {
 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作为事务管理器。

e)PROPAGATION_NOT_SUPPORTED: 总是非事务地执行,并挂起任何存在的事务。

事务属性 PROPAGATION_REQUIRED   
methodA(){  
  doSomeThingA();  
  serviceB.methodB();  
  doSomeThingB();  
}  

事务属性 PROPAGATION_NOT_SUPPORTED   
methodB(){  
  ……  
}  

当单独调用methodB时,不启用任何事务机制,非事务地执行。
当调用methodA时,相当于下面的效果

public static void main(String[] args) {
 TransactionManager tm = null;  
 try{  
  //获得一个JTA事务管理器  
   tm = getTransactionManager();  
   tm.begin();//开启一个新的事务  
   Transaction ts1 = tm.getTransaction();  
   doSomeThing();  
   tm.suspend();//挂起当前事务  
   methodB();  
   //methodB执行完后,复恢第一个事务  
   tm.resume(ts1);  
   doSomeThingB();  
   ts1.commit();//提交第一个事务  
 }catch(RunTimeException ex){  
   ts1.rollback();//回滚第一个事务  
 }finally{  
  //释放资源  
 }  
}  

使用PROPAGATION_NOT_SUPPORTED,也需要使用JtaTransactionManager作为事务管理器。

f)PROPAGATION_NEVER :总是非事务地执行,如果存在一个活动事务,则抛出异常:

事务属性 PROPAGATION_REQUIRED   
methodA(){  
  doSomeThingA();  
  seviceB.methodB();  
  doSomeThingB();  
}  

事务属性 PROPAGATION_NEVER   
methodB(){  
  ……  
}

单独调用methodB,则非事务的执行。 调用methodA则会抛出异常

g)PROPAGATION_NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行

这是一个嵌套事务,使用JDBC 3.0驱动时,仅仅支持DataSourceTransactionManager作为事务管理器。需要JDBC 驱动的java.sql.Savepoint类。有一些JTA的事务管理器实现可能也提供了同样的功能。

使用PROPAGATION_NESTED,还需要把PlatformTransactionManager的nestedTransactionAllowed属性设为true;
而nestedTransactionAllowed属性值默认为false;

如下:

<bean id="system.platformTransactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">  
        <property name="sessionFactory" ref="system.sessionFactory"/>  
        <property name="nestedTransactionAllowed" value="true"/>  
 </bean>
事务属性 PROPAGATION_REQUIRED   
methodA(){  
  doSomeThingA();  
  serviceB.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_REQUIRED应该是我们首先的事务传播行为。它能够满足我们大多数的事务需求。

事务的超时性、回滚和只读

  • 超时:
    @Transactional(timeout=30) //默认是30秒
    异常回滚:
    指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)
    指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})
    该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。

正常的情况下也可以回滚:
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

  • 只读:
    @Transactional(readOnly=true)
    该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容