刚刚,发生了一个件不可思议的事情,前端竟然可以在毫秒级的时间间隔,向同一个接口提交了两次请求,因为这里涉及到线程,两次请求产生的日志都是交叉在一起,一开始很难分析出来。
目前还未找到重复请求的根源,估计和浏览器响应有关。前端已经做了防重复提交的代码,依然没拦住,就是请求时候第一时间出现遮罩,防止第二次点击,好像选择性失效了。那么就在后端做拦截吧。
后端拦截需要有一个前提,就是必须是带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