分享

数据库事务 | JDBC中使用事务 | spring中的事务管理

 bylele 2019-12-11

事务的基本概念

什么是事务

事务的ACID特性

事务的隔离级别

JDBC中使用事务

事务控制语句

设置隔离级别

设置事务回滚点

JDBC中使用事务例子

spring中的事务管理

Spring事务原理

TransactionDefinition 基本事务属性的定义

传播行为

隔离级别

只读

事务超时

回滚规则

Spring 编程式事务和声明式事务的区别 

spring事务实例

不用事务实现转账

编程式事务处理实现转账(TransactionTemplate )

声明式事务处理实现转账(基于AOP的 xml 配置)  

声明式事务处理实现转账(基于AOP的 注解 配置) 

Spring的事务管理默认只对运行期异常进行回滚


 

事务的基本概念

什么是事务

        事务是用户定义的一个数据库操作序列,这些操作要么全做,要么全不做,是一个不可分割的工作单位。例如,在关系数据库中,一个事务可以是一个sql语句,一组sql语句或整个程序。

       事务的开始和结束可以由用户显式控制,如果用户没有显式的定义事务,则由数据库管理系统按照默认规定自动划分事务。例如:mysql数据库默认一条sql语句一个事务,默认会开启并提交事务.

       在SQL中,定义事务的语句一般有三条:

BEGIN TRANSACTION;

COMMIT;

ROLLBACK;

事务通常是以 BEGIN TRANSACTION开始,以COMMIT或ROLLBACK结束;COMMIT表示提交,即提交事务的所有操作,具体地说,就是将事务中所有对数据库的更新写回到磁盘上的物理数据库中,事务正常结束;ROLLBACK表示回滚,即在事务运行过程中发生了某种故障,事务不能继续执行,系统将事务中对数据库的所有已完成的更新操作全部撤销,回到事务开始时的状态。

事务的ACID特性

原子性(Atomicity)

   原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败。比如在同一个事务中的SQL语句,要么全部执行成功,要么全部执行失败。

一致性(Consistency)

  事务必须使数据库从一个一致性状态变换到另外一个一致性状态。以转账为例子,A向B转账,假设转账之前这两个用户的钱加起来总共是2000,那么A向B转账之后,不管这两个账户怎么转,A用户的钱和B用户的钱加起来的总额还是2000,这个就是事务的一致性。

隔离性(Isolation)

   一个事务的执行不能被其他事务干扰。事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。

持久性(Durability)

  持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

  事务的四大特性中最麻烦的是隔离性,下面重点介绍一下事务的隔离级别

事务的隔离级别

多个线程开启各自的事务来操作数据库中数据时,数据库系统要负责隔离操作,以保证各个线程在获取数据时的准确性。

如果事务不考虑隔离性,可能会引发如下问题:

1、脏读

  脏读指一个事务读取了另外一个事务未提交的数据。 

         事务A访问了数据库,它干了一件事情,往数据库里加上了新来的牛人的名字,但是没有提交事务。

         insert into T values (4, '牛D');

         这时,来了另一个事务B,他要查询所有牛人的名字。

         select Name from T;

         这时,如果没有事务之间没有有效隔离,那么事务B返回的结果中就会出现“牛D”的名字。这就是“脏读(dirty read)”。

2、不可重复读

  不可重复读指在一个事务内读取表中的某一行数据,多次读取结果不同。

       事务A访问了数据库,他要查看ID是1的牛人的名字,于是执行了

       select Name from T where ID = 1;

       这时,事务B来了,因为ID是1的牛人改名字了,所以要更新一下,然后提交了事务。

       update T set Name = '不牛' where ID = 1;

       接着,事务A还想再看看ID是1的牛人的名字,于是又执行了

       select Name from T where ID = 1;

       结果,两次读出来的ID是1的牛人名字竟然不相同,这就是不可重复读(unrepeatable read)。

3、虚读(幻读)

  虚读(幻读)是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致

       事务A访问了数据库,他想要看看数据库的牛人都有哪些,于是执行了

       select * from T;

       这时候,事务B来了,往数据库加入了一个新的牛人。

       insert into T values(4, '牛D');

       这时候,事务A忘了刚才的牛人都有哪些了,于是又执行了。

       select * from T;

       结果,第一次有三个牛人,第二次有四个牛人。

       相信这个时候事务A就蒙了,刚才发生了什么?这种情况就叫“幻读(phantom problem)”。

