有个需求:需要限制每个账户请求服务器的次数(该次数可以配置在DB,xml文件或其他)。单位:X次/分钟。若1分钟内次数<=X 则允许访问,1分钟内次数>X则不再允许访问。 这类需求很常见的,像请求Baidu Map Api服务接口等,都有这类限制,只是单位不同。
一般来说,接口的请求在服务器端都会有记录的,比如记在DB中,记录 账户、请求时间、请求信息、请求操作、服务器响应信息 等。所以逻辑上完全可以获取请求当前时间段的请求量(执行记录 的count()操作,在DB中为Sql : SELECT COUNT(*) FROM RequestRecordTable where Account = .. And ReqTime .. )。但这样不现实:为这么一个小功能竟然还要耗时去计算,完全浪费计算资源、内存资源,一旦请求量、记录数大增,那么性能肯定很差。
于是就想到了缓存:1.把该账户的最大请求量放在缓存里,永不过期。2.把该账户的1分钟以来的请求量放在缓存中,1分钟过期自动释放。 3.每来一次请求,则当前请求量+1,后与该账户的最大请求量比对,<=则允许,>则超过次数。为了简单起见,使用.Net 本地缓存。详见代码(该账户的最大请求量已获取到,若获取失败则读配置:
1 /// <summary> 2 /// 验证该账户当前请求量是否超过限制 3 /// </summary> 4 /// <param name="account">账户</param> 5 /// <param name="maxNumLimit">最大请求量,已获取到</param> 6 /// <returns>true-超过,false-未超过</returns> 7 public bool VerifyMaxNumLimit(string account, int maxNumLimit) 8 { 9 string currentSendNumCacheKey = string.Format("SysCode_{0}_CurrentSendNum", account); 10 object currentSendNumObj = HttpContext.Current.Cache[currentSendNumCacheKey]; 11 int currentSendNum = 0; 12 if (currentSendNumObj != null) //缓存存在 13 { 14 currentSendNum = (int)currentSendNumObj; 15 currentSendNum++; 16 HttpContext.Current.Cache[currentSendNumCacheKey] = currentSendNum; 17 } 18 else //缓存失效,已到期或者缓存问题。重新写入:目前请求次数 1次,过期时间 1分钟 19 { 20 currentSendNum = 1; 21 HttpContext.Current.Cache.Insert(currentSendNumCacheKey, 1,null, DateTime.Now.AddMinutes(1),Cache.NoSlidingExpiration); 22 } 23 24 return currentSendNum > maxNumLimit; 25 }
乍一看,完全没有问题,大功告成。可是,当你本地自己测试时,却发现 :前几次不超过限制量次数的调用接口都是成功的,但是一旦超过该请求量以后,哪怕是5分钟后,也无法在此请求。究其原因:VerifyMaxNumLimit 此后一直返回true=> 1分钟后缓存没有清除,仍然存在。
最终原因是什么??
1.是15行: HttpContext.Current.Cache[currentSendNumCacheKey] = currentSendNum;
这句代码看似只是修改缓存值。可实际上,这句代码等效于:HttpContext.Current.Cache.Insert(currentSendNumCacheKey, currentSendNum)!即覆盖缓存,并将缓存设置为永不过期(除非IIS应用程序池回收,或内存不足等外部情况)=》缓存不失效=》当前请求量不断++,所以 return currentSendNum > maxNumLimit 一直是true.
2.即使把1解决了,还有一个不易发现的BUG,不容易看出来,是多个线程使用并修改统一资源=》诱发并发问题:当同账号的请求1和请求2同时来临,可能请求1的line 16 与请求2的line21 是先后执行的。就出现了脏读写。
所以,需要解决:1.找到一个可修改缓存值又不修改缓存失效时间的方法(本地缓存HttpContext.Current.Cache 似乎无法实现,放弃,2.加锁或做成单例。
我公司正好有Redis组件,满足上述要去并且本身封装时即为线程安全的。所以最终我用了Redis来记录账户当前请求量。具体代码其实类似 上述代码段,只是把和HttpContext.Current.Cache 有关的地方全部改为Redis 相关语句。
至此结束,实现请求量限制。
经验:关于HttpContext.Current.Cache,有些代码看似平淡但是内部有说不清楚的作用。
以后还是要多写写代码,多积累经验,遇到的坑多了,才会吃一堑长一智。当然,若能得到高人指点或者自己之前在其他地方见过这个坑的相关介绍,那么能避免再好不过,少走弯路节省时间。 至于,多个请求同时请求、并发、共享资源的事情要小心,最好加锁(加锁会导致效率变差),这也是设计时要考虑好的,不然出了BUG难以调试重现出来。