同事的问题代码(第一期)

简介: 本文分享了三个开发中常见的编码问题及其解决方案,涉及乐观锁、锁与事务的使用以及MyBatis一级缓存。通过具体案例分析,展示了5年以上经验的开发者在批量审批、数据导入和数据库查询中遇到的问题,并提供了改进建议。适合初中级程序员学习参考。

ps 推荐阅读:
同事的代码问题(第二期)
同事的代码问题(第三期)
同事的代码问题(第四期)

前言

分享几个最近在开发中发现的编码问题,这些问题也不是才入门的同事写的。都是5年以上开发经验的爪哇同事写的。
当然下面这些问题也都不是很难发现的问题。初中级的程序员还是可以仔细看看的(伪代码不用担心泄露公司代码)。

错误案例一(乐观锁的运用)

功能描述:
实现一个批量审批功能,单个审批功能是有现成的方法 approveOne。所以在做批量审批时候,就异步调用approveOne方法,异步执行批量审批的时候优先把批量审批状态更新了(先把数据锁住),打个标记数据数据再次被审批;
代码主要逻辑:
1、先查询出请求参数ids的数据是否都是待审批的状态,不是的话则抛出异常。
2、请求中的数据都是待审的数据,执行前先更新数据状态为审批中,然后异步执行审批业务逻辑

public void batchAprrove(List<Long> ids) {
   
  log.info("开始批量审批数据");
  //查询出待审批的数量,校验是否都是待审批的数据
  Integer waitApproveNum = service.count(APPROVE_STATUS_PRE,ids);
  if(waitApproveNum != ids.size()){
   
    throw new RuntimeException("数据状态有更新,请重新操作");
  }
  // 更新成审批中的状态 
  service.updateStatusByIds(APPROVE_STATUS_ING,ids);
  //循环任务丢到线程池
  for (Long id : ids) {
   
    approveExecutor.execute(()->{
   
      approveOne(id);
    });
  }
}

存在的问题

上面查询校验之后再更新数据状态,有可能查询的时候数据都是没问题的 ,更新时,数据就被其他线程给更新了,这时候再去更新已经没有意义了。

正确用法

1、执行使用数据库乐观锁思想,直接更新这批数据,多加一个待审批状态的条件,并且后台系统并发低,适合用乐观锁。
2、更新之后,更新行数和ids长度对比,如果长度不同则抛异常,让事务回滚。

@Transactional(rollbackFor = Exception.class)
public void batchAprrove(List<Long> ids) {
   
  log.info("开始批量审批数据");
  //查询出待审批的数量,校验是否都是待审批的数据
  // 更新成审批中的状态
  Integer row = service.updateIdsAndStatus(APPROVE_STATUS_ING,ids,APPROVE_STATUS_PRE);
  if(row.intValue() != ids.size()){
   
      throw new RuntimeException("数据状态有更新,请重新操作");
  }
 //循环任务丢到线程池
 for (Long id : ids) {
   
   approveExecutor.execute(()->{
   
     approveOne(id);
   });
 }
}

其实还有个问题没有解决?

approveJoinUnion(Long id) 方法在当前类中,是加了事务注解的。batchApprove方法就算加了事务注解,事务执行approveOne方法的时候,事务也失效了? 只有在异步方法里面加编程式事务了

错误案例二(锁和事务的运用)

功能描述:
主要实现一个批量导入功能,因为导入的时间可能比较长,为了防止重复导入,就在方法里面加了锁。并且加了事务,中途异常让数据回滚。

代码主要逻辑:
1、获取锁
2、获取锁之后读取导入记录,解析导入文件执行数据解析,数据验证,数据分组
3、把正确的数据入库
4、导入成功,更新导入记录为成功状态,失败则把记录更新为失败状态

@Transactional(rollbackFor = Exception.class)
public void importData2(Long importId) {
   
  RLock lock = redissonLockClient.getLock(RedisKeyConstant.MEMBER_IMPORT_LOCK_KEY);

  try {
   
    lock.lock();
    // 读取导入记录ID,获取导入文件地址,解析数据,数据校验,数据分组
    GroupData data = handleData(importId);
    //插入
    batchInsert(data.getNeedInsertData());
    //更新导入记录成功
    updateSuccessImportRecord(importId);
  } catch (Exception e){
   
    //表导入记录失败
    updateFailImportRecord(importId);
  }finally {
   
    if (lock.isHeldByCurrentThread()) {
   
      lock.unlock();
    }
  }
}

存在的问题

  1. 锁失效:方法内部执行到最后,已经解锁了。解锁之后,才开始提交事务。这个时候我又导入,在数据校验过程的时候可能上一批数据事务都没提交完成,校验过程中就查询不到上个事务的数据,导出重复导入等问题
  2. 事务无法回滚,代码里面用try catch,无法捕捉异常

正确用法

  1. 删除@Transactional(rollbackFor = Exception.class)注解,使用编程式事务;这样就解决了上面两个问题

    public void importData2(Long importId) {
         
    RLock lock = redissonLockClient.getLock(RedisKeyConstant.MEMBER_IMPORT_LOCK_KEY);
    
    try {
         
    lock.lock();
    // 读取导入记录ID,获取导入文件地址,解析数据,数据校验,数据分组
    GroupData data = handleData(importId);
    //插入
    transactionTemplate.execute((TransactionCallback<Void>) status -> {
         
      try {
         
        //插入数据
        batchInsert(data.getNeedInsertData());
        //更新导入记录成功
        updateSuccessImportRecord(importId);
        return null;
      } catch (Exception e) {
         
        status.setRollbackOnly();
        throw e;
      }
    });
    
    } catch (Exception e){
         
    //表导入记录失败
    updateFailImportRecord(importId);
    }finally {
         
    if (lock.isHeldByCurrentThread()) {
         
      lock.unlock();
    }
    }
    }
    

