zoukankan      html  css  js  c++  java
  • 请求量限制方法-使用本地Cache记录当前请求量[坑]

      有个需求:需要限制每个账户请求服务器的次数(该次数可以配置在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难以调试重现出来。

    ------2015/10/08续集--------------------------

    最近因为我这边因为Redis环境问题,导致我的程序-发送量限制有问题、不稳定,所以又研究了下不用Redis来实现的方法。

    1.直接写sql查库,读出当前分钟内的发送量。

    1   const string dateTimeFormat = "yyyy-MM-dd HH:mm";
    2   string sql = "Select Count(*) From Message With(Nolock) Where SystemId = " + systemId + " CreatedOn >= '" +
    3     DateTime.Now.ToString(dateTimeFormat) + "' And CreatedOn <'" + DateTime.Now.AddMinutes(1).ToString(dateTimeFormat) + "'";

    如果请求并发量不大、消息Message表不大或者定期归档,那么上述方法是可用的。当然,还需请DBA对照次sql增加数据库表索引(SystemId,CreatedOn),最好还要优化该sql。

    但是我一直感觉这种方法太不好、小题大做、浪费性能。想用本地缓存做。

    2.本地缓存做。

    本地缓存实现的最大痛点是:难以实现缓存值自增修改与过期失效两个功能的兼得。所以,必须得解决或者绕过这个问题。

    参考同事的程序、代码,找到了解决方法:将缓存值设置为   时间_计数 的形式,把时间直接写在缓存值中,代码自己判断是否是当前分钟的缓存。若当前时间-分钟在缓存中,则加1并判断是否过量;不是则直接修改缓存值为当前时间_数量1。若缓存不存在,则一定是当前分钟的第一次请求-一定不过量、新建缓存。

      1         /// <summary>
      2         /// 发送量缓存值:日期格式
      3         /// </summary>
      4         private const string DateTimeFormat = "yyyy-MM-dd HH:mm";
      5 
      6         /// <summary>
      7         /// 发送量缓存值:日期和发送量的分隔符
      8         /// </summary>
      9         private const string CacheValueSplit = "_";
     10 
     11         /// <summary>
     12         /// 账号是否过量请求
     13         /// </summary>
     14         /// <param name="accountCode">账号代码</param>
     15         /// <param name="accountMaxSendNumLimit">账号每分钟的最大请求量</param>
     16         /// <returns></returns>
     17         public bool VerifySendNumLimit(string accountCode, int accountMaxSendNumLimit)
     18         {
     19             int currentSendNum = GetCurrentSendNum(accountCode);
     20             if (currentSendNum != -1)       //缓存未失效,请求次数+1并更新缓存
     21             {
     22                 currentSendNum++;
     23                 SetCurrentSendNum(accountCode, currentSendNum);
     24             }
     25             else   //缓存已过期释放,重新写入:请求次数 1次,默认过期时间 1分钟
     26             {
     27                 currentSendNum = 1;
     28                 SetCurrentSendNum(accountCode, 1);
     29             }
     30 
     31             return currentSendNum <= accountMaxSendNumLimit;
     32         }
     33 
     34         /// <summary>
     35         /// 获取当前一分钟内的请求量
     36         /// </summary>
     37         /// <param name="currentSendNumCacheKey">账号发送量缓存键</param>
     38         private static int GetCurrentSendNum(string currentSendNumCacheKey)
     39         {
     40             //缓存键:AccountCode,示例:testSystem
     41             //缓存值:yyyy-MM-dd HH:mm_Count,示例:2015-10-08 15:53_3
     42 
     43             object cacheObj = Helper.GetCache(currentSendNumCacheKey);
     44             if (cacheObj == null)
     45                 return -1;
     46 
     47             string[] cacheValue = cacheObj.ToString().StringSplit(CacheValueSplit); //自己写的扩展方法,按CacheValueSplit分割成字符串数组
     48             string minute = cacheValue[0];
     49             int requesNum = int.Parse(cacheValue[1]);
     50             if (minute == DateTime.Now.ToString(DateTimeFormat))
     51                 return requesNum;
     52             else
     53                 return -1;
     54         }
     55 
     56         /// <summary>
     57         /// 设置当前发送量,过期时间为一分钟
     58         /// </summary>
     59         /// <param name="currentSendNumCacheKey">账号发送量缓存键</param>
     60         /// <param name="currentSendNum">要设置的当前发送量</param>
     61         private static void SetCurrentSendNum(string currentSendNumCacheKey, int currentSendNum)
     62         {
     63             //缓存键:AccountCode,示例:testSystem
     64             //缓存值:yyyy-MM-dd HH:mm_Count,示例:2015-10-08 15:53_3
     65 
     66             Helper.SetCache(currentSendNumCacheKey, DateTime.Now.ToString(DateTimeFormat) + CacheValueSplit + currentSendNum, 1);
     67         }
     68 
     69         /// <summary>
     70         /// 设置本地缓存
     71         /// </summary>
     72         /// <param name="key">缓存键</param>
     73         /// <param name="obj">缓存值</param>
     74         /// <param name="minutes">过期时间(分钟),可为空</param>
     75         public static void SetCache(string key, object obj, int? minutes)
     76         {
     77             try
     78             {
     79                 if(minutes.HasValue)
     80                     CacheHelper.Save(key, obj, null, DateTime.Now.AddMinutes(minutes.Value), Cache.NoSlidingExpiration);
     81                 else
     82                     CacheHelper.Save(key, obj);
     83             }
     84             catch (Exception ex)
     85             {
     86                 new Response("本地缓存写入异常:"+ ex.Message);
     87                 return;
     88             } 
     89         }
     90 
     91         /// <summary>
     92         /// 获取缓存值
     93         /// </summary>
     94         /// <param name="key">缓存键</param>
     95         /// <returns>缓存值</returns>
     96         public static object GetCache(string key)
     97         {
     98             try
     99             {
    100                 return CacheHelper.Get(key);
    101             }
    102             catch (Exception ex)
    103             {
    104                 new Response("本地缓存读取异常:" + ex.Message);
    105                 return null;
    106             }
    107         }
    View Code

    至此,完毕。

    ------备注----------------

    1.使用本地缓存后,就不便使用负载、集群了。因为本地缓存只存在于单个应用程序域内,多机器、多网站应用程序域不能共享。若要支持负载,还是redis,mc 吧

    2.即使把1解决了,还有一个不易发现的BUG,不容易看出来,是多个线程使用并修改统一资源=》诱发并发问题:当同账号的请求1和请求2同时来临,可能请求1的line 16 与请求2的line21 是先后执行的。就出现了脏读写。

    此话错误。.Net 的HttpRuntime.Cache读写 内部封装了锁机制来处理并发、是线程安全的,用户无需再写。而Application貌似不是。  当然,若请求量并发量不大、作用不是很重要(比如只是计数限制量)的业务场景下,不考虑也罢。

    3.分析一下这三种ASP.NET缓存过期策略。

    ◆永不过期。直接赋值缓存的Key和Value即可

    ◆绝对时间过期。DateTime.Now.AddSeconds(10)表示缓存在10秒后过期,TimeSpan.Zero表示不使用平滑过期策略。

    ◆变化时间过期(平滑过期)。DateTime.MaxValue表示不使用绝对时间过期策略,TimeSpan.FromSeconds(10)表示缓存连续10秒没有访问就过期。

    Cache.Insert("Data1", "Data1"); //只要服务器内存够、没重启回收,缓存一直在

    Cache.Insert("Data2", "Data2", null, DateTime.Now.AddSeconds(10), TimeSpan.Zero); //加入缓存10秒后过期,不管有没有访问

    Cache.Insert("Data3", "Data3", null, DateTime.MaxValue, TimeSpan.FromSeconds(5)); //最后一次访问5秒后过期,若访问一直存在则一直不过期

    4.Cache的Insert()Add()方法,两者都能向缓存中添加项。不同之处在于: Add()方法只能添加缓存中没有的项并返回项值,如果添加缓存中已有的项将失败(但不会抛出异常);而Insert()方法无论有没有原值,一律能覆盖原来的项,不返回任何。

  • 相关阅读:
    《精通Ext JS 》
    《OpenStack部署实践》
    《方寸指间——移动设计实战手册》
    《产品经理那些事儿》
    C# 基于Aspose.Cells的数据导出到Excel
    mongodb中直接根据某个字段更新另外一个字段值
    C# mongodb中内嵌文档数组条件查询
    c# 在mongo中查询经纬度范围
    (转)MySql 获取所有级联父级或所有级联子级
    mongodb对数组元素及内嵌文档进行增删改查操作(转)
  • 原文地址:https://www.cnblogs.com/nlh774/p/4457159.html
Copyright © 2011-2022 走看看