1、spring事务管理基本介绍
Spring 支持编程式事务管理以及声明式事务管理两种方式。
编程式事务管理是侵入性事务管理,编程式事务每次实现都要单独实现,但业务量大功能复杂时,使用编程式事务无疑是痛苦的,所以并不推荐使用。
声明式事务属于无侵入式,不会影响业务逻辑的实现,只需要在配置文件中做相关的事务规则声明或者通过注解的方式,便可以将事务规则应用到业务逻辑中。声明式事务管理建立在AOP之上,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。显然声明式事务管理要优于编程式事务管理,这正是Spring倡导的非侵入式的编程方式。
Spring事务的本质其实就是数据库对事务的支持,使用JDBC的事务管理机制就是利用 java.sql.Connection 对象完成对事务的提交。在没有Spring帮我们管理事务之前,我们的做法:
Connection conn = DriverManager.getConnection(); try { conn.setAutoCommit(false); //将自动提交设置为false //执行CRUD操作 ... conn.commit(); //当两个操作成功后手动提交 } catch (Exception e) { conn.rollback(); //一旦其中一个操作出错都将回滚,所有操作都不成功 e.printStackTrace(); } finally { conn.colse(); }
事务是一系列的动作,一旦其中有一个动作出现错误,必须全部回滚,系统将事务中对数据库的所有已完成的操作全部撤消,滚回到事务开始的状态,避免出现由于数据不一致而导致的接下来一系列的错误。事务的出现是为了确保数据的完整性和一致性,在目前企业级应用开发中,事务管理是必不可少的。
在企业级应用中,多用户访问数据库是常见的场景,这就是所谓的事务的并发。事务并发所可能存在的问题:
- 脏读:一个事务读到另一个事务未提交的更新数据。
- 不可重复读:一个事务两次读同一行数据,可是这两次读到的数据不一样。
- 幻读:一个事务执行两次查询,但第二次查询比第一次查询多出了一些数据行。
- 丢失更新:撤消一个事务时,把其它事务已提交的更新的数据覆盖了。
有了Spring,我们再也无需要去处理获得连接、关闭连接、事务提交和回滚等这些操作,使得我们把更多的精力放在处理业务上。事实上Spring并不直接管理事务,而是提供了多种事务管理器。他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。
具体的事务管理机制对 Spring 来说是透明的,它并不关心那些,那些是对应各个平台需要关心的,所以 Spring 事务管理的一个优点就是为不同的事务API提供一致的编程模型,如JTA、JDBC、Hibernate、JPA。
Spring并不直接管理事务,而是提供了多种事务管理器,他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现。 Spring事务管理器的接口是 org.springframework.transaction.PlatformTransactionManager,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但是具体的实现由各个平台自己实现。
事务管理器接口 PlatformTransactionManager 通过 getTransaction(TransactionDefinition definition) 方法来得到事务,这个方法里面的参数是 TransactionDefinition 类,这个类就定义了一些基本的事务属性。事务属性包含了5个方面,如图所示:
2、声明式事务管理之基于注解方式
如果应用程序中直接使用JDBC来进行持久化,DataSourceTransactionManager会为你处理事务边界。为了使用DataSourceTransactionManager,你需要使用如下的XML将其装配到应用程序的上下文定义中:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean>
然后需要开启事务注解,开启事务注解需要引入命名空间 tx。完整的配置文件如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"> <!--开启组件扫描--> <context:component-scan base-package="test, service, dao"></context:component-scan> <!--引入外部配置文件--> <context:property-placeholder location="classpath:jdbc.properties"/> <!--配置数据库连接池--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${prop.driverClass}"></property> <!--通过${}使用外部配置文件的值--> <property name="url" value="${prop.url}"></property> <property name="username" value="${prop.username}"></property> <property name="password" value="${prop.password}"></property> </bean> <!-- 配置JdbcTmplate --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!-- 注入dataSource --> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 创建事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <!-- 开启事务注解--> <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven> </beans>
然后在需要事务管理的地方加上 @Transactional 注解即可,在类上或者在方法上添加该注解都可以。如果注解是添加到类上,则表示给该类里的所有方法都添加事务;如果是添加到方法上,则表示给该方法添加事务。
假设类有一个方法,A给B转账,需要给 A 的账户减少100元,同时需要给 B 增加100元,如果没有使用事务,而执行过程中发生了异常,则可能导致数据发生不一致的问题:
@Transactional @Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Override public void updateUser(User user) { userDao.updateUser(user); } @Override public void transferAccount() { //减少100元 userDao.reduceMoney(); //模拟异常 int i = 100 / 0; //增加100元 userDao.addMoney(); } }
如果没有添加 @Transactional 注解,则 A 减少了100元,但后面发生了异常,导致 B 并没有增加100元,由此导致了数据不一致的问题。如果添加了注解,则发生异常会自动回滚,A账户的金额不会减少,B账户也不会增加。
2.1、定义事务的传播行为
多事务方法(事务方法指的是将改变数据库表数据的操作)之间互相调用时,对事务之间的管理就称之为传播行为(propagation behavior)。当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
Spring 中定义了七种传播行为:
传播行为 | 值 | 含义 |
PROPAGATION_REQUIRED(默认值) | 0 | 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务,并在自己的事务内运行 |
PROPAGATION_SUPPORTS | 1 | 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行 |
PROPAGATION_MANDATORY | 2 | 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常 |
PROPAGATION_REQUIRED_NEW | 3 | 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
PROPAGATION_NOT_SUPPORTED | 4 | 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用JTATransactionManager的话,则需要访问TransactionManager |
PROPAGATION_NEVER | 5 | 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常 |
PROPAGATION_NESTED | 6 |
表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与PROPAGATION_REQUIRED一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务 |
定义传播行为只需在 @Transactional 注解后面添加 propagation 参数即可:
@Transactional(propagation = Propagation.REQUIRED) @Service public class UserServiceImpl implements UserService { }
假设 A 方法调用了 B 方法,给 B 定义了 REQUIRED 传播行为。如果 A 方法已经添加了事务,则 B 方法也在 A 的事务内运行;如果 A 方法没有事务,则 B 方法会启动一个新事务,并在该事务内运行。
2.2、定义事务的隔离级别
MySQL数据库为我们提供了四种隔离级别:
- Read uncommitted (读未提交):最低级别,任何情况都无法保证。
- Read committed (读已提交):只可避免脏读的发生。
- Repeatable read (可重复读,默认值):可避免脏读、不可重复读的发生。
- Serializable (串行化):脏读、不可重复读、幻读均可避免
事务的隔离级别可参考:https://www.cnblogs.com/wenxuehai/p/13485440.html
spring 设置事务的隔离级别只需要在 @Transactional 注解后面添加 isolation 参数即可,如下设置为 Read commited 级别:
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED) @Service public class UserServiceImpl implements UserService { }
2.3、@Transactional 注解的其它参数
2.3.1、超时时间(timeout)
为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。
定义超时时间会规定事务必须在一定时间内进行提交,如果超时会自动进行回滚。
spring 设置事务的超时时间只需要在 @Transactional 注解后面添加 timeout 参数即可。该值默认是 -1,即永不超时。我们可以手动设置超时时间,单位为秒:
@Transactional(timeout = 10) @Service public class UserServiceImpl implements UserService { }
2.3.2、是否只读(readOnly)
事务的其中一个特性是它是否为只读事务,即只查询。如果事务只对后端的数据库进行该操作,数据库可以利用事务的只读特性来进行一些特定的优化。通过将事务设置为只读,你就可以给数据库一个机会,让它应用它认为合适的优化措施。
spring 设置事务是否只读只需要在 @Transactional 注解后面添加 readOnly 参数即可,默认为 false,非只读,即可读可写。我们可以手动设置为 true,即只读:
@Transactional(readOnly = true) @Service public class UserServiceImpl implements UserService { }
2.3.3、回滚(rollbackFor)
设置出现哪些异常时进行事务回滚。默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚。但是我们可以设置事务在遇到特定的异常时进行回滚。同样,你还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常。
spring 设置事务对哪些异常进行回滚需要在 @Transactional 注解后面添加 rollbackFor 参数,并且该参数值为需要设置的异常的 class。
2.3.4、不回滚(noRollbackFor)
设置出现哪些异常不进行事务回滚。
spring 设置事务对哪些异常进行回滚需要在 @Transactional 注解后面添加 noRollbackFor 参数,并且该参数值为需要设置的异常的 class。
3、声明式事务管理之基于xml配置方式
通过 spring 的 xml 配置文件来实现事务管理:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"> <!--开启组件扫描--> <context:component-scan base-package="test, service, dao"></context:component-scan> <!--引入外部配置文件--> <context:property-placeholder location="classpath:jdbc.properties"/> <!--配置数据库连接池--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${prop.driverClass}"></property> <!--通过${}使用外部配置文件的值--> <property name="url" value="${prop.url}"></property> <property name="username" value="${prop.username}"></property> <property name="password" value="${prop.password}"></property> </bean> <!-- 配置JdbcTmplate --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!-- 注入dataSource --> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 创建事务管理器--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> <!-- 配置通知 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!--指定需要添加事务的方法的规则--> <tx:method name="*" propagation="REQUIRED" /> <!-- <tx:method name="transferAccount" propagation="REQUIRED" />--> </tx:attributes> </tx:advice> <!-- 配置切入点和切面 --> <aop:config> <!-- 配置切入点 --> <aop:pointcut id="pt" expression="execution(* service.UserServiceImpl.*(..))" /> <!-- 配置切面 --> <aop:advisor advice-ref="txAdvice" pointcut-ref="pt" /> </aop:config> </beans>