当前位置:科技动态 > 8个Spring事务失败场景,你遇到过几个?

8个Spring事务失败场景,你遇到过几个?

  • 发布:2023-10-01 18:32

前言

作为java开发工程师,相信大家对于spring的使用并不陌生。但你可能只停留在基本的使用水平。当遇到一些特殊场景时,交易可能无法生效,直接暴露在生产中,从而可能导致严重的生产事故。今天我们先简单讲解一下Spring事务的原理,然后总结Spring事务失败的场景并提出相应的解决方案。

Spring交易原理

还记得JDBC中事务是如何操作的吗?伪代码可能如下所示:

//获取数据库连接
Connection connection = DriverManager.getConnection();
//设置autoCommit为false
connection.setAutoCommit(false);
//使用sql操作数据库
.........
//提交或回滚
connection.commit()/connection.rollback

connection.close();

需要在每个业务代码中编写commit()close()等代码来控制事务。

但是Spring不愿意这样做。对业务代码侵入性太大。所以使用事务注解@Transactional来控制事务。底层实现是基于切面编程AOP实现,AOPSpring中实现。该机制采用动态代理,具体分为JDK动态代理和CGLIB动态代理两种模式。

  1. Springbean初始化过程中,找到的方法为Transactional注解,需要代理对应的Bean、生成代理对象。
  2. 那么当调用该方法时,就会执行切面的逻辑,这里切面的逻辑包括启动事务、提交事务、回滚事务的逻辑。

另外需​​要注意的是Spring本身并没有实现事务,底层仍然依赖于数据库事务。如果没有数据库事务的支持,Spring事务将无法生效。

接下来我们进入正题,看看哪些场景会导致Spring事务失败。

Spring事务失败场景

1。抛出已检查异常

例如您的交易控制代码如下:

@Transactional
public void transactionTest() throws IOException{
User user = new User();
UserService.insert(user);
throw new IOException(); }

如果@Transactional没有特别指定,Spring只会在遇到运行时异常RuntimeException或error时进行回滚,而IOException等检查异常不会影响回滚。

public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}

解决方案:

知道原因后,解决办法也很简单。配置 rollbackFor 属性,例如 @Transactional(rollbackFor = Exception.class)

2。业务方法本身捕获了异常

@Transactional(rollbackFor = Exception.class)
public void transactionTest() {
尝试{
用户user = new User();
UserService.insert(user); int i = 1 / 0;
}catch(异常 e){
e.printStackTrace();
}
}

这种场景下,事务失败的原因也很简单,Spring是否回滚是由你是否抛出异常决定的,所以如果你自己捕获了异常,Spring 你无能为力。

看完上面的代码,你可能会觉得你不可能为了这么简单的问题犯这么低级的错误,但我想告诉你的是,我身边几乎有一半的人都曾被这样的问题所困扰过:这一幕。

在编写业务代码时,代码可能很复杂,嵌套方法很多。如果您不小心,可能会触发此问题。举一个非常简单的例子,假设您有一个审计功能。每个方法执行完后,审计结果都会保存到数据库中,那么代码就可以这样写。

@Service
public class TransactionService {

@Transactional(rollbackFor = Exception.class)
public void transactionTest() throws IOException {
User user = new User();
用户服务.insert(user);
throw new IOException();

}
}

@Component
公共类 AuditAspect {

@Autowired 私人审计服务审计服务;

@Around(value = "execution (* com.alvin.*.*(..))")
public Object around(ProceedingJoinPoint pjp) {
try {
Auditaudit = new Audit();
签名签名 = pjp.getSignature();
MethodSignature methodSignature = (MethodSignature)签名;
String[] strings = methodSignature.getParameterNames();
audit.setMethod(signature.getName()) ;
audit.setParameters(strings);
对象继续 = pjp.proceed();
audit.success(true);
返回继续;
} catch (Throwable e) {
log.error("{}", e);
audit.success(false);
}

审计服务.save(audit);
return null;
}

}

在上面的例子中,事务将会失败。原因是Spring的事务切面优先级最低,所以如果异常被切面捕获,Spring自然无法正常处理事务,因为事务管理器无法捕获异常。

解决方案:

看,虽然我们知道业务代码在处理事务时无法自己捕获异常,但只要代码变得复杂,我们就有可能再次出错,所以处理事务时一定要小心,或者不要使用声明式事务,并使用编程事务 — transactionTemplate.execute()

3。同一个类中的方法调用

@Service
public class DefaultTransactionService Implement Service {

public void saveUser() throws Exception {
//做某事
doInsert();
}

@Transactional(rollbackFor = Exception.class)
public void doInsert() throws IOException {
User user = new User();
UserService.insert(user);
throw new IOException();

}
}

这也是一个容易出错的场景。事务失败的原因也很简单,因为Spring的事务管理功能是通过动态代理实现的,而Spring使用了JDK动态默认情况下。代理,而JDK动态代理是通过接口实现,通过反射调用目标类。简单理解就是,saveUser()方法调用this.doInsert(),其中this是真实的对象,所以会直接去doInsert 的业务逻辑不遵循切面逻辑,因此事务失败。

