首页 > 其他分享 >【开发】高效解决并发中的重复请求问题

【开发】高效解决并发中的重复请求问题

时间:2024-11-29 12:55:05浏览次数:4  
标签:高效 requestTime 请求 重复 并发 参数 1000 String

对于一些用户请求,在某些情况下是可能重复发送的,如果是查询类操作并无大碍,但其中有些是涉及写入操作的,一旦重复了,可能会导致很严重的后果,例如交易的接口如果重复请求可能会重复下单。

重复的场景有可能是:

黑客拦截了请求,重放
前端/客户端因为某些原因请求重复发送了,或者用户在很短的时间内重复点击
网关重发

本文讨论的是如何在服务端优雅地统一处理这种情况,如何禁止用户重复点击等客户端操作不在本文的讨论范畴。

利用唯一请求编号去重

可能会想到的是,只要请求有唯一的请求编号,那么就能借用Redis做这个去重——只要这个唯一请求编号在redis存在,证明处理过,那么就认为是重复的
代码大概如下:

String KEY = "REQ12343456788";//请求唯一编号
long expireTime =  1000;// 1000毫秒过期,1000ms内的重复请求会认为重复
long expireAt = System.currentTimeMillis() + expireTime;
String val = "expireAt@" + expireAt;
//redis key还存在的话要就认为请求是重复的
Boolean firstSet = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(KEY.getBytes(), val.getBytes(), Expiration.milliseconds(expireTime), RedisStringCommands.SetOption.SET_IF_ABSENT));

final boolean isConsiderDup;
if (firstSet != null && firstSet) {// 第一次访问
    isConsiderDup = false;
} else {// redis值已存在,认为是重复了
    isConsiderDup = true;
}

业务参数去重

上面的方案能解决具备唯一请求编号的场景,例如每次写请求之前都是服务端返回一个唯一编号给客户端,客户端带着这个请求号做请求,服务端即可完成去重拦截。
但是,很多的场景下,请求并不会带这样的唯一编号!那么我们能否针对请求的参数作为一个请求的标识呢?
先考虑简单的场景,假设请求参数只有一个字段reqParam,我们可以利用以下标识去判断这个请求是否重复。
用户ID:接口名:请求参数

String KEY = "dedup:U="+userId + "M=" + method + "P=" + reqParam;

那么当同一个用户访问同一个接口,带着同样的reqParam过来,我们就能定位到他是重复的了。
但是问题是,我们的接口通常不是这么简单,以目前的主流,我们的参数通常是一个JSON。那么针对这种场景,我们怎么去重呢?
计算请求参数的摘要作为参数标识
假设我们把请求参数(JSON)按KEY做升序排序,排序后拼成一个字符串,作为KEY值呢?但这可能非常的长,所以我们可以考虑对这个字符串求一个MD5作为参数的摘要,以这个摘要去取代reqParam的位置。

String KEY = "dedup:U="+userId + "M=" + method + "P=" + reqParamMD5;

这样,请求的唯一标识就打上了!
:MD5理论上可能会重复,但是去重通常是短时间窗口内的去重(例如一秒),一个短时间内同一个用户同样的接口能拼出不同的参数导致一样的MD5几乎是不可能的。
继续优化,考虑剔除部分时间因子。
上面的问题其实已经是一个很不错的解决方案了,但是实际投入使用的时候可能发现有些问题:某些请求用户短时间内重复的点击了(例如1000毫秒发送了三次请求),但绕过了上面的去重判断(不同的KEY值)。

原因是这些请求参数的字段里面,是带时间字段的,这个字段标记用户请求的时间,服务端可以借此丢弃掉一些老的请求(例如5秒前)。如下面的例子,请求的其他参数是一样的,除了请求时间相差了一秒:

//两个请求一样,但是请求时间差一秒
    String req = "{\n" +
            "\"requestTime\" :\"20190101120001\",\n" +
            "\"requestValue\" :\"1000\",\n" +
            "\"requestKey\" :\"key\"\n" +
            "}";

    String req2 = "{\n" +
            "\"requestTime\" :\"20190101120002\",\n" +
            "\"requestValue\" :\"1000\",\n" +
            "\"requestKey\" :\"key\"\n" +
            "}";

这种请求,我们也很可能需要挡住后面的重复请求。所以求业务参数摘要之前,需要剔除这类时间字段。还有类似的字段可能是GPS的经纬度字段(重复请求间可能有极小的差别)。

请求去重工具类,Java实现