为了防止出现脏读、不可重复读、幻读等情况,我们就需要根据我们的实际需求来设置数据库的隔离级别。

事务隔离级别

  1. Serializable(串行化):可避免脏读、不可重复读、虚读情况的发生。

  2. Repeatable read(可重复读):可避免脏读、不可重复读情况的发生。

  3. Read committed(读已提交):可避免脏读情况发生。

  4. Read uncommitted(读未提交):最低级别,以上情况均无法保证。

mysql数据库默认的事务隔离级别是:Repeatable read(可重复读)

oracle数据库默认的事务隔离级别是:read commited(读已提交)

查看数据库默认的隔离级别:select @@tx_isolation; 
设置隔离级别:set session transaction isolation level

JDBC中使用事务

事务控制语句

 当Jdbc程序向数据库获得一个Connection对象时,默认情况下这个Connection对象会自动向数据库提交在它上面发送的SQL语句。若想关闭这种默认提交方式,让多条SQL在一个事务中执行,可使用下列的JDBC控制事务语句.

  • Connection.setAutoCommit(false);     //开启事务(start transaction)

  • Connection.rollback();                         //回滚事务(rollback)

  • Connection.commit();                          //提交事务(commit)

设置隔离级别

Connection.setTransactionIsolation(int level) :参数可选值如下:

  • Connection.TRANSACTION_READ_UNCOMMITTED;  //读未提交

  • Connection.TRANSACTION_READ_COMMITTED;       //读已提交

  • Connection.TRANSACTION_REPEATABLE_READ;      //可重复读

  • Connection.TRANSACTION_READ_SERIALIZABLE。    //序列化

设置事务回滚点

  在开发中,有时候可能需要手动设置事务的回滚点,在JDBC中使用如下的语句设置事务回滚点:

  Savepoint sp = connection.setSavepoint();
  connection.rollback(sp);
  connection.commit();                                      //回滚后必须通知数据库提交事务

JDBC中使用事务例子

