书接上文:https://blog.csdn.net/weixin_43303530/article/details/127227147?spm=1001.2014.3001.5502,为满足产品提的在24小时内能重试尽量去重试,不计较重试的次数的要求,在第三方电子卡系统无法提升接口并发数量的情况下,优化用户领取红包策略;
1、业务与技术背景
1、为了促进公司商城项目的推广与引流,项目组引入了第三方的电子卡系统,该电子卡系统可以通过生成相应的红包类型,运营人员可以通过该系统将红包发放给需要引流的客户来促进消费与推广。
2、由于目前该电子卡发红包行为只能在小程序端触发,无法从后台进行派发和控制,希望礼品卡可以实现运营端应用,业务可以像发优惠券那样派发红包,实现多样化的用户运营。
3、由于目前该电子卡每个红包只能有一百个用户领取,而业务希望开发可以在后台系统中增加用户列表excel文件的解析,对列表中的用户去领取红包,这就不可避免的需要发送大量的领取红包请求到该电子卡系统。
4、为了防止由于不稳定的因素(如网络波动、流量峰值过高等)在excel文件解析后直接调用该电子卡系统导致用户领取红包的请求失败,导致数据丢失,将请求的消息放入mq队列。
5、由于该电子卡的系统性能很差,请求高频率的失败,所以需要使用mq的消息重试,但由于上文中采用Spring RetryTemplate做RabbitMQ消息重试机制发现可能会造成队列堵塞,且同样避免不了第三方接口性能差的问题,大批量的请求可能导致第三方接口堵塞。
2、解决思路
- 1、降低接口请求的频率,防止大量请求执行至第三方导致接口堵塞;
- 2、短时间内大批量失败不再请求第三方接口;
- 3、对于短时间内已经执行失败的请求不做处理,采取补偿;
- 4、当接口请求成功后尝试补偿。
3、解决方案
-
1、降低请求第三方接口频率:
- mq消费者每次消费后进行一定时间的sleep
- 降低最大消费者数量
-
2、采用feign调用,增加hystrix熔断(熔断配置已省略):
-
3、失败的请求保存记录mongo:
-
3.1、mongo表结构:
/**
* 批量领红包重试
* @author liurui
* @date 2022/8/16
*/
@Document(collection = "ReceiveRedPacketRetryTask")
public class ReceiveRedPacketRetryTask {
@Id
private String id;
/**
* 调用卡系统入参
*/
private BatchReceiveRedPacketInput batchReceiveRedPacketInput;
/**
* 创建任务id
*/
private String prepareRedPacketTaskId;
/**
* 上传发放红包任务id
*/
private String uploadUserTaskId;
/**
* 状态:0:待重试,1:重试成功,2:重试失败(可以增加一个进行中的状态,但此处已经做了分布式锁,一个时间只存在一个重试任务,所以没有增加)
*/
private Integer status;
/**
* 重试次数
*/
private Integer retryCount;
/**
* 创建人ID
*/
private Long createUserid;
/**
* 创建时间
*/
private Long createTime;
/**
* 最后修改人ID
*/
private Long updateUserid;
/**
* 最后修改时间
*/
private Long updateTime;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public BatchReceiveRedPacketInput getBatchReceiveRedPacketInput() {
return batchReceiveRedPacketInput;
}
public void setBatchReceiveRedPacketInput(BatchReceiveRedPacketInput batchReceiveRedPacketInput) {
this.batchReceiveRedPacketInput = batchReceiveRedPacketInput;
}
public String getPrepareRedPacketTaskId() {
return prepareRedPacketTaskId;
}
public void setPrepareRedPacketTaskId(String prepareRedPacketTaskId) {
this.prepareRedPacketTaskId = prepareRedPacketTaskId;
}
public String getUploadUserTaskId() {
return uploadUserTaskId;
}
public void setUploadUserTaskId(String uploadUserTaskId) {
this.uploadUserTaskId = uploadUserTaskId;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Integer getRetryCount() {
return retryCount;
}
public void setRetryCount(Integer retryCount) {
this.retryCount = retryCount;
}
public Long getCreateUserid() {
return createUserid;
}
public void setCreateUserid(Long createUserid) {
this.createUserid = createUserid;
}
public Long getCreateTime() {
return createTime;
}
public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
public Long getUpdateUserid() {
return updateUserid;
}
public void setUpdateUserid(Long updateUserid) {
this.updateUserid = updateUserid;
}
public Long getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Long updateTime) {
this.updateTime = updateTime;
}
}
- 4、补偿机制:
原意是考虑当有请求成功发送时就进行补偿,但是会存在一个问题,就是如果最后一大批数据都请求失败了,就不会有请求去唤醒补偿了或者说要等待下一次发红包请求才有可能进行补偿,对此,采取了请求成功时进行补偿和job补偿相结合的机制,又考虑到并发场景的问题,虽然第三方的电子卡系统做了幂等,但是为了减免压力,进行补偿的时候做了一次redis分布式锁,没有获取到锁,说明有补偿任务在进行,那么就继续之前的补偿任务不做处理,没有就进行补偿
- 4.1、请求成功采取异步线程进行补偿(可考虑采用线程池,优化cpu资源利用):
- 4.2、xxljob每隔半个小时执行一次补偿任务:
/**
* 礼品卡-批量领红包重试
* @author liurui
* @date 2022/8/1
*/
@Component
public class RetryReceiveRedPacketTaskJob extends IJobHandler {
private final Logger logger = LogUtils.getLogger(this.getClass());
@Resource
private UserReceiveDetailWriteService userReceiveDetailWriteService;
@Override
@XxlJob("retryReceiveRedPacketTaskJob")
public ReturnT<String> execute(String param) throws Exception {
ReturnT<String> result = new ReturnT<>();
long beginTime = System.currentTimeMillis();
try {
logger.info("retryReceiveRedPacketTaskJob开始执行");
XxlJobLogger.log("retryReceiveRedPacketTaskJob开始:"+ beginTime);
result.setCode(ReturnT.SUCCESS_CODE);
userReceiveDetailWriteService.executeRetryTask();
result.setMsg("执行成功");
} catch (Exception e) {
logger.error("retryReceiveRedPacketTaskJob执行失败",e);
result.setCode(ReturnT.FAIL_CODE);
result.setMsg("retryReceiveRedPacketTaskJob执行失败" + e);
} finally {
logger.info("retryReceiveRedPacketTaskJob结束执行");
XxlJobLogger.log("retryReceiveRedPacketTaskJob结束:"+ (System.currentTimeMillis() - beginTime));
}
return result;
}
}
xxljob相关信息:
- 4.3、补偿逻辑,此处采用redisson做redis分布式锁:
public void executeRetryTask() {
RLock rLock = redisson.getLock(GiftCardCenterConstant.GIFTCARD_RETRYRECEIVE_REDPACKET_LOCK);
try {
boolean lockSuccess = rLock.tryLock(0, TimeUnit.SECONDS);
if (lockSuccess) {
int pageNum = 1;
// 一次查询20条
int pageSize = 20;
List<ReceiveRedPacketRetryTask> list;
// 分页获取数据
do {
list = receiveRedPacketRetryTaskReadService.getReceiveRedPacketRetryTaskByPageParams(pageNum, pageSize);
// 重新请求
list.forEach(this::retryBatchReceiveRedPacket);
pageNum++;
} while (CollectionUtils.isNotEmpty(list));
}
} catch (Exception e) {
logger.error("批量领红包重试发生异常", e);
} finally {
if (rLock.isLocked()) {
try {
rLock.unlock();
} catch (Exception e) {
}
}
}
}
mongo待补偿记录查询
public List<ReceiveRedPacketRetryTask> getReceiveRedPacketRetryTaskByPageParams(int pageNum, int pageSize) {
Query query = new Query(Criteria.where("status").ne(1));
query.with(PageRequest.of(pageNum - 1, pageSize));
query.with(Sort.by(Sort.Direction.ASC, "status","retryCount"));
query.with(Sort.by(Sort.Direction.DESC, "createTime"));
return mongoTemplate.find(query, ReceiveRedPacketRetryTask.class);
}