使用Redis限流

在最近接口的开发过程中,为了减小接口的压力和一些恶意的请求,做了接口限流,常用的限流算法有:令牌桶算法和漏桶算法。具体的原理这里不做分析了,可以谷歌一下。

大致实现原理就是访问者在一定的时间内对某一个接口只能访问多少次。

Redis实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static boolean accessLimit(String key, int limit, int time, Jedis jedis) {
boolean result = true;
key = "rate.limit:" + key;
if (jedis.exists(key)) {
long afterValue = jedis.incr(key);
if (afterValue > limit) {
result = false;
}
} else {
Transaction transaction = jedis.multi();
transaction.incr(key);
transaction.expire(key, time);
transaction.exec();
}
return result;
}

以上这种方式存在竟争条件,测试所得,产生结果不准确, 解决方法是用 WATCH 监控 rate.limit:key 的变动, 相对比较麻烦些。

Redis+Lua实现

  1. 创建rateLimit.lua脚本文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local key = "rate.limit:" .. KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = ARGV[2]

local is_exists = redis.call("EXISTS", key)
if is_exists == 1 then
if redis.call("INCR", key) > limit then
return 0
else
return 1
end
else
redis.call("SET", key, 1)
redis.call("EXPIRE", key, expire_time)
return 1
end
  1. Java 代码
  • springboot 配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Redis config
jedis:
pool:
host: 127.0.0.1
port: 6379
timeout: 10000
config:
maxTotal: 10000
minIdle: 100
maxIdle: 1000
testOnBorrow: true
maxWaitMillis: 10000
testOnReturn: true
testWhileIdle: true
timeBetweenEvictionRunsMillis: 10000
  • Redis配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Configuration
public class RedisConfig {

@Value("${jedis.pool.host}")
private String host;
@Value("${jedis.pool.port}")
private int port;
@Value("${jedis.pool.timeout}")
private int timeout;

@Value("${jedis.pool.config.maxTotal}")
private int maxTotal;
@Value("${jedis.pool.config.maxIdle}")
private int maxIdle;
@Value("${jedis.pool.config.minIdle}")
private int minIdle;
@Value("${jedis.pool.config.testOnBorrow}")
private boolean testOnBorrow,
@Value("${jedis.pool.config.testOnReturn}")
private boolean testOnReturn,
@Value("${jedis.pool.config.testWhileIdle}")
private boolean testWhileIdle, @Value("${jedis.pool.config.timeBetweenEvictionRunsMillis}")
private int timeBetweenEvictionRunsMillis,
@Value("${jedis.pool.config.maxWaitMillis}")
private int maxWaitMillis

@Bean(name = "jedis.pool")
@Autowired
public JedisPool shardedJedis(@Qualifier("jedis.pool.config")
JedisPoolConfig redisConfig) {

JedisPool pool = new JedisPool(redisConfig, host, port, timeout);
Jedis jedis = pool.getResource();
String ping = jedis.ping();
log.info("jedis.ping:" + ping + ", jedis server :" + host + ":" + port);
return pool;
}

@Bean(name = "jedis.pool.config")
public JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(maxTotal);
config.setMaxIdle(maxIdle);
config.setMinIdle(minIdle);
config.setTestOnBorrow(testOnBorrow);
config.setTestOnReturn(testOnReturn);
config.setTestWhileIdle(testWhileIdle);
config.setMaxWaitMillis(maxWaitMillis);
config.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
return config;
}
}
  • Redis工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* redis 限流
* @param key key
* @param limit 限制次数
* @param timeout 限制时间(秒)
* @return
* @throws IOException
*/
public boolean accessLimit(String key, int limit, int timeout) throws IOException {
long result = 0;
Jedis jedis = getResource();
try {
List<String> keys = Collections.singletonList(key);
List<String> argv = Arrays.asList(String.valueOf(limit),
String.valueOf(timeout));
//加载lua脚本
Reader reader = new InputStreamReader(
Client.class.getClassLoader().getResourceAsStream("rateLimit.lua"));
result = (long) jedis.eval(CharStreams.toString(reader), keys, argv);
} catch (IOException e) {
e.printStackTrace();
}finally {
returnBrokenResource(jedis);
}
return 1 == result;
}
  • 注解类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 限流注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RateLimit {
/**
* 时间限制
*/
int seconds() default 0;
/**
* 次数限制
*/
int maxCount() default 100;
}
  • 控制层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping(value = "/user", produces = "application/json;charset=utf-8")
public class UserController {

@RateLimit(seconds = 2, maxCount = 5)
@PostMapping("/getUserInfo")
public RespData getUserInfo(@RequestBody String rawData) throws Exception {
if (StringUtil.isEmpty(rawData)) {
return RespData.errorMsg(RetCode.ERROR_PARAMS_NOT_NULL.getCode(), "用户信息不能为空");
}
RespData data = userService.save(rawData);
return data;
}
}
  • 拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Component
public class AuthInterceptor implements HandlerInterceptor {

@Autowired
private RedisUtil redisUtil;

@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object o) throws Exception {
String methodName = null;
if (o instanceof HandlerMethod) {
HandlerMethod handler = (HandlerMethod) o;
String className = handler.getBean().getClass().getName();
methodName = handler.getMethod().getName();
//每个用户请求每个方法在限定的时间内限定请求次数
String key = className + "." + methodName + "_" + openId;
RateLimit limit = handler.getMethodAnnotation(RateLimit.class);
if (limit != null) {
int seconds = limit.seconds();
int maxCount = limit.maxCount();
boolean result = redisUtil.accessLimit(key, maxCount, seconds);
if (result) {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
RespData data = new RespData();
data.setCode(RetCode.ACTIVE_FAILURE.getCode());
data.setMsg("请求太频繁");
log.info("请求太频繁----wechat-imin");
out.write(JSON.toJSONString(data));
out.flush();
out.close();
return false;
}
}
}
}
}

以上是基于最近开发的项目的实现过程,具体还得根据自己的相关业务实现。

Lua 嵌入 Redis 优势:

  • 减少网络开销,不使用 Lua 的代码需要向 Redis 发送多次请求, 而脚本只需一次即可, 减少网络传输;
  • 原子操作: Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务;
  • 复用: 脚本会永久保存 Redis 中, 其他客户端可继续使用.

参考:Redis两种方式实现限流

原文作者: dgb8901,yinxing

原文链接: https://www.itwork.club/2018/09/29/redis-ratelimit/

版权声明: 转载请注明出处

为您推荐

体验小程序「简易记账」

关注公众号「特想学英语」

springboot+shiro+JWT 系统权限管理