当前位置: 首页 > news >正文

开发中遇到的关于Spring事务[传播行为和隔离级别]的相关问题的记录

代码和问题描述

问题描述

最开始三个被调用的方法没有@Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = Exception.class)

导致的问题是:

updateZtYfkWithIndependentTxupdateSftjWithIndependentTx的更新对数据库生效了,但是当checkAllOver方法返回false的时候触发了异常,导致了事务回滚。使得当callFlowCompleteTask方法执行完后,两个对数据库更新的操作也回滚失效了。

然后就给updateZtYfkWithIndependentTxupdateSftjWithIndependentTx两个方法加上了@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;
}

分析

问题原因分析

原代码中出现"事务回滚导致状态更新失效"的核心原因如下:

  1. 事务边界问题:原方法callFlowCompleteTask@Transactional注解修饰,整个方法处于一个统一的事务中。当执行到UserTask_0d74x2d节点时,先调用updateZtYfkupdateSftj更新当前用户状态,随后调用checkAllOver校验所有人的状态。
  2. 回滚连锁反应:若checkAllOver校验失败(存在未完成反馈的人员),会抛出ServiceException,触发整个外层事务回滚。此时,之前执行的updateZtYfkupdateSftj对当前用户状态的更新也会被一并回滚,导致"当前用户已操作但状态未保存"的矛盾。
  3. 业务逻辑冲突:业务需求是"先更新当前人状态,再检查所有状态(包含当前人)“,但原事务机制导致"校验失败时连当前人状态也被撤销”,与实际业务期望(即使校验失败,当前人的操作也应被记录)相悖。

解决方案原理

修改后的代码通过调整事务传播行为解决了上述问题,核心逻辑如下:

  1. 独立事务拆分:将updateZtYfkupdateSftj方法改为updateZtYfkWithIndependentTxupdateSftjWithIndependentTx,并添加注解@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
  2. 事务传播行为作用Propagation.REQUIRES_NEW表示"创建新事务,若当前存在事务则挂起"。这意味着:
    1. 两个状态更新操作会在独立于外层事务的新事务中执行
    2. 状态更新完成后会立即提交,不受外层事务后续是否回滚的影响
    3. 即使后续checkAllOver校验失败导致外层事务回滚,已提交的当前人状态更新也不会被撤销
  3. 业务逻辑对齐:既保证了"先更新当前人状态,再校验所有状态"的执行顺序,又确保了"当前人操作记录不丢失",符合业务实际期望。

开发经验总结

  1. 事务边界设计需匹配业务语义
    1. 当某些操作需要"即使后续流程失败也必须保留结果"时,需将其剥离为独立事务
    2. 例:用户操作记录、状态变更日志等核心轨迹数据,通常需要独立事务保障持久性
  2. 合理使用事务传播行为
    1. Propagation.REQUIRES_NEW适用于"需要与主事务隔离的关键操作",如状态更新、日志记录等
    2. 注意:过度使用独立事务可能导致数据一致性风险(如主事务回滚但独立事务已提交),需在"持久性"和"一致性"间权衡
  3. 异常处理与事务的联动关系
    1. 外层事务中抛出的未捕获异常会导致整个事务回滚,需明确哪些操作允许被回滚,哪些不允许
    2. 关键操作的异常应在独立事务内部处理,避免影响主流程
  4. 业务校验的位置设计
    1. 涉及"多人协作状态校验"时,应先确保当前操作人的状态已持久化,再进行全局校验
    2. 校验失败时,应仅阻止后续流程,而非撤销已完成的操作记录

通过本次调整,既解决了事务回滚导致的状态丢失问题,也明确了"操作记录"与"流程控制"的事务边界,为类似业务场景提供了可复用的设计思路。

又出现的问题[事务隔离级别与数据可见性冲突]

此时,checkAllOver方法还没有加上@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)。又会出现[事务隔离级别与数据可见性冲突]的问题。

分析

问题分析:为何checkAllOver需要添加独立事务注解

潜在问题场景

checkAllOver未添加@Transactional(propagation = Propagation.REQUIRES_NEW)时,出现的问题是:校验结果与实际数据不一致 即:明明已更新当前用户状态,却仍提示"存在未完成反馈的人员")。

根本原因:事务隔离级别与数据可见性冲突
  1. 事务上下文关系未添加独立事务注解时,checkAllOver默认运行在外层事务(****callFlowCompleteTask**的事务) 中。而updateZtYfkWithIndependentTxupdateSftjWithIndependentTx独立事务(REQUIRES_NEW)** ,这两个独立事务会先于checkAllOver执行并提交。
  2. 数据可见性问题数据库默认隔离级别通常为REPEATABLE READ(MySQL默认隔离级别)(可重复读),在此级别下:
    1. 外层事务启动后会创建一个一致性读视图 (事务开始时的数据快照)。
    2. 独立事务提交的新数据(当前用户的状态更新),不会被外层事务的后续查询看到 (外层事务始终基于初始快照读取数据)。
      • 因此,checkAllOver在外侧事务中执行时,会读取到"更新前的旧数据",导致误判"当前用户未完成反馈",即使独立事务已实际更新了状态。
外层事务(callFlowCompleteTask)启动 → 创建一致性读视图  
→ 调用updateZtYfkWithIndependentTx:挂起外层事务→新事务执行→提交(更新数据)  
→ 调用updateSftjWithIndependentTx:同上(独立提交)  
→ 调用checkAllOver(原逻辑):回到外层事务→基于初始快照读数据→误判未更新  
解决方案:为checkAllOver添加独立事务