解决方案:

选项1:解决方案可以是直接在启动类中添加@Transactional注解saveUser()

?意识到。

4。方法使用final或static关键字

如果Spring使用Cglib代理实现(例如,您的代理类没有实现接口) ),并且您的业务方法恰好使用 Finalstatic 关键字,那么交易也会失败。更具体地说,它应该抛出异常,因为Cglib使用字节码增强技术来生成被代理类的子类并覆盖被代理类的方法来实现代理。如果代理方法使用 finalstatic 关键字,则子类无法重写代理方法。

如果Spring使用JDK动态代理实现,JDK动态代理是基于接口的实现了,然后最终 由 static 修改的方法无法被代理。

简而言之,如果该方法连代理都没有,那么事务回滚肯定是不可能的。

解决方案:

找到删除最终或静态关键字的方法

5。方法不公开

如果方法不是publicSpring事务也会失败,因为Spring的事务管理源码 有判断力AbstractFallbackTransactionAttributeSourcecomputeTransactionAttribute()。 如果目标方法不是公共的,TransactionAttribute返回null

// 按要求不允许使用非公共方法。
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}

解决方案:

是将当前方法访问级别更改为public

6。滥用传播机制

Spring事务传播机制是指当多个事务方法互相调用时,决定事务应该如何传播的策略。 Spring提供了七种事务传播机制:REQUIREDSUPPORTS、 强制REQUIRES_NEW NOT_SUPPORTED 从不嵌套。不了解这些沟通策略的工作原理可能会导致交易失败。

@Service
公共类TransactionService{


@Autowired
私有UserMapper userMapper;

@Autowired
私有AddressMapper地址Mapper;


@Transactional(传播 = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)
public void doInsert(User user,Address address) throws Exception {
//做某事
userMapper.insert(user);
saveAddress(地址);
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAddress(Address 地址) {
//做某事
addressMapper.insert(address);
}
}

在上面的示例中,如果用户未能插入,则不会导致saveaddress()被向后回滚,因为此处使用的传播是requires_new,传播机制REQUIRES_NEW的原理是,如果当前方法中没有事务,就会创建一个新事务。如果交易已存在,则当前交易将被挂起并创建新交易。在当前事务完成之前,父事务不会被提交。如果父事务发生异常,子事务的提交不会受到影响。

交易传播机制解释如下:

  • REQUIRED 如果当前上下文中存在事务,则加入该事务,如果不存在事务,则创建事务,这是默认的传播属性值。
  • 支持 如果当前上下文中存在事务,则支持事务加入。如果没有事务,则以非事务方式执行。
  • 强制 如果当前上下文中存在事务,否则抛出异常。
  • REQUIRES_NEW 每次都会创建一个新的事务,同时上下文中的事务会被挂起。当前新事务执行完成后,上下文事务将恢复并再次执行。
  • NOT_SUPPORTED 如果当前上下文中存在事务,则挂起当前事务,然后在没有事务的环境中执行新方法。
  • NEVER 如果当前上下文中存在事务,则会抛出异常,否则代码将在无事务环境中执行。
  • NESTED 如果当前上下文中存在事务,则执行嵌套事务,如果不存在事务,则创建新事务。

解决方案

将事务传播策略更改为默认必需REQUIRED原则是如果当前有事务正在添加到事务中,如果没有则创建新事务,并且父事务和调用的事务在同一个事务中。即使捕获了调用的异常,整个事务仍然会回滚。

7。不受 Spring 管理

// @Service
public class OrderServiceImpl 实现 OrderService {
@Transactional
public void updateOrder(Order order) {
// 更新订单
}
}

如果此时注释掉@Service注解,这个类就不会被加载到Bean,那么这个类就不会被Spring加载如果管了,事情自然就变得无效了。

解决方案

需要保证每个事务注解的每个bean都由Spring管理。

8。多线程

@Service
公共类UserService{

@Autowired
私有UserMapper userMapper;
@Autowired
私有RoleService角色服务;

@Transactional
public void add(UserModel userModel ) 抛出异常 {

userMapper.insertUser(userModel);
new Thread(() -> {
try {
test();
} catch (异常 e) { roleService.doOtherThing();
}
}).start();
}
}

@Service
公共类 RoleService {

@Transactional
公共无效doOtherThing() {
try {
int i = 1/0;
System.out.println("保存角色表数据");
}catch (异常 e) {
throw new RuntimeException();
}
}
}

我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。

这会导致两个方法不在同一个线程,获取不同的数据库连接,从而产生两个不同的事务。如果doOtherThing方法抛出异常,add方法无法回滚。

我们所说的同一个事务实际上是指同一个数据库连接。只有同一个数据库连接才能同时提交和回滚。如果在不同的线程中,获取到的数据库连接肯定是不同的,所以是不同的事务。

解决方案:

这感觉有点像分布式交易。尽量保证在同一个事务中处理。

总结

本文简单讲解了Spring中事务实现的原理,并列出了8种Spring事务失败场景。相信很多朋友可能都遇到过。失败的原因也有详细说明。希望大家对Spring事务有新的认识。


相关文章