public class ReqDedupHelper {
    /**
     *
     * @param reqJSON 请求的参数,这里通常是JSON
     * @param excludeKeys 请求参数里面要去除哪些字段再求摘要
     * @return 去除参数的MD5摘要
     */
    public String dedupParamMD5(final String reqJSON, String... excludeKeys) {
        String decreptParam = reqJSON;

        TreeMap paramTreeMap = JSON.parseObject(decreptParam, TreeMap.class);
        if (excludeKeys!=null) {
            List<String> dedupExcludeKeys = Arrays.asList(excludeKeys);
            if (!dedupExcludeKeys.isEmpty()) {
                for (String dedupExcludeKey : dedupExcludeKeys) {
                    paramTreeMap.remove(dedupExcludeKey);
                }
            }
        }

        String paramTreeMapJSON = JSON.toJSONString(paramTreeMap);
        String md5deDupParam = jdkMD5(paramTreeMapJSON);
        log.debug("md5deDupParam = {}, excludeKeys = {} {}", md5deDupParam, Arrays.deepToString(excludeKeys), paramTreeMapJSON);
        return md5deDupParam;
    }

    private static String jdkMD5(String src) {
        String res = null;
        try {
            MessageDigest messageDigest = MessageDigest.getInstance("MD5");
            byte[] mdBytes = messageDigest.digest(src.getBytes());
            res = DatatypeConverter.printHexBinary(mdBytes);
        } catch (Exception e) {
            log.error("",e);
        }
        return res;
    }
}

下面是一些测试日志:

public static void main(String[] args) {
    //两个请求一样,但是请求时间差一秒
    String req = "{\n" +
            "\"requestTime\" :\"20190101120001\",\n" +
            "\"requestValue\" :\"1000\",\n" +
            "\"requestKey\" :\"key\"\n" +
            "}";

    String req2 = "{\n" +
            "\"requestTime\" :\"20190101120002\",\n" +
            "\"requestValue\" :\"1000\",\n" +
            "\"requestKey\" :\"key\"\n" +
            "}";

    //全参数比对,所以两个参数MD5不同
    String dedupMD5 = new ReqDedupHelper().dedupParamMD5(req);
    String dedupMD52 = new ReqDedupHelper().dedupParamMD5(req2);
    System.out.println("req1MD5 = "+ dedupMD5+" , req2MD5="+dedupMD52);

    //去除时间参数比对,MD5相同
    String dedupMD53 = new ReqDedupHelper().dedupParamMD5(req,"requestTime");
    String dedupMD54 = new ReqDedupHelper().dedupParamMD5(req2,"requestTime");
    System.out.println("req1MD5 = "+ dedupMD53+" , req2MD5="+dedupMD54);
}

日志输出

req1MD5 = 9E054D36439EBDD0604C5E65EB5C8267 , req2MD5=A2D20BAC78551C4CA09BEF97FE468A3F
req1MD5 = C2A36FED15128E9E878583CAAAFEFDE9 , req2MD5=C2A36FED15128E9E878583CAAAFEFDE9

日志说明

一开始两个参数由于requestTime是不同的,所以求去重参数摘要的时候可以发现两个值是不一样的。

第二次调用的时候,去除了requestTime再求摘要(第二个参数中传入了”requestTime”),则发现两个摘要是一样的,符合预期。

总结

至此,我们可以得到完整的去重解决方案,如下:


String userId= "12345678";//用户
String method = "pay";//接口名
String dedupMD5 = new ReqDedupHelper().dedupParamMD5(req,"requestTime");//计算请求参数摘要,其中剔除里面请求时间的干扰
String KEY = "dedup:U=" + userId + "M=" + method + "P=" + dedupMD5;

long expireTime =  1000;// 1000毫秒过期,1000ms内的重复请求会认为重复
long expireAt = System.currentTimeMillis() + expireTime;
String val = "expireAt@" + expireAt;

// NOTE:直接SETNX不支持带过期时间,所以设置+过期不是原子操作,极端情况下可能设置了就不过期了,后面相同请求可能会误以为需要去重,所以这里使用底层API,保证SETNX+过期时间是原子操作
Boolean firstSet = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(KEY.getBytes(), val.getBytes(), Expiration.milliseconds(expireTime),
        RedisStringCommands.SetOption.SET_IF_ABSENT));

final boolean isConsiderDup;
if (firstSet != null && firstSet) {
    isConsiderDup = false;
} else {
    isConsiderDup = true;
}

原创 一安未来

