高级程序员也会犯这些低级错误?看看你中招了几个

作者:佚名 时间:2025-11-17 15:53

字号

前言

我是

提前退休的java猿

,一名7年java开发经验的开发组长,分享工作中的各种问题!

我们公司都是只招 “高级java开发”,每个开发必须具备独立主导项目研发的能力,需要需求评审、技术调研、技术方案选型,开发工时评估。

今天就来看看我们写的代码都会犯些什么错,如果你是一名初中级开发,那么这篇文章请你一定看完。最后还对我们公司中、高级开发的能力做了主观的评价

“高级程序员”的代码

一、分布式锁运用

简单描述一下业务场景,就是活动报名。主要的逻辑就是判断用户是否报名,没有报名就插入报名记录,并且报名的剩余名额减一。

逻辑和秒杀的逻辑看起来还差不多呢,只是我们这个业务没有什么并发量。看以下代码吧,看看你能找出几个问题。

原代码

controller 层伪代码如下:

public Response cancelActivityEnroll(@RequestBody Param req) {
    RLock lock = redissonClient.getLock(RedisKeyConstant.ACTIVITY_ENROLL_KEY + req.getActivityId());
    try {
        lock.lock();
        service.cancelActivityEnroll(req);
        return Response.ok();
    } finally {
        lock.unlock();
    }
}

点评

  1. 建议直接锁用户吧,Key改成活动ID+userID,这样能提高并发,提升用户体验
  2. 不能立即获取到锁,直接返回
  3. 假设lock方法报错,没有获取锁,finally岂不是把其他请求的锁给解开了(当然RedissonLock解锁的时候会自动判断是否当前线程持有,其他实现的就要注意了)

public void unlock() {
   try {
       get(unlockAsync(Thread.currentThread().getId()));
   } catch (RedisException e) {
       ........
   }

service 层代码如下:

@Transactional
public Response activityEnroll(WorkersActivityEnrollCreateReq req) {
    LoginUser user = UserUtil.getUser();
    Activity activity = service.getById(req.getActivityId());
    //1.校验活动信息:是否有剩余报名名额,活动状态是否正常.............................
    Boolean checkRet = checkActivity();
    if(!checkRet){
        Response.error("活动已结束....");
    }
    //2.查询用户是否存在报名信息,含HTTP请求
    Boolean enroll = getUserEnroll(user.getId,req.getActivityId);
    if(enroll){
        Response.error("已经报过名了.....");
    }
    //3.插入报名信息
     baseMapper.insert(enrollEntity);
    //4.活动剩余名额 -1 
    service.lambdaUpdate().eq(Activity::getId, req.getActivityId()).
      set(Activity::getCouldEnrollNum, activity.getCouldEnrollNum() -1).update();
    return Result.OK();
}

点评

  1. 事务的范围可以缩小,HTTP请求不要放到事务内
  2. 名额扣减得时候可以使用"update t set num =num-1 where id =? and num>1 "形式防止名额报超(update by Id 行锁比分布式锁更加可靠)

优化后的代码

controller: 锁用户,防止用户重复点击;锁优化防止极端情况解锁到其他线程的持有锁;

// 1.调整为锁用户
RLock lock = redissonClient.getLock(RedisKeyConstant.ACTIVITY_ENROLL_KEY + req.getActivityId() + userID);
try {
    //获取锁,设置释放锁时间; 或者不指定释放时间,启动看门狗机制(默认最长续期30s)
    Boolean lockRet = lock.tryLock(0,10, TimeUnit.SECONDS);
    // 获取成功,执行业务,失败则直接返回
    return lockRet ? lbWorkersActivityEnrollService.activityEnroll(req) :  
    Result.error("网络繁忙");
} catch (InterruptedException e) {
    log.error("lock fail",e);
    return Result.error("网络繁忙");
} finally {
    // 解锁的时候,判断是否是当前线程持有
    if(lock.isHeldByCurrentThread()){
        lock.unlock();
    }
}

service: 缩小事务范围,用编程式事务或者把前置校验单独提出来,写到controller层。扣减优化,防止锁失效命令报超的情况。

public Response activityEnroll(WorkersActivityEnrollCreateReq req) {
    LoginUser user = UserUtil.getUser();
    //1.校验活动信息、用户信息:是否有剩余报名名额,活动状态是否正常.............................
    checkMethod()
    //2.校验成功、插入数据、名额扣减
    //通过execute方法执行事务逻辑
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                try {
                   baseMapper.insert(enrollEntity);
                    // 调整更新语句SQL,防止名额报超:update t set num =num-1 where id =? and num>1
                   Boolean suc = service.updateNumById();
                  // 名额扣减失败
                  if(!suc){
                      thorw new RuntimeException("xxxx");
                  }
                } catch (Exception e) {
                    // 手动标记事务回滚(可选,默认异常会触发回滚)
                    status.setRollbackOnly();
                    throw new RuntimeException("报名失败", e);
                }
            }
        });
      
    return Response.OK();
}

