有一些须要限制次数的场景,好比api调用次数限制、在一段时间内只能使用几回的限制,在几秒内、几分钟时间内只能使用几回的限制。简单的实现能够把须要作限制的次数放在redis中,利用redis的特色进行限制。这里只是对本人的一些用法作个简单的小结。java
常见于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("超过限制"); // }
举个例子,最近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