1. 优享JAVA首页
  2. 默认分类

那些年,我们见过的 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 管理平台上点击”消息验证”即可。

流程定义不合理


原有的采购流程
这是一个简易的采购流程,由库管系统发起采购,采购员开始采购,采购员完成采购,同时回流采集订单到库管系统。

那些年,我们见过的 Java 服务端“问题”

其中,完成采购动作的核心代码如下:

/** 完成采购动作函数(此处省去获取采购单/验证状态/锁定采购单等逻辑) */
public void finishPurchase(PurchaseOrder order) {
    // 完成相关处理
    ......

    // 回流采购单(调用HTTP接口)
    backflowPurchaseOrder(order);

    // 设置完成状态
    purchaseOrderDAO.setStatus(order.getId(), PurchaseOrderStatus.FINISHED.getValue());
}

由于函数 backflowPurchaseOrder(回流采购单)调用了 HTTP 接口,可能引起以下问题:

  • 该函数可能耗费时间较长,导致完成采购接口成为慢接口;
  • 该函数可能失败抛出异常,导致客户调用完成采购接口失败。

优化的采购流程

通过需求分析,把”采购员完成采购并回流采集订单”动作拆分为”采购员完成采购”和”回流采集订单”两个独立的动作,把”采购完成”拆分为”采购完成”和”回流完成”两个独立的状态,更方便采购流程的管理和实现。

那些年,我们见过的 Java 服务端“问题”

拆分采购流程的动作和状态后,核心代码如下:

/** 完成采购动作函数(此处省去获取采购单/验证状态/锁定采购单等逻辑) */
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(执行回流)由定时作业触发执行。如果回流采购单失败,采购单状态并不会修改为”已回流”;等下次定时作业执行时,将会继续执行回流动作;直到回流采购单成功为止。

本文来自阿里巴巴中间件:常意,经授权后发布,本文观点不代表优享JAVA立场,转载请联系原作者。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

QR code