开发中遇到的关于Spring事务[传播行为和隔离级别]的相关问题的记录
代码和问题描述
问题描述
最开始三个被调用的方法没有
@Transactional(propagation = Propagation.
REQUIRES_NEW
,rollbackFor = Exception.class)
导致的问题是:
updateZtYfkWithIndependentTx
、updateSftjWithIndependentTx
的更新对数据库生效了,但是当checkAllOver
方法返回false
的时候触发了异常,导致了事务回滚。使得当callFlowCompleteTask
方法执行完后,两个对数据库更新的操作也回滚失效了。
然后就给
updateZtYfkWithIndependentTx
、updateSftjWithIndependentTx
两个方法加上了@Transactional(propagation = Propagation.
REQUIRES_NEW
,rollbackFor = Exception.class)
然后问题解决了。
原先的代码
/*** 完成任务(同意/驳回/终止等)- 新增终止流程逻辑*/
@Override
@Transactional(rollbackFor = Exception.class)
public JSONObject callFlowCompleteTask(FlowWrapperDto<CommonDto> flowWrapperDto) {// 1. 获取流程配置JSONObject flowConfig = flowWrapperDto.getFlowConfig();if (ObjectUtil.isEmpty(flowConfig)) {flowConfig = new JSONObject();}// 2. 获取业务数据CommonDto data = flowWrapperDto.getData();String jcdbh = data.getJcdbh();// 3. 查询业务主表数据CdmCCxDto cCxDto = cdmCCxService.getByJcdbh(jcdbh);if (cCxDto == null) {log.error("未找到CDM决策单业务主表数据,jcdbh = {}", jcdbh);throw new ServiceException(GeneralServiceExceptionResult.GENERAL_UNKNOWN_FAILED.getCode(),"未找到CDM决策单业务主表数据");}// 4. 获取流程关键字段String taskId = flowWrapperDto.getTaskId();String instId = flowWrapperDto.getInstId();String flowKey = flowWrapperDto.getFlowKey();String currentTaskKey = flowWrapperDto.getTaskKey();// 5. 校验必填字段if (StringUtils.isEmpty(taskId)) {throw new IllegalArgumentException("taskId 不能为空");}if (StringUtils.isEmpty(instId)) {throw new IllegalArgumentException("instId 不能为空");}if (StringUtils.isEmpty(flowKey)) {throw new IllegalArgumentException("flowKey 不能为空");}// 6. 提取操作类型String actionStr = flowConfig.getString(FlowVarsConstant.ACTION_NAME);if (StringUtils.isEmpty(actionStr)) {throw new IllegalArgumentException("缺少操作类型(action)");}FlowAction flowAction = FlowAction.fromAlias(actionStr.trim());if (flowAction == null) {throw new IllegalArgumentException("不支持的操作类型: " + actionStr);}// 7. 构造流程配置JSONObject cleanFlowConfig = new JSONObject();cleanFlowConfig.put("taskId", taskId);cleanFlowConfig.put("instId", instId);cleanFlowConfig.put("flowKey", flowKey);cleanFlowConfig.put("taskKey", currentTaskKey);cleanFlowConfig.put("action", actionStr);cleanFlowConfig.put("remark", flowConfig.getString("remark"));// 8. 添加当前操作人信息String currentUserId = LoginUserUtils.servletLoginUser().getUserId();String currentUserName = LoginUserUtils.servletLoginUser().getUsername();cleanFlowConfig.put("userId", currentUserId);cleanFlowConfig.put("userName", currentUserName);// 9. 根据操作类型处理逻辑if (FlowAction.AGREE.equals(flowAction)) {log.info("用户同意任务,taskId={}, instId={}", taskId, instId);cleanFlowConfig.put("flowStatus", FlowStatus.INPRG.getName());if ("UserTask_0d74x2d".equals(currentTaskKey)) {//cdmYYyfxListService.updateZtYfk(jcdbh, currentUserId);cdmYYyfxListService.updateZtYfk(jcdbh, currentUserId);cdmYFxyjyfgService.updateSftj(jcdbh, currentUserId);// 校验所有人员完成反馈boolean allCompleted = commonService.checkAllOver(jcdbh);if (!allCompleted) {throw new ServiceException(GeneralServiceExceptionResult.GENERAL_VERIFICATION_FAILED.getCode(),"存在未完成反馈的人员,无法进入下一节点");}} else if ("UserTask_1txknja".equals(currentTaskKey)) {cleanFlowConfig.put("flowStatus", FlowStatus.CLOSE.getName());}} else if (FlowAction.REJECT.equals(flowAction)) {log.info("用户驳回任务,taskId={}, instId={}", taskId, instId);cleanFlowConfig.put("flowStatus", FlowStatus.REJECT.getName());cleanFlowConfig.put("action", "reject");// 设置驳回目标节点String destinationNode = getRejectDestination(currentTaskKey);cleanFlowConfig.put("destination", destinationNode);} else if (FlowAction.END_PROCESS.equals(flowAction)) {// 新增:处理流程终止逻辑log.info("用户终止流程,taskId={}, instId={}, jcdbh={}", taskId, instId, jcdbh);cleanFlowConfig.put("flowStatus", FlowStatus.CLOSE.getName());cleanFlowConfig.put("endReason", flowConfig.getString("remark") != null ?flowConfig.getString("remark") : "用户主动终止流程");cleanFlowConfig.put("isForce", true); // 强制终止标识}// 10. 调用工作流处理方法JSONObject completeResp;try {log.info("调用工作流操作,cleanFlowConfig: {}", cleanFlowConfig);if (FlowAction.REJECT.equals(flowAction)) {String destinationNode = cleanFlowConfig.getString("destination");completeResp = eipBpmManager.transportAnyNode(taskId, destinationNode);} else if (FlowAction.END_PROCESS.equals(flowAction)) {// 新增:调用流程终止方法completeResp = eipBpmManager.doEndProcess(cleanFlowConfig);} else {completeResp = eipBpmManager.complete(cleanFlowConfig);}log.info("工作流操作成功,返回结果: {}", completeResp);// 11. 更新业务状态if (Boolean.TRUE.equals(completeResp.getBoolean("state"))) {updateBusinessStatus(jcdbh, currentTaskKey, flowAction, cCxDto);}} catch (Exception e) {log.error("调用工作流操作失败,cleanFlowConfig: {}", cleanFlowConfig, e);throw new ServiceException(GeneralServiceExceptionResult.GENERAL_UPDATE_FAILED.createExceptionResult(), null);}// 12. 组装返回结果JSONObject result = new JSONObject();result.putAll(completeResp);result.put("businessBh", jcdbh);result.put("taskId", taskId);result.put("instId", instId);result.put("success", true);result.put("flowStatus", cleanFlowConfig.getString("flowStatus"));return result;
}
@Override
public boolean checkAllOver(String jcdbh) {List<CdmYJcdDhryDto> cdmYJcdDhryDtos = cdmYJcdDhryService.listByJcdbh(jcdbh);for (CdmYJcdDhryDto cdmYJcdDhryDto : cdmYJcdDhryDtos) {if (cdmYJcdDhryDto.getIsDxqx() == 1) {String zrcs = cdmYJcdDhryDto.getZrcs();String uid = cdmYJcdDhryDto.getUserId();CdmYFxyjyfgDto cdmYFxyjyfgDto = new CdmYFxyjyfgDto();cdmYFxyjyfgDto.setJcdbh(jcdbh);cdmYFxyjyfgDto.setCs(zrcs);CdmYFxyjyfgDto fxyjyfgDto = cdmYFxyjyfgMapper.selectOne(cdmYFxyjyfgDto);if (fxyjyfgDto == null) {return false;}CdmYYyfxListDto yyfxyjyDto = new CdmYYyfxListDto();yyfxyjyDto.setJcdbh(jcdbh);yyfxyjyDto.setUserId(uid);CdmYYyfxListDto yyfxyjy = cdmYYyfxListMapper.selectOne(yyfxyjyDto);if (yyfxyjy == null || !yyfxyjy.getZt().equals("已反馈")) {return false;}}}return true;
}@Override
public int updateZtYfk(String jcdbh, String userId) {int affectRows = baseMapper.updateZtYfk(jcdbh, userId);return affectRows;
}@Override
public int updateSftj(String jcdbh, String userId) {int affectRows = baseMapper.updateSftj(jcdbh, userId);return affectRows;
}
修改后的代码
// 9. 根据操作类型处理逻辑if (FlowAction.AGREE.equals(flowAction)) {log.info("用户同意任务,taskId={}, instId={}", taskId, instId);cleanFlowConfig.put("flowStatus", FlowStatus.INPRG.getName());if ("UserTask_0d74x2d".equals(currentTaskKey)) {//cdmYYyfxListService.updateZtYfk(jcdbh, currentUserId);cdmYYyfxListService.updateZtYfkWithIndependentTx(jcdbh, currentUserId);cdmYFxyjyfgService.updateSftjWithIndependentTx(jcdbh, currentUserId);// 校验所有人员完成反馈boolean allCompleted = commonService.checkAllOver(jcdbh);if (!allCompleted) {throw new ServiceException(GeneralServiceExceptionResult.GENERAL_VERIFICATION_FAILED.getCode(),"存在未完成反馈的人员,无法进入下一节点");}} else if ("UserTask_1txknja".equals(currentTaskKey)) {cleanFlowConfig.put("flowStatus", FlowStatus.CLOSE.getName());
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public int updateZtYfkWithIndependentTx(String jcdbh, String userId) {int affectRows = baseMapper.updateZtYfk(jcdbh, userId);log.info("执行独立事务更新ztYfk,jcdbh={}, userId={},影响行数={}", jcdbh, userId, affectRows);// 独立事务会自动提交,即使外层事务回滚,这里的更新也不会被撤销return affectRows;
}@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public int updateSftjWithIndependentTx(String jcdbh, String userId) {int affectRows = baseMapper.updateSftj(jcdbh, userId);log.info("执行独立事务更新ztYfk,jcdbh={}, userId={},影响行数={}", jcdbh, userId, affectRows);// 独立事务会自动提交,即使外层事务回滚,这里的更新也不会被撤销return affectRows;
}
分析
问题原因分析
原代码中出现"事务回滚导致状态更新失效"的核心原因如下:
- 事务边界问题:原方法
callFlowCompleteTask
被@Transactional
注解修饰,整个方法处于一个统一的事务中。当执行到UserTask_0d74x2d
节点时,先调用updateZtYfk
和updateSftj
更新当前用户状态,随后调用checkAllOver
校验所有人的状态。 - 回滚连锁反应:若
checkAllOver
校验失败(存在未完成反馈的人员),会抛出ServiceException
,触发整个外层事务回滚。此时,之前执行的updateZtYfk
和updateSftj
对当前用户状态的更新也会被一并回滚,导致"当前用户已操作但状态未保存"的矛盾。 - 业务逻辑冲突:业务需求是"先更新当前人状态,再检查所有状态(包含当前人)“,但原事务机制导致"校验失败时连当前人状态也被撤销”,与实际业务期望(即使校验失败,当前人的操作也应被记录)相悖。
解决方案原理
修改后的代码通过调整事务传播行为解决了上述问题,核心逻辑如下:
- 独立事务拆分:将
updateZtYfk
和updateSftj
方法改为updateZtYfkWithIndependentTx
和updateSftjWithIndependentTx
,并添加注解@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
。 - 事务传播行为作用:
Propagation.REQUIRES_NEW
表示"创建新事务,若当前存在事务则挂起"。这意味着:- 两个状态更新操作会在独立于外层事务的新事务中执行
- 状态更新完成后会立即提交,不受外层事务后续是否回滚的影响
- 即使后续
checkAllOver
校验失败导致外层事务回滚,已提交的当前人状态更新也不会被撤销
- 业务逻辑对齐:既保证了"先更新当前人状态,再校验所有状态"的执行顺序,又确保了"当前人操作记录不丢失",符合业务实际期望。
开发经验总结
- 事务边界设计需匹配业务语义:
- 当某些操作需要"即使后续流程失败也必须保留结果"时,需将其剥离为独立事务
- 例:用户操作记录、状态变更日志等核心轨迹数据,通常需要独立事务保障持久性
- 合理使用事务传播行为:
Propagation.REQUIRES_NEW
适用于"需要与主事务隔离的关键操作",如状态更新、日志记录等- 注意:过度使用独立事务可能导致数据一致性风险(如主事务回滚但独立事务已提交),需在"持久性"和"一致性"间权衡
- 异常处理与事务的联动关系:
- 外层事务中抛出的未捕获异常会导致整个事务回滚,需明确哪些操作允许被回滚,哪些不允许
- 关键操作的异常应在独立事务内部处理,避免影响主流程
- 业务校验的位置设计:
- 涉及"多人协作状态校验"时,应先确保当前操作人的状态已持久化,再进行全局校验
- 校验失败时,应仅阻止后续流程,而非撤销已完成的操作记录
通过本次调整,既解决了事务回滚导致的状态丢失问题,也明确了"操作记录"与"流程控制"的事务边界,为类似业务场景提供了可复用的设计思路。
又出现的问题[事务隔离级别与数据可见性冲突]
此时,checkAllOver
方法还没有加上@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
。又会出现[事务隔离级别与数据可见性冲突]的问题。
分析
问题分析:为何checkAllOver
需要添加独立事务注解
潜在问题场景
在checkAllOver
未添加@Transactional(propagation = Propagation.REQUIRES_NEW)
时,出现的问题是:校验结果与实际数据不一致 即:明明已更新当前用户状态,却仍提示"存在未完成反馈的人员")。
根本原因:事务隔离级别与数据可见性冲突
- 事务上下文关系未添加独立事务注解时,
checkAllOver
默认运行在外层事务(****callFlowCompleteTask
**的事务) 中。而updateZtYfkWithIndependentTx
和updateSftjWithIndependentTx
是独立事务(REQUIRES_NEW
)** ,这两个独立事务会先于checkAllOver
执行并提交。 - 数据可见性问题数据库默认隔离级别通常为
REPEATABLE READ
(MySQL默认隔离级别)(可重复读),在此级别下:- 外层事务启动后会创建一个一致性读视图 (事务开始时的数据快照)。
- 独立事务提交的新数据(当前用户的状态更新),不会被外层事务的后续查询看到 (外层事务始终基于初始快照读取数据)。
- 因此,
checkAllOver
在外侧事务中执行时,会读取到"更新前的旧数据",导致误判"当前用户未完成反馈",即使独立事务已实际更新了状态。
- 因此,
外层事务(callFlowCompleteTask)启动 → 创建一致性读视图
→ 调用updateZtYfkWithIndependentTx:挂起外层事务→新事务执行→提交(更新数据)
→ 调用updateSftjWithIndependentTx:同上(独立提交)
→ 调用checkAllOver(原逻辑):回到外层事务→基于初始快照读数据→误判未更新
解决方案:为checkAllOver
添加独立事务
当checkAllOver
添加@Transactional(propagation = Propagation.REQUIRES_NEW)
后:
checkAllOver
会在新的独立事务 中执行(挂起外层事务)。- 新事务启动时,会基于最新的数据状态 创建读视图,能够正常读取到之前两个独立事务提交的"当前用户已更新的状态"。
- 校验逻辑可以获取真实的全局状态,避免因数据不可见导致的误判。
总结:事务传播与数据可见性的关键经验
- **跨事务数据读取需注意隔离级别,**当需要读取"其他独立事务已提交的数据"时,若当前事务隔离级别为
REPEATABLE READ
或更高,必须通过REQUIRES_NEW
开启新事务,否则可能读取到旧数据。 - 校验逻辑的事务边界设计涉及"多事务更新后的数据校验"时,校验方法应与更新方法保持相同的事务独立性,确保读取到最新的全局状态。
- 事务传播行为的组合使用
REQUIRES_NEW
用于"必须立即提交且被其他事务可见的操作"(如状态更新)。- 依赖这些操作结果的后续逻辑(如校验),也需用
REQUIRES_NEW
确保数据可见性,形成完整的事务链。
通过为checkAllOver
添加独立事务注解,最终解决了"更新后的数据无法被校验逻辑读取"的问题,确保了业务流程的正确性。
最终代码
在checkAllOver
上也加上@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public boolean checkAllOver(String jcdbh) {List<CdmYJcdDhryDto> cdmYJcdDhryDtos = cdmYJcdDhryService.listByJcdbh(jcdbh);for (CdmYJcdDhryDto cdmYJcdDhryDto : cdmYJcdDhryDtos) {if (cdmYJcdDhryDto.getIsDxqx() == 1) {String zrcs = cdmYJcdDhryDto.getZrcs();String uid = cdmYJcdDhryDto.getUserId();CdmYFxyjyfgDto cdmYFxyjyfgDto = new CdmYFxyjyfgDto();cdmYFxyjyfgDto.setJcdbh(jcdbh);cdmYFxyjyfgDto.setCs(zrcs);CdmYFxyjyfgDto fxyjyfgDto = cdmYFxyjyfgMapper.selectOne(cdmYFxyjyfgDto);if (fxyjyfgDto == null) {return false;}CdmYYyfxListDto yyfxyjyDto = new CdmYYyfxListDto();yyfxyjyDto.setJcdbh(jcdbh);yyfxyjyDto.setUserId(uid);CdmYYyfxListDto yyfxyjy = cdmYYyfxListMapper.selectOne(yyfxyjyDto);if (yyfxyjy == null || !yyfxyjy.getZt().equals("已反馈")) {return false;}}}return true;
}
Spring事务的传播行为
- REQUIRED:如果当前存在事务,则加入该事务,如果当前没有事务,则创建一个新的事务。这是最常用的传播行 为,也是默认的,适用于大多数情况。
- REQUIRES_NEW:无论当前是否存在事务,都创建一个新的事务。如果当前存在事务,则将当前事务挂起。适用于 需要独立事务执行的场景,不受外部事务的影响。
- SUPPORTS:如果当前存在事务,则加入该事务,如果当前没有事务,则以非事务方式执行。适用于不需要强制事务 的场景,可以与其他事务方法共享事务。
- NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则将当前事务挂起。适用于不需要事务支持的场景, 可以在方法执行期间暂时禁用事务。
- MANDATORY:如果当前存在事务,则加入该事务,如果当前没有事务,则抛出异常。适用于必须在事务中执行的 场景,如果没有事务则会抛出异常。
- NESTED:如果当前存在事务,则在嵌套事务中执行,如果当前没有事务,则创建一个新的事务。嵌套事务是外部事 务的一部分,可以独立提交或回滚。适用于需要在嵌套事务中执行的场景。
- NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。适用于不允许在事务中执行的场景,如果存在事务则 会抛出异常。 通过
@Transactional注解的propagation属性来指定事务传播行为。
Spring事务的隔离级别
Spring的事务隔离级别是指在并发环境下,事务之间相互隔离的程度。
Spring框架支持多种事务隔离级别,可以根据具体的业务需求来选择适合的隔离级别。以下是 常见的事务隔离级别:
- DEFAULT(默认):使用数据库默认的事务隔离级别。通常为数据库的默认隔离级别,如Oracle为READCOMMITTED,MySQL为REPEATABLEREAD。
- READ_UNCOMMITTED:最低的隔离级别,允许读取未提交的数据。事务可以读取其他事务未提交的数据,可能会导致脏读、不可重复读和幻读的问题。
- READ_COMMITTED:保证一个事务只能读取到已提交的数据。事务读取的数据是其他事务已经提交的数据,避免了脏读的问题。但可能会出现不可重复读和幻 读的问题。
- REPEATABLE_READ:保证一个事务在同一个查询中多次读取的数据是一致的。事务期间,其他事务对数据的修改不可见,避免了脏读和不可重复读的问题。但 可能会出现幻读的问题。
- SERIALIZABLE:最高的隔离级别,保证事务串行执行,避免了脏读、不可重复读和幻读的问题。但会降低并发性能,因为事务需要串行执行。
通过@Transactional注解的isolation属性来指定事务隔离级别。
@Transactional(isolation = Isolation.READ_COMMITTED)
public void method1(){
//...
}
@Transactional(isolation = Isolation.REPEATABLE_READ)
public void method2(){
//...
}
需要根据具体的业务需求和并发情况来选择合适的事务隔离级别,以确保事务的隔离性和数据一致性。同时,需要注意不同数据库对事务隔离级别的支持可能有所差 异,需要进行适当的测试和验证。