幂等性的研究及后台验证短时间内同一申请是否重复提交的方案

本文介绍幂等性概念及其在防止短时间内重复提交的应用案例。通过分析现有问题,提出使用缓存和时间戳验证的方法来替代线程操作,提高系统效率。

幂等性的研究及实例应用

引入:
这段时间在做新渠道的接入,把以前的核心拿过来copy一份进行改造,在进行代码重读的时候,发现了一个好玩的东西,在申请入件的时候,需要经过一步校验,注释上写的是,对于短时间重复提交的验证。当时我就很好奇点了进去,看一看到底是什么东西,然后点开之后大吃一惊,里面做的操作是:取到这笔件的身份证信息,使用其作为标识去查询有没有这个标识的线程,如果有则返回失败,这笔件短时间内重复提交了,如果没有返回成功,这笔件短时间内没有重复提交,并以身份证为标识创建一个线程然后挂起这个线程n毫秒(算作重复性提交的时间)。

这里问题就大了,意思是一笔件进入系统的时候,如果需要做重复申请验证,则会新开一个子线程挂起,我们都知道对于线程的操作,无论是创建挂起销毁都好,会需要进行OS切换操作,这种OS操作十分占用资源,在没有必要的时候尽量避免反复的创建线程,而且这个验证会在每一次入件时都会执行。这对我们写的程序是一个极大的负担,于是我向上级反映此问题并且接到了优化代码的任务。在查询资料之后,发现对于这种验证短时间内是否重复提交的操作有一个专用的名词去形容他,也就是我要去研究的内容:幂等性验证。

一、幂等性的概念

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。复杂的操作幂等保证是利用唯一交易号(流水号)实现.【百度百科】

数学中的幂等是指一个数对自己进行函数运算,其结果等于他自己。数学表达式为(f(x)) = f(x):也就是某个函数或者某个接口使用相同参数调用一次或者无限次,其造成的后果是一样的

在业务层面上来说,由于我们是提供接口的一方供他人调用,别人在调用时完全可以发送无数笔相同的数据(由于bug之类)调用你的接口,这样你在提供服务的时候,必须进行幂等性运算来控制重复的申请。

实际上TCP传输协议是支持幂等的,他使用了一个唯一的序列号作为标示来保证数据的幂等性。我们在进行应用层面上的幂等性验证的时候也是同理,需要使用一个唯一的序列号进行验证,通常是我们与对接方约定的流水号来进行控制,其特点是唯一性。

二、幂等性的验证实验

在网上保证接口幂等的方法很多,最简单常用的方法时,select+insert 先查询有没有此笔数据,然后再进行数据变更操作,但是这种方法很明显效率比较低下,在高并发的程序中不建议使用。然后比较多的就是乐观锁和悲观锁,悲观锁的原理是使用select for update的形式,这种方式要求你select的条件必须是主键或者唯一索引,否则会导致锁表,在你更新时任何人做不了表格操作。乐观锁是只在更新的一瞬间进行锁表,效率上要高很多,但要通过辅助的状态参数来进行验证,如:
  update table set name=’name’,version=version+1 where version=version
由于进行更新的时候会使其版本+1如果是并发的两条信息同时对表格操作时,第二笔就会因为版本号不对更新失败,这样也能保证幂等性。当然,你使用其他状态进行控制也是可行的,如果可以最好加上唯一索引,能够大幅度提高效率。
正常与其他机构进行交互的时候,往往对方会提供流水号并且保证流水号唯一,这样作为己方就可以使用数据来源+对方提供的流水号作为唯一索引来进行幂等性校验。

三、解决实际问题

虽然查了这么多资料,但是发现幂等性校验并没有解决我的实际问题,应需求(可能是为了分担我们后台的核心专家系统压力,也可能是以前出过什么大量重复bug),我需要在程序中不牵扯到数据库操作的情况下,控制同一个人(身份证号,这两笔申请可能是不同的,但是只要是一个人就不允许连续操作)在短时间内不能进行重复申请操作。于是我想到了,设置一个缓存区来解决这个问题,思路如下:

  • 设置一个hashmap,key储存身份证信息,value储存申请时的时间信息
  • 在一笔申请提交进来的时候,将身份证信息和时间信息存入map
  • 在信息存入map之前进行判断,如果这个key在map中存在则判断其value与现在时间是否超过允许重复提交时长,如果是则更新数据,返回通过,否则返回不通过。
  • 由于入键数可能比较多,一直往map中增加值可能会导致map非常大,后入的键验证效率变差,于是在map达到一定大小时将开一个子线程,循环遍历map中的值,将与当前时间相差大于允许重复提交时间的键全部删除
    实现代码如下:
public static final Map<String,Long> MAP = new HashMap<String,Long>();
    public static final long OUT_TIME=1000;
    public static final int OUT_SIZE=100;
    //验证是否短时间重复申请
    public static boolean isRepeat(String idNum){
        boolean flag =false;
        //检查是否存在key
        if(MAP.containsKey(idNum)){
            if(System.currentTimeMillis()-MAP.get(idNum)>OUT_TIME){
                flag= true;
            }else{
                flag= false;
            }
        }else{
            flag= true;
        }
        MAP.put(idNum, System.currentTimeMillis());
        //当数量过多做一次清除
        if(MAP.size()>OUT_SIZE){
            new Thread(){
                @Override
                public void run() {
                    for (Entry<String, Long> entry :MAP.entrySet() ) {
                        if(System.currentTimeMillis()-entry.getValue()>OUT_TIME){
                            MAP.remove(entry.getKey());
                        }       
                    }
                }
            }.start();
        }
        return flag;
    }
<think>首先,用户的问题是:“我想解决短时间内多次请求同一接口时后台只返回一次结果的问题请问短时间多次请求同一接口后台仅返回一次结果解决方案”。我需要提供解决方案。系统级指令:-所有行内数学表达式使用$...$格式。-独立公式使用$$...$$格式并单独成段。-LaTeX语法正确。-使用中文回答。-生成相关问题。-回答中引用的段落末尾自然地添加引用标识。用户可见层:-回答结构清晰,帮助用户逐步解决问题。-尽量保证回答真实可靠。参考站内引用:-引用[1]:关于海量请求下的接口并发解决方案,包括拒绝策略如随机拒绝、拒绝旧请求、拒绝非核心请求。-引用[2]:关于防止接口被同一个账号在同一台设备短时间重复调用的解决方案。用户的问题是关于短时间内多次请求同一接口时,后台只返回一次结果。这意味着我们需要一种机制来确保对于相同的请求,在短时间内只处理一次,并返回相同的结果,而不是每次都重新处理。关键点:-避免重复处理相同的请求。-后台只返回一次结果,意思是后续请求直接返回缓存的结果。-这可能适用于幂等操作,即多次请求不会改变状态。解决方案思路:1.**请求去重**:识别重复请求并缓存结果。2.**缓存机制**:对于相同的请求参数,缓存响应结果。3.**时间窗口**:定义一个短时间窗口,在这个窗口内,后续请求直接返回缓存结果。4.**键值设计**:如何唯一标识一个请求?基于用户、设备、接口、参数等。参考引用[2]提到防止重复调用,可以通过一些策略如限制调用频率或缓存结果。可能的解决方案:-**使用缓存**:例如Redis或Memcached,存储请求的哈希值作为键,响应作为值。-**设置过期时间**:缓存结果只在短时间内有效。-**锁机制**:对于写操作,使用锁来确保只有一个请求被处理。-**幂等性设计**:确保接口支持幂等,即多次调用效果相同。针对用户需求:“后台仅返回一次结果”,我们需要在服务器端实现请求去重和结果缓存。步骤:1.**识别重复请求**:创建一个唯一键,基于请求参数、用户ID、设备ID等。2.**检查缓存**:在接口处理前,检查这个键是否在缓存中,如果有缓存结果,直接返回。3.**处理请求**:如果没有缓存,处理请求,并将结果存储到缓存中。4.**设置缓存过期**:例如,设置5秒的过期时间,以防止缓存永久存在。在代码中实现:-使用中间件或AOP(面向切面编程)来拦截请求。-在Java中,可以使用Spring的拦截器或AspectJ。-在Python中,可以使用装饰器或中间件。示例代码(伪代码或实际代码):```java//伪代码,使用缓存publicResponsehandleRequest(Requestrequest){Stringkey=generateKey(request.getUserId(),request.getDeviceId(),request.getParams());if(cache.contains(key)){returncache.get(key);//返回缓存结果}//否则,处理请求Responseresponse=processRequest(request);cache.set(key,response,5);//缓存5秒returnresponse;}```在引用[2]中提到,防止合法用户恶意操作,所以我们需要考虑安全性和性能。相关问题生成:根据系统指令,我需要生成相关问题。结构回答:-先解释问题。-提供解决方案。-给出实现示例。-生成相关问题。引用标识:在回答中自然地添加引用标识。引用[1]和[2]是关于并发和防止重复调用的,所以可以引用到相关部分。例如,在讨论拒绝策略或缓存时,可以引用[1]或[2]。完整回答结构:1.引言:理解问题。2.解决方案概述。3.详细步骤。4.代码示例。5.生成相关问题。确保使用中文。</think>### 短时间内多次请求同一接口后台仅返回一次结果的解决方案 在接口开发中,频繁处理重复请求会浪费服务器资源,导致响应延迟或系统过载。针对您的问题——短时间内多次请求同一接口时,后台仅处理一次并返回相同结果——核心在于实现**请求去重**和**结果缓存**机制。这通常适用于幂等操作(多次调用不影响最终状态),如查询、支付确认等场景。下面我将逐步解释解决方案,确保结构清晰、真实可靠。 #### 解决方案概述 关键目标: - **识别重复请求**:基于请求参数、用户ID、设备ID等生成唯一键(Key)。 - **缓存结果**:首次请求处理后将结果缓存,后续相同请求直接返回缓存值。 - **设置时间窗口**:缓存仅在短时间内有效(如1-5秒),避免数据过期问题。 - **并发控制**:防止多个请求同时触发处理,确保原子性。 核心组件: - **缓存系统**:推荐使用Redis或Memcached,因其高性能和过期机制。 - **唯一键设计**:例如$key = \text{userID} + \text{接口路径} + \text{参数哈希值}$,确保请求标识唯一。 - **时间窗口**:定义“短时间”阈值,例如$t=2\text{s}$,可通过业务需求调整。 #### 详细实现步骤 1. **定义唯一键(Key Generation)** 为每个请求生成唯一标识。例如: - 用户ID + 设备ID + 接口名 + 请求参数哈希(如MD5或SHA-1)。 - 数学表达:$key = \text{userID} \oplus \text{hash}(\text{params})$,其中$\oplus$表示连接符。 - 示例:用户A在设备X调用`/pay`接口,参数为`{"orderId":123}`,则$key = "A_X_pay_" + \text{md5}(\text{"orderId=123"})$。 2. **检查缓存(Cache Lookup)** 在接口逻辑入口处,先查询缓存: - 若Key存在且未过期,直接返回缓存结果。 - 若不存在,则继续处理请求。 - 引用[2]提到需防止合法用户恶意操作,此步能有效拦截重复请求[^2]。 3. **处理请求并缓存结果(Processing & Caching)** - 加锁处理:使用分布式锁(如Redis的`SETNX`)确保只有一个请求进入处理逻辑。 - 处理完成后,将结果存入缓存,设置过期时间(如$t=3\text{s}$)。 - 示例代码(Java + Spring Boot + Redis): ```java @RestController public class ApiController { @Autowired private RedisTemplate<String, Object> redisTemplate; @PostMapping("/api/endpoint") public Response handleRequest(@RequestBody Request request, @RequestHeader String userId, @RequestHeader String deviceId) { // 生成唯一键 String paramsHash = DigestUtils.md5Hex(request.toString()); String key = userId + "_" + deviceId + "_" + "/api/endpoint_" + paramsHash; // 检查缓存 Response cachedResponse = (Response) redisTemplate.opsForValue().get(key); if (cachedResponse != null) { return cachedResponse; // 直接返回缓存结果 } // 加锁处理(防止并发) String lockKey = key + "_lock"; boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 1, TimeUnit.SECONDS); if (!locked) { // 等待或拒绝请求,引用[1]的拒绝策略可在此应用[^1] throw new RuntimeException("请求过于频繁,请稍后重试"); } try { // 实际业务处理(如数据库查询) Response response = processBusinessLogic(request); // 缓存结果,设置3秒过期 redisTemplate.opsForValue().set(key, response, 3, TimeUnit.SECONDS); return response; } finally { // 释放锁 redisTemplate.delete(lockKey); } } } ``` 4. **设置时间窗口(Time Window)** - 缓存过期时间根据业务调整:高频接口设$t=1\text{s}$,低频可设$t=5\text{s}$。 - 过期后自动清除,避免旧数据影响。 - 数学表达:缓存有效期$T$满足$T \leq \text{业务允许的最大延迟}$。 5. **异常处理与优化** - **错误重试**:缓存失败时(如Redis宕机),降级为直接处理请求。 - **性能监控**:记录缓存命中率,优化Key设计。引用[1]提到拒绝非核心请求策略,可在系统过载时启用[^1]。 - **安全考虑**:结合引用[2],验证用户合法性,防止恶意请求[^2]。 #### 适用场景与注意事项 - **最佳场景**:读多写少操作(如查询库存)、支付确认等幂等接口。 - **非适用场景**:非幂等操作(如创建订单),需结合事务或唯一ID保证一致性。 - **性能影响**:缓存系统(如Redis)可支撑10k+ QPS,延迟<1ms。 通过此方案后台短时间窗口内仅处理一次请求,显著降低服务器负载,提升响应速度。实际部署时,建议使用AOP(面向切面编程)封装去重逻辑,减少代码侵入。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值