这是单元测试类,所以有@Before   @Test

  1. package com.test.transaction.tset;
  2. import org.junit.Before;
  3. import org.junit.Test;
  4. import java.sql.*;
  5. public class TransactionTest {
  6. private Connection con = null;
  7. private Statement stmt = null;
  8. private ResultSet rs = null;
  9. private String url = "jdbc:mysql://localhost:3306/transaction?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC";
  10. private String username = "root" ;
  11. private String password = "123456789" ;
  12. private String sql1 = "update account set money = money+100 where name='B'";
  13. private String sql2 = "update account set money = money-100 where name='A'";
  14. /**
  15. * @Description:连接数据库
  16. */
  17. @Before
  18. public void connect(){
  19. try{
  20. //加载MySql的驱动类
  21. Class.forName("com.mysql.cj.jdbc.Driver") ;
  22. }catch(ClassNotFoundException e){
  23. System.out.println("找不到驱动程序类 ,加载驱动失败!");
  24. e.printStackTrace() ;
  25. }
  26. try{
  27. con = DriverManager.getConnection(url, username, password ) ;
  28. stmt = con.createStatement() ;
  29. }catch(SQLException se){
  30. System.out.println("数据库连接失败!");
  31. se.printStackTrace() ;
  32. }
  33. }
  34. /**
  35. * @Description:模拟转账成功时的业务场景
  36. */
  37. @Test
  38. public void testTransaction1(){
  39. try {
  40. //通知数据库开启事务(start transaction)
  41. con.setAutoCommit(false);
  42. stmt.execute(sql1);
  43. stmt.execute(sql2);
  44. //提交事务
  45. con.commit();
  46. System.out.println("success");
  47. } catch (Exception e) {
  48. e.printStackTrace();
  49. }finally {
  50. try {
  51. con.close();
  52. } catch (SQLException e) {
  53. System.out.println("数据库连接关闭失败!");
  54. e.printStackTrace();
  55. }
  56. }
  57. }
  58. /**
  59. * @Description:模拟转账过程中出现异常导致有一部分SQL执行失败后让数据库自动回滚事务
  60. */
  61. @Test
  62. public void testTransaction2(){
  63. try {
  64. //通知数据库开启事务(start transaction)
  65. con.setAutoCommit(false);
  66. stmt.execute(sql1);
  67. //用这句代码模拟执行完SQL1之后程序出现了异常而导致后面的SQL无法正常执行,事务也无法正常提交,此时数据库会自动执行回滚操作
  68. int x = 1 / 0;
  69. stmt.execute(sql2);
  70. //提交事务
  71. con.commit();
  72. System.out.println("success");
  73. } catch (Exception e) {
  74. e.printStackTrace();
  75. }finally {
  76. try {
  77. con.close();
  78. } catch (SQLException e) {
  79. System.out.println("数据库连接关闭失败!");
  80. e.printStackTrace();
  81. }
  82. }
  83. }
  84. /**
  85. * @Description:模拟转账过程中出现异常导致有一部分SQL执行失败时手动通知数据库回滚事务
  86. */
  87. @Test
  88. public void testTransaction3(){
  89. try {
  90. //通知数据库开启事务(start transaction)
  91. con.setAutoCommit(false);
  92. stmt.execute(sql1);
  93. //用这句代码模拟执行完SQL1之后程序出现了异常而导致后面的SQL无法正常执行,事务也无法正常提交,此时数据库会自动执行回滚操作
  94. int x = 1 / 0;
  95. stmt.execute(sql2);
  96. //提交事务
  97. con.commit();
  98. System.out.println("success");
  99. } catch (SQLException e) {
  100. try {
  101. //捕获到异常之后通知数据库回滚
  102. con.rollback();
  103. } catch (Exception ex) {
  104. ex.printStackTrace();
  105. }
  106. e.printStackTrace();
  107. }finally {
  108. try {
  109. con.close();
  110. } catch (SQLException e) {
  111. System.out.println("数据库连接关闭失败!");
  112. e.printStackTrace();
  113. }
  114. }
  115. }
  116. /**
  117. * @Description:设置事务回滚点
  118. */
  119. @Test
  120. public void testTransaction4(){
  121. Savepoint sp = null;
  122. String sql3 = "update account set money = money + 100 where name='C'";
  123. try {
  124. //通知数据库开启事务(start transaction)
  125. con.setAutoCommit(false);
  126. stmt.execute(sql1);
  127. //stmt.execute(sql2);
  128. sp = con.setSavepoint();
  129. stmt.execute(sql2);
  130. //用这句代码模拟执行完SQL1之后程序出现了异常而导致后面的SQL无法正常执行,事务也无法正常提交,此时数据库会自动执行回滚操作
  131. int x = 1 / 0;
  132. stmt.execute(sql3);
  133. //提交事务
  134. con.commit();
  135. System.out.println("success");
  136. } catch (Exception e) {
  137. try {
  138. //捕获到异常之后通知数据库回滚
  139. con.rollback(sp);
  140. con.commit();
  141. } catch (SQLException ex) {
  142. ex.printStackTrace();
  143. }
  144. e.printStackTrace();
  145. }finally {
  146. try {
  147. con.close();
  148. } catch (SQLException e) {
  149. System.out.println("数据库连接关闭失败!");
  150. e.printStackTrace();
  151. }
  152. }
  153. }
  154. }

spring中的事务管理

Spring事务原理

Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:
 1.获取连接 Connection con = DriverManager.getConnection()
 2.开启事务con.setAutoCommit(false);
 3.执行CRUD
 4.提交事务/回滚事务 con.commit() / con.rollback();
 5.关闭连接 con.close(); 

使用Spring的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由Spirng 自动完成。
那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。下面简单地介绍下,注解方式为例子
 1.配置文件开启注解驱动,在相关的类和方法上通过注解@Transactional标识。
 2.spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有相关注解的类和方法,并且为这些类和方法生成代理,并根据@Transaction的相关参数进行相关配置注入,这样就在代理中为我们把相关的事务处理掉了(开启正常提交事务,异常回滚事务)。
 3.真正的数据库层的事务提交和回滚是通过bin log或者redo log实现的。

TransactionDefinition 基本事务属性的定义

什么是事务属性呢?事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。事务属性包含了5个方面,如图所示:

  

 

传播行为

当事务方法被另一个事务方法调用时,必须指定事务应该如何传播

在TransactionDefinition类中,spring提供了7种传播属性,接下来分别用简单示例来说明。

温馨提醒:下文提到的加入当前事务,指的是底层使用同一个Connection,但是事务状态对象是可以重新创建的,并不影响。文章提到的当前只存在一个事务,表示的是共用底层的一个Connection,而不在乎创建了多少个事务状态对象(TransactionStatus)。

1、PROPAGATION_REQUIRED

