利用redis做次数限制的小结

有一些需要限制次数的场景,比如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;
	}

以上就是一些个人的用法总结,遇到其他的再继续完善。

转载于:https://my.oschina.net/u/2274874/blog/1584144

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值