一、什么是事务
事务,是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行,要么都不执行;事务是一组不可再分割的操作集合(工作逻辑单元)。
我们知道,在JavaEE的开发过程中,service方法用于处理主要的业务逻辑,而业务逻辑的处理往往伴随着对数据库的多个操作。以我们生活中常见的转账为例,service方法要实现将A账户转账到B账户,则该方法内必定要有两个操作:先将A账户的金额减去要转账的数目,然后将B账户加上相应的金额数目。这两个操作必定要全部成功,方才表示本次转账成功;若有任何一方失败,则另一方必须回滚(即全部失败)。事务指的就是这样一组操作:这组操作是不可分割的,要么全部成功,要么全部失败。
二、事务的ACID特性
-
原子性:一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚。
主要涉及InnoDB事务。相关特性:事务的提交、回滚、信息表。 -
一致性:数据库总是从一个一致性的状态转换到另一个一致性的状态。在事务开始前后,数据库的完整性约束没有被破坏。
若违反了唯一性,必须撤销事务,返回初始状态。主要涉及内部InnoDB处理,以保护数据不受崩溃,相关特性:双写缓冲、崩溃恢复。 -
隔离性:每个读写事务的对象对其他事务的操作对象能相互分离,即:事务提交前对其他事务是不可见的,通常内部加锁实现。
主要涉及事务,尤其是事务隔离级别,相关特性:隔离级别、innoDB锁的底层实现细节。 -
持久性:一旦事务提交,则其所做的修改会永久保存到数据库。涉及到mysql软件特性与特定硬件配置的相互影响。
相关特性:4个配置项:双写缓冲开关、事务提交刷新log的级别、binlog同步频率、表文件;写缓存、操作系统对fsync()的支持、备份策略等。
数据库在事务的提交、回滚或运行中,总是处于一个一致的状态。若关联数据跨多张表更新,查询时只能看见全部老数据/全部新数据,而非新老数据混合。
三、事务的传播机制
事务的传播性一般用在事务嵌套的场景,比如一个事务方法里面调用了另一个事务方法,那么两个方法是各自作为独立的方法提交还是内层的事务合并到外层的事务一起提交,这就需要事务传播机制的配置来确定如何执行。
划重点:因为Spring是使用AOP来代理事务控制,是针对于接口或类的,所以在同一个Service类中两个方法的调用,传播机制是不生效的。
Spring定义了七种传播行为如下:
假设外层事务 Service A 的Method A()调用Service B的Method B()。
1.PROPAGATION_REQUIRED(默认)(常用)
若ServiceB.methodB()的事务级别定义为PROPAGATION_REQUIRED,那么执行ServiceA.methodA()的时候已经起了事务,这时调用ServiceB.methodB(),ServiceB.methodB()看到自己已经运行在ServiceA.methodA()的事务内部,就不再起新的事务。
假设ServiceB.methodB()运行的时候发现自己没有在事务中,它就会为自己分配一个事务。这样,在Service.methodA()或者在ServiceB.methodB()内的任何地方出现异常,事务都会被回滚。
2.PROPAGATION_REQUIRES_NEW(次常用:一般用在子方法需要单独事务的情况)
若我们定义ServiceA.methodA()的事务级别为PROPAGATION_REQUIRED,ServiceB.methodB()的事务级别为PROPAGATION_REQUIRED_NEW。
那么当执行到ServiceB.methodB()的时候,ServiceA.methodA()所在的事务就会被挂起,Service.methodB()会起一个新的事务,
等待ServiceB.methodB()的事务完成以后,它才继续执行。
它与PROPAGATION_REQUIRED的区别在于事务的回滚程度了。因为ServiceB.methodB()是新起一个事务,那么就是存在两个不同的事务。
如果ServiceB.methodB()已经提交,那么ServiceA.methodA()失败回滚,ServiceB.methodB()是不会回滚的。
如果ServiceB.methodB()失败回滚,如果它抛出的异常被ServiceA.methodA()捕获,ServiceA.methodA()事务仍然可能提交(主要看B抛出的异常是不是A会回滚的异常)。
3.PROPAGATION_NESTED
现在的情况就变得比较复杂了,ServiceB.methodB()的事务属性被设置为PROPAGATION_NESTED,此时两者之间又将如何协作呢?
ServiceB.methodB()如果rollback,那么内部事务即(ServiceB.methodB())将回滚到它执行前的SavePoint,而外部事务(即ServiceA.methodA())可以有以下两种处理方式:
1)捕获异常,执行异常分支逻辑
void methodA(){
try{
ServiceB.methodB();
}catch(SomeException)
//执行其他业务,比如methodC()
serviceC.methodC();
}
这种方式也是嵌套事务最有价值的地方,它起到了分支执行的效果,如果ServiceB.methodB()失败,那么执行ServiceC.methodC(),而ServiceB.methodB()已经回滚到它执行之前的SavaPoint,所以不会产生脏数据(相当于此方法从未执行过),这种特性可以用在某些特殊的业务中,而PROPAGATION_REQUIRED和PROPAGATION_REQUIRED_NEW都无法做到这一点。
2)外部事务回滚/提交
如果内部事务(ServiceB.methodB) rollback, 那么首先 ServiceB.methodB 回滚到它执行之前的 SavePoint(在任何情况下都会如此),外部事务(即 ServiceA.methodA) 将根据具体的配置决定自己是 commit 还是 rollback,代码不做任何修改。
4.PROPAGATION_SUPPORTS
假设ServiceB.methodB()的事务级别为PROPAGATION_SUPPORTS,那么当执行到ServiceB.methodB()时,如果发现ServiceA.methodA()已经开启了一个事务,则加入当前的事务,如果发现ServiceA.methodA()没有开启事务,则自己也不开启事务。这种时候,内部方法
的事务性完全依赖于最外层的事务。
5.PROPAGATION_NOT_SUPPORT
该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码。
6.PROPAGATION_NEVER
该传播机制不支持外层事务,即如果外层有事务就抛出异常。
7.PROPAGATION_MANDATORY
与NEVER相反,如果外层没有事务则抛出异常。
四、事务的隔离级别
一个数据库可能有多个客户端访问,这些客户端访问数据库时,如果不考虑并发事务隔离性,就会出现以下问题。这些问题分为五类,包括三类数据读问题:脏读、不可重复读、幻读。两类数据更新问题:第一类丢失更新、第二类丢失更新。
并发事务下出现的问题
1.脏读
A事务读取B事务尚未提交的更改数据,并在这个数据的基础上进行操作,这时候如果事务B回滚,那么A事务读到的数据是不被承认的。例如常见的取款事务和转账事务:
2.不可重复读
不可重复读是指A事务读取了B事务已经提交的更改数据。假如A在取款事务的过程中,B往该账户转账100,A两次读取的余额发生不一致。
3.幻读
A事务读取B事务提交的新增数据,会引发幻读问题。幻读一般发生在计算统计数据的事务中,例如银行系统在同一个事务中两次统计存款账户的总金额,在两次统计中,刚好新增了一个存款账户,存入了100,这时候两次统计的总金额不一致。
划重点:不可重复读和幻读的区别是:前者是读到了已经提交的事务的更改数据(修改或删除);后者是读到了其他已提交事务的新增数据。对于这两种问题采用不同的方法解决:防止读到更改数据,只需要对操作的数据添加行级锁,防止操作中的数据发生变化;防止读到新增数据,往往需要添加表级锁,将整张表锁定。
4.第一类丢失更新
A事务撤销时,把已经提交的B事务的更新数据覆盖了。
这时候取款事务A撤销事务,余额恢复为1000,这就丢失了更新。
5.第二类丢失更新
A事务覆盖B事务已提交的数据,造成B事务所做的操作丢失。
为了解决上述问题,数据库通过锁机制解决并发访问的问题。根据锁定对象不同:分为行级锁和表级锁;根据并发事务锁定的关系上看:分为共享锁定和独占锁定,共享锁定会防止独占锁定但允许其他的共享锁定。而独占锁定既防止共享锁定也防止其他独占锁定。为了更改数据,数据库必须在进行更改的行上施加行独占锁定,insert、update、delete和selsct for update语句都会隐式采用必要的行锁定。
但是直接使用锁机制管理是很复杂的,基于锁机制,数据库给用户提供了不同的事务隔离级别,只要设置了事务隔离级别,数据库就会分析事务中的sql语句然后自动选择合适的锁。
隔离级别
1.READ UNCOMMITED 读未提交
如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据,该隔离级别可以通过“排他写锁”,但是不排斥读线程实现。这样就避免了更新丢失,却可能出现脏读,也就是说事务A读取到了事务B未提交的数据。
解决了更新丢失,但还是可能会出现脏读。
2.READ COMMITED 不可重复读
如果是一个读事务(线程),则允许其他事务读写,如果是写事务将会禁止其他事务访问该行数据,该隔离级别避免了脏读,但是可能出现不可重复读。事务A事先读取了数据,事务B紧接着更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。
解决了更新丢失和脏读问题。
3.REPEATABLE READ 可重复读
可重复读取是指在一个事务内,多次读同一个数据,在这个事务还没结束时,其他事务不能访问该数据(包括了读写),这样就可以在同一个事务内两次读到的数据是一样的,因此称为是可重复读隔离级别,读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务(包括了读写),这样避免了不可重复读和脏读,但是有时可能会出现幻读。(读取数据的事务)可以通过“共享读镜”和“排他写锁”实现。
解决了更新丢失、脏读、不可重复读、但是还会出现幻读。
补充:可重复读在读事务内是禁止针对数据内容的写操作的,但是不能禁止其他操作,比如增加删除记录,所以会造成幻读。
4.SERIALIZABLE 串行化
提供严格的事务隔离,它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行,如果仅仅通过“行级锁”是无法实现序列化的,必须通过其他机制保证新插入的数据不会被执行查询操作的事务访问到。序列化是最高的事务隔离级别,同时代价也是最高的,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻读
解决了更新丢失、脏读、不可重复读、幻读(虚读)。
5.四种隔离机制对比
在MYSQL数据库中,支持上面四种隔离级别,默认的为Repeatable read(可重复读);而在Oracle数据库中,只支持Serializeble(串行化)级别和Read committed(读已提交)这两种级别,其中默认的为Read committed级别。
划重点:事务的隔离级别和数据库并发性是成反比的,隔离级别越高,并发性越低。
五、Spring事务管理
Spring 事务管理为我们提供了三个高层抽象的接口,分别是TransactionProxyFactoryBean,TransactionDefinition,TransactionStatus。
1.PlatformTransactionManager事务管理器
Spring事务管理器的接口是org.springframework.transaction.PlatformTransactionManager
,Spring框架并不直接管理事务,而是通过这个接口为不同的持久层框架提供了不同的PlatformTransactionManager接口实现类,也就是将事务管理的职责委托给Hibernate或者iBatis等持久化框架的事务来实现。
//使用JDBC或者iBatis进行持久化数据时使用
org.springframework.jdbc.datasource.DataSourceTransactionManager
//使用hibernate5版本进行持久化数据时使用
org.springframework.orm.hibernate5.HibernateTransactionManager
//使用JPA进行持久化数据时使用
org.springframework.orm.jpa.JpaTransactionManager
//当持久化机制是jdo时使用
org.springframework.jdo.JdoTransactionManager
//使用一个JTA实现来管理事务,在一个事务跨越多个资源时必须使用
org.springframework.transaction.jta.JtaTransactionManager
PlatformTransactionManager接口源码:
public interface PlatformTransactionManager {
// 根据指定的传播行为,返回当前活动的事务或创建新事务。
TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
// 就给定事务的状态提交给定事务。
void commit(TransactionStatus status) throws TransactionException;
// 执行给定事务的回滚。
void rollback(TransactionStatus status) throws TransactionException;
}
2.TransactionDefinition定义事务基本属性
org.springframework.transaction.TransactionDefinition
接口用于定义一个事务,它定义了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;
// 如果当前存在事物,则在嵌套事物内执行;如果当前没有事物,则与PROPAGATION_REQUIRED传播特性相同
int PROPAGATION_NESTED = 6;
// 使用后端数据库默认的隔离级别。
int ISOLATION_DEFAULT = -1;
// READ_UNCOMMITTED 隔离级别
int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED;
// READ_COMMITTED 隔离级别
int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED;
// REPEATABLE_READ 隔离级别
int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ;
// SERIALIZABLE 隔离级别
int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE;
// 默认超时时间
int TIMEOUT_DEFAULT = -1;
// 获取事物传播特性
int getPropagationBehavior();
// 获取事物隔离级别
int getIsolationLevel();
// 获取事物超时时间
int getTimeout();
// 判断事物是否可读
boolean isReadOnly();
// 获取事物名称
@Nullable
String getName();
}
传播机制和隔离级别在上面我们已经阐述过了,我们来看其它三个属性。
是否只读
如果将事务设置为只读,表示这个事务只读取数据但不更新数据, 这样可以帮助数据库引擎优化事务。
事务超时
为了使一个应用程序很好地执行,它的事务不能运行太长时间。因此,声明式事务的下一个特性就是它的超时。
假设事务的运行时间变得格外的长,由于事务可能涉及对数据库的锁定,所以长时间运行的事务会不必要地占用数据库资源。这时就可以声明一个事务在特定秒数后自动回滚,不必等它自己结束。
由于超时时钟在一个事务启动的时候开始的,因此,只有对于那些具有可能启动一个新事务的传播行为(PROPAGATION_REQUIRES_NEW、PROPAGATION_REQUIRED、ROPAGATION_NESTED)的方法来说,声明事务超时才有意义。
事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。在 TransactionDefinition 中以 int 的值来表示超时时间,默认值是-1,其单位是秒。
回滚规则
在默认设置下,事务只在出现运行时异常(runtime exception)时回滚,而在出现受检查异常(checked exception)时不回滚(这一行为和EJB中的回滚行为是一致的)。
不过,可以声明在出现特定受检查异常时像运行时异常一样回滚。同样,也可以声明一个事务在出现特定的异常时不回滚,即使特定的异常是运行时异常。
3.TransactionStatus事务状态描述
org.springframework.transaction.TransactionStatus 接口用来记录事务的状态,该接口定义了一组方法,用来获取或判断事务的相应状态信息。
TransactionStatus接口源码
public interface TransactionStatus extends SavepointManager, Flushable {
// 返回当前事务是否为新事务(否则将参与到现有事务中,或者可能一开始就不在实际事务中运行)
boolean isNewTransaction();
// 返回该事务是否在内部携带保存点,也就是说,已经创建为基于保存点的嵌套事务。
boolean hasSavepoint();
// 设置事务仅回滚。
void setRollbackOnly();
// 返回事务是否已标记为仅回滚
boolean isRollbackOnly();
// 将会话刷新到数据存储区
@Override
void flush();
// 返回事物是否已经完成,无论提交或者回滚。
boolean isCompleted();
}
SaveManager接口源码
public interface SavepointManager {
// 创建一个新的保存点。
Object createSavepoint() throws TransactionException;
// 回滚到给定的保存点。
// 注意:调用此方法回滚到给定的保存点之后,不会自动释放保存点,
// 可以通过调用releaseSavepoint方法释放保存点。
void rollbackToSavepoint(Object savepoint) throws TransactionException;
// 显式释放给定的保存点。(大多数事务管理器将在事务完成时自动释放保存点)
void releaseSavepoint(Object savepoint) throws TransactionException;
}
六、Spring事务配置方式
Spring同时支持 编程式事务 和 声明式事务,大部分时候,我们都推荐采用声明式事务策略。
使用声明式事务策略优势如下:
-
声明式事务能大大降低开发者的代码书写量,而且声明式事务几乎不影响应用的代码。因此,无论底层事务策略如何 变化,应用程序都无需改变。
-
应用程序代码无需任何事务处理代码,可以更专注于业务逻辑的实现。
-
Spring可对任何POJO的方法提供事务管理,而且Spring的声明式事务管理无需容器的支持,可在任何环境使用。
-
EJB的CMT无法提供声明式回滚规则;而通过配置文件,Spring可指定事务在遇到特定异常时自动回滚。Spring不仅可在代码中使用setRollBackOnly回滚事务,也可在配置文件中配置回滚规则。
-
由于Spring采用AOP的方式管理事务,因此,可以在事务回滚动作中插入用户自己的动作,而不仅仅是执行系统默认的回滚。
Spring 编程式事务
Spring声明式事务
声明式事务管理有三种实现方式:
- 基于TransactionProxyFactoryBean的方式
- 基于AspectJ的XML方式
- 基于@Transactional方式
准备工作
1.初始化数据库表
create table account
(
id bigint auto_increment primary key,
name varchar(32) not null,
money bigint not null,
constraint account_name_uindex
unique (name)
);
insert into account (name, money) values('Bill', 2000),('Jack', 2000);
2.创建DAO实现类
public class TransferDaoImpl extends JdbcDaoSupport implements TransferDao {
/**
* @param name 账户名称
* @param amount 支出金额
*/
@Override
public void payMoney(String name, Long amount) {
String sql = "update account set money=money-? where name=?";
this.getJdbcTemplate().update(sql, amount, name);
}
/**
* @param name 账户名称
* @param amount 收入金额
*/
@Override
public void collectMoney(String name, Long amount) {
String sql = "update account set money=money+? where name=?";
this.getJdbcTemplate().update(sql, amount, name);
}
}
3.创建Service实现类(事务管理类)
public class TransferServiceImpl implements TransferService {
private TransferDao transferDao;
public void setTransferDao(TransferDao transferDao) {
this.transferDao = transferDao;
}
/**
* @param source 支出方账户名称
* @param name 收入方账户名称
* @param amount 转账金额
*/
@Override
public void transferMoney(String source, String destination, Long amount) {
transferDao.payMoney(source, amount);
int i = 100/0;//此处用于测试抛异常时是否会回滚
transferDao.collectMoney(destination, amount);
}
}
4.创建Spring核心配置文件
<!-- 读取db.properties配置信息 -->
<context:property-placeholder location="db.properties"></context:property-placeholder>
<!-- 配置c3p0数据源 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${db.driverClass}" />
<property name="jdbcUrl" value="${db.url}" />
<property name="user" value="${db.username}" />
<property name="password" value="${db.password}" />
</bean>
<bean id="transferDao" class="com.tx.dao.impl.TransferDaoImpl">
<property name="dataSource" ref="dataSource" />
</bean>
<bean id="transferService" class="com.tx.service.impl.TransferServiceImpl">
<property name="transferDao" ref="transferDao" />
</bean>
1.基于TransactionProxyFactoryBean的方式
在spring核心配置文件中添加事务管理器的配置和TransactionProxyFactoryBean代理对象。
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!--配置业务层的代理-->
<bean id="transferServiceProxy" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
<!--配置目标对象-->
<property name="target" ref="transferService" />
<!--注入事务管理器-->
<property name="transactionManager" ref="transactionManager" />
<!--注入事务属性-->
<property name="transactionAttributes">
<props>
<!--
prop的格式:
* PROPAGATION :事务的传播行为
* ISOLATION :事务的隔离级别
* readOnly :是否只读
* -Exception :发生哪些异常回滚事务
* +Exception :发生哪些异常不回滚事务
-->
<prop key="transfer*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringTransactionApplicationTests {
@Autowired
TransferService transferService;
@Resource(name="transferServiceProxy")
TransferService transferServiceProxy;
@Test
public void contextLoads() {
//注意,此处引入的是代理对象transferServiceProxy,而不是transferService
transferServiceProxy.transferMoney("Bill","Jack", 200L);
}
}
2.基于AspectJ的XML方式
在spring核心配置文件中添加事务管理器的配置、事务的增强以及切面。
<!--配置事务管理器-->
<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="transfer*" propagation="REQUIRED" />
</tx:attributes>
</tx:advice>
<!--配置切面-->
<aop:config>
<aop:pointcut id="pointcut1" expression="execution(* com.tx.service.impl.*ServiceImpl.*(..))" />
<aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut1" />
</aop:config>
测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringTransactionApplicationTests {
@Autowired
TransferService transferService;
@Test
public void contextLoads() {
transferService.transferMoney("Bill","Jack", 200L);
}
}
3.基于@Transactional注解的声明式事务
其底层建立在AOP的基础上,对方法前后进行拦截,然后在目标方法开始之前创建或加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。通过声明式事物,无需在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过等价的基于标注的方式),便可以将事务规则应用到业务逻辑中。
在spring核心配置文件中添加事务管理器的配置和开启事务注解
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!--开启事务注解-->
<tx:annotation-driven transaction-manager="transactionManager" />
在事务方法中添加@Transaction注解
@Transactional
public void transferMoney(String source, String destination, Long amount) {
transferDao.payMoney(source, amount);
int i = 100/0;
transferDao.collectMoney(destination, amount);
}
测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringTransactionApplicationTests {
@Autowired
TransferService transferService;
@Test
public void contextLoads() {
transferService.transferMoney("Bill","Jack", 200L);
}
}
4.小结
在声明式事务管理的三种实现方式中,基于TransactionProxyFactoryBean的方式需要为每个进行事务管理的类配置一个TransactionProxyFactoryBean对象进行增强,所以开发中很少使用;基于AspectJ的XML方式一旦在XML文件中配置好后,不需要修改源代码,所以开发中经常使用;基于注解的方式开发较为简单,配置好后只需要在事务类上或方法上添加@Transaction注解即可,所以开发中也经常使用。
七、Spring事务不生效的常见原因
-
1、@Transactional 注解的 rollbackFor 捕获的异常范围小于代码抛出的异常,导致不回滚。
-
2、设置了事务的timeout时间,代码逻辑执行超时了,导致事务失效。
-
3、被@Transactional 注解修饰的方法, 修饰符非 public 或者被final修饰,Aop没办法为其生成一个代理。
@Transactional 注解只能应用到 public 修饰的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错,事务也会失效。
在应用系统调用声明 @Transactional 的目标方法时,Spring Framework 默认使用 AOP代理,在代码运行时生成一个代理对象,再由这个代理对象来统一管理。
Spring 事务是使用 AOP环绕通知和异常通知,就是对方法进行拦截,在方法执行前开启事务,在捕获到异常时进行事务回滚,在方法执行完成后提交事务。
- 4.在类中内部调用,这个时候会用this关键字,导致没有通过代理去走此方法,从而没有开启事物。
在同一个类的方法之间调用中,如果 A 方法调用了 B 方法,不管 A 方法有没有开启事务, B 方法的事务是无效的。
-
5、如果使用的是MySql数据库且引擎是MyISAM,则事务会不起作用,原因是MyISAM不支持事务,可以改成InnoDB。
-
6、在不同类之间的方法调用中,如果 A 方法开启了事务,B 方法没有开启事务,B 方法调用了 A 方法。如果 B 方法中发生异常,但不是调用的 A 方法产生的,则异常不会使 A 方法的事务回滚,此时事务无效。如果 B 方法中发生异常,异常是调用的 A 方法产生的,则 A 方法的事务回滚,此时事务有效。在 B 方法上加上注解 @Trasactional,这样 A 和 B 方法就在同一个事务里了,不管异常产生在哪里,事务都是有效的。
简单地说,不同类之间方法调用时,异常发生在无事务的方法中,但不是被调用的方法产生的,被调用的方法的事务无效。只有异常发生在开启事务的方法内,事务才有效。
-
7、如果使用了Spring + MVC,则
context:component-scan
重复扫描问题可能会引起事务失效。 -
8、Spring团队建议在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。在接口上使用 @Transactional 注解,只能当你设置了基于接口的代理时它才生效。因为注解是不能继承 的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。
【事务隔离机制】参考
https://blog.csdn.net/starlh35/article/details/76445267
https://blog.csdn.net/zhouym_/article/details/90381606
http://www.codeceo.com/article/spring-transactions.html
【Spring事务管理】参考
https://www.cnblogs.com/liantdev/p/10149443.html
https://www.cnblogs.com/mseddl/p/11577846.html
https://www.cnblogs.com/java-chen-hao/p/11635525.html
【Spring事务管理及失效总结】
https://zhuanlan.zhihu.com/p/98357492
关注一下吧~【Pretty Kathy】