前言
我是[提前退休的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();
}
}
点评:
- 建议直接锁用户吧,Key改成活动
ID+userID
,这样能提高并发,提升用户体验- 不能立即获取到锁,直接返回
- 假设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();
}
点评:
- 事务的范围可以缩小,HTTP请求不要放到事务内
- 本身库存扣减的时候,多加一个条件(加一个剩余名额大于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);
}
}
点评:
- 异常处理:for循环里面遍历策略,可能中途的某个策略会报错。要么做好异常捕捉处理,要么丢尽线程池执行
- 线程安全:这个把具体的策略实现 设置到
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();
"高级程序员"和 "中级程序员"的区别
基于我司的情况说一下个人主观的感受吧,技术上表现最大的差别就是 设计思路 和 代码规范(可读性)上吧。综合能力上就是 工时的评估、风险的预判。
中级程序员:
经常设计的表有点死板了,不能很多的结合业务场景去设计表,经常会出现表设计冗余,思路有点绕。以及代码的可读性,可维护性确实还是有明显差别
- 明明一个表可以实现的,非得拆成多个,同时还喜欢在SQL放很多case when 等逻辑;
- 页面配置列表居然设计出来没有ID,编辑一行数据 需要把整个列表全部提交全部覆盖;
- 一个方法里面需要调用第三方的多个HTTP请求,异常也没处理,中间步骤第三方可能需要等待一段时间才会查询到结果,直接就对线程sleep一分钟🤪。 (请求多一点服务就炸,还会引起脏数据)
高级程序员:
说一下我们公司的"高级程序员"吧😁
他们参与整个项目的周期上,从需求评估------>工时评估------>设计、编码 这个过程没有啥大问题,都能独立的负责项目。
从技术上来说的话,基本的规范,代码的扩展性、可读性、表设计都没啥问题。但是在一些常用技术的原理理解上还是比较缺乏的。
- 比如上面说的分布式锁的运用,到底是锁库存还是锁用户。不会利用数据锁,去实现超卖问题
- 解决问题的能很多还是停留在
search
阶段,有明显的报错的,但是网上没有解决方案的,就不知道通过debug
源码的方式去解决- 比如 一个left join 查询缓慢,但是数据都不多,不知道去分析执行计划,不了解left join 的底层算法
🤞当然,技术的知识点很多,有些不知道 不了解的很正常。但是常用技术原理上,以及解决疑难杂症问题的能力还有待提高。我们公司优秀的高级开发也有,编码速度非常快、bug还很少、代码也非常的规范(还是名校🙈)
总结
分享了最近公司同事,出现的一些典型的代码问题。同时在文章末尾也对身边的高级开发 和 中级开发的能力做了一个主观的评价。希望这篇文章能帮到你,感谢点赞评论的朋友!
往期好文推荐:
事务报错,为何数据还是插入成功了
同事的问题代码(第五期)
同事的问题代码(第四期)