作为java开发工程师,相信大家对于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
实现,AOP
在Spring
中实现。该机制采用动态代理,具体分为JDK
动态代理和CGLIB
动态代理两种模式。
如果Spring
使用Cglib
代理实现(例如,您的代理类没有实现接口) ),并且您的业务方法恰好使用 Final
或 static
关键字,那么交易也会失败。更具体地说,它应该抛出异常,因为Cglib
使用字节码增强技术来生成被代理类的子类并覆盖被代理类的方法来实现代理。如果代理方法使用 final
或 static
关键字,则子类无法重写代理方法。
如果Spring
使用JDK
动态代理实现,JDK
动态代理是基于接口的实现了,然后最终
和 由 static
修改的方法无法被代理。
简而言之,如果该方法连代理都没有,那么事务回滚肯定是不可能的。
解决方案:
找到删除最终或静态关键字的方法
如果方法不是public
、Spring
事务也会失败,因为Spring的事务管理源码
有判断力AbstractFallbackTransactionAttributeSource
computeTransactionAttribute()。
如果目标方法不是公共的,TransactionAttribute
返回null
。
// 按要求不允许使用非公共方法。
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
解决方案:
是将当前方法访问级别更改为public
。
Spring
事务传播机制是指当多个事务方法互相调用时,决定事务应该如何传播的策略。 Spring
提供了七种事务传播机制:REQUIRED
、SUPPORTS
、 强制、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
原则是如果当前有事务正在添加到事务中,如果没有则创建新事务,并且父事务和调用的事务在同一个事务中。即使捕获了调用的异常,整个事务仍然会回滚。
// @Service
public class OrderServiceImpl 实现 OrderService {
@Transactional
public void updateOrder(Order order) {
// 更新订单
}
}
如果此时注释掉@Service
注解,这个类就不会被加载到Bean
,那么这个类就不会被Spring加载
如果管了,事情自然就变得无效了。
解决方案:
需要保证每个事务注解的每个bean都由Spring管理。
@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
事务有新的认识。