说明: 如果当前已经存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。

看一个小例子,代码如下:

@Transactional

public void service(){

 serviceA();

 serviceB();

}

@Transactional

serviceA();

@Transactional

serviceB();

serviceA 和 serviceB 都声明了事务,默认情况下,propagation=PROPAGATION_REQUIRED,整个service调用过程中,只存在一个共享的事务,当有任何异常发生的时候,所有操作回滚。

2、PROPAGATION_SUPPORTS

说明:如果当前已经存在事务,那么加入该事务,否则创建一个所谓的空事务(可以认为无事务执行)。

看一个小例子,代码如下:

public void service(){

  serviceA();

  throw new RunTimeException();

}

@Transactional(propagation=Propagation.SUPPORTS)

serviceA();

serviceA执行时当前没有事务,所以service中抛出的异常不会导致 serviceA回滚。

再看一个小例子,代码如下:

public void service(){

  serviceA();

}

@Transactional(propagation=Propagation.SUPPORTS)

serviceA(){

 do sql 1

 1/0;

 do sql 2

}

由于serviceA运行时没有事务,这时候,如果底层数据源defaultAutoCommit=true,那么sql1是生效的,如果defaultAutoCommit=false,那么sql1无效,如果service有@Transactional标签,serviceA共用service的事务(不再依赖defaultAutoCommit),此时,serviceA全部被回滚。

3、 PROPAGATION_MANDATORY

说明:当前必须存在一个事务,否则抛出异常。

看一个小例子,代码如下:

public void service(){

  serviceB();

  serviceA();

}

serviceB(){

 do sql

}

@Transactional(propagation=Propagation.MANDATORY)

serviceA(){

 do sql

}

这种情况执行 service会抛出异常,如果defaultAutoCommit=true,则serviceB是不会回滚的,defaultAutoCommit=false,则serviceB执行无效。

4、PROPAGATION_REQUIRED_NEW

说明:如果当前存在事务,先把当前事务相关内容封装到一个实体,然后重新创建一个新事务,接受这个实体为参数,用于事务的恢复。更直白的说法就是暂停当前事务(当前无事务则不需要),创建一个新事务。 针对这种情况,两个事务没有依赖关系,可以实现新事务回滚了,但外部事务继续执行。

看一个小例子,代码如下:

@Transactional

public void service(){

 serviceB();

 try{

  serviceA();

 }catch(Exception e){

 }

}

serviceB(){

 do sql

}

@Transactional(propagation=Propagation.REQUIRES_NEW)

serviceA(){

 do sql 1

 1/0;

 do sql 2

}

当调用service接口时,由于serviceA使用的是REQUIRES_NEW,它会创建一个新的事务,但由于serviceA抛出了运行时异常,导致serviceA整个被回滚了,而在service方法中,捕获了异常,所以serviceB是正常提交的。 注意,service中的try … catch 代码是必须的,否则service也会抛出异常,导致serviceB也被回滚。

5、Propagation.NOT_SUPPORTED

说明:如果当前存在事务,挂起当前事务,然后新的方法在没有事务的环境中执行,没有spring事务的环境下,sql的提交完全依赖于 defaultAutoCommit属性值 。

看一个小例子,代码如下:

@Transactional

public void service(){

  serviceB();

  serviceA();

}

serviceB(){

 do sql

}

@Transactional(propagation=Propagation.NOT_SUPPORTED)

serviceA(){

 do sql 1

 1/0;

 do sql 2

}

当调用service方法的时候,执行到serviceA方法中的1/0代码时,抛出了异常,由于serviceA处于无事务环境下,所以 sql1是否生效取决于defaultAutoCommit的值,当defaultAutoCommit=true时,sql1是生效的,但是service由于抛出了异常,所以serviceB会被回滚。

6、 PROPAGATION_NEVER

说明: 如果当前存在事务,则抛出异常,否则在无事务环境上执行代码。

看一个小例子,代码如下:

public void service(){

 serviceB();

 serviceA();

}

serviceB(){

 do sql

}

@Transactional(propagation=Propagation.NEVER)

serviceA(){

 do sql 1

 1/0;

 do sql 2

}

上面的示例调用service后,若defaultAutoCommit=true,则serviceB方法及serviceA中的sql1都会生效。

7、 PROPAGATION_NESTED

