国企“高级”程序员写的那些问题代码(六期)

前言

我是[提前退休的java猿],一名7年java开发经验的开发组长,分享工作中的各种问题!

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

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

"高级程序员"的代码

一、分布式锁运用

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

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

原代码

controller 层伪代码如下:

java 复制代码
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岂不是把其他请求的锁给解开了

service 层代码如下:

java 复制代码
@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. 本身库存扣减的时候,多加一个条件(加一个剩余名额大于1的判断 )

优化后的代码

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

java 复制代码
// 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层。扣减优化,防止锁失效命令报超的情况。

java 复制代码
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);
                    // 增加剩余名额大于1的判断:
                   Boolean suc = service.lambdaUpdate().eq(Activity::getId, req.getActivityId()).
                    lt(LbWorkersActivity::getCouldEnrollNum,1).
      set(Activity::getCouldEnrollNum, activity.getCouldEnrollNum() -1).update();
                  // 名额扣减失败
                  if(!suc){
                      thorw new RuntimeException("xxxx");
                  }
                } catch (Exception e) {
                    // 手动标记事务回滚(可选,默认异常会触发回滚)
                    status.setRollbackOnly();
                    throw new RuntimeException("报名失败", e);
                }
            }
        });

      
    return Response.OK();
}

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

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

原代码

消费者

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

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

处理业务逻辑

java 复制代码
public void actionSendTask(Long id) {
  Task task = getById(id);
  //1.校验状态
  checkTask();
  //2.查询手机号
  List<String> 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

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<String> phones) {
      //发送 短信、语音.........
      smsStrategy.sendSms(task,phones);
      //后置处理
      smsStrategy.sendAfterHandle(task);
    return true;
  }
........................................
}

线程安全问题如下图:

📕上面这块代码确实问题也挺多的,结合实际业务场景,按照严重级别排序的话,应该是线程安全<----异常处理<------消息幂等

优化后的代码

消息消费:除了以下的修改,还绑定了死信队列(出现了异常直接丢入到死信队列)。

java 复制代码
public void doSmsTask(Message message, Channel channel ) {
    String id = new String(message.getBody());
    String redisKey = MESSAGE_PREFIX + messageId;
    // 检查Redis中是否存在该消息ID
    Boolean suc = redisTemplate.opsForValue().setIfAbsent(redisKey, "processed", 
        EXPIRATION_TIME, TimeUnit.SECONDS);
    // 没有被消费过
    if(suc){
        service.actionSendTask(id);
    }
    
}

处理业务逻辑: 存在多个策略,各策略之间的执行互不影响,且执行策略的方法为线程安全.同时处理异常情况

java 复制代码
public void actionSendTask(Long id) {
  Task task = getById(id);
  //1.校验状态
  checkTask();
  //2.查询手机号
  List<String> phones = queryPhones();
  //3.获取任务发送的类型,根据类型执行对应的策略
  String[] types = getTaskType().split(COMMA_EN);
  // 遍历任务 类型,根据类型执行对应的策略
  for (String type : types) {
      try {
          smsStrategyContext.sendByType(task, phones,type);
      } catch (Exception e) {
          log.error("策略异常:",e);
      }
  }
}

StrategyContext.java: 调整成线程安全的类,加锁或者使用ThreadLocal进行策略接口的存储,下面就改成简单的方式把,用锁的方式

java 复制代码
public class StrategyContext {
  private ISmsStrategy smsStrategy;
  @Resouce
  private StgFactory stgFactory;

  private void setSmsStrategy(ISmsStrategy smsStrategy) {
    this.smsStrategy = smsStrategy;
  }

  /**
   * 发送信息: 加锁,设置策略,执行策略
   *
   * @param task
   * @param phones
   */
  public synchronized Boolean sendByType(Task task, List<String> phones,Integer type) throws Exception{
      //设置策略
      setSmsStrategy(StgFactory.getStrategy(type));
      //发送 短信、语音.........
      smsStrategy.sendSms(task,phones);
      //后置处理
      smsStrategy.sendAfterHandle(task);
    return true;
  }
........................................
}

三、一些细节和规范问题

多此一举的分布式锁

本身这个定时任务用的是Quartz定时任务,配置中也是开启了集群模式的,有些同事还是自己去实现了分布式锁,多此一举了哈🤪

java 复制代码
@Slf4j
public class DeptSyncJob implements Job {
  
    @Resource
    private RedissonClient redissonClient;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        RLock lock = redissonClient.getLock(RedisKeyConstant.DEPT_SYNC_JOB);
        if (lock.tryLock()) {
            .......................
        }
    }

魔法变量、select*

数据量大的情况,mybatis plus 查询不指定查询字段,默认查询所有 还是比较吃性能。

java 复制代码
// 魔法变量
if (activity.getPushStatus()==1) {...}
if(activity.getJoinCondition()==2){...}
// mybatis plus 没有指定查询字段, 等同与 select * 
List<ActivityEnroll>activityEnrolls = this.lambdaQuery().eq(ActivityEnroll::getId, req.getId())
        .eq(ActivityEnroll::getUserId, req.getUserId())
        .eq(ActivityEnroll::getIsDel, false).list();

"高级程序员"和 "中级程序员"的区别

基于我司的情况说一下个人主观的感受吧,技术上表现最大的差别就是 设计思路 和 代码规范(可读性)上吧。综合能力上就是 工时的评估、风险的预判。
中级程序员:

经常设计的表有点死板了,不能很多的结合业务场景去设计表,经常会出现表设计冗余,思路有点绕。以及代码的可读性,可维护性确实还是有明显差别

  1. 明明一个表可以实现的,非得拆成多个,同时还喜欢在SQL放很多case when 等逻辑;
  2. 页面配置列表居然设计出来没有ID,编辑一行数据 需要把整个列表全部提交全部覆盖;
  3. 一个方法里面需要调用第三方的多个HTTP请求,异常也没处理,中间步骤第三方可能需要等待一段时间才会查询到结果,直接就对线程sleep一分钟🤪。 (请求多一点服务就炸,还会引起脏数据)

高级程序员:

说一下我们公司的"高级程序员"吧😁

他们参与整个项目的周期上,从需求评估------>工时评估------>设计、编码 这个过程没有啥大问题,都能独立的负责项目。

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

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

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

总结

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

往期好文推荐:
事务报错,为何数据还是插入成功了
同事的问题代码(第五期)
同事的问题代码(第四期)

相关推荐
oak隔壁找我13 小时前
SpringBoot 将项目打包成 Fat JAR(肥包),核心原理
后端
言慢行善14 小时前
sqlserver模糊查询问题
java·数据库·sqlserver
专吃海绵宝宝菠萝屋的派大星14 小时前
使用Dify对接自己开发的mcp
java·服务器·前端
大数据新鸟14 小时前
操作系统之虚拟内存
java·服务器·网络
Tong Z14 小时前
常见的限流算法和实现原理
java·开发语言
凭君语未可14 小时前
Java 中的实现类是什么
java·开发语言
He少年14 小时前
【基础知识、Skill、Rules和MCP案例介绍】
java·前端·python
克里斯蒂亚诺更新15 小时前
myeclipse的pojie
java·ide·myeclipse
迷藏49415 小时前
**eBPF实战进阶:从零构建网络流量监控与过滤系统**在现代云原生架构中,**网络可观测性**和**安全隔离**已成为
java·网络·python·云原生·架构