大家好,我是烤鸭:
如果作为第三方支付平台,需要通知调用方付款成功。但是出现通知失败的情况,怎么处理。
支付宝的异步通知,每个订单的异步通知实行分频率发送:15s 3m 10m 30m 30m 1h 2h 6h 15h。
如果没有收到success,就会一直按上边的进行通知。
就上述的情景说一下想到的解决方案,并不一定是有效的,只是一些想法:
最开始想到的是用定时任务来做。通知后,如果没有收到结果,就会一直扫表。
扫描状态是未通知的,下次通知的时间小于当前时间的,如果再通知再未送达到的话,
更新下次通知时间和通知次数。
这个做法有一些弊端,如果订单到达一定数量,一直扫表会对数据库压力。
而且如果按照上面的时间间隔的话,在大量订单的情况下很难保证精度。
为了避免数据库的压力,想到的是用redis来代替。
当第一次通知失败的时候,将失败的订单标识(+订单号)存到redis中。
其中redis中存放的是两种数据结构,一种是Set集合,订单号集合。
另一种String.key-value,key是前缀+订单号,value是已通知次数。
还有一种是key是前缀+订单号,value是下次通知时间。
简易代码如下:
第一次通知失败:
//通知失败
if (IDBConstant.RESULT_ERROR.equals(status)) {
logger.info("返回结果为error 将订单id存到redis中 orderId===" + orderId);
//将订单号放到redis中
String key = IDBConstant.SCYD_NOTIFY_PREFIX_PRE + orderId;
//集合的通用key,根据这个key能获取到需要通知的订单集合
redisClient.hset(IDBConstant.SCYD_NOTIFY_AGAIN_PRE, key, key);
//通知次数
redisClient.set(IDBConstant.SCYD_NOTIFY_NUM_PRE + orderId, "2");
//下一次通知时间,应该跟次数有关,可以写个枚举类,将次数和下次的加长时间对应
redisClient.set(IDBConstant.SCYD_NOTIFY_TIME_PRE + orderId, System.currentTimeMillis() + 10 * DateConstant.ONE_THOUSAND * DateConstant.SIXTY_SECONDS + "");
}
定时任务每隔一分钟获取redis数据
// 获取订单号
Set list = redisClient.hkeys(IDBConstant.SCYD_NOTIFY_AGAIN_PAY);
if (!list.isEmpty()) {
list.forEach(item -> {
String itemStr = (String) item;
String[] strs = itemStr.split("_");
String orderId = strs[1];
String applyNum = redisClient.hget(IDBConstant.SCYD_NOTIFY_AGAIN_PAY, itemStr);
System.out.println(IDBConstant.SCYD_NOTIFY_TIME_PAY + orderId);
// 订单发送的时间毫秒值
String notifyTime = redisClient.get(IDBConstant.SCYD_NOTIFY_TIME_PAY + orderId);
//被锁不等待
if (redisClient.tryLock(item, 0L, TimeUnit.SECONDS)) {
// 如果通知时间 < 当前时间,发送通知
if (Long.valueOf(notifyTime) < System.currentTimeMillis()) {
// 通知次数
String num = redisClient.get(IDBConstant.SCYD_NOTIFY_NUM_PAY + orderId);
switch (num) {
case "2":
// 通知时间 + 10min
notifyTime = Long.valueOf(notifyTime)
+ 20 * DateConstant.ONE_THOUSAND * DateConstant.SIXTY_SECONDS + "";
doSCYDPayNotifyAgainHandler(item, notifyTime, num ,applyNum);
break;
case "3":
// 通知时间 + 10min
notifyTime = Long.valueOf(notifyTime)
+ 20 * DateConstant.ONE_THOUSAND * DateConstant.SIXTY_SECONDS + "";
doSCYDPayNotifyAgainHandler(item, notifyTime, num ,applyNum);
break;
case "4":
// 通知时间 + 15min
notifyTime = Long.valueOf(notifyTime)
+ 30 * DateConstant.ONE_THOUSAND * DateConstant.SIXTY_SECONDS + "";
doSCYDPayNotifyAgainHandler(item, notifyTime, num ,applyNum);
break;
case "5":
// 通知时间 + 1h* DateConstant.SIXTY_MINUTES
notifyTime = Long.valueOf(notifyTime)
+ 60 * DateConstant.ONE_THOUSAND * DateConstant.SIXTY_SECONDS + "";
doSCYDPayNotifyAgainHandler(item, notifyTime, num ,applyNum);
break;
default:
break;
}
}
}
});
}
上面方法中doSCYDPayNotifyAgainHandler() 就是对当前的订单再次通知。
如果通知失败,更新次数和下次通知时间。如果成功就移除。最后别忘记释放锁。
方法如下:
public void doSCYDPayNotifyAgainHandler(String item, String notifyTime, String num,String applyNum) {
taskAsyncPool.execute(new Runnable() {
@Override
public void run() {
String[] strs = item.split("_");
String orderId = strs[1];
logger.info("[通知]" + orderId + ":第" + num + "次任务启动");
//根据orderId 获取订单信息
//订单信息假装已经获取到了
try {
//发送请求
//过程1
//过程2
//获取结果,成功的话
if (IDBConstant.RESULT_SUCCESS.equals(status)) {
//清空缓存数据
redisClient.delKey(IDBConstant.SCYD_NOTIFY_NUM_PAY + orderId);
redisClient.delKey(IDBConstant.SCYD_NOTIFY_TIME_PAY + orderId);
//移除操作成功的
redisClient.hdel(IDBConstant.SCYD_NOTIFY_AGAIN_PAY, item);
} else {
int count = Integer.parseInt(num);
if(count==5) {//回调第五次还是失败,直接返回
return;
}
//如果还是没有回调,更新回调时间
redisClient.set(IDBConstant.SCYD_NOTIFY_TIME_PAY + orderId, notifyTime);
count += 1;
//更新回调次数
redisClient.set(IDBConstant.SCYD_NOTIFY_NUM_PAY + orderId, count + "");
}
} catch (Exception e) {
logger.error("[回调通知]" + item + ":{}第" + num + "次任务异常:method{}" ,e);
redisClient.set(IDBConstant.SCYD_NOTIFY_TIME_PAY + orderId, notifyTime);
int count = Integer.parseInt(num);
count += 1;
//更新回调次数
redisClient.set(IDBConstant.SCYD_NOTIFY_NUM_PAY + orderId, count + "");
} finally {
//解锁
redisClient.unLock(item);
}
}
});
}
这样多条线程执行,主线程从redis中获取待通知订单集合,另起线程做通知操作。
每通知一单就是一条线程,延迟性也得到了比较好的解决,上面从数据库取的结果也可以多线程。
线程池也是有上限的,无限获取很可能将内存和cpu耗尽。
推荐第三种方式。redis+队列
将已经获取到的订单扔到队列中,在队列里执行 doSCYDPayNotifyAgainHandler(String item, String notifyTime, String num,String applyNum)
这个方法,延时和cpu问题就能比较好的解决了。
至于丢失问题,暂时没考虑过。就目前来说,第二种方式够用。其他的只是有一些想法,欢迎交流。