分布式限流最关键的是要将限流服务做成原子化,而解决方案可以使用 redis+lua 或者 nginx+lua 技术进行实现,通过这两种技术可以实现的高并发和高性能。
首先我们来使用 redis+lua 实现时间窗内某个接口的请求数限流,实现了该功能后可以改造为限流总并发/请求数和限制总资源数。lua 本身就是一种编程语言,也可以使用它实现复杂的令牌桶或漏桶算法。
因操作是在一个 lua 脚本中(相当于原子操作),又因 redis 是单线程模型,因此是线程安全的。
相比 redis 事务来说,lua 脚本有以下优点
减少网络开销:不使用 lua 的代码需要向 redis 发送多次请求,而脚本只需一次即可,减少网络传输;
原子操作:redis 将整个脚本作为一个原子执行,无需担心并发,也就无需事务;
复用:脚本会永久保存 redis 中,其他客户端可继续使用。
package com.example.demo.common.annotation;
import com.example.demo.common.enums.LimitType;
import java.lang.annotation.*;
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisLimit {
// 资源名称
String name() default "";
// 资源key
String key() default "";
// 前缀
String prefix() default "";
// 时间
int period();
// 最多访问次数
int count();
// 类型
LimitType limitType() default LimitType.CUSTOMER;
// 提示信息
String msg() default "系统繁忙,请稍后再试";
}
创建注解 RedisLimit
package com.example.demo.common.aspect;
import com.example.demo.common.annotation.RedisLimit;
import com.example.demo.common.enums.LimitType;
import com.example.demo.common.exception.LimitException;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
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.lang.reflect.Method;
import java.util.Objects;
@Slf4j
@Aspect
@Configuration
public class RedisLimitAspect {
private final RedisTemplate<String, Object> redisTemplate;
public RedisLimitAspect(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Around("@annotation(com.example.demo.common.annotation.RedisLimit)")
public Object around(ProceedingJoinPoint pjp){
MethodSignature methodSignature = (MethodSignature)pjp.getSignature();
Method method = methodSignature.getMethod();
RedisLimit annotation = method.getAnnotation(RedisLimit.class);
LimitType limitType = annotation.limitType();
String name = annotation.name();
String key;
int period = annotation.period();
int count = annotation.count();
switch (limitType){
case IP:
key = getIpAddress();
break;
case CUSTOMER:
key = annotation.key();
break;
default:
key = StringUtils.upperCase(method.getName());
}
ImmutableList<String> keys = ImmutableList.of(StringUtils.join(annotation.prefix(), key));
try {
String luaScript = buildLuaScript();
DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number number = redisTemplate.execute(redisScript, keys, count, period);
log.info("Access try count is {} for name = {} and key = {}", number, name, key);
if(number != null && number.intValue() == 1){
return pjp.proceed();
}
throw new LimitException(annotation.msg());
}catch (Throwable e){
if(e instanceof LimitException){
log.debug("令牌桶={},获取令牌失败",key);
throw new LimitException(e.getLocalizedMessage());
}
e.printStackTrace();
throw new RuntimeException("服务器异常");
}
}
public String buildLuaScript(){
return "redis.replicate_commands(); local listLen,time" +
"\nlistLen = redis.call('LLEN', KEYS[1])" +
// 不超过最大值,则直接写入时间
"\nif listLen and tonumber(listLen) < tonumber(ARGV[1]) then" +
"\nlocal a = redis.call('TIME');" +
"\nredis.call('LPUSH', KEYS[1], a[1]*1000000+a[2])" +
"\nelse" +
// 取出现存的最早的那个时间,和当前时间比较,看是小于时间间隔
"\ntime = redis.call('LINDEX', KEYS[1], -1)" +
"\nlocal a = redis.call('TIME');" +
"\nif a[1]*1000000+a[2] - time < tonumber(ARGV[2])*1000000 then" +
// 访问频率超过了限制,返回0表示失败
"\nreturn 0;" +
"\nelse" +
"\nredis.call('LPUSH', KEYS[1], a[1]*1000000+a[2])" +
"\nredis.call('LTRIM', KEYS[1], 0, tonumber(ARGV[1])-1)" +
"\nend" +
"\nend" +
"\nreturn 1;";
}
public String getIpAddress(){
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String ip = request.getHeader("x-forwarded-for");
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
ip = request.getHeader("Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
ip = request.getHeader("WL-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
ip = request.getRemoteAddr();
}
return ip;
}
}
//注解 aop 实现```
//注解使用
//count 代表请求总数量
//period 代表限制时间
//即 period 时间内,只允许有 count 个请求总数量访问,超过的将被限制不能访问
package com.example.demo.module.test;
import com.example.demo.common.annotation.Limit;
import com.example.demo.common.annotation.RedisLimit;
import com.example.demo.common.dto.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@Slf4j
@RestController
public class TestController {
@RedisLimit(key = "cachingTest", count = 2, period = 2, msg = "当前排队人数较多,请稍后再试!")
// @Limit(key = "cachingTest", permitsPerSecond = 1, timeout = 500, msg = "当前排队人数较多,请稍后再试!")
@GetMapping("cachingTest")
public R cachingTest(){
log.info("------读取本地------");
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
return R.ok(list);
}
}