刚刚,发生了一个件不可思议的事情,前端竟然可以在毫秒级的时间间隔,向同一个接口提交了两次请求,因为这里涉及到线程,两次请求产生的日志都是交叉在一起,一开始很难分析出来。

        目前还未找到重复请求的根源,估计和浏览器响应有关。前端已经做了防重复提交的代码,依然没拦住,就是请求时候第一时间出现遮罩,防止第二次点击,好像选择性失效了。那么就在后端做拦截吧。

        后端拦截需要有一个前提,就是必须是带token的请求,我总不可能让每个人都等待别人完成再来!假设把短时间的请求设置为1秒,就是1秒内,同一个用户不允许重复请求一个接口。在系统中,内置了一个Session对象,这个对象不是http请求中的session,是框架封装的一个用户缓冲容器,其本质是扩展的kv容器,例如static Dictionry<string,object> Session;这里细节不说。有了这个容器就好办了,可以参考

当进入方法的时候,

//这里做一下改进
lock(Session) //这里要锁住Session对象,防止多线程抢占资源导致出错。
{
        if(Session.TryGetValue(methodKey,out DateTime lastdt)
        {
                //其实这里不用管取出来的数据是什么,都直接返回就可以了,毕竟缓存已经有这个数据了
                //也可以把lastdt这个最后调用的时间写到日志
               logHelper.Warn($"出现短时间内重复请求{methodKey});
                return true;
        }
        //设置超时时间1秒.
        Session.Set(methodKey,DateTime.Now,DateTimeOffset.Now.AddSeconds(1));
}

超过设定时间,methodKey将自动从Session移除,注意这里用的是DateTimeOffset,是绝对超时,不管中间methodKey被使用了多少次,到点就移除掉;如果是使用TimeSpan,则会顺延移除,就是使用一次,倒计时时间又重置为1秒。

好了,问题就这么愉快的解决了。

其实可以做的更好一点,就是使用Aop,把上述代码封装到一个标签上,程序员就可以节约大量的时间了,也方便后续维护,同时还可以设置标签当遇到重复调用的时候,是返回正确的状态还是报错状态,灵活度就大了很多。

再次提出一个问题,如果允许两次或者三次,或者设置次数限制,那么也是可以的,修改为Session.Inc(methodKey);

//todo
//当方法执行完成后,
var cnt = Session.Dec(methodKey);

这里就和超时没有什么关系了,当Dec后的cnt变量是0的时候,就是完成所有的调用了。其实这种情况很好出现,起码我是没有遇到过。只是顺带提一下。

补充基于ActionFilter方法实现的代码,以下是基于.net framework的代码

[AttributeUsage(AttributeTargets.Method,AllowMultiple =false,Inherited =false)]
    public class OneCallAttribute : ActionFilterAttribute, IActionFilter
    {
        int _interval = 500;//默认500毫秒
        static cyb.Utility.Tools.CYBMemoryCache cache = new cyb.Utility.Tools.CYBMemoryCache();
        public OneCallAttribute(int interval)
        {
            _interval= interval;
        }
        public override void OnActionExecuting(HttpActionContext context)
        {
            
            var key = $"{context.ControllerContext.Controller.GetType().Name}.{context.ActionDescriptor.ActionName}";
            if (context.ControllerContext.Controller is TokenApiController c)
            {
                var token = c.Token; //这里的token是每个用户登录后的令牌
                if (string.IsNullOrEmpty(token))
                    return;
                key = $"{token}_{key}";
            }

            if (cache.TryGetValue(key, out DateTime dt))
            {
                //出现了重复请求
                context.Response = context.Request.CreateResponse(new { state = "warn", rows = "", message = $"过于频繁发送请求,请稍后再试" });//.CreateErrorResponse(new InvalidByteRangeException(new ContentRangeHeaderValue(5), $"过于频繁发送请求,请稍后再试"));
                return;
            }
            else
            {
                cache.Set(key, DateTime.Now, DateTimeOffset.Now.AddMilliseconds(_interval));
            }

            base.OnActionExecuting(context);
        }
        public override void OnActionExecuted(HttpActionExecutedContext context)
        {
            var key = $"{context.ActionContext.ControllerContext.Controller.GetType().Name}.{context.ActionContext.ActionDescriptor.ActionName}";
            cache.Remove(key);
        }


    }

    public class MyActionResult
    {
        public string state { get; set; }
        public string message { get; set; }
        public object rows { get; set; }
    }

以下是基于.net core的代码

[AttributeUsage(AttributeTargets.Method,AllowMultiple =false,Inherited =false)]
    public class OneCallAttribute : ActionFilterAttribute, IActionFilter
    {
        int _interval = 500;//默认500毫秒
        static cyb.Utility.Tools.CYBMemoryCache cache = new cyb.Utility.Tools.CYBMemoryCache();
        public OneCallAttribute(int interval)
        {
            _interval= interval;
        }
        public override void OnActionExecuted(ActionExecutedContext context)
        {
            var key = $"{context.Controller.GetType().Name}.{context.ActionDescriptor.Id}";
            cache.Remove(key);
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            var key = $"{context.Controller.GetType().Name}.{context.ActionDescriptor.Id}";
            if (context.Controller is TokenApiController c)
            {
                var token = c.Token;
                if (string.IsNullOrEmpty(token))
                    return;
                key = $"{token}_{key}";
            }

            if (cache.TryGetValue(key, out DateTime dt))
            {
                //出现了重复请求
                context.Result = new JsonResult(new { state = "warn", rows = "", message = $"过于频繁发送请求,请稍后再试" });
                return;
                //var response = context.HttpContext.Request.CreateErrorResponse(new InvalidByteRangeException(new ContentRangeHeaderValue(5), $"访问过于频繁!请间隔{LimitSecond.ToString()}秒再次访问!"));
                //filterContext.Response = response;

            }
            else
            {
                cache.Set(key, DateTime.Now, DateTimeOffset.Now.AddMilliseconds(_interval));
            }

            base.OnActionExecuting(context);
        }
    }

两段代码大同小异。

目前我采用的是handler和middleware方案,在微服务向发现服务注册的时候,就明确了需要限制频繁点击的接口,此方法原理其实一样的,只是不通用,基于我自己的框架编写的代码,就不帖出来了。

注意的是,在完成调用后,第一时间删除限制,不一定非要等到方法执行完才做解除限制。如果做唯一性,可以去掉cache的自动超时设置,在ActionExecuted方法中才去掉限制key