说明: 如果当前存在事务,则使用 SavePoint 技术把当前事务状态进行保存,然后底层共用一个连接,当NESTED内部出错的时候,自行回滚到 SavePoint这个状态,只要外部捕获到了异常,就可以继续进行外部的事务提交,而不会受到内嵌业务的干扰,但是,如果外部事务抛出了异常,整个大事务都会回滚。

注意: spring配置事务管理器要主动指定 nestedTransactionAllowed=true,如下所示:

<bean id="dataTransactionManager"

class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

 <property name="dataSource" ref="dataDataSource" />

 <property name="nestedTransactionAllowed" value="true" />

</bean>

看一个小例子,代码如下:

@Transactional

public void service(){

 serviceA();

 try{

  serviceB();

 }catch(Exception e){

 }

}

serviceA(){

 do sql

}

@Transactional(propagation=Propagation.NESTED)

serviceB(){

 do sql1

 1/0;

 do sql2

}

serviceB是一个内嵌的业务,内部抛出了运行时异常,所以serviceB整个被回滚了,由于service捕获了异常,所以serviceA是可以正常提交的。

再来看一个例子,代码如下:

@Transactional

public void service(){

  serviceA();

  serviceB();

  1/0;

}

@Transactional(propagation=Propagation.NESTED)

serviceA(){

 do sql

}

serviceB(){

 do sql

}

由于service抛出了异常,所以会导致整个service方法被回滚。(这就是跟PROPAGATION_REQUIRES_NEW不一样的地方了,NESTED方式下的内嵌业务会受到外部事务的异常而回滚。)

隔离级别

只读

   这是事务的第三个特性,是否为只读事务。如果事务只对后端的数据库进行该操作,数据库可以利用事务的只读特性来进行一些特定的优化。通过将事务设置为只读,你就可以给数据库一个机会,让它应用它认为合适的优化措施。

事务超时

  为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。

回滚规则

  事务五边形的最后一个方面是一组规则,这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚(这一行为与EJB的回滚行为是一致的) 。但是你可以声明事务在遇到特定的检查型异常时像遇到运行期异常那样回滚。同样,你还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常。

Spring 编程式事务和声明式事务的区别 

  编程式事务处理:所谓编程式事务指的是通过编码方式实现事务,允许用户在代码中精确定义事务的边界。即类似于JDBC编程实现事务管理。管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。

  声明式事务处理:管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。

  简单地说,编程式事务侵入到了业务代码里面,但是提供了更加详细的事务管理;而声明式事务由于基于AOP,所以既能起到事务管理的作用,又可以不影响业务代码的具体实现。

spring事务实例

不用事务实现转账

  我们还是以转账为实例。不用事务看如何实现转账。在数据库中有如下表 account ,内容如下:

  

  有两个用户 Tom 和 Marry 。他们初始账户余额都为 10000。这时候我们进行如下业务:Tom 向 Marry 转账 1000 块。那么这在程序中可以分解为两个步骤:

  ①、Tom 的账户余额 10000 减少 1000 块,剩余 9000 块。

  ②、Marry 的账户余额 10000 增加 1000 块,变为 11000块。

  上面两个步骤要么都执行成功,要么都不执行。我们通过 TransactionTemplate 编程式事务来控制:

  第一步:创建Java工程并导入相应的 jar 包(这里不用事务其实不需要这么多jar包,为了后面的讲解需要,我们一次性导入所有的jar包)

  

   第二步:编写 Dao 层

    AccountDao 接口:

package com.ys.dao;

public interface AccountDao {

    /**

     * 汇款

     * @param outer 汇款人

     * @param money 汇款金额

     */

    public void out(String outer,int money);

    /**

     * 收款

     * @param inner 收款人

     * @param money 收款金额

     */

    public void in(String inner,int money);

}

    AccountDaoImpl 接口实现类

package com.ys.dao.impl;

import org.springframework.jdbc.core.support.JdbcDaoSupport;

import com.ys.dao.AccountDao;

public class AccountDaoImpl extends JdbcDaoSupport implements AccountDao {

    /**

     * 根据用户名减少账户金额

     */

    @Override

    public void out(String outer, int money) {

        this.getJdbcTemplate().update("update account set money = money - ? where username = ?",money,outer);

    }

    /**

     * 根据用户名增加账户金额

     */

    @Override

    public void in(String inner, int money) {

        this.getJdbcTemplate().update("update account set money = money + ? where username = ?",money,inner);

    }

}

  第三步:实现 Service 层

    AccountService 接口

