甲方:“为什么这个需求,人家只要100元,你确要1万元?”

236 阅读3分钟

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

需求

商家有多个门店,每天的固定时间点需要给门店的会员发送固定格式的短信。

又不是不能用版

LV1看到需求后:这还不简单,最多半天时间,来咱根据需求先画个流程图(内心OS:我是不是很专业):

image

流程图是不是很清晰,来,咱马上开始 复制粘贴 艺术创作:

public void sendSmsMessage() {
    shopDao.findAll().forEach(shop -> {
      String telNums = memberDao
          .findByShopId(shop.getId())
          .stream()
          .map(Member::getTelNum)
          .collect(Collectors.joining(","));
      httpSendSmsMessage.batchSendMessage("祝你生日快乐!", telNums);
    });
}

LV1实现完成后,提交PR等待大佬的夸奖,没想到,仅仅过了几分钟,大佬就拒绝了LV1的PR并细心的附上了方案的优缺点:

优点

  1. 能用
  2. 开发周期端

缺点

  1. 一个门店发生错误,会影响所有门店的短信发送。
  2. 仅可以正常正确运行,一旦发生错误,只能手工介入。

互相不打扰版

LV1盯着大佬发过来的优缺点,仔细思索了下,意识到自己将需求想的太简单了,经过一番搜索,找到了可以通过异步的方式来解决门店间互相影响的问题,既然找到解决方案了,咱就说干就干,还是先来一张流程图:

image

有了流程图和异步知识的积累,代码也很快写了出来:

public void eachOtherSendSmsMessage() {
    CountDownLatch latch = new CountDownLatch((int) shopDao.count());
    shopDao.findAll().forEach(shop -> {
      asyncBetterThinkService.sendSmsMessage(shop.getId(), latch);
    });
    try {
      latch.await();
    } catch (InterruptedException e) {
      log.error("线程在等待时被中断", e);
    }
}

完成代码后,LV1也升级到了LV3,虽然这样第一个缺点解决了,第二个该怎么解决呢,LV3再次陷入了沉思。

错误重试版

又是一番搜索和讨论后,LV3将错误分成了两类,一类是第三方短信接口的错误,一类是自己程序内部的错误,当是第一类错误发生时,程序应当自动进行重试且超过设定的重试次数后,判定为错误,保存日志,另一类错误发生时,不进行自动重试,直接保存日志。有了解决方案后,咱还是先来一张流程图:

image

实现代码如下:


-- 主方法
public void errorSendSmsMessage() {
    CountDownLatch latch = new CountDownLatch((int) shopDao.count());
    shopDao.findAll().forEach(shop -> {
      try {
        asyncBetterThinkService.errorRetrySendSmsMessage(shop.getId(), latch);
      } catch (Exception e) {
        // 发生非第三方错误是保存错误日志
        SendSmsMsgError error = new SendSmsMsgError();
        error.setReason("xxxx");
        error.setShopId(shop.getId());
      }
    });
    try {
      latch.await();
    } catch (InterruptedException e) {
      log.error("线程在等待时被中断", e);
    }
}

-- 异步方法
@Async
public void errorRetrySendSmsMessage(String shopId, CountDownLatch latch) {
    String telNums = memberDao
        .findByShopId(shopId)
        .stream()
        .map(Member::getTelNum)
        .collect(Collectors.joining(","));
    // 每个线程独立重试次数
    int selfRetry = retry;
    boolean successRetry = false;
    while (selfRetry > 0) {
      try {
        httpSendSmsMessage.batchSendMessage("祝你生日快乐!", telNums);
        // 发送成功后,直接终止重试
        selfRetry = 0;
        successRetry = true;
      } catch (Exception e) {
        log.error("发送短信错误", e);
      }
      if (selfRetry > 0) {
        try {
          // 一般发生的错误都会是网络波动,所以先等待两秒后再重试
          Thread.sleep(TimeUnit.SECONDS.toMillis(2));
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        selfRetry--;
      }
    }
    if (!successRetry) {
      // 保存错误信息
      SendSmsMsgError error = new SendSmsMsgError();
      error.setReason("xxxx");
      error.setShopId(shopId);
    }
    latch.countDown();
}

-- 手工重试方法

public void handleRetry(String msgErrorId) {
    sendSmsMsgErrorDao.findById(msgErrorId).ifPresent(sendSmsMsgError -> {
        String telNums = memberDao
          .findByShopId(sendSmsMsgError.getShopId())
          .stream()
          .map(Member::getTelNum)
          .collect(Collectors.joining(","));
        httpSendSmsMessage.batchSendMessage("祝你生日快乐!", telNums);
    });
}

这次完成后,LV3信心满满的提交了PR,大佬也很快通过了LV3的请求。

尾声

实际上,即使到最后,我们这个方案也未达到最优,比如:未考虑门店或会员过多时查询的耗时及发送的耗时等等,但笔者这个项目的实际情况时门店和会员数量都不会太多,所以方案的选择也要结合实际项目的需求和开发的周期,如果开发周期很紧张,笔者认为因为数据量过多引发的查询耗时问题是可以往后延期的。

好了,最后,再说几句题外话,就是切勿花费大量的时间去比较开发语言的好坏,说到底,开发语言只是一种工具,每种工具有各自擅长的领域罢了,并没有优劣高低之分,而真正能桎梏你成长的是你解决问题的思路和深度。

代码地址:gitee.com/lanrain/art…