Spring事务最佳实践【Java版】
2023年11月28日 · 2239 字 · 5 分钟 · Java
背景
在开发需求采用Java声明式事务对两个对象插入两张表 下方代码编写,单测发现事务失效(release对象插入成功,action插入失败,但事务没有回滚,release对象还是插入成功)
@Slf4j
@Service
public class xxService {
public String xxx(xxRequest request) throws JsonProcessingException {
// 参数校验
checkParam(request);
// 前置查询校验信息...
//省略xxx多行代码解析和build逻辑
// ....
ReleaseEntity release =
buildReleaseEntity(xxx);
String reason = request.getReason();
ActionEntity action = buildActionEntity(xxx);
// 执行db操作
execute(release, action)
// 返回执行记录id
return action.getUid();
}
@Transactional(rollbackFor = Exception.class)
public void execute(ReleaseEntity release, ActionEntity action) {
try {
// 新增release
releaseService.create(release);
// 新增action
actionService.create(action);
} catch (BizException | JsonProcessingException ex) {
throw new BizException("执行流量分发报错,请重试err:" + ex.getMessage());
}
}
}
@Slf4j
@Service
public class ReleaseService {
@Resource
private ReleaseMapper releaseMapper;
public void create(ReleaseEntity entity ) throws JsonProcessingException {
log.info("新增执行版本 entity:{}", JsonUtils.getMapper().writeValueAsString(entity));
try {
releaseMapper.insertSelective(entity);
} catch (Exception e) {
throw new BizException(String.format("新增执行版本,请重试err:%s", e.getMessage()));
}
}
public List<ReleaseEntity> getReleaseListByPlanId(String planId) {
return releaseMapper.getReleaseListByPlanId(planId);
}
}
@Slf4j
@Service
public class ActionService {
@Resource private ActionMapper actionMapper;
public void create(ActionEntity entity) throws JsonProcessingException {
log.info("新增执行记录 entity:{}", JsonUtils.getMapper().writeValueAsString(entity));
try {
actionMapper.insertSelective(entity);
} catch (Exception e) {
throw new BizException(String.format("新增执行记录报错,请重试err:%s", e.getMessage()));
}
}
}
排查经过
尝试:将数据库操作函数代码放在函数distribute中,在distribute方法上增加@Transactional注解
结论:执行单测,事务没有失效,成功事务回滚没有问题
疑问思考:是不是spring方法中传递事物有什么黑魔法?如果按照这样写能实现需要会不会大事务(原则:尽可能事务的开启在db操作代码前后,尽可能缩小事务影响范围),还是spring会在运行到db操作代码时候,智能的开启事务和提交事务??
@Slf4j
@Service
public class RgwService {
@Transactional(rollbackFor = Exception.class)
public String distribute(DistributeRequest request) throws JsonProcessingException {
// 参数校验
checkDistributeParam(request);
// 前置查询校验预案信息...
//省略xxx多行代码解析和build逻辑
// ....
ReleaseEntity release =
buildReleaseEntity(planId, newParam, description, operator, curReleaseVersion);
String reason = request.getReason();
ActionEntity action = buildActionEntity(planId, release.getUid(), reason, operator);
// 执行db操作
execute(release, action)
try {
// 新增release
releaseService.create(release);
// 新增action
actionService.create(action);
} catch (BizException | JsonProcessingException ex) {
throw new BizException("执行流量分发报错,请重试err:" + ex.getMessage());
}
}
// 返回执行记录id
return action.getUid();
}
}
问题根因
由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理 ps:在springAOP的用法中,只有代理的类才会被切入,我们在controller层调用service的方法的时候,是可以被切入的, 但是如果我们在service层 A方法中,调用B方法,切点切的是B方法,那么这时候是不会切入的
解决方式
使用编程式事务管理
在这种方式下,我们显式地管理事务,手动开启、提交和回滚事务,确保methodA和methodB都能在自己的事务中执行
优点 |
---|
1. 更细粒度的控制控制事务影响范围 |
2. 方便处理条件式回滚可以 |
3. 手动处理特定的异常 |
缺点 |
---|
1. 代码冗余:开启事务、提交事务、回滚事务和异常处理等 |
2. 可读性差:事务的边界和处理通常会散布在业务逻辑中 |
@Service
public class MyService {
@Autowired
private PlatformTransactionManager transactionManager;
public void methodA() {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 执行一些业务逻辑
// 调用methodB
methodB();
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
throw e;
}
}
@Transactional
public void methodB() {
// 执行一些业务逻辑
// 抛出异常
throw new RuntimeException("Something went wrong");
}
}
拆分到不同的类中
通过将methodA和methodB拆分到不同的类中,每个方法都有自己的代理,事务注解不会相互影响,可以正常工作。
优点 |
---|
简单明了,易于维护,不需要复杂的额外配置;不需要显式的编程式事务管理或AOP配置 |
缺点 |
---|
容易冗余多个类,职责不清晰 |
@Service
public class MyService {
@Autowired
private MyOtherService otherService;
@Transactional
public void methodA() {
// 执行一些业务逻辑
// 调用MyOtherService中的methodB
otherService.methodB();
}
}
@Service
public class MyOtherService {
@Transactional
public void methodB() {
// 执行一些业务逻辑
// 抛出异常
throw new RuntimeException("Something went wrong");
}
}
使用ApplicationContext获取当前代理对象
使用ApplicationContext来获取Bean的实例,从而确保事务生效;就是在该类中自动注入本类bean,使用@Autowired即可,然后使用这个注入的bean去调用本类方法,即可达到两方法事务都起效
优点 |
---|
不需要引入额外的配置或依赖于特定的AOP框架功能 |
缺点 |
---|
1. 潜在性能问题:一定的性能开销,因为它需要在运行时动态确定Bean的实例 |
2. 可读性差:读者可能难以理解为什么需要在同一个类中的方法之间使用它 |
3. 潜在的递归问题: 需要小心避免在同一个方法内部产生无限递归调用问题,导致栈溢出异常 |
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class MyService implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Transactional
public void methodA() {
// 执行一些业务逻辑
// 获取MyService的代理实例
MyService proxy = applicationContext.getBean(MyService.class);
// 调用methodB
proxy.methodB();
}
@Transactional
public void methodB() {
// 执行一些业务逻辑
// 抛出异常
throw new RuntimeException("Something went wrong");
}
}
使用AopContext.currentProxy()获取当前代理对象【推荐】
methodA中使用了AopContext.currentProxy()获取了当前代理对象,并调用了methodB。这样,methodB将会在相同的事务中执行,而不会失效
优点 |
---|
简化代码;避免了不必要的类拆分和编程式事务代码 |
缺点 |
---|
1. 潜在性能问题:引入一定的性能开销,因为它需要在运行时动态确定代理对象。这可能对高性能应用程序产生一定影响,特别是在高并发case |
2. 线程安全问题:可能会引入线程安全问题,因为它涉及到共享状态(当前代理对象)。在多线程应用中,需要谨慎使用,并确保适当的同步机制可读性差 |
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
public class MyService {
@Transactional
public void methodA() {
// 执行一些业务逻辑
// 调用methodB
MyService proxy = (MyService) AopContext.currentProxy();
proxy.methodB();
}
@Transactional
public void methodB() {
// 执行一些业务逻辑
// 抛出异常
throw new RuntimeException("Something went wrong");
}
}