redis实践的一点思路,关于支付回调

大家好,我是烤鸭:

如果作为第三方支付平台,需要通知调用方付款成功。但是出现通知失败的情况,怎么处理。
    支付宝的异步通知,每个订单的异步通知实行分频率发送: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":
                                // 通知时间 + 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+队列

    3. redis+队列


    将已经获取到的订单扔到队列中,在队列里执行 doSCYDPayNotifyAgainHandler(String item, String notifyTime, String num,String applyNum) 
    这个方法,延时和cpu问题就能比较好的解决了。
    至于丢失问题,暂时没考虑过。就目前来说,第二种方式够用。其他的只是有一些想法,欢迎交流。

猜你喜欢

转载自blog.csdn.net/Angry_Mills/article/details/80880078