错误案例三(mybatis 一级缓存)

代码主要逻辑

  1. 执行starFlow方法,startFlow 调用handFlow 方法(当然实际情况代码十分复杂)
  2. 这两个方法都调用了 service.getChildren方法参数也一样
@Transactional
public void startFlow(Long flowId) {
   
    //获取下级节点
    List<Long> ids = service.getChildren(flowId);
      // 把本级节点也加入到list
    ids.add(2l);
    //代码上省略
   handFlow(flowId);
}

public void handFlow(Long flowId){
   
   //查询下级节点
  List<Long> ids = service.getChildren(flowId);
  //.....代码上省略
}

存在的问题

1.第一次调用方法的时候 service.getChildren(flowId) 将结果集ids修改了,调用了ids.add(21) 。后去方法里面有调用同样的方法,同样的参数,mybatis直接从缓存里面去了。导致执行结果偏预期(同一个事务内)。

正确用法

1.尽量不要修改mapper返回的引用结果吧,不然后面执行同样的sql会直接去缓存查数据(同一个事务内)。

ps:文章【回顾一下一级缓存

有服务器需求的联系我返dian,提供技术支持哦

相关文章
|
8月前
|
运维 Kubernetes 监控
K8S异常诊断之俺的内存呢
本文讲述作者如何解决客户集群中出现的OOM(Out of Memory)和Pod驱逐问题。文章不仅详细记录了问题的发生背景、现象特征,还深入探讨了排查过程中的关键步骤和技术细节。
588 108
K8S异常诊断之俺的内存呢
|
8月前
|
XML Java Maven
防止反编译,保护你的SpringBoot项目
ClassFinal-maven-plugin 是一个用于加密 Java 字节码的工具,能够保护 Spring Boot 项目中的源代码和配置文件不被非法获取或篡改。使用步骤包括:安装并设置 Maven、创建 Maven 项目、将 jar 包作为依赖添加到 pom.xml 文件中、下载并安装 ClassFinal-maven-plugin 插件、配置插件参数(如加密密钥和目标机器 ID),最后通过命令 `mvn clean package classfinal:encrypt` 执行加密。插件通过 JNI 实现编译时混淆和加密,并在运行时动态解密类文件。
497 14
|
8月前
|
XML Java 测试技术
Spring IOC—基于注解配置和管理Bean 万字详解(通俗易懂)
Spring 第三节 IOC——基于注解配置和管理Bean 万字详解!
540 26
|
6月前
|
机器学习/深度学习 人工智能 编解码
快速生成商业级高清图!SimpleAR:复旦联合字节推出图像生成黑科技,5亿参数秒出高清大图
SimpleAR是复旦大学与字节Seed团队联合研发的自回归图像生成模型,仅用5亿参数即可生成1024×1024分辨率的高质量图像,在GenEval等基准测试中表现优异。
214 4
快速生成商业级高清图!SimpleAR:复旦联合字节推出图像生成黑科技,5亿参数秒出高清大图
|
9月前
|
开发工具 git iOS开发
阿里同学都在用的开发环境和工具
本文主要介绍后端开发同学常用的工具以及开发环境搭建。
|
8月前
|
人工智能 Java 测试技术
本地玩转 DeepSeek 和 Qwen 最新开源版本(入门+进阶)
本文将介绍如何基于开源工具部署大模型、构建测试应用、调用大模型能力的完整链路。
1532 135
|
设计模式 Java 关系型数据库
设计模式——工厂模式
工厂模式介绍、静态简单工厂模式、工厂方法模式、抽象工厂模式、JDK 源码分析
设计模式——工厂模式
|
9月前
|
设计模式 Java 开发者
「全网最细 + 实战源码案例」设计模式——适配器模式
适配器模式(Adapter Pattern)是一种结构型设计模式,通过引入适配器类将一个类的接口转换为客户端期望的另一个接口,使原本因接口不兼容而无法协作的类能够协同工作。适配器模式分为类适配器和对象适配器两种,前者通过多重继承实现,后者通过组合方式实现,更常用。该模式适用于遗留系统改造、接口转换和第三方库集成等场景,能提高代码复用性和灵活性,但也可能增加代码复杂性和性能开销。
188 28
|
8月前
|
XML Java 测试技术
Spring AOP—通知类型 和 切入点表达式 万字详解(通俗易懂)
Spring 第五节 AOP——切入点表达式 万字详解!
394 25
|
8月前
|
IDE Java 应用服务中间件
spring boot 启动流程
Spring Boot 启动流程简介: 在使用 Spring Boot 之前,启动 Java Web 应用需要配置 Web 容器(如 Tomcat),并将应用打包放入容器目录。而使用 Spring Boot,只需运行 main() 方法即可启动 Web 应用。Spring Boot 的核心启动方法是 SpringApplication.run(),它负责初始化和启动应用上下文。 主要步骤包括: 1. **应用启动计时**:使用 StopWatch 记录启动时间。 2. **打印 Banner**:显示 Spring Boot 的 LOGO。 3. **创建上下文实例**:通过反射创建
435 5