促销后台-客服批量发券实现方案

    电商的各个业务都有人工发券的需求,需要客服或者运营在CRM后台直接发券,PM扔给我批量发券的需求。

关键字段:批次号(BatchId)、卡券编码(rpCodes支持输入多个,逗号隔开)、用户ID(userIds支持输入多个,逗号隔开;支持批量上传Excel格式)。

前端调用后台的批量发券接口后,直接返回到查询界面,后台批量发券接口开多线程异步发券。

发券结果字段:批次号(BatchId)、卡券编码、批次数量(rpCodes*userIds)、成功数量、失败数量、发放时间、状态

----------------------------------------------------------------------------------------------------------------------------------------------------------

以上为需求,下边说说实现方案:

由于只是做辅助发券,避免其他业务线的人员大批量发券都走这个接口,所以,校验发券总量,大于2000张券的流量全部拦下。

1.校验发券数量

2.记录批量发券基本信息,由于userIds字段太大,DBA坚持另存一个表,用基本信息表的主键做关联。

3.发券

4.记录操作记录

第三部发券的实现细节上跟Boss的意见有些出入:

userIds小于500另起一个异步单线程发券,这一点没问题,关键在于当userIds大于500时,起5个线程去发券,怎么处理多线程返回的结果?

我的意见是,使用Java的Fork/Join框架,将所有线程的结果做下汇总,然后更新一次批量发券基本信息表,但是这样一来,要么线程全部完成,然后更新数据库,要么,不更新数据库,如果线程异常,那么CRM就会看不到实际上发成功多少券,失败多少券。

Boss的意见是,每个线程返回时,直接更新一次批量发券基本信息表,需要更新5次,由于在MySQL做update时加了条件,而且条件里包含了索引字段,所以利用MySQL的行锁,即使线程并发地更新批量发券基本信息表里的某条记录,也不会发生写入脏数据的情况,即使某个线程出错,也不影响其他线程返回。

附上发券部分的代码:

/**
 * 批量发券,人数大于500时,开5个线程并行发券
 * @param rpCodes
 * @param memberIds
 * @param batchId
 */
private void receiveRps(String[] rpCodes, String[] memberIds, String batchId) {
    List<String> memberId = Arrays.asList(memberIds);
    if(memberIds.length <= 500){
        sendCoupons(rpCodes, memberId, batchId);
    } else {
        List<List<String>> splitList = splitMemberIds(memberId);
        for(List<String> subList : splitList){
            sendCoupons(rpCodes, subList, batchId);
        }
    }
}
/**
 * 在子线程里发券
 * @param rpCodes
 * @param memberIds
 * @param batchId
 */
private void sendCoupons(String[] rpCodes, List<String> memberIds, String batchId){
    threadPool.execute(()-> {
        try {
            LOG.info("****.sendCoupons 子线程发券开始,memberIds:{}, rpCodes:{}, batchId:{}", JsonUtil.toString(memberIds), JsonUtil.toString(rpCodes), batchId);
            //对每个人发券
            int successCount = 0;
            int faultCount = 0;
            for(String memberId : memberIds){
                SendPersonalRewardDto sendPersonalRewardDto = sendPersonalReward(memberId, rpCodes, batchId);
                successCount += sendPersonalRewardDto.getSuccessCount();
                faultCount += sendPersonalRewardDto.getFaultsCount();
            }
            //更新发券记录,并行转为串行,由MySQL的update提供行锁的行锁可能会产生死锁
            synchronized (this){
                HashMap<String, Object> param = new HashMap<>();
                param.put("batchId",batchId);
                param.put("successCount",successCount);
                param.put("faultCount",faultCount);
                int updateRes = iBatchCouponDao.updateBatchBasicInfoRecord(param);
                if(updateRes != 1){
                    LOG.error("****.sendCoupons 子线程发券完毕,更新数据库出错,memberIds:{}, rpCodes:{}, batchId:{}" , JsonUtil.toString(memberIds), JsonUtil.toString(rpCodes), batchId);
                }
            }
        }catch (Exception e){
            LOG.error("****.sendCoupons 子线程发券异常,memberIds:{}, rpCodes:{}, batchId:{}", JsonUtil.toString(memberIds), JsonUtil.toString(rpCodes), batchId);
        }
    });
}

 

private List<List<String>> splitMemberIds(List<String> memberId){
    List<List<String>> list = new ArrayList<>();
    int avgNumber= memberId.size() / 5;
    for(int i = 0; i < 4; i++){
        List<String> sublist = memberId.subList(i * avgNumber, i * avgNumber + avgNumber);
        list.add(sublist);
    }
    list.add(memberId.subList(4 * avgNumber, memberId.size()));
    return list;
}

说明:sendPersonalReward函数太细节了,包含了敏感业务信息,不方便展示。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值