Java语言环境下使用redis进行限流
- 啥也不说先上代码
- 分析 lua 和 Pipeline 两种方式优缺点
- 补充说明 redis 的zset 结构
啥也不说先上代码
/**
* 基于redis做的 滑动窗口限流
*
* @param key redis的key
* @param period 时间段(秒),比如: 限流60(period)秒内, 不能超过100(maxCount)次
* @param maxCount 最大运行访问次数
* @return bool true表示放行,false表示被限制未放行
* @author wuqiong 2022/3/10 14:31
*/
public boolean isAllowed(String key, int period, long maxCount) {
// 方式一: 使用pipeline 实现
try (Jedis jedis = jedisPool.getResource()) {
long now = System.currentTimeMillis();
// 1、管道一
Pipeline pipeline = jedis.pipelined();
pipeline.zremrangeByScore(key, 0, now - period * 1000);
Response<Long> countResponse = pipeline.zcard(key);
pipeline.close();
if (countResponse.get() >= maxCount) return false; // 直接返回失败,被限制了
// 2、管道二
Pipeline pip = jedis.pipelined();
pip.zadd(key, now, now + "Random"); // 假装一个随机数,各位看官老爷请自己实现. (主要是防止极端情况下时间戳也会重复的问题)
pip.expire(key, period);// 设置过期时间
pip.close();
return true;
} catch (Exception ex) {
log.error("[Pipeline]滑动窗口限流失败", ex.getMessage());
}
// 方式二: 使用lua 脚本
try (Jedis jedis = jedisPool.getResource()) {
String script = "redis.call('zremrangeByScore', KEYS[1], 0, ARGV[1])\n" +
"local res = redis.call('zcard', KEYS[1])\n" +
"if res and (tonumber(res) < tonumber(ARGV[4])) then\n" +
" redis.call('zadd', KEYS[1], ARGV[2], ARGV[3])\n" +
" redis.call('expire',KEYS[1],ARGV[5]) \n" +
" return 1\n" +
"else return 0 end\n";
long now = System.currentTimeMillis();
String args1 = "" + (now - period * 1000);
String args2 = "" + now;
String args3 = now + "Random"; // 假装一个随机数,各位看官老爷请自己实现. (主要是防止极端情况下时间戳也会重复的问题)
String args4 = "" + maxCount; // 最大次数
String args5 = "" + period; // 过期时间
Object eval = jedis.eval(script, Arrays.asList(key), Arrays.asList(args1, args2, args3, args4, args5));
return eval.equals(1L);
} catch (Exception ex) {
log.error("[lua]滑动窗口限流失败", ex.getMessage());
}
return false;
}
此处楼主展示了 基于 Lua 和 Pipeline 两种方式,使用zset结构进行的滑动窗口限流。
该限流方式相较于使用 setnx 要好一些,因为使用setnx 限流可能会出现一个bug , 比如: 每60秒限100次访问,在59秒访问了100次,那么下一分钟的0秒再访问100次,综合来看,就是两秒钟对程序访问了200次,因此这种限流方式是不科学的。
分析 lua 和 Pipeline 两种方式优缺点
1、先说结论,楼主更推荐使用lua脚本
2、在使用 pipeline 时可以明显的看到需要开启两次 管道,并不能在一次内完成
3、恰好当第一个管道执行完毕后待执行第二个管道时,其他线程可能会进入造成影响(这是一个比较极端罕见的情况)
4、但 Lua 脚本则不存在此问题,可在一次原子性的操作内完成整个流程,不给其他线程入侵的机会
5、经楼主测试,在网络环境较弱的情况下,Lua 脚本相较于 Pipeline 管道有明显的优势。
补充说明 redis 的zset 结构
1、楼主此处对 zset 结构的member 设置了和 score 相同的时间戳值,此时member字段无意义,只是存了一个值而已, 如下图: