接口联调重复传问题处理

最近项目上有个跨平台的接口会偶然出现重复传输问题。逻辑是A单据审核后生成B单据,然后将B单据调接口传另一个平台生成相应的后续单据。但现在另一个平台生成了两张后续单据,能追溯到我这边的A单据,但是不能追溯到直接上游单据B单据。一开始以为是重启服务导致的部分单据偶发这种问题,后面发现与重启时间对不上,便着手解决。由于问题是偶发的,不能复现,所以只能通过服务器日志和看代码去分析问题并解决。

一、分析问题

   由于该接口所在的服务是部署在两台服务器上做负载均衡的,而且每次出现这种问题的时候业务反馈都是说耗时特别久,一直在卡在审核的界面没有反应。那初步判断是由于审核触发的后续操作以及接口到另一个平台的操作耗时太久,近而导致服务器触发重试,然后这个模块是用了两个服务器去承载服务的,于是重新传输的时候,两个服务器都进行了重传,进而导致了重复的问题。

  后续的服务器日志也验证了我的判断,两个服务器分别发现了一个单据的接口传输的日志,即一个单据调了两次接口。同时发现出现这种重复问题时,从审批触发审批后规则进行调接口到完成,耗时共计30秒。而且其中一台服务器日志显示,在调接口时,接口都返回报错提示了。那为什么另一个平台还是能生成单据呢?说明这个服务器单独调用该接口两次,然后后面一次接口调用返回了错误,于是另一个服务器再次重调。
在这里插入图片描述

  还有个问题是,为什么只有一张能溯源到我这边的单据呢?分析代码发现,原来是B单据保存方法报错了,但报错被塞到一个返回对象里,于是上层方法没有catch到错误,就继续运行了。于是就另一个平台就生成了两张单据,但是我这边却只有一张上游单据的问题。

/**
 * A单据审核后规则方法
 */
public void process(Confirm clientEntity) {
    ConfirmDto cDto = confirmService.findOne(clientEntity.getId());
    ReturnInfo pushExpense = confirmService.pushExpenseBill(cDto);
    if (pushExpense.getStatus() == 0) {
        throw new BusinessException(pushExpense.getMessage());
    }
    ReturnInfo sendDtoToNcc = expenseService.sendDtoToNcc((ExpenseDto) pushExpense.getData());
    if (sendDtoToNcc.getStatus() == 0) {
        throw new BusinessException(sendDtoToNcc.getMessage());
    }
}

 /**
 * A单据审批后转换为B单据方法
 */
public ReturnInfo pushExpenseBill(ConfirmDto dto) {
    ReturnInfo withFriendly = new ReturnInfo();
    List<ConfirmDto> confirmDtoList = new ArrayList<>();
    confirmDtoList.add(dto);
    ReturnInfo returnInfo = expenseService.convertSelectFeeActivityScheme(confirmDtoList);
    ExpenseDto expenseDto = (ExpenseDto) returnInfo.getData();
    try {
        withFriendly = expenseService.createWithFriendly(expenseDto);
    } catch (Exception e) {
        withFriendly.setStatus(0);
        logger.error(e.toString(), e);
        throw e;
    }
    return withFriendly;
}

 /**
 * B单据保存方法
 */
public ReturnInfo createWithFriendly(ExpenseDto clientDto) {
    ReturnInfo returnInfo = new ReturnInfo();
    returnInfo.setData(clientDto);
    returnInfo = pubSaveCheck("create", returnInfo);
    if (returnInfo.getStatus() == 0) {
        return returnInfo;
    }
    ExpenseDto backExpense = create(clientDto);
    returnInfo.setstatus(1);
    returnInfo.setData(backExpense);
    return returnInfo;
}

二、解决问题

  这种问题一开始的想法肯定是做个redis分布式锁,把A单据id作为key,去判断短时间内是否调用了两次,让接口只能调用一次。代码如下所示


@Autowired
private StringRedisTemplate stringRedisTemplate;
static final String lockKeyPrefix = "confirm_approve";

/**
 * A单据审核后规则方法
 */
public void process(Confirm clientEntity) {
    ConfirmDto cDto = confirmService.findOne(clientEntity.getId());
    //如果已经有锁了,说明重复传了,这里就直接返回
    boolean lock = this.getLock(lockKeyPrefix, clientEntity.getId());
    if (!lock){
        return;
    }
    ReturnInfo pushExpense = confirmService.pushExpenseBill(cDto);
    if (pushExpense.getStatus() == 0) {
        this.unLock(lockKeyPrefix, clientEntity.getId());
        throw new BusinessException(pushExpense.getMessage());
    }
    ReturnInfo sendDtoToNcc = expenseService.sendDtoToNcc((ExpenseDto) pushExpense.getData());
    if (sendDtoToNcc.getStatus() == 0) {
        this.unLock(lockKeyPrefix, clientEntity.getId());
        throw new BusinessException(sendDtoToNcc.getMessage());
    }
}


/**
 * 加锁
 */
public boolean getLock(String resourceKeyPrefix, String resourceId) {
    String lock = resourceKeyPrefix + resourceId;
    //如果该userId已经有该单据的锁
    if(StringUtils.equals("yes",stringRedisTemplate.opsForValue().get(lock.intern()))){
        return false;
    }
    //加锁
    stringRedisTemplate.opsForValue().set(lock.intern(),"yes",5L,TimeUnit.MINUTES);
    return true;
}

/**
 * 解锁
 */
public void unLock(String resourceKeyPrefix, String resourceId) {
    String lock = resourceKeyPrefix + resourceId;
    stringRedisTemplate.delete(lock.intern());
}

  到这里只解决了重复传的问题。但是还有个问题还存在,记得上面分析问题提到的,一个服务器短时间内调用了两次接口么?于是我接着检查了接口调用方法,发现Options设置的参数为(1000,3500),这对于一个后续操作处理繁多,调用时间偶尔为10秒的接口来说,请求时间3.5秒就判断为超时,太短了些。

FeeBusiPubApi fbpa = Feign.builder()
        .decoder(new Decoder.Default())
        .encoder(new Encoder.Default())
        .options(new Request.Options(1000, 3500))
        .retryer(new Retryer.Default(5000, 5000, 3))
        .target(FeeBusiPubApi.class, nccConfig.getIpport());

  于是直接修改为new Request.Options(1000, 35000)。让接口能正常流程走完,那问题出现的就少很多了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值