有个需求:需要限制每个账户请求服务器的次数(该次数可以配置在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 本地缓存。详见代码(该账户的最大请求量已获取到,若获取失败则读配置:

  /// <summary>
/// 验证该账户当前请求量是否超过限制
/// </summary>
/// <param name="account">账户</param>
/// <param name="maxNumLimit">最大请求量,已获取到</param>
/// <returns>true-超过,false-未超过</returns>
public bool VerifyMaxNumLimit(string account, int maxNumLimit)
{
string currentSendNumCacheKey = string.Format("SysCode_{0}_CurrentSendNum", account);
object currentSendNumObj = HttpContext.Current.Cache[currentSendNumCacheKey];
int currentSendNum = ;
if (currentSendNumObj != null) //缓存存在
{
currentSendNum = (int)currentSendNumObj;
currentSendNum++;
HttpContext.Current.Cache[currentSendNumCacheKey] = currentSendNum;
}
else //缓存失效,已到期或者缓存问题。重新写入:目前请求次数 1次,过期时间 1分钟
{
currentSendNum = ;
HttpContext.Current.Cache.Insert(currentSendNumCacheKey, ,null, DateTime.Now.AddMinutes(),Cache.NoSlidingExpiration);
} return currentSendNum > maxNumLimit;
}

乍一看,完全没有问题,大功告成。可是,当你本地自己测试时,却发现 :前几次不超过限制量次数的调用接口都是成功的,但是一旦超过该请求量以后,哪怕是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查库,读出当前分钟内的发送量。

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

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

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

2.本地缓存做。

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

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

         /// <summary>
/// 发送量缓存值:日期格式
/// </summary>
private const string DateTimeFormat = "yyyy-MM-dd HH:mm"; /// <summary>
/// 发送量缓存值:日期和发送量的分隔符
/// </summary>
private const string CacheValueSplit = "_"; /// <summary>
/// 账号是否过量请求
/// </summary>
/// <param name="accountCode">账号代码</param>
/// <param name="accountMaxSendNumLimit">账号每分钟的最大请求量</param>
/// <returns></returns>
public bool VerifySendNumLimit(string accountCode, int accountMaxSendNumLimit)
{
int currentSendNum = GetCurrentSendNum(accountCode);
if (currentSendNum != -) //缓存未失效,请求次数+1并更新缓存
{
currentSendNum++;
SetCurrentSendNum(accountCode, currentSendNum);
}
else //缓存已过期释放,重新写入:请求次数 1次,默认过期时间 1分钟
{
currentSendNum = ;
SetCurrentSendNum(accountCode, );
} return currentSendNum <= accountMaxSendNumLimit;
} /// <summary>
/// 获取当前一分钟内的请求量
/// </summary>
/// <param name="currentSendNumCacheKey">账号发送量缓存键</param>
private static int GetCurrentSendNum(string currentSendNumCacheKey)
{
//缓存键:AccountCode,示例:testSystem
//缓存值:yyyy-MM-dd HH:mm_Count,示例:2015-10-08 15:53_3 object cacheObj = Helper.GetCache(currentSendNumCacheKey);
if (cacheObj == null)
return -; string[] cacheValue = cacheObj.ToString().StringSplit(CacheValueSplit); //自己写的扩展方法,按CacheValueSplit分割成字符串数组
string minute = cacheValue[];
int requesNum = int.Parse(cacheValue[]);
if (minute == DateTime.Now.ToString(DateTimeFormat))
return requesNum;
else
return -;
} /// <summary>
/// 设置当前发送量,过期时间为一分钟
/// </summary>
/// <param name="currentSendNumCacheKey">账号发送量缓存键</param>
/// <param name="currentSendNum">要设置的当前发送量</param>
private static void SetCurrentSendNum(string currentSendNumCacheKey, int currentSendNum)
{
//缓存键:AccountCode,示例:testSystem
//缓存值:yyyy-MM-dd HH:mm_Count,示例:2015-10-08 15:53_3 Helper.SetCache(currentSendNumCacheKey, DateTime.Now.ToString(DateTimeFormat) + CacheValueSplit + currentSendNum, );
} /// <summary>
/// 设置本地缓存
/// </summary>
/// <param name="key">缓存键</param>
/// <param name="obj">缓存值</param>
/// <param name="minutes">过期时间(分钟),可为空</param>
public static void SetCache(string key, object obj, int? minutes)
{
try
{
if(minutes.HasValue)
CacheHelper.Save(key, obj, null, DateTime.Now.AddMinutes(minutes.Value), Cache.NoSlidingExpiration);
else
CacheHelper.Save(key, obj);
}
catch (Exception ex)
{
new Response("本地缓存写入异常:"+ ex.Message);
return;
}
} /// <summary>
/// 获取缓存值
/// </summary>
/// <param name="key">缓存键</param>
/// <returns>缓存值</returns>
public static object GetCache(string key)
{
try
{
return CacheHelper.Get(key);
}
catch (Exception ex)
{
new Response("本地缓存读取异常:" + ex.Message);
return null;
}
}

至此,完毕。

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

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()方法无论有没有原值,一律能覆盖原来的项,不返回任何。

请求量限制方法-使用本地Cache记录当前请求量[坑]的更多相关文章

  1. Ubuntu Linux系统三种方法添加本地软件库

    闲着没事教教大家以Ubuntu Linux系统三种方法添加本地软件库,ubuntu Linux使用本地软件包作为安装源——转2007-04-26 19:47新手重新系统的概率很高,每次重装系统后都要经 ...

  2. 30多条mysql数据库优化方法,千万级数据库记录查询轻松解决(转载)

    1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引. 2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索 ...

  3. 转载:30多条mysql数据库优化方法,千万级数据库记录查询轻松解决

    1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引. 2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索 ...

  4. 第45篇-查找native方法的本地实现函数native_function

    在之前介绍为native方法设置解释执行的入口时讲到过Method实例的内存布局,如下: 对于第1个slot来说,如果是native方法,其对应的本地函数的实现会放到Method实例的native_f ...

  5. JAVA方法和本地方法(转载)

    转载自:http://blog.sina.com.cn/s/blog_5b9b4abe01016zw0.html JAVA中有两种方法:JAVA方法和本地方法   JAVA方法是由JAVA编写的,编译 ...

  6. 错误 在类中找不到main方法请将main方法定义为 public static void main String args否则JavaFX应用程序类必须扩展javafx-ap

    最近在使用eclipse编写java程序时遇到这样一个问题: 错误在类中找不到main方法,请将main方法定义为 public static void main(String[] args)否则 J ...

  7. 错误: 在类中找不到 main 方法, 请将 main 方法定义为:public static void main(String[] args)否则 JavaFX 应用程序类必须扩展javafx.ap

    最近在使用eclipse编写java程序时遇到这样一个问题: 错误在类中找不到main方法,请将main方法定义为 public static void main(String[] args)否则 J ...

  8. 找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args) 否则 JavaFX 应用程序类必须扩展javafx.应用程序类必 须扩展javafx.application.Application”

    用eclipse写代码的时候,写了一个简单的程序,编译的时候突然出现“错误: 在类 com.test.demo 中找不到 main 方法, 请将 main 方法定义为: public static v ...

  9. java方法和本地方法

    java中的方法有两种,java方法和本地方法. java方法:是由java语言编写,编译成字节码,存储在class文件中的.java方法是与平台无关的. 本地方法:本地方法是由其他语言(如C.C++ ...

随机推荐

  1. HDU5812 Distance(枚举 + 分解因子)

    题目 Source http://acm.hdu.edu.cn/showproblem.php?pid=5812 Description In number theory, a prime is a ...

  2. sql over的作用及用法

    over不能单独使用,要和分析函数:rank(),dense_rank(),row_number()等一起使用.其参数:over(partition by columnname1 order by c ...

  3. BZOJ1894 : Srm444 avoidfour

    首先只有质数个$4$且个数不超过$10$的限制条件才有用, 也就是长度不能为$44,444,44444,4444444$的倍数. 考虑容斥,计算长度必须是它们$lcm$的倍数,且没有连续$4$个$4$ ...

  4. change,propertychange,input事件小议

    github上关于mootools一个issue的讨论很有意思,所以就想测试记录下.感兴趣的可以点击原页面看看. 这个问题来自IE(LTE8)中对checkbox和radio change事件的实现问 ...

  5. 种树 & 乱搞

    题意: 在一个(n+1)*(m+1)的网格点上种k棵树,树必须成一条直线,相邻两棵树距离不少于D,求方案数. SOL: 这题吧...巨坑无比,本来我的思路是枚举每一个从(0,0)到(i,j)的矩形,然 ...

  6. jquery.cookie.js使用

    1.下载jquery.cookie.js 官网:http://plugins.jquery.com/cookie/ 或 http://pan.baidu.com/s/1mgynA8g 2.使用方法 $ ...

  7. 关于CCSprite不能及时显示的问题

    今天在利用AFNetworking做网络请求时总是能看到添加的CCSprite精灵总是延迟一会才显示,google了半天没有找到答案, 考虑到CCSprite要被渲染才能显示,于是直接在场景中的CCL ...

  8. NOIp 2014 #4 无线网络发射器选址 Label:模拟

    题目描述 随着智能手机的日益普及,人们对无线网的需求日益增大.某城市决定对城市内的公共场所覆盖无线网. 假设该城市的布局为由严格平行的129 条东西向街道和129 条南北向街道所形成的网格状,并且相邻 ...

  9. 洛谷 P1967 货车运输 Label: 倍增LCA && 最小瓶颈路

    题目描述 A 国有 n 座城市,编号从 1 到 n,城市之间有 m 条双向道路.每一条道路对车辆都有重量限制,简称限重.现在有 q 辆货车在运输货物, 司机们想知道每辆车在不超过车辆限重的情况下,最多 ...

  10. 【JAVA】Spring 事物管理

            在Spring事务管理中通过TransactionProxyFactoryBean配置事务信息,此类通过3个重要接口完成事务的配置及相关操作,分别是PlatformTransactio ...