接口如何处理重复请求?

本文主要来源于 处理重复请求的三种方式
服务端如何高效的处理重复请求
对其整理和总结,用于学习记录。

重复请求常用的处理方式就是幂等性处理,幂等性可以理解为:无论执行了多少次重复请求,数据只会处理一次,在数据库里也只会有一条数据。和数据库的唯一索引是一样的。

方式一:前端处理

优点:前端可在用户点击之后将按钮锁定,不可点击,直到后端返回数据才释放锁。前端处理是最为简单有效,且对用户比较友好的方式。可用提示语提醒用户等待。

缺点:无法应对直接刷后端接口的情况,如果用户直接调后端接口,无法处理(属于特殊情况)。

方式1:ajax防止重复提交

这里我用登录页面功能作为例子讲解,按下面的方法,用户在点击登录后,按钮会不可点击,直到后端返回结果,关键js代码如下:

function login(){
    //1.去用户名密码
	var user=$("#inputName").val();
	var pwd=$("#inputPassword").val();

	//2.让提交按钮失效,以实现防止按钮重复点击
	$("#loginBtn").attr('disabled', 'disabled');
  
	//3.给用户提供友好状态提示
	$("#loginBtn").text('登录中...');
  
	//4.异步提交
	$.ajax({
		type:"POST",
		url:"/verifylogin",
		dataType:"json",
		data:{
			"username":user,
			"password":pwd
		},
		success : function(data) {
			//登录成功跳转
		},
		error:function(){
			//5.失败后,登录按钮重新有效
			$("#loginBtn").removeAttr('disabled');
			alert("登录 发生错误");
		}
	});
}

方式2:vue按钮防止短时间内多次点击

定义了一个Vue指令,名为preventReClick
它的作用是防止用户在2秒内重复点击某个按钮。

当用户点击按钮时,按钮的disabled属性会被设置为true,并且按钮的背景颜色会被设置为#ccc,边框会被设置为none,鼠标样式也会被设置为not-allowed。

2秒后,按钮会被重置,disabled属性会被设置为false,背景颜色被设置为#002FA8,边框被设置为1px solid #002FA8,鼠标样式也会被设置为pointer。

import Vue from 'vue'
const preventReClick = Vue.directive('preventReClick',{
	inserted: function(el, binding, vNode, oldVnode){
		el.addEventListener('click', () => {
			if(!el.disabled){
			    el.disabled = true
			    el.style.backgroundColor = '#ccc'
			    el.style.border = 'none'
                el.style.cursor = 'not-allowed'
       			setTimeout(() => {
          			el.disabled = false
          			 el.style.backgroundColor = '#002FA8'	
          			 el.style.border = '1px solid #002FA8'
                     el.style.cursor = 'pointer'
        		}, 2000)
			}
		})
	}
})

export default {
	preventReClick 
}

使用方法也很简单

在main.js里引用

//防频繁点击
import  preventReClick  from '@/utils/directive'
Vue.use( preventReClick )

直接调用即可

 <el-button v-preventReClick>确认</el-button>

方式二:后端AOP处理

后端处理逻辑是需要使用额外的存储数据,记录用户访问接口次数。在重复提交时拦截请求,对于重复的请求直接返回就行,不做任何处理。
方式:自定义注解+AOP

这里我用springboot2+redis+maven 构造示例,请求的思路是将用户访问记录缓存,5s内,请求的记录在redis中有记录时直接抛出异常。

自定义注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface AccessLimit {
		//有效时间
    int seconds() default 5;
		//有效时间内最大计数
    int maxCount() default 1;
}

拦截器

@Service
public class AccessInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            //获取方法中的注解,是否有限制注解
            AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class);
            if (Objects.isNull(accessLimit)) {
                return true;
            }
            int seconds = accessLimit.seconds();
            int maxCount = accessLimit.maxCount();
            //获取入参
            String userId = request.getParameter("userId");
            if (Objects.isNull(userId)) {
                return false;
            }
            ValueOperations operations = redisTemplate.opsForValue();
            Integer count = (Integer) operations.get(userId);

            //第一次访问,userId作为key,value为1
            if (count == null) {
                operations.set(userId, 1);
                redisTemplate.expire(userId, seconds, TimeUnit.SECONDS);
            } else if (count < maxCount) {
                operations.increment(userId);
            } else {
                //超出访问次数
                throw new Exception("请求过快");
            }
        }
        return true;
    }
}

测试接口

    @AccessLimit
    @GetMapping("/test/accessLimit")
    public String testAccessLimit(@RequestParam("userId") String userId) {
        return "正常返回";
    }

在这里插入图片描述
通过重复请求就会抛出异常

方式三:数据库唯一索引

(1) 数据库处理就是设置唯一索引,可设联合唯一索引用来处理重复数据。
缺点:如果业务场景就是应该存储重复的数据,则该种方式不可用。

(2)利用唯一请求编号去重
你可能会想到的是,只要请求有唯一的请求编号,那么就能借用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;
}

方式四:业务参数去重

很多的场景下,请求并不会带这样的唯一编号,计算请求参数的摘要作为参数标识,剔除部分时间因子,最后对其MD5加密。
请求去重工具类ReqDedupHelper

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;
}

参考文章
处理重复请求的三种方式
服务端如何高效的处理重复请求
vue按钮防止短时间内多次点击怎么做

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值