那些年,我们见过的 Java 服务端“问题”
多线程使用不正确
多线程最主要目的就是”最大限度地利用 CPU 资源”,可以把串行过程变成并行过程,从而提高了程序的执行效率。
一个慢接口案例
假设在用户登录时,如果是新用户,需要创建用户信息,并发放新用户优惠券。例子代码如下:
// 登录函数(示意写法)
public UserVO login(String phoneNumber, String verifyCode) {
// 检查验证码
if (!checkVerifyCode(phoneNumber, verifyCode)) {
throw new ExampleException("验证码错误");
}
// 检查用户存在
UserDO user = userDAO.getByPhoneNumber(phoneNumber);
if (Objects.nonNull(user)) {
return transUser(user);
}
// 创建新用户
return createNewUser(user);
}
// 创建新用户函数
private UserVO createNewUser(String phoneNumber) {
// 创建新用户
UserDO user = new UserDO();
...
userDAO.insert(user);
// 绑定优惠券
couponService.bindCoupon(user.getId(), CouponType.NEW_USER);
// 返回新用户
return transUser(user);
}
其中,绑定优惠券(bindCoupon)是给用户绑定新用户优惠券,然后再给用户发送推送通知。如果随着优惠券数量越来越多,该函数也会变得越来越慢,执行时间甚至超过1秒,并且没有什么优化空间。现在,登录(login)函数就成了名副其实的慢接口,需要进行接口优化。
采用多线程优化
通过分析发现,绑定优惠券(bindCoupon)函数可以异步执行。首先想到的是采用多线程解决该问题,代码如下:
// 创建新用户函数
private UserVO createNewUser(String phoneNumber) {
// 创建新用户
UserDO user = new UserDO();
...
userDAO.insert(user);
// 绑定优惠券
executorService.execute(()->couponService.bindCoupon(user.getId(), CouponType.NEW_USER));
// 返回新用户
return transUser(user);
}
现在,在新线程中执行绑定优惠券(bindCoupon)函数,使用户登录(login)函数性能得到很大的提升。但是,如果在新线程执行绑定优惠券函数过程中,系统发生重启或崩溃导致线程执行失败,用户将永远获取不到新用户优惠券。除非提供用户手动领取优惠券页面,否则就需要程序员后台手工绑定优惠券。所以,用采用多线程优化慢接口,并不是一个完善的解决方案。
采用消息队列优化
如果要保证绑定优惠券函数执行失败后能够重启执行,可以采用数据库表、Redis 队列、消息队列的等多种解决方案。由于篇幅优先,这里只介绍采用 MetaQ 消息队列解决方案,并省略了 MetaQ 相关配置仅给出了核心代码。消息生产者代码:
// 创建新用户函数
private UserVO createNewUser(String phoneNumber) {
// 创建新用户
UserDO user = new UserDO();
...
userDAO.insert(user);
// 发送优惠券消息
Long userId = user.getId();
CouponMessageDataVO data = new CouponMessageDataVO();
data.setUserId(userId);
data.setCouponType(CouponType.NEW_USER);
Message message = new Message(TOPIC, TAG, userId, JSON.toJSONBytes(data));
SendResult result = metaqTemplate.sendMessage(message);
if (!Objects.equals(result, SendStatus.SEND_OK)) {
log.error("发送用户({})绑定优惠券消息失败:{}", userId, JSON.toJSONString(result));
}
// 返回新用户
return transUser(user);
}
注意:可能出现发生消息不成功,但是这种概率相对较低。
消息消费者代码:
// 优惠券服务类
@Slf4j
@Service
public class CouponService extends DefaultMessageListener<String> {
// 消息处理函数
@Override
@Transactional(rollbackFor = Exception.class)
public void onReceiveMessages(MetaqMessage<String> message) {
// 获取消息体
String body = message.getBody();
if (StringUtils.isBlank(body)) {
log.warn("获取消息({})体为空", message.getId());
return;
}
// 解析消息数据
CouponMessageDataVO data = JSON.parseObject(body, CouponMessageDataVO.class);
if (Objects.isNull(data)) {
log.warn("解析消息({})体为空", message.getId());
return;
}
// 绑定优惠券
bindCoupon(data.getUserId(), data.getCouponType());
}
}
解决方案优点:采集 MetaQ 消息队列优化慢接口解决方案的优点:
- 如果系统发生重启或崩溃,导致消息处理函数执行失败,不会确认消息已消费;由于 MetaQ 支持多服务订阅同一队列,该消息可以转到别的服务进行消费,亦或等到本服务恢复正常后再进行消费。
- 消费者可多服务、多线程进行消费消息,即便消息处理时间较长,也不容易引起消息积压;即便引起消息积压,也可以通过扩充服务实例的方式解决。
- 如果需要重新消费该消息,只需要在 MetaQ 管理平台上点击”消息验证”即可。
流程定义不合理
原有的采购流程
这是一个简易的采购流程,由库管系统发起采购,采购员开始采购,采购员完成采购,同时回流采集订单到库管系统。

其中,完成采购动作的核心代码如下:
/** 完成采购动作函数(此处省去获取采购单/验证状态/锁定采购单等逻辑) */
public void finishPurchase(PurchaseOrder order) {
// 完成相关处理
......
// 回流采购单(调用HTTP接口)
backflowPurchaseOrder(order);
// 设置完成状态
purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.FINISHED.getValue());
}
由于函数 backflowPurchaseOrder(回流采购单)调用了 HTTP 接口,可能引起以下问题:
- 该函数可能耗费时间较长,导致完成采购接口成为慢接口;
- 该函数可能失败抛出异常,导致客户调用完成采购接口失败。
优化的采购流程
通过需求分析,把”采购员完成采购并回流采集订单”动作拆分为”采购员完成采购”和”回流采集订单”两个独立的动作,把”采购完成”拆分为”采购完成”和”回流完成”两个独立的状态,更方便采购流程的管理和实现。

拆分采购流程的动作和状态后,核心代码如下:
/** 完成采购动作函数(此处省去获取采购单/验证状态/锁定采购单等逻辑) */
public void finishPurchase(PurchaseOrder order) {
// 完成相关处理
......
// 设置完成状态
purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.FINISHED.getValue());
}
/** 执行回流动作函数(此处省去获取采购单/验证状态/锁定采购单等逻辑) */
public void executeBackflow(PurchaseOrder order) {
// 回流采购单(调用HTTP接口)
backflowPurchaseOrder(order);
// 设置回流状态
purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.BACKFLOWED.getValue());
}
其中,函数 executeBackflow(执行回流)由定时作业触发执行。如果回流采购单失败,采购单状态并不会修改为”已回流”;等下次定时作业执行时,将会继续执行回流动作;直到回流采购单成功为止。
