一、需求
现在有需求如下:限制1秒中,每个用户最多访问10次后台接口
二、方案
1、方案一:
采用Redis String数据结构,以用户id为key,访问次数为value。过期时间为1s。
每次访问都使用INCR命令递增该键的键值,如果递增后的值为1(第一次访问),设置过期时间。这样每次访问先获取该键值,当键值超过100时,说明访问频率超过限制,返回“访问频率”。该键过期后,自动删除,所以下一个1s后,重新计数。
$isKeyExists = EXISTS rate.limiting:$userId // 存在返回 1,不存在返回 0
if $isKeyExists is 1
$times = INCR rate.limiting:$userId
if $times > 10 // 第10次访问会增加到11
print 访问过于频繁,请稍后再试
exit
else
MULTI // 开启一个事务
INCR rate.limiting:$userId
EXPIRE $keyName, 1
EXEC
缺点:如果用户在第1s的最后0.1s内访问了9次。在第2s的前0.1s内访问了9次,也就是0.2s内访问了18次,与需求不符,尽管这种情况比较极端,但仍然存在,如果要实现更小粒度的控制方式,需要采用方案二。
2、方案二
采用Redis List数据结构,以用户id为key,访问时间存入list为value。不设置过期时间。
用list类型的键,记录最近10次的访问时间。先比较list中的元素是否大于10,如果小于10直接将时间加入list中。如果大于10,就判断时间最早的元素(lindex为-1)距离现在的时间是否小于1s,如果是,则表示用户1s内的访问次数超过10次。如果不是就将当前时间加入list,同时把最早的元素删除。
伪代码如下:
$limitLength = LLEN rate.limiting:$userId
if $limitLength < 10
LPUSH rate.limiting:$userId, now()
else
$time = LINDEX rate.limiting:$userId, -1 // 取最后一个元素
if now() - $time < 1
print 访问频率超过限制,请稍后再试
else
LPUSH rate.limiting:$userId, now()
LTRIM rate.limiting:$userId, 0, 9 // 删除[0~9]以外的元素
缺点:由于拦截了每个接口,采用Redis会造成网络开销。每个接口的耗时大概为100ms左右。会影响整体的性能。
方案3:java进程内缓存
在java中实现进程内缓存,主要思想是,用1个map做缓存,缓存有个生效时间。过期就删除缓存。这里有2种删除策略,一种是起一个线程,定期删除过期的key,第二个是剔除模式,比较懒,访问到某个key时,才检查key是否过期。
这种思想类似于方案1。key为用户id,value为ValueWithExpireTime。ValueWithExpireTime包含访问次数和过期时间。
采用进程内缓存,可以避免网络开销,准确度要低一些,需求为限制1s内1个用户访问10次。我们部署两个结点,也就是2个JVM。则代码中的次数为5次。
因为恶意用户频繁访问毕竟是极少数,所以就采用了此方案。
ValueWithExpireTime类:
public class ValueWithExpireTime {
private long expireTime;
private int value;
public ValueWithExpireTime(long expireTime, int value) {
this.expireTime = expireTime;
this.value = value;
}
public long getExpireTime() {
return expireTime;
}
public void setExpireTime(long expireTime) {
this.expireTime = expireTime;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
expireTime:过期时间
value:访问次数
CacheMap类:
/**
* @author:lishuo 这里使用懒惰模式,获取key的时候,才剔除过期的key
* @Date:2018/12/3
* @Time:19:43
*/
@Service
public class CacheMap {
@Value("${service.access.maxAccessNumber}")
private Integer maxAccessNumber;
@Autowired
private AsyncService asyncService;
private Map<String, ValueWithExpireTime> cache = new ConcurrentHashMap<>();
public void put(String key, Integer value, long expireTime) {
ValueWithExpireTime valueWithTimeStamp = new ValueWithExpireTime(expireTime, value);
cache.put(key, valueWithTimeStamp);
if (cache.entrySet().size() > maxAccessNumber) {
asyncService.removeExpireKey(cache);
}
}
public ValueWithExpireTime get(String key) {
ValueWithExpireTime valueWithExpireTime = cache.get(key);
return valueWithExpireTime;
}
}