二、定时发送短信、语音...

简单描述一下业务场景,用户需要在后台可以批量选择用户,同时可以多选发送的方式比如 短信+语音。然后设计的思路是,RabbitMQ延迟队列实现定时触发,策略模式来实现业务逻辑。看一下代码吧,看看你能发现这些问题么

原代码

消费者

@RabbitListener(queues = RabbitMqConstant.SMS_DELAY_QUEUE)
public void doSmsTask(Message message, Channel channel ) {
    String id = new String(message.getBody());
    //执行发送业务逻辑
    service.actionSendTask(id);
}

点评: 没有捕捉异常(没有绑定死信),没有做幂等;异常之后会出现重复消费消息

处理业务逻辑

public void actionSendTask(Long id) {
  Task task = getById(id);
  //1.校验状态
  checkTask();
  //2.查询手机号
  List phones = queryPhones();
  //3.获取任务发送的类型,根据类型执行对应的策略
  String[] types = getTaskType().split(COMMA_EN);
  // 遍历任务 类型,根据类型执行对应的策略
  for (String type : types) {
   // 注意这个地方 获取到策略类 之后,设置到context的属性上
    strategyContext.setSmsStrategy(StgFactory.getStrategy(Integer.valueOf(type)));
    // 执行发送业务逻辑(底层封装http调用三方接口)
    smsStrategyContext.send(task, phones);
  }
}

点评:

  1. 异常处理:for循环里面遍历策略,可能中途的某个策略会报错。要么做好异常捕捉处理,要么丢尽线程池执行
  2. 线程安全:这个把具体的策略实现 设置到smsStrategyContext的属性上存在线程安全问题。

    (比如我多个线程同时调用setSendSmsStrategy,执行完成之后多个线程 在调用send的时候,执行的策略就是最后生效的那一个了)

StrategyContext.java

public class StrategyContext {
  private ISmsStrategy smsStrategy;
  public void setSmsStrategy(ISmsStrategy smsStrategy) {
    this.smsStrategy = smsStrategy;
  }
  /**
   * 发送信息
   *
   * @param task
   * @param phones
   */
  public Boolean send(Task task, List phones) {
      //发送 短信、语音.........
      smsStrategy.sendSms(task,phones);
      //后置处理
      smsStrategy.sendAfterHandle(task);
    return true;
  }
........................................
}

线程安全问题如下图:

bf9d314c4c5dd35e9582a4fcd823d77.png

上面这块代码确实问题也挺多的,结合实际业务场景,按照严重级别排序的话,应该是线程安全设计、编码 这个过程没有啥大问题,都能独立的负责项目。

从技术上来说的话,基本的规范,代码的扩展性、可读性、表设计都没啥问题。但是在一些常用技术的原理理解上还是比较缺乏的。

  1. 比如上面说的分布式锁的运用,到底是锁库存还是锁用户。不会利用数据锁,去实现超卖问题
  2. 解决问题的能很多还是停留在 search 阶段,有明显的报错的,但是网上没有解决方案的,就不知道通过debug源码的方式去解决
  3. 比如 一个left join 查询缓慢,但是数据都不多,不知道去分析执行计划,不了解left join 的底层算法

当然,技术的知识点很多,有些不知道 不了解的很正常。但是常用技术原理上,以及解决疑难杂症问题的能力还有待提高。我们公司优秀的高级开发也有,编码速度非常快、bug还很少、代码也非常的规范(还是名校)

总结

分享了最近公司同事,出现的一些典型的代码问题。同时在文章末尾也对身边的高级开发 和 中级开发的能力做了一个主观的评价。希望这篇文章能帮到你,感谢点赞评论的朋友!

往期好文推荐:

事务报错,为何数据还是插入成功了

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

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

责任编辑:CQITer新闻报料:400-888-8888   本站原创,未经授权不得转载
继续阅读
热新闻
推荐
关于我们联系我们免责声明隐私政策 友情链接