请求量限制方法-使用本地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 本地缓存。详见代码(该账户的最大请求量已获取到,若获取失败则读配置:
/// <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记录当前请求量[坑]的更多相关文章
- Ubuntu Linux系统三种方法添加本地软件库
闲着没事教教大家以Ubuntu Linux系统三种方法添加本地软件库,ubuntu Linux使用本地软件包作为安装源——转2007-04-26 19:47新手重新系统的概率很高,每次重装系统后都要经 ...
- 30多条mysql数据库优化方法,千万级数据库记录查询轻松解决(转载)
1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引. 2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索 ...
- 转载:30多条mysql数据库优化方法,千万级数据库记录查询轻松解决
1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引. 2.应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索 ...
- 第45篇-查找native方法的本地实现函数native_function
在之前介绍为native方法设置解释执行的入口时讲到过Method实例的内存布局,如下: 对于第1个slot来说,如果是native方法,其对应的本地函数的实现会放到Method实例的native_f ...
- JAVA方法和本地方法(转载)
转载自:http://blog.sina.com.cn/s/blog_5b9b4abe01016zw0.html JAVA中有两种方法:JAVA方法和本地方法 JAVA方法是由JAVA编写的,编译 ...
- 错误 在类中找不到main方法请将main方法定义为 public static void main String args否则JavaFX应用程序类必须扩展javafx-ap
最近在使用eclipse编写java程序时遇到这样一个问题: 错误在类中找不到main方法,请将main方法定义为 public static void main(String[] args)否则 J ...
- 错误: 在类中找不到 main 方法, 请将 main 方法定义为:public static void main(String[] args)否则 JavaFX 应用程序类必须扩展javafx.ap
最近在使用eclipse编写java程序时遇到这样一个问题: 错误在类中找不到main方法,请将main方法定义为 public static void main(String[] args)否则 J ...
- 找不到 main 方法, 请将 main 方法定义为: public static void main(String[] args) 否则 JavaFX 应用程序类必须扩展javafx.应用程序类必 须扩展javafx.application.Application”
用eclipse写代码的时候,写了一个简单的程序,编译的时候突然出现“错误: 在类 com.test.demo 中找不到 main 方法, 请将 main 方法定义为: public static v ...
- java方法和本地方法
java中的方法有两种,java方法和本地方法. java方法:是由java语言编写,编译成字节码,存储在class文件中的.java方法是与平台无关的. 本地方法:本地方法是由其他语言(如C.C++ ...
随机推荐
- JAVA StringBuilder StringBuffer String比较
字符串连接时的效率: StringBuilder>StringBuffer>String 区别: StringBuilder效率最高,但是不是线程安全的,适用于单线程.多线程用String ...
- mysql case then使用
表的创建CREATE TABLE `lee` (`id` int(10) NOT NULL AUTO_INCREMENT, `name` char(20) DEFAULT NULL, `birthda ...
- 洛谷 P1449 后缀表达式 Label:表达式计算系列
题目描述 所谓后缀表达式是指这样的一个表达式:式中不再引用括号,运算符号放在两个运算对象之后,所有计算按运算符号出现的顺序,严格地由左而右新进行(不用考虑运算符的优先级). 如:3*(5–2)+7对应 ...
- [知识点]Trie树和AC自动机
// 此博文为迁移而来,写于2015年5月27日,不代表本人现在的观点与看法.原始地址:http://blog.sina.com.cn/s/blog_6022c4720102w1s8.html 1.前 ...
- 匈牙利 算法&模板
匈牙利 算法 一. 算法简介 匈牙利算法是由匈牙利数学家Edmonds于1965年提出.该算法的核心就是寻找增广路径,它是一种用增广路径求二分图最大匹配的算法. 二分图的定义: 设G=(V,E)是一个 ...
- UVA 10780 - Again Prime? No Time.
题目链接 思路好想,注意细节.错了很多次. #include <cstdio> #include <cstring> #include <string> #incl ...
- POJ 2420 A Star not a Tree?(模拟退火)
题目链接 居然1Y了,以前写的模拟退火很靠谱啊. #include <cstdio> #include <cstring> #include <string> #i ...
- Crystal Reports 2008(水晶报表) 安装
这篇blog主要是介绍Crystal Reports2008(水晶报表)的安装. 首先我们应该知道Crystal Reports 有什么作用? 从这里Crystal Reports 你可以了解到它的一 ...
- GNU for x86汇编语法
作者:冯老师,华清远见嵌入式学院讲师. 译自“Using as The GNU Assembler January 1994”. 参考Tornado随机文档“GNU Toolkit User's Gu ...
- Hightcharts动态创建series
第一种方法: 申明options时动态设置series,然后再创建chart对象 代码如下: <html> <head> <title>Highcharts Exa ...