问题背景
在开发在线考试系统时,遇到了一个典型的Spring事务管理问题:org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only。这个异常发生在用户提交考试答案时,尽管主要的业务逻辑应该成功提交,但由于嵌套事务处理不当导致整个事务被意外回滚。
问题原因分析
异常触发场景
UnexpectedRollbackException 通常出现在以下情况:
- 主事务中嵌套了子事务
- 子事务被标记为回滚状态(通常是由于异常)
- 当子事务结束时,Spring检测到事务被标记为回滚,因此主事务也无法提交
具体代码场景
在我们的考试系统中,[ExamAppServiceImpl](file://C:\JPA_Project\Java\SourceCode\ProjectCode\exam-system\exam-system-application\src\main\java\com\exam\application\service\impl\ExamAppServiceImpl.java#L47-L1234) 的 [submit](file://C:\JPA_Project\Java\SourceCode\ProjectCode\exam-system\exam-system-ui\src\views\exam\EditMockExam.vue#L644-L750) 方法包含完整的考试提交逻辑,其中还包含了错题收集的功能:
java
@Override
@Transactional
public Object submit(ExamSubmitReq submitReq) {
// ... 主要的考试提交逻辑 ...
// 4. 自动收集错题到错题集
if (!wrongAnswers.isEmpty()) {
try {
// 获取用户的错题集列表
WrongQuestionCollectionListReq collectionListReq = new WrongQuestionCollectionListReq();
collectionListReq.setUserId(submitReq.getUserId());
WrongQuestionCollectionListResp collectionListResp = wrongQuestionAppService.getWrongQuestionCollectionList(collectionListReq);
// ... 错题集处理逻辑 ...
// 调用另一个事务性服务方法
wrongQuestionAppService.batchAddWrongQuestionsToCollection(batchAddReq);
} catch (Exception e) {
log.error("自动收集错题失败: {}", e.getMessage(), e);
}
}
return result;
}
这里的 wrongQuestionAppService.batchAddWrongQuestionsToCollection() 方法本身也标注了 @Transactional,这就形成了嵌套事务。
Spring 事务传播机制原理
Spring 提供了多种事务传播行为,理解这些机制对于解决此类问题至关重要:
| 传播行为 | 说明 |
|---|---|
REQUIRED |
如果当前存在事务,则加入该事务;如果不存在,则创建一个新的事务 |
REQUIRES_NEW |
创建一个新的事务,如果当前存在事务,则暂停当前事务 |
SUPPORTS |
如果当前存在事务,则加入该事务;如果不存在,则以非事务方式执行 |
NOT_SUPPORTED |
以非事务方式执行操作,如果当前存在事务,则暂停当前事务 |
在我们的案例中,两个都使用 REQUIRED(默认)的事务方法嵌套,导致子事务的回滚状态影响了父事务。
解决方案
方案一:使用 REQUIRES_NEW 传播行为(推荐)
将错题收集功能放在独立的事务中执行:
java
/**
* 收集错题到错题集(使用新事务以避免影响主交卷流程)
*
* @param userId 用户ID
* @param examId 考试ID
* @param wrongAnswers 错题列表
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void collectWrongQuestionsToCollection(Long userId, Long examId, List<Map<String, Object>> wrongAnswers) {
try {
// 获取用户的错题集列表
WrongQuestionCollectionListReq collectionListReq = new WrongQuestionCollectionListReq();
collectionListReq.setUserId(userId);
WrongQuestionCollectionListResp collectionListResp = wrongQuestionAppService.getWrongQuestionCollectionList(collectionListReq);
// ... 错题处理逻辑 ...
wrongQuestionAppService.batchAddWrongQuestionsToCollection(batchAddReq);
log.info("已将{}道错题添加到错题集{}", wrongAnswers.size(), collectionId);
} catch (Exception e) {
log.error("收集错题到错题集时发生异常: {}", e.getMessage(), e);
throw e;
}
}
方案二:调整事务边界
重构代码,将不同业务逻辑的事务边界明确分开,避免不必要的嵌套。
注意事项与最佳实践
1. 合理设计事务边界
- 一个方法只负责一个明确的业务单元
- 避免在一个事务性方法中调用多个事务性方法
- 考虑业务逻辑的ACID特性需求
2. 选择合适的事务传播行为
- 对于不影响主业务流程的辅助功能,使用
REQUIRES_NEW - 对于需要与主业务保持一致性的操作,使用
REQUIRED - 对于读操作,考虑使用
SUPPORTS
3. 异常处理策略
java
try {
// 业务逻辑
} catch (SpecificException e) {
// 记录日志,但不中断主流程
log.warn("非关键业务失败,不影响主流程", e);
} catch (CriticalException e) {
// 关键业务失败,应中断整个流程
throw e;
}
4. 监控与调试
- 在事务边界处添加日志
- 监控事务执行时间
- 注意数据库连接池使用情况
5. 测试覆盖
- 编写针对事务回滚的测试用例
- 模拟各种异常场景
- 验证数据一致性
总结
UnexpectedRollbackException 是Spring事务管理中常见的问题,通常由嵌套事务处理不当引起。通过合理使用事务传播行为、明确事务边界、以及良好的异常处理策略,可以有效避免这类问题。在实际开发中,我们应该深入理解Spring的事务传播机制,根据业务需求选择合适的事务管理策略,确保系统的稳定性和数据的一致性。