标签:高效,requestTime,请求,重复,并发,参数,1000,String
From: https://www.cnblogs.com/o-O-oO/p/18576370

相关文章

  • 精准高效-C++语言集成翔云VIN码识别接口、vin码识别sdk
    在当今快节奏的商业环境中,汽车行业面临着前所未有的挑战与机遇。无论是二手车交易、保险评估还是供应链管理,准确快速地获取车辆信息已成为提高效率、增强竞争力的关键。针对市场需求,翔云提供了VIN码识别接口,能够精确捕捉VIN码并输出,用科技的力量助力企业优化业务流程。......
  • 借助AI助手如何高效排查SQL问题
    快乐的时光总是转瞬即逝,尤其是当我们面对bug时,不仅浪费了宝贵的时间,更让人感到沮丧。因为bug往往是非常奇怪、难以捉摸的,找来找去你始终无法确定问题所在,最终意识到这些bug并没有多大技术含量。尽管如此,它们却能占据你大量的精力和时间。这样反复琢磨,往往并没有多大意义,甚至对于......
  • 请求分页管理方式
    请求分页管理方式‍​​‍一、页表机制的不同与基本分页存储管理的页表区别:​​‍二、缺页中断机构在请求分页系统中,每当要访问的页面不在内存时,便产生一个缺页中断,然后由操作系统的缺页中断处理程序处理中断。此时缺页的进程阻塞,放入阻塞队列,调页完成后再将其唤醒,放回......
  • 高效团队协作软件:板栗看板入门教程
    团队协作的效率,往往直接影响到工作成果的质量与速度。如果你正在寻找一种简便、高效的工具来提升团队的协作与信息流动,不妨尝试板栗看板。今天,我们将带你了解如何快速上手板栗看板,让你的团队协作更加顺畅、高效!一、什么是看板管理看板(Kanban)起源于日本,最初应用于丰田生产系统,用......
  • 国产信创FTP解决方案:安全、可靠、高效!
    FTP(文件传输协议)在全世界范围内都有广泛的应用,许多企业和组织应用于专业的文件传输场景。但随着关键传输的文件数量和大小以及发送者和接收者人数的不断增加,在信创环境下,FTP已经无法继续满足文件传输的需求,存在一定的局限性,企业需要寻找适配信创环境的信创FTP进行替代:1.兼容性问......
  • Java设计模式——职责链模式:解锁高效灵活的请求处理之道
    嘿,各位Java编程大神和爱好者们!今天咱们要一同深入探索一种超厉害的设计模式——职责链模式。它就像一条神奇的“处理链”,能让请求在多个对象之间有条不紊地传递,直到找到最合适的“处理者”。准备好跟我一起揭开它神秘的面纱,看看如何用代码实现这种强大的模式,让我们的程序变得更......
  • CTranslate2:在 AMD GPU 上高效推理 Transformer 模型
    CTranslate2:EfficientInferencewithTransformerModelsonAMDGPUs—ROCmBlogsTransformer模型通过在机器翻译、文本摘要、文本生成和语音识别等任务中提供高性能结果,彻底改变了自然语言处理(NLP)的领域。然而,由于这些模型对计算和内存的高需求,在生产环境中部署它们......
  • 告别传统浏览器书签,全新导航网站助您高效管理网络资源!
    前言在这个信息泛滥的时代,每个人的浏览器书签栏可能都堆积了众多网页链接。简化书签管理显得尤为重要,为此,我开发了一款可视化书签管理平台【菠萝书签网】,让整理和查找书签变得轻松高效。 网站地址:bl-admin主页白天主题主页夜晚主题关于“菠萝书签网......
  • 家具组装的智慧引导:智能工具与产品说明书的高效协作
    在家具市场中,消费者在购买家具后往往需要自行组装。然而,传统的产品说明书往往存在信息表述不清、步骤繁琐等问题,给消费者的组装过程带来诸多不便。为了帮助消费者更轻松地完成家具组装,将HelpLook与家具产品的产品说明书相结合,成为了一个切实可行的解决方案。一、家具市场的特点......
  • 高效数据集成:销售退货单从旺店通到金蝶
    销售退货单旺店通→金蝶:高效数据集成案例分享在企业信息化管理中,销售退货单的准确处理和及时更新至关重要。本文将聚焦于如何通过轻易云数据集成平台,实现旺店通·企业奇门与金蝶云星空之间的无缝对接,具体方案为“销售退货单旺店通→金蝶”。为了确保数据集成过程的高效性和可靠......