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

前言

我是[提前退休的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还很少、代码也非常的规范(还是名校🙈)

总结

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

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

相关推荐
啾啾大学习7 分钟前
让我们快速入门DDD
后端·领域驱动设计
JavaArchJourney8 分钟前
LinkedList 源码分析
java
回家路上绕了弯11 分钟前
Spring AOP 详解与实战:从入门到精通
java·spring
老张聊数据集成14 分钟前
数据分析师如何构建自己的底层逻辑?
后端·数据分析
咕噜分发企业签名APP加固彭于晏25 分钟前
市面上有多少智能体平台
前端·后端
掘金一周1 小时前
我开源了一款 Canvas “瑞士军刀”,十几种“特效与工具”开箱即用 | 掘金一周 8.14
前端·人工智能·后端
缉毒英雄祁同伟1 小时前
企业级WEB应用服务器TOMCAT
java·前端·tomcat
村姑飞来了1 小时前
Spring 扩展:动态使某个 @Import 方式导入的 @Configuration 类失效
后端
开心就好20251 小时前
前端性能优化移动端网页滚动卡顿与掉帧问题实战
后端