点击上方“linkoffer”,
选择关注公众号高薪职位第一时间送达
参考资料
https://dwz.cn/Gvviwswi
https://dwz.cn/pO9mWjhq
简介
采用了redis来作为限流器的实现
redis作为高性能缓存系统,性能上能够满足多机之间高并发访问的要求
redis有比较好的api来支持限流器令牌桶算法的实现
对于我们的系统来说,通过spring data redis来操作比较简单和常见,避免了引入新的中间件带来的风险
但是我们也知道,限流器在每次请求令牌和放入令牌操作中,存在一个协同的问题,即获取令牌操作要尽可能保证原子性,否则无法保证限流器是否能正常工作。在RateLimiter的实现中使用了mutex作为互斥锁来保证操作的原子性,那么在redis中就需要一个类似于事务的机制来保证获取令牌中多重操作的原子性。 面对这样的需求,我们有几个选择:
用redis实现分布式锁来保证操作的原子性,这个方案实现起来应该比较简单,分布式锁有现成的例子,然后就是把Rate Limiter的代码套用分布式锁就行了,但是这样的话效率会显得不太高,特别是在大量访问的情况下。
用redis的transaction,在我查阅redis官方文档和stackoverflow之后发现redis的transaction官方并不推荐,并且有可能在未来取消事务,因此不可取。
通过redis分布式锁和本地锁组成一个双层结构,每次分布式获取锁之后可以预支一部分令牌量,然后放到本地通过本地的锁来分配这些令牌,消耗完之后再到请求redis。这样的好处是相比第一个方案,网络访问延迟开销会比较好,但是实现难度和复杂程度比较难估量,而且这样的做法如果在多机不能保证均匀分配流量的情况下并不理想
通过将获取锁封装到lua脚本中,提交给redis进行eval和evalsha操作来完成lua脚本的执行,由于lua脚本在redis中天然的原子性,我们的需求能够比较好的满足,问题是将业务逻辑封装在lua中,对于开发人员自身的能力和调试存在一定的问题。
经过权衡,我采用了第四种方式,通过redis和lua来编写令牌桶算法来完成分布式限流的需求。
项目实战
本项目基于SpringBoot 2.1.5,使用到 Redis + Lua 限流脚本
一. 引入依赖 pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.1.5.RELEASE
com.xd
redis-lua-limit
0.0.1-SNAPSHOT
redis-lua-limit
基于Spring Boot Redis+Lua高并发限流
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-redis
org.springframework.boot
spring-boot-starter-aop
org.apache.commons
commons-lang3
org.springframework.boot
spring-boot-starter-test
org.projectlombok
lombok
1.18.8
org.springframework.boot
spring-boot-maven-plugin
二. application.properties
spring.application.name=spring-boot-redis-lua-limit
# Redis数据库索引 默认为0
spring.redis.database=0
# Redis地址
spring.redis.host=localhost
# Redis端口 默认6379
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.jedis.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.jedis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.jedis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.jedis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=10000
三. Lua 脚本
--Lua脚本
--- 限流KEY资源唯一标识
local key = "rate.limit:" .. KEYS[1]
--- 时间窗最大并发数
local limit = tonumber(ARGV[1])
--- 时间窗内当前并发数
local current = tonumber(redis.call('get', key) or "0")
--如果超出限流大小
if current + 1 > limit then
return 0
else --请求数+1,并设置2秒过期
redis.call("INCRBY", key,"1")
redis.call("expire", key,"2")
return current + 1
end
--IP限流Lua脚本
--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、我们通过KEYS[1] 获取传入的key参数 2、通过ARGV[1]获取传入的limit参数 3、redis.call方法,从缓存中get和key相关的值,如果为nil那么就返回0 4、接着判断缓存中记录的数值是否会大于限制大小,如果超出表示该被限流,返回0 5、如果未超过,那么该key的缓存值+1,并设置过期时间为1秒钟以后,并返回缓存值+1
三. 注解
自定义注解的目的,是在需要限流的方法上使用
package com.xd.redislualimit.annotation;
import java.lang.annotation.*;
/**
* @Author 李号东
* @Description 限流注解
* @Date 17:49 2019-05-25
**/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
/**
* 限流唯一标示
*
* @return
*/
String key() default "";
/**
* 限流时间
*
* @return
*/
int time();
/**
* 限流次数
*
* @return
*/
int count();
}
四. 配置
package com.xd.redislualimit.config;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Component;
import java.io.Serializable;
/**
* @Classname commons
* @Description 配置
* @Author 李号东 lihaodongmail@163.com
* @Date 2019-05-25 20:13
* @Version 1.0
*/
@Component
public class commons {
/**
* 读取限流脚本
*
* @return
*/
@Bean
public DefaultRedisScript<Number> redisluaScript() {
DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("redisLimit.lua")));
//返回类型
redisScript.setResultType(Number.class);
return redisScript;
}
/**
* RedisTemplate
*
* @return
*/
@Bean
public RedisTemplate<String, Serializable> limitRedisTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
五. 限流注解拦截器
package com.xd.redislualimit.config;
import com.xd.redislualimit.annotation.RateLimit;
import com.xd.redislualimit.utils.IPUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* @Classname LimitAspect
* @Description 注解拦截
* @Author 李号东 lihaodongmail@163.com
* @Date 2019-05-25 20:15
* @Version 1.0
*/
@Slf4j
@Aspect
@Configuration
public class LimitAspect {
@Autowired
private RedisTemplate<String, Serializable> redisTemplate;
@Autowired
private DefaultRedisScript<Number> redisluaScript;
//执行redis的具体方法,限制method,保证没有其他的东西进来
@Around("execution(* com.xd.redislualimit.controller ..*(..) )")
public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Class> targetClass = method.getDeclaringClass();
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
if (rateLimit != null) {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String ipAddress = IPUtil.getIp(request);
String string = ipAddress + "-" + targetClass.getName() + "- " + method.getName() + "-" + rateLimit.key();
List<String> keys = Collections.singletonList(string);
Number number = redisTemplate.execute(redisluaScript, keys, rateLimit.count(), rateLimit.time());
if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {
log.info("限流时间段内访问第:{} 次", number.toString());
return joinPoint.proceed();
}
} else {
return joinPoint.proceed();
}
log.error("已经到设置限流次数");
throw new RuntimeException("已经到设置限流次数");
}
}
六. 控制层
package com.xd.redislualimit.controller;
import com.xd.redislualimit.annotation.RateLimit;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.support.atomic.RedisAtomicInteger;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
/**
* @Classname LimiterController
* @Description 测试控制层
* @Author 李号东 lihaodongmail@163.com
* @Date 2019-05-25 20:16
* @Version 1.0
*/
@RestController
public class LimiterController {
@Autowired
private RedisTemplate redisTemplate;
// 10 秒中,可以访问5次
@RateLimit(key = "test", time = 10, count = 5)
@GetMapping("/test")
public String luaLimiter() {
// 简单测试方法
RedisAtomicInteger entityIdCounter = new RedisAtomicInteger("counter", redisTemplate.getConnectionFactory());
String date = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss.SSS");
return date + " 累计访问次数:" + entityIdCounter.getAndIncrement();
}
}
七. 测试
启动程序 打开浏览器访问 http://localhost:8080/test
连续访问5次 控制台打印
限流成功!
项目地址: https://github.com/LiHaodong888/SpringBootLearn/