前言
我是[提前退休的java猿],一名7年java开发经验的开发组长,分享工作中的各种问题!
👴我们公司都是只招 “高级java开发”,每个开发必须具备独立主导项目研发的能力,需要需求评审、技术调研、技术方案选型,开发工时评估。
😁今天就来看看我们写的代码都会犯些什么错,如果你是一名初中级开发,那么这篇文章请你一定看完。最后还对我们公司中、高级开发的能力做了主观的评价
“高级程序员”的代码
一、分布式锁运用
简单描述一下业务场景,就是活动报名。主要的逻辑就是判断用户是否报名,没有报名就插入报名记录,并且报名的剩余名额减一。
逻辑和秒杀的逻辑看起来还差不多呢😁,只是我们这个业务没有什么并发量。看以下代码吧,看看你能找出几个问题。
原代码
controller 层伪代码如下:
java
体验AI代码助手
代码解读
复制代码
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(); } }
点评:
- 建议直接锁用户吧,Key改成活动
ID+userID,这样能提高并发,提升用户体验- 不能立即获取到锁,直接返回
- 假设lock方法报错,没有获取锁,finally岂不是把其他请求的锁给解开了(当然
RedissonLock解锁的时候会自动判断是否当前线程持有,其他实现的就要注意了)java
体验AI代码助手
代码解读
复制代码
public void unlock() { try { get(unlockAsync(Thread.currentThread().getId())); } catch (RedisException e) { ........ }
service 层代码如下:
java
体验AI代码助手
代码解读
复制代码
@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(); }
点评:
- 事务的范围可以缩小,HTTP请求不要放到事务内
- 名额扣减得时候可以使用"
update t set num =num-1 where id =? and num>1"形式防止名额报超(update by Id 行锁比分布式锁更加可靠)
优化后的代码
controller:锁用户,防止用户重复点击;锁优化防止极端情况解锁到其他线程的持有锁;
java
体验AI代码助手
代码解读
复制代码
// 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
体验AI代码助手
代码解读
复制代码
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延迟队列实现定时触发,策略模式来实现业务逻辑。 看一下代码吧,看看你能发现这些问题么
原代码
消费者:
java
体验AI代码助手
代码解读
复制代码
@RabbitListener(queues = RabbitMqConstant.SMS_DELAY_QUEUE) public void doSmsTask(Message message, Channel channel ) { String id = new String(message.getBody()); //执行发送业务逻辑 service.actionSendTask(id); }
点评: 没有捕捉异常(没有绑定死信),没有做幂等;异常之后会出现重复消费消息
处理业务逻辑:
java
体验AI代码助手
代码解读
复制代码
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); } }
点评:
- 异常处理:for循环里面遍历策略,可能中途的某个策略会报错。要么做好异常捕捉处理,要么丢尽线程池执行
- 线程安全:这个把具体的策略实现 设置到
smsStrategyContext的属性上存在线程安全问题。
(比如我多个线程同时调用setSendSmsStrategy,执行完成之后多个线程 在调用send的时候,执行的策略就是最后生效的那一个了)
StrategyContext.java
java
体验AI代码助手
代码解读
复制代码
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
体验AI代码助手
代码解读
复制代码
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
体验AI代码助手
代码解读
复制代码
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
体验AI代码助手
代码解读
复制代码
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
体验AI代码助手
代码解读
复制代码
@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
体验AI代码助手
代码解读
复制代码
// 魔法变量 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();
“高级程序员”和 “中级程序员”的区别
基于我司的情况说一下个人主观的感受吧,技术上表现最大的差别就是 设计思路 和 代码规范(可读性)上吧。综合能力上就是 工时的评估、风险的预判。
中级程序员:
经常设计的表有点死板了,不能很多的结合业务场景去设计表,经常会出现表设计冗余,思路有点绕。以及代码的可读性,可维护性确实还是有明显差别
- 明明一个表可以实现的,非得拆成多个,同时还喜欢在SQL放很多case when 等逻辑;
- 页面配置列表居然设计出来没有ID,编辑一行数据 需要把整个列表全部提交全部覆盖;
- 一个方法里面需要调用第三方的多个HTTP请求,异常也没处理,中间步骤第三方可能需要等待一段时间才会查询到结果,直接就对线程sleep一分钟🤪。 (请求多一点服务就炸,还会引起脏数据)
高级程序员:
说一下我们公司的“高级程序员”吧😁
他们参与整个项目的周期上,从需求评估——>工时评估——>设计、编码 这个过程没有啥大问题,都能独立的负责项目。
从技术上来说的话,基本的规范,代码的扩展性、可读性、表设计都没啥问题。但是在一些常用技术的原理理解上还是比较缺乏的。
- 比如上面说的分布式锁的运用,到底是锁库存还是锁用户。不会利用数据锁,去实现超卖问题
- 解决问题的能很多还是停留在
search阶段,有明显的报错的,但是网上没有解决方案的,就不知道通过debug源码的方式去解决- 比如 一个left join 查询缓慢,但是数据都不多,不知道去分析执行计划,不了解left join 的底层算法
🤞当然,技术的知识点很多,有些不知道 不了解的很正常。但是常用技术原理上,以及解决疑难杂症问题的能力还有待提高。我们公司优秀的高级开发也有,编码速度非常快、bug还很少、代码也非常的规范(还是名校🙈)
总结
分享了最近公司同事,出现的一些典型的代码问题。同时在文章末尾也对身边的高级开发 和 中级开发的能力做了一个主观的评价。希望这篇文章能帮到你,感谢点赞评论的朋友!