利用redis作次数限制的小结

有一些须要限制次数的场景,好比api调用次数限制、在一段时间内只能使用几回的限制,在几秒内、几分钟时间内只能使用几回的限制。简单的实现能够把须要作限制的次数放在redis中,利用redis的特色进行限制。这里只是对本人的一些用法作个简单的小结。java

1.在单位时间内只能使用N次的限制

常见于api调用次数限制,时间能够是1秒、1分钟、1小时,1天。其余规则的时间限制,须要自定义。这种的用法相对简单,直接用incr方法就能够实现。这里有个小细节,是先用get方法来获取key的值判断是否达到上限,仍是直接用incr的返回值?这里我选择直接获取incr的值,由于先作get判断,以后再作incr操做,若是遇到并发,可能会形成脏读,固然也能够放在事务中实现。redis

key的构造是一个前缀+对应的时间格式。好比要求是每秒的限制,时间格式就设置为yyyyMMddHHmmss,若是是每1分钟的限制,时间格式就设置为yyyyMMddHHmm。若是是每5秒的限制呢?相似每秒的限制,可是设置时间点的时候,须要再计算,本身定义一个规则,好比取0秒、5秒、10秒,[0,5)秒取0,[5,10)取5,能够[0,5)秒取2,[5,10)取7,总之就是定义规则,判断时间点对应的区间,取区间的表明值,构造最后的key。最后等待key失效的策略清理过时的key。api

/**
	 * 
	 * @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;
	}

以上就是一些我的的用法总结,遇到其余的再继续完善。spa

相关文章
相关标签/搜索