checkAllOver添加@Transactional(propagation = Propagation.REQUIRES_NEW)后:

  1. checkAllOver会在新的独立事务 中执行(挂起外层事务)。
  2. 新事务启动时,会基于最新的数据状态 创建读视图,能够正常读取到之前两个独立事务提交的"当前用户已更新的状态"。
  3. 校验逻辑可以获取真实的全局状态,避免因数据不可见导致的误判。

总结:事务传播与数据可见性的关键经验

  1. **跨事务数据读取需注意隔离级别,**当需要读取"其他独立事务已提交的数据"时,若当前事务隔离级别为REPEATABLE READ或更高,必须通过REQUIRES_NEW开启新事务,否则可能读取到旧数据。
  2. 校验逻辑的事务边界设计涉及"多事务更新后的数据校验"时,校验方法应与更新方法保持相同的事务独立性,确保读取到最新的全局状态。
  3. 事务传播行为的组合使用
    1. REQUIRES_NEW用于"必须立即提交且被其他事务可见的操作"(如状态更新)。
    2. 依赖这些操作结果的后续逻辑(如校验),也需用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事务的传播行为

  1. REQUIRED:如果当前存在事务,则加入该事务,如果当前没有事务,则创建一个新的事务。这是最常用的传播行
为,也是默认的,适用于大多数情况。
  2. REQUIRES_NEW:无论当前是否存在事务,都创建一个新的事务。如果当前存在事务,则将当前事务挂起。适用于
需要独立事务执行的场景,不受外部事务的影响。
  3. SUPPORTS:如果当前存在事务,则加入该事务,如果当前没有事务,则以非事务方式执行。适用于不需要强制事务
的场景,可以与其他事务方法共享事务。
  4. NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则将当前事务挂起。适用于不需要事务支持的场景,
可以在方法执行期间暂时禁用事务。
  5. MANDATORY:如果当前存在事务,则加入该事务,如果当前没有事务,则抛出异常。适用于必须在事务中执行的
场景,如果没有事务则会抛出异常。
  6. NESTED:如果当前存在事务,则在嵌套事务中执行,如果当前没有事务,则创建一个新的事务。嵌套事务是外部事
务的一部分,可以独立提交或回滚。适用于需要在嵌套事务中执行的场景。
  7. NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。适用于不允许在事务中执行的场景,如果存在事务则
会抛出异常。
通过

@Transactional注解的propagation属性来指定事务传播行为。

Spring事务的隔离级别

Spring的事务隔离级别是指在并发环境下,事务之间相互隔离的程度。

Spring框架支持多种事务隔离级别,可以根据具体的业务需求来选择适合的隔离级别。以下是
常见的事务隔离级别:

  1. DEFAULT(默认):使用数据库默认的事务隔离级别。通常为数据库的默认隔离级别,如Oracle为READCOMMITTED,MySQL为REPEATABLEREAD。
  2. READ_UNCOMMITTED:最低的隔离级别,允许读取未提交的数据。事务可以读取其他事务未提交的数据,可能会导致脏读、不可重复读和幻读的问题。
  3. READ_COMMITTED:保证一个事务只能读取到已提交的数据。事务读取的数据是其他事务已经提交的数据,避免了脏读的问题。但可能会出现不可重复读和幻
读的问题。
  4. REPEATABLE_READ:保证一个事务在同一个查询中多次读取的数据是一致的。事务期间,其他事务对数据的修改不可见,避免了脏读和不可重复读的问题。但
可能会出现幻读的问题。
  5. SERIALIZABLE:最高的隔离级别,保证事务串行执行,避免了脏读、不可重复读和幻读的问题。但会降低并发性能,因为事务需要串行执行。

通过@Transactional注解的isolation属性来指定事务隔离级别。

@Transactional(isolation = Isolation.READ_COMMITTED)

public void method1(){
//...
}

@Transactional(isolation = Isolation.REPEATABLE_READ)

public void method2(){
//...
}

需要根据具体的业务需求和并发情况来选择合适的事务隔离级别,以确保事务的隔离性和数据一致性。同时,需要注意不同数据库对事务隔离级别的支持可能有所差
异,需要进行适当的测试和验证。

http://www.dtcms.com/a/474213.html

相关文章:

  • CVE-2019-2729反序列化(unserialize)漏洞学习与分析
  • 一流的句容网站建设自己做的网站找不到了
  • TDengine 数学函数 CEIL 用户手册
  • 石家庄好用的招聘网站做网站网站会被判多久
  • 北京平台网站建设代运营公司介绍
  • AI编程作品:Android 极简秒表应用
  • 网络五子棋对战游戏测试报告
  • html做网站的原则自建站排名
  • 互联网彩票网站开发珠海seo关键词排名
  • springboot095交通事故档案管理系统lgl(源码+部署说明+演示视频+源码介绍+lw)
  • 新郑郑州网站建设铭讯网站建设
  • 在next项目中使用iconfont图标方法
  • 重新定义AI编程协作:深入解析Claude Code多智能体系统架
  • 深入解析如何高效处理PDF?
  • uniapp运行微信小程序uni为什么是undefined
  • 2100AI智能生活(下)
  • 什么是后端开发-常见问题
  • 产品做优化好还是超级网站好WordPress来应力
  • wordpress 慢2017郴州网站seo优化
  • 05_零基础搭建AI智能体开发环境:全网开源资源完全指南
  • UDSONIP学习
  • 照片网站cmswordpress 做问卷
  • 除了crontab,如何实现自动化MySQL备份?
  • 积分器电路(波形转换电路)
  • 免费远程新标杆:UU远程对比ToDesk、向日葵,个人体验更优
  • 做视频网站的服务器深圳福田地址随便来一个
  • Git介绍和使用
  • LeetCode经典算法题解详解
  • Java基于SpringBoot的农场管理系统小程序【附源码、文档说明】
  • 建站系统社区网站建设建站在线建站