package com.ys.service;

public interface AccountService {

    /**

     * 转账

     * @param outer 汇款人

     * @param inner 收款人

     * @param money 交易金额

     */

    public void transfer(String outer,String inner,int money);

}

    AccountServiceImpl 接口实现类

package com.ys.service.impl;

import com.ys.dao.AccountDao;

import com.ys.service.AccountService;

public class AccountServiceImpl implements AccountService{

    private AccountDao accountDao;

    public void setAccountDao(AccountDao accountDao) {

        this.accountDao = accountDao;

    }

    @Override

    public void transfer(String outer, String inner, int money) {

        accountDao.out(outer, money);

        accountDao.in(inner, money);

    }

}

  第四步:Spring 全局配置文件 applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www./schema/beans"

       xmlns:xsi="http://www./2001/XMLSchema-instance"

       xmlns:context="http://www./schema/context"

       xmlns:aop="http://www./schema/aop"

       xsi:schemaLocation="http://www./schema/beans

                           http://www./schema/beans/spring-beans.xsd

                           http://www./schema/aop

                           http://www./schema/aop/spring-aop.xsd

                           http://www./schema/context

                           http://www./schema/context/spring-context.xsd">   

    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">

        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>

        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property>

        <property name="user" value="root"></property>

        <property name="password" value="root"></property>

    </bean>

    <bean id="accountDao" class="com.ys.dao.impl.AccountDaoImpl">

        <property name="dataSource" ref="dataSource"></property>

    </bean>

    <bean id="accountService" class="com.ys.service.impl.AccountServiceImpl">

        <property name="accountDao" ref="accountDao"></property>

    </bean>

</beans>

  第五步:测试

public class TransactionTest {

    @Test

    public void testNoTransaction(){

        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

        AccountService account = (AccountService) context.getBean("accountService");

        //Tom 向 Marry 转账1000

        account.transfer("Tom""Marry"1000);

    }

}

  第六步:查看数据库表 account

  

  上面的结果和我们想的一样,Tom 账户 money 减少了1000块。而 Marry 账户金额增加了1000块。

   这时候问题来了,比如在 Tom 账户 money 减少了1000块正常。而 Marry 账户金额增加时发生了异常,实际应用中比如断电(这里我们人为构造除数不能为0的异常),如下:

  

  那么这时候我们执行测试程序,很显然会报错,那么数据库是什么情况呢?

  

  数据库account :

  

  我们发现,程序执行报错了,但是数据库 Tom 账户金额依然减少了 1000 块,但是 Marry 账户的金额却没有增加。这在实际应用中肯定是不允许的,那么如何解决呢?

编程式事务处理实现转账(TransactionTemplate )

  上面转账的两步操作中间发生了异常,但是第一步依然在数据库中进行了增加操作。实际应用中不会允许这样的情况发生,所以我们这里用事务来进行管理。

  Dao 层不变,我们在 Service 层注入 TransactionTemplate 模板,因为是用模板来管理事务,所以模板需要注入事务管理器  DataSourceTransactionManager 。而事务管理器说到底还是用底层的JDBC在管理,所以我们需要在事务管理器中注入 DataSource。这几个步骤分别如下:

  AccountServiceImpl 接口:

package com.ys.service.impl;

import org.springframework.transaction.TransactionStatus;

import org.springframework.transaction.support.TransactionCallbackWithoutResult;

import org.springframework.transaction.support.TransactionTemplate;

import com.ys.dao.AccountDao;

import com.ys.service.AccountService;

public class AccountServiceImpl implements AccountService{

    private AccountDao accountDao;

    private TransactionTemplate transactionTemplate;

    public void setTransactionTemplate(TransactionTemplate transactionTemplate) {

        this.transactionTemplate = transactionTemplate;

    }

    public void setAccountDao(AccountDao accountDao) {

        this.accountDao = accountDao;

    }

    @Override

