大家好,我是烤鸭:
如果作为第三方支付平台,需要通知调用方付款成功。但是出现通知失败的情况,怎么处理。
支付宝的异步通知,每个订单的异步通知实行分频率发送:15s 3m 10m 30m 30m 1h 2h 6h 15h。
如果没有收到success,就会一直按上边的进行通知。
就上述的情景说一下想到的解决方案,并不一定是有效的,只是一些想法:
1. 定时任务
最开始想到的是用定时任务来做。通知后,如果没有收到结果,就会一直扫表。
扫描状态是未通知的,下次通知的时间小于当前时间的,如果再通知再未送达到的话,
更新下次通知时间和通知次数。
这个做法有一些弊端,如果订单到达一定数量,一直扫表会对数据库压力。
而且如果按照上面的时间间隔的话,在大量订单的情况下很难保证精度。
2. 定时任务 + redis实现
为了避免数据库的压力,想到的是用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<String> 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":// 通知时间 + 10minnotifyTime = Long.valueOf(notifyTime)+ 20 * DateConstant.ONE_THOUSAND * DateConstant.SIXTY_SECONDS + "";doSCYDPayNotifyAgainHandler(item, notifyTime, num ,applyNum);break;case "3":// 通知时间 + 10minnotifyTime = Long.valueOf(notifyTime)+ 20 * DateConstant.ONE_THOUSAND * DateConstant.SIXTY_SECONDS + "";doSCYDPayNotifyAgainHandler(item, notifyTime, num ,applyNum);break;case "4":// 通知时间 + 15minnotifyTime = Long.valueOf(notifyTime)+ 30 * DateConstant.ONE_THOUSAND * DateConstant.SIXTY_SECONDS + "";doSCYDPayNotifyAgainHandler(item, notifyTime, num ,applyNum);break;case "5":// 通知时间 + 1h* DateConstant.SIXTY_MINUTESnotifyTime = 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() {@Overridepublic 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+队列
3. redis+队列
将已经获取到的订单扔到队列中,在队列里执行 doSCYDPayNotifyAgainHandler(String item, String notifyTime, String num,String applyNum)
这个方法,延时和cpu问题就能比较好的解决了。
至于丢失问题,暂时没考虑过。就目前来说,第二种方式够用。其他的只是有一些想法,欢迎交流。