有一些需要限制次数的场景,比如api调用次数限制、在一段时间内只能使用几次的限制,在几秒内、几分钟时间内只能使用几次的限制。简单的实现可以把需要做限制的次数放在redis中,利用redis的特点进行限制。这里只是对本人的一些用法做个简单的小结。
1.在单位时间内只能使用N次的限制
常见于api调用次数限制,时间可以是1秒、1分钟、1小时,1天。其他规则的时间限制,需要自定义。这种的用法相对简单,直接用incr方法就可以实现。这里有个小细节,是先用get方法来获取key的值判断是否达到上限,还是直接用incr的返回值?这里我选择直接获取incr的值,因为先做get判断,之后再做incr操作,如果遇到并发,可能会造成脏读,当然也可以放在事务中实现。
key的构造是一个前缀+对应的时间格式。比如要求是每秒的限制,时间格式就设置为yyyyMMddHHmmss,如果是每1分钟的限制,时间格式就设置为yyyyMMddHHmm。如果是每5秒的限制呢?类似每秒的限制,但是设置时间点的时候,需要再计算,自己定义一个规则,比如取0秒、5秒、10秒,[0,5)秒取0,[5,10)取5,可以[0,5)秒取2,[5,10)取7,总之就是定义规则,判断时间点对应的区间,取区间的代表值,构造最后的key。最后等待key失效的策略清理过期的key。
/**
*
* @param key
* @param limitSeconds key有效期
* @param limitTimes 限制次数
* @return -1表示超过限制
*/
public Long incr(final String key, final int limitSeconds, final int limitTimes) {
if(StringUtils.isEmpty(key)) {
return 0L;
}
Long ret = 1L;
if (!redisTemplate.hasKey(key)) {
redisTemplate.opsForValue().increment(key, 1);
redisTemplate.expire(key, limitSeconds, TimeUnit.SECONDS);
return ret;
}
ret = redisTemplate.opsForValue().increment(key, 1);
return ret > limitTimes ? -1L : ret;
}
//调用,省略各种设置,每秒10次的限制
// RedisTest test = new RedisTest();
// if(test.incr("test_20171203170252", 5, 10) < 0){ // key 设置5秒过期
// System.out.println("超过限制");
// }
2.在最近单位时间内只能使用N次的限制
举个例子,最近1分钟内要求限制N次。
关于这个需求,脑子中第一种想到的方法就是利用keys 操作来实现。首先是构造key,直接一个前缀+时间戳,对应的值设置过期时间为1分钟,这样keys 前缀就可以得到1分钟内有效的个数。这种做法很简单,但是效率不高,如果遇到redis集群的情况,效率更低。曾经遇到过一次因为并发高了,用keys 获取数据导致cpu 100%的情况。
public Long incr2(final String key, final int limitSeconds, final int limitTimes) {
if(StringUtils.isEmpty(key)) {
return 0L;
}
long ret = -1L;
if ((ret = redisTemplate.keys(key + "*").size()) >= limitTimes) {
return -1L;
}
final String k = key + System.currentTimeMillis();
redisTemplate.execute(new RedisCallback<Object>() {
public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
redisConnection.setEx(k.getBytes(), limitSeconds, "1".getBytes());
return null;
}
});
return ret + 1;
}
之后在实际项目的测试环境中,redis使用了cluster,估计是环境的什么配置有问题,用keys 操作的时候,得不到想要的结果。想到redis中还有list这种数据结构,有种预感,应该可以用list实现这种限制,于是看了api,发现llen 还有lrange,突然脑洞一开,想到可以利用这几个命令来实现。通过lpush,把最久的数据放在最右边,通过lrange获取前N个数据,通过ltrim删除过期的数据,于是用list来实现的限制就完成了。没有对应的key时,lpush 并且设置过期时间。设置新的值,再更新一下key的有效期。代码中没做事务,就是简单的实现,有需要再简单加个事务实现就行。
public Long incr3(final String key, final int limitSeconds, final int limitTimes) {
if(StringUtils.isEmpty(key)) {
return 0L;
}
long ret = -1L;
long time = System.currentTimeMillis();
final String timestamp = "" + time;
if (!redisTemplate.hasKey(key)) {
redisTemplate.execute(new RedisCallback<Object>() {
public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
redisConnection.lPush(key.getBytes(), timestamp.getBytes());
redisConnection.expire(key.getBytes(), limitSeconds);
return null;
}
});
return 1L;
}
List<String> list = redisTemplate.opsForList().range(key, 0, limitTimes);
int t = 0;
// 倒序遍历,查找最后一个没过期的下标
for (int i = list.size() - 1; i >= 0; i--) {
if (Long.parseLong(list.get(i)) > time - limitSeconds * 1000) {
t = i;
break;
}
}
// 清除过期的list值
redisTemplate.opsForList().trim(key, 0, t);
if (t + 1 < limitTimes) {
redisTemplate.execute(new RedisCallback<Object>() {
public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
redisConnection.lPush(key.getBytes(), timestamp.getBytes());
redisConnection.expire(key.getBytes(), limitSeconds);
return null;
}
});
ret = t + 2;
} else {
ret = -1L;
}
return ret;
}
以上就是一些个人的用法总结,遇到其他的再继续完善。