    public void transfer(final String outer,final String inner,final int money) {

        transactionTemplate.execute(new TransactionCallbackWithoutResult() {

            @Override

            protected void doInTransactionWithoutResult(TransactionStatus arg0) {

                accountDao.out(outer, money);

                //int i = 1/0;

                accountDao.in(inner, money);

            }

        });

    }

}

  Spring 全局配置文件 applicationContext.xml:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www./schema/beans"

       xmlns:xsi="http://www./2001/XMLSchema-instance"

       xmlns:context="http://www./schema/context"

       xmlns:aop="http://www./schema/aop"

       xsi:schemaLocation="http://www./schema/beans

                           http://www./schema/beans/spring-beans.xsd

                           http://www./schema/aop

                           http://www./schema/aop/spring-aop.xsd

                           http://www./schema/context

                           http://www./schema/context/spring-context.xsd">   

    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">

        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>

        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property>

        <property name="user" value="root"></property>

        <property name="password" value="root"></property>

    </bean>

    <bean id="accountDao" class="com.ys.dao.impl.AccountDaoImpl">

        <property name="dataSource" ref="dataSource"></property>

    </bean>

    <bean id="accountService" class="com.ys.service.impl.AccountServiceImpl">

        <property name="accountDao" ref="accountDao"></property>

        <property name="transactionTemplate" ref="transactionTemplate"></property>

    </bean>

    <!-- 创建模板 -->

    <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">

        <property name="transactionManager" ref="txManager"></property>

    </bean>

    <!-- 配置事务管理器 ,管理器需要事务,事务从Connection获得,连接从连接池DataSource获得 -->

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <property name="dataSource" ref="dataSource"></property>

    </bean>

</beans>

  测试文件保持不变,可以分两次测试,第一次两次操作没有发生异常,然后数据库正常改变了。第二次操作中间发生了异常,发现数据库内容没变。

  如果大家有兴趣也可以试试底层的PlatformTransactionManager来进行事务管理,我这里给出主要代码:

//定义一个某个框架平台的TransactionManager,如JDBC、Hibernate

        DataSourceTransactionManager dataSourceTransactionManager =

                new DataSourceTransactionManager();

        dataSourceTransactionManager.setDataSource(this.getJdbcTemplate().getDataSource()); // 设置数据源

        DefaultTransactionDefinition transDef = new DefaultTransactionDefinition(); // 定义事务属性

        transDef.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED); // 设置传播行为属性

        TransactionStatus status = dataSourceTransactionManager.getTransaction(transDef); // 获得事务状态

        try {

            // 数据库操作

            accountDao.out(outer, money);

            int i = 1/0;

            accountDao.in(inner, money);

            dataSourceTransactionManager.commit(status);// 提交

        catch (Exception e) {

            dataSourceTransactionManager.rollback(status);// 回滚

        }

声明式事务处理实现转账(基于AOP的 xml 配置)  

  Dao 层和 Service 层与我们最先开始的不用事务实现转账保持不变。主要是 applicationContext.xml 文件变化了。

  我们在 applicationContext.xml 文件中配置 aop 自动生成代理,进行事务管理:

  ①、配置管理器

  ②、配置事务详情

  ③、配置 aop

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www./schema/beans"

       xmlns:xsi="http://www./2001/XMLSchema-instance"

       xmlns:context="http://www./schema/context"

       xmlns:aop="http://www./schema/aop"

       xmlns:tx="http://www./schema/tx"

       xsi:schemaLocation="http://www./schema/beans

                           http://www./schema/beans/spring-beans.xsd

                           http://www./schema/aop

                           http://www./schema/aop/spring-aop.xsd

                           http://www./schema/context

                           http://www./schema/context/spring-context.xsd

                           http://www./schema/tx

                           http://www./schema/tx/spring-tx.xsd">

    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">

        <property name="driverClass" value="com.mysql.jdbc.Driver"></property>

        <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property>

        <property name="user" value="root"></property>

        <property name="password" value="root"></property>

    </bean>

    <bean id="accountDao" class="com.ys.dao.impl.AccountDaoImpl">

        <property name="dataSource" ref="dataSource"></property>

    </bean>

    <bean id="accountService" class="com.ys.service.impl.AccountServiceImpl">

        <property name="accountDao" ref="accountDao"></property>

    </bean>

    <!-- 1 事务管理器 -->

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <property name="dataSource" ref="dataSource"></property>

    </bean>

    <!-- 2 事务详情(事务通知)  , 在aop筛选基础上,比如对ABC三个确定使用什么样的事务。例如:AC读写、B只读 等

        <tx:attributes> 用于配置事务详情(属性属性)

            <tx:method name=""/> 详情具体配置

                propagation 传播行为 , REQUIRED:必须;REQUIRES_NEW:必须是新的

                isolation 隔离级别

    -->

    <tx:advice id="txAdvice" transaction-manager="txManager">

        <tx:attributes>

            <tx:method name="transfer" propagation="REQUIRED" isolation="DEFAULT"/>

        </tx:attributes>

    </tx:advice>

    <!-- 3 AOP编程,利用切入点表达式从目标类方法中 确定增强的连接器,从而获得切入点 -->

    <aop:config>

        <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.ys.service..*.*(..))"/>

    </aop:config>

