一、spring事务介绍
1、事务
事务(Transaction):访问并可能更新数据库中各种数据项的一个程序执行单元(unit),它通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起。当在数据库中更改数据成功时,在事务中更改的数据便会提交,不再改变。否则,事务就取消或者回滚,更改无效。
事务(Transaction)是基于关系型数据库(RDBMS)的企业应用的重要组成部分。在软件开发领域,事务扮演者十分重要的角色,用来确保应用程序数据的完整性和一致性。
2、特点
事务具有 4 个特性:原子性、一致性、隔离性和持久性,简称为 ACID 特性。
原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。
事务隔离(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。
持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
3、spring事务的隔离级别
① ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。以mysql为例,见mysql事务。
② ISOLATION_READ_UNCOMMITTED:读未提交,允许事务在执行过程中,读取其他事务未提交的数据。
③ ISOLATION_READ_COMMITTED:读已提交,允许事务在执行过程中,读取其他事务已经提交的数据。
④ ISOLATION_REPEATABLE_READ:可重复读,在同一个事务内,任意时刻的查询结果都是一致的。
⑤ ISOLATION_SERIALIZABLE:所有事务逐个依次执行。
4、spring的事务传播机制:
传播行为是说方法之间的调用问题。spring事务的传播机制说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。事务传播机制实际上是使用简单的ThreadLocal实现的,所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。
① PROPAGATION_REQUIRED:(默认传播行为)如果当前没有事务,就创建一个新事务;如果当前存在事务,就加入该事务。
② PROPAGATION_REQUIRES_NEW:无论当前存不存在事务,都创建新事务进行执行。
③ PROPAGATION_SUPPORTS:如果当前存在事务,就加入该事务;如果当前不存在事务,就以非事务执行。‘
④ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
⑤ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则按REQUIRED属性执行。
⑥ PROPAGATION_MANDATORY:如果当前存在事务,就加入该事务;如果当前不存在事务,就抛出异常。
⑦ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
二、Spring 事务管理器
在 Spring 框架中提供了多种事务管理器来进行事务管理。Spring 的事务管理器是基于AOP 实现的。在 Spring 的事务管理器中包含了配置事务传播行为、隔离级别、只读和超时属性,这些属性提供了事务应用的方法和描述策略。
在 Java EE 项目开发经常会使用分层模式,Spring 的事务处理位于业务逻辑层,它提供了针对事务的解决方案。
1、Spring原始方式PlatformTransactionManager
在 Spring 的事务模块(spring-tx-5.2.7.RELEASE.jar)中包括事务管理的三个核心接口。
1.1、PlatformTransactionManager 接口
PlatformTransactionManager 接口是 Spring 提供的事务管理器接口,用于管理事务。Spring 将事务的配置详细信息封装到 TransactionDefinition 对象中,然后通过事务管理器的getTransaction() 方法获得事务的状态(TransactionStatus),并对事务进行下一步的操作。该接口中提供了三个事务操作方法,
1.2、TransactionDefinition
TransactionDefinition 接口是事务定义(描述)的对象,它提供了事务相关信息获取的方法,其中包括五个操作,具体如下:
1.3、TransactionStatus
TransactionStatus 接口是事务的状态,它描述了某一时间点上事务的状态信息。其中包括六个操作,
2、Spring模板类TransactionTemplate
默认TransactionAutoConfiguration自动装配,使用时直接注入TransactionTemplate 接口,
推荐使用。
语法:在try中编写业务代码,在catch中手动设置回滚。这样,如无异常会自动提交,如遇异常会回滚。
transactionTemplate.execute(transactionStatus -> {
try {
} catch (Exception e) {
// 异常手动设置回滚
transactionStatus.setRollbackOnly();
}
return true;
});
三、Spring事务实现方式
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。Spring只提供统一事务管理接口,具体实现都是由各数据库自己实现,数据库事务的提交和回滚是通过 redo log 和 undo log实现的。Spring会在事务开始时,根据当前环境中设置的隔离级别,调整数据库隔离级别,由此保持一致。
Spring 支持两种事务方式,分别是编程式事务和声明式事务。
1、编程式事务
1.1、实现方法
编程式事务是指将事务管理代码嵌入嵌入到业务代码中,来控制事务的提交和回滚。基于上面介绍的事务管理器实现,推荐使用TransactionTemplate。
1)jdbc访问数据,处理事务。Connection conn;conn.commit();conn.rollback();
(2)mybatis访问数据库,处理事务。SqlSession.commit() ;SqlSession.rollback();
1.2、demo
@Autowired
private TransactionTemplate transactionTemplate;
@Autowired
private UserMapper userMapper;
@Override
public void insert(UserDTO userDTO) {
transactionTemplate.execute(transactionStatus -> {
try {
userMapper.insert(userDTO);
int a = 1/0;
} catch (Exception e) {
// 异常手动设置回滚
transactionStatus.setRollbackOnly();
}
return true;
});
}
2、声明式事务
2.1、原理
声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
2.2、实现
相应方法上加注解即可
@Transactional
3、比较
将事务管理代码从业务方法中抽离了出来,以声明式的方式来实现事务管理,对于开发者来说,声明式事务显然比编程式事务更易用、更好用。声明式事务虽然优于编程式事务,但也有不足,声明式事务管理的粒度是方法级别,而编程式事务是可以精确到代码块级别的。
四、Spring声明式事务失效的几种情况
1、类未被spring管理
例:
// @Service
public class AServiceImpl implements AService {
@Transactional
public void updateA(A a) {
// update a
}
}
将 @Service注解注释后,该类就不受 Spring容器管理,导致事务失效!这是因为 Spring事务是由 AOP机制实现的,AOP机制的本质就是动态代理,从 Spring IOC容器获取 bean时,Spring会为目标类创建代理,从而支持事务。
2、事务方法不是 public
public abstract class AbstractFallbackTransactionAttributeSource
implements TransactionAttributeSource, EmbeddedValueResolverAware {
// 获取事务属性
@Override
@Nullable
public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// 部分代码省略
if (cached != null) {
// 部分代码省略
}
else {
// 调用 计算事务属性 方法
// We need to work it out.
TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass);
// 部分代码省略
return txAttr;
}
}
// 计算事务属性
@Nullable
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// 按照配置,不允许使用非公共方法。
// Don't allow non-public methods, as configured.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// 部分代码省略
return null;
}
}
通过源码可以看到add方法的访问权限被定义成了private,这样会导致事务失效。从spring源码中可以看出被代理方法必须是public的。在AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。
3、方法用final修饰
@Service
public class AService {
@Transactional
public final void add(Model model){
saveData(model);
int i = 1/0;
}
}
spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,添加事务功能。
4、方法内部调用事务方法
@Service
public class UserServiceImpl implements UserService {
@Override
public void addUser(User user){
this.addUser(user);
}
@Transactional(rollbackFor = Exception.class)
public void addUser (User user){
userMapper.save(user);
int i = 1/0;
}
}
事务是通过Spring AOP代理来实现的,而 addUser()内部事务方法其实是this对象调用的,而不是通过AOP代理来调用的,因此事务失效。
5、异常被 catch
@Service
public class UserServiceImpl implements UserService {
@Transactional
@Override
public void addUser(User user) {
try{
userMapper.save(user);
// 模拟异常,数据库的记录应该回滚
throw new RuntimeException();
} catch (RuntimeException e) {
}
}
}
通过源码,我们可以很清晰的看出,在 invokeWithinTransaction() 方法中,当 Spring catch到 Throwable异常,就会调用 completeTransactionAfterThrowing()方法进行事务回滚的逻辑。但是,在 UserServiceImpl类的业务代码中直接把异常catch住了,Spring自然就 catch不到异常,因此事务回滚的逻辑就不会执行,事务就失效了。
public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
// 省略部分代码
if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
Object retVal;
try {
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
// target invocation exception
// 回滚事务 是在 spring 的catch中处理,也就是说,如果Spring catch不到对应的异常,就不会进入回滚事务的逻辑
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
cleanupTransactionInfo(txInfo);
}
// 省略部分代码
// 提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
}
/**
* Handle a throwable, completing the transaction.
* We may commit or roll back, depending on the configuration.
* @param txInfo information about the current transaction
* @param ex throwable encountered
*/
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
if (txInfo != null && txInfo.getTransactionStatus() != null) {
// 省略部分代码
if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
try {
// 回滚事务
txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
}
catch (TransactionSystemException ex2) {
logger.error("Application exception overridden by rollback exception", ex);
ex2.initApplicationException(ex);
throw ex2;
}
catch (RuntimeException | Error ex2) {
logger.error("Application exception overridden by rollback exception", ex);
throw ex2;
}
}
// 省略部分代码
}
}
}
6、表不支持事务
Spring 事务是业务层的事务,其底层还是依赖于数据库本身的事务支持。比如 MySQL 数据库,MyISAM 引擎不支持事务而 InnoDB 引擎支持事务。所以,开发中如果需要使用事务,一定要确保你选择的数据库支持事务。
7、rollbackFor属性配置错误
@Service
public class UserServiceImpl implements UserService {
@Override
public void addUser(User user){
// 调用内部方法
this.doAddUser(user);
}
@Transactional(rollbackFor = Error.class)
public void addUserInfo (User user){
userMapper.save(user);
// 模拟异常,数据库的记录应该回滚
throw new RuntimeException();
}
}
通过 Transactional注解源码,我们可以发现 rollbackFor属性指定的异常必须是 Throwable及其子类,并且在默认情况下,Spring对 RuntimeException 和 Error 两种异常会自动回滚事务,也就是说,如果业务抛出来的异常是 RuntimeException 和 Error类型,可以不需要通过 rollbackFor属性指定,Spring 默认会识别处理。代码中抛出的异常是 Exception(throw new Exception()),而 Exception 和 Error没有任何关系,也就是说,事务需要捕获到 Error才会回滚,可是抛出一个和 Error不相关的 Exception异常,因此事务自然无效,不能回滚。
8、 错误的事物传播特性
@Service
public class UserService {
@Transactional(propagation = Propagation.NEVER)
public void add(UserModel userModel) {
saveData(userModel);
updateData(userModel);
}
}
spring目前支持7种传播特性:
REQUIRED 如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。
SUPPORTS 如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
MANDATORY 如果当前上下文中存在事务,否则抛出异常。
REQUIRES_NEW 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
NOT_SUPPORTED 如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
NEVER 如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
NESTED 如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。
我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。
目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。
9、多线程调用
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleService roleService;
@Transactional
public void add(UserModel userModel) throws Exception {
userMapper.insertUser(userModel);
new Thread(() -> {
roleService.save();
}).start();
}
}
@Service
public class RoleService {
@Transactional
public void save() {
// 保存数据
}
}
我们可以看到事务方法add中,调用了事务方法save,但是事务方法save是在另外一个线程中调用的。这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果save方法中抛了异常,add方法也回滚是不可能的。如果看过spring事务源码的朋友,可能会知道spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。
private static final ThreadLocal<Map<Object, Object>> resources =new NamedThreadLocal<>("Transactional resources");
我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。
10、未开启事务
如果你使用的是springboot项目,那么springboot通过DataSourceTransactionManagerAutoConfiguration
类,已经默默的帮你开启了事务。
但如果你使用的还是传统的spring项目,则需要在applicationContext.xml文件中,手动配置事务相关参数。如果忘了配置,事务肯定是不会生效的。
<!-- 配置事务管理器 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> <property name="dataSource" ref="dataSource"></property>
</bean>
<tx:advice id="advice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*" propagation="REQUIRED"/></tx:attributes>
</tx:advice>
<!-- 用切点把事务切进去 -->
<aop:config> <aop:pointcut expression="execution(* com.susan.*.*(..))" id="pointcut"/> <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/>
</aop:config>