</beans>

  测试类这里我们就不描述了,也是分有异常和无异常进行测试,发现与预期结果是吻合的。

声明式事务处理实现转账(基于AOP的 注解 配置) 

  分为如下两步:

  ①、在applicationContext.xml 配置事务管理器,将并事务管理器交予spring

  ②、在目标类或目标方法添加注解即可 @Transactional

  首先在 applicationContext.xml 文件中配置如下:

<!-- 1 事务管理器 -->

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">

        <property name="dataSource" ref="dataSource"></property>

    </bean>

    <!-- 2 将管理器交予spring

        * transaction-manager 配置事务管理器

        * proxy-target-class

            true : 底层强制使用cglib 代理

    -->

    <tx:annotation-driven transaction-manager="txManager" proxy-target-class="true"/>

  其次在目标类或者方法添加注解@Transactional。如果在类上添加,则说明类中的所有方法都添加事务,如果在方法上添加,则只有该方法具有事务。

package com.ys.service.impl;

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import org.springframework.transaction.annotation.Isolation;

import org.springframework.transaction.annotation.Propagation;

import org.springframework.transaction.annotation.Transactional;

import com.ys.dao.AccountDao;

import com.ys.service.AccountService;

@Transactional(propagation=Propagation.REQUIRED , isolation = Isolation.DEFAULT)

@Service("accountService")

public class AccountServiceImpl implements AccountService{

    @Resource(name="accountDao")

    private AccountDao accountDao;

    public void setAccountDao(AccountDao accountDao) {

        this.accountDao = accountDao;

    }

    @Override

    public void transfer(String outer, String inner, int money) {

        accountDao.out(outer, money);

        //int i = 1/0;

        accountDao.in(inner, money);

    }

}

Spring的事务管理默认只对未被捕获的运行期异常进行回滚

Spring的事务管理默认只对出现未被捕获的运行期异常(java.lang.RuntimeException及其子类)进行回滚; 如果一个方法抛出Exception或者Checked异常,Spring事务管理默认不进行回滚。 关于异常的分类:https://www.jb51.net/article/32246.htm

改变默认方式 
在@Transaction注解中定义noRollbackFor和RollbackFor指定某种异常是否回滚。 
@Transaction(noRollbackFor=RuntimeException.class) 
@Transaction(RollbackFor=Exception.class) 
这样就改变了默认的事务处理方式。 

Spring事务异常回滚,捕获异常不抛出就不会回滚

为了打印清楚日志,很多方法我都加tyr catch,在catch中打印日志。但是这边情况来了,当这个方法异常时候日志是打印了,但是加的事务却没有回滚。

  例:  
   类似这样的方法不会回滚 (一个方法出错,另一个方法不会回滚) :  

  1. if(userSave){          
  2.    try {         
  3.         userDao.save(user);          
  4.         userCapabilityQuotaDao.save(capabilityQuota);         
  5.      } catch (Exception e) {          
  6.         logger.info(e);         
  7.      }         
  8.  }  

下面的方法回滚(一个方法出错,另一个方法会回滚):

  1. if(userSave){         
  2.      try {          
  3.         userDao.save(user);          
  4.         userCapabilityQuotaDao.save(capabilityQuota);         
  5.        } catch (Exception e) {         
  6.         logger.info(e);          
  7.         throw new RuntimeException();         
  8.      }          
  9. }  

或者:

  1. if(userSave){          
  2.     try {          
  3.         userDao.save(user);          
  4.         userCapabilityQuotaDao.save(capabilityQuota);          
  5.     } catch (Exception e) {          
  6.         logger.info(e);          
  7.         TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();         
  8.     }         
  9.  }  

为什么不回滚呢??是对spring的事务机制不明白。!! 
   *****默认spring事务只在发生未被捕获的 runtimeexcetpion时才回滚 ****** 
         spring aop  异常捕获原理:被拦截的方法需显式抛出异常,并不能经任何处理,这样aop代理才能捕获到方法的异常,才能进行回滚,默认情况下aop只捕获runtimeexception的异常,但可以通过  。
 

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多