接口幂等性与限流(二)
下面介绍几种接口限流的方式:
1. 基于自定义注解+Redis实现限流
- 自定义RateLimit注解
import java.lang.annotation.*;@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {/*** 限制时间段,单位为秒*/int time() default 60;/*** 在限制时间段内允许的最大请求次数*/int count() default 10;/*** 限流的key,支持SpEL表达式*/String key() default "";/*** 提示信息*/String message() default "操作太频繁,请稍后再试";
}
- 自定义RateLimitAspect切面
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;@Aspect
@Component
@Slf4j
public class RateLimitAspect {@Autowiredprivate StringRedisTemplate redisTemplate;@Around("@annotation(rateLimit)")public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {// 获取请求的方法名String methodName = pjp.getSignature().getName();// 获取请求的类名String className = pjp.getTarget().getClass().getSimpleName();// 组合限流keyString limitKey = getLimitKey(pjp, rateLimit, methodName, className);// 获取限流参数int time = rateLimit.time();int count = rateLimit.count();// 执行限流逻辑boolean limited = isLimited(limitKey, time, count);if (limited) {throw new RuntimeException(rateLimit.message());}// 执行目标方法return pjp.proceed();}private String getLimitKey(ProceedingJoinPoint pjp, RateLimit rateLimit, String methodName, String className) {// 获取用户自定义的keyString key = rateLimit.key();if (StringUtils.hasText(key)) {// 支持SpEL表达式解析StandardEvaluationContext context = new StandardEvaluationContext();log.error("context:{}", context);MethodSignature signature = (MethodSignature) pjp.getSignature();log.error("signature:{}", signature);String[] parameterNames = signature.getParameterNames();log.error("parameterNames:{}", parameterNames);Object[] args = pjp.getArgs();log.error("args:{}", args);for (int i = 0; i < parameterNames.length; i++) {context.setVariable(parameterNames[i], args[i]);log.error("context new:{}", context);}ExpressionParser parser = new SpelExpressionParser();Expression expression = parser.parseExpression(key);log.error("expression:{}", expression);log.error(" finally context:{}", context);key = expression.getValue(context, String.class);} else {// 默认使用类名+方法名+IP地址作为keyHttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();log.error("request:{}", request);String ip = IpUtils.getIpAddress(request);key = ip + ":" + className + ":" + methodName;}return "rate_limit:" + key;}private boolean isLimited(String key, int time, int count) {// 使用Redis的计数器实现限流try {Long currentCount = redisTemplate.opsForValue().increment(key, 1);// 如果是第一次访问,设置过期时间if (currentCount == 1) {redisTemplate.expire(key, time, TimeUnit.SECONDS);}return currentCount > count;} catch (Exception e) {log.error("限流异常", e);return false;}}
}
- IpUtils工具类
@Slf4j
public class IpUtils {public static String getIpAddress(HttpServletRequest request) {String ip = request.getHeader("X-Forwarded-For");log.error("ip:{}", ip);try {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-Proxy-Client-IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("HTTP_CLIENT_IP");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getHeader("HTTP_X_FORWARDED_FOR");}if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}} catch (Exception e) {log.error("getIpAddress ERROR", e);}return ip;}
}
- UserController
@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {@Autowiredprivate IUserService userService;@Autowiredprivate UserMapper userMapper;@RateLimit(time = 10, count = 3, message = "请求太频繁,请稍后再试")@GetMapping("/getUserById")public User getUserById(@RequestParam String id) {return userService.getBaseMapper().selectById(id);}//使用SpEL表达式指定key@RateLimit(time = 60, count = 1, key = "#userInfo.id + '_' + #request.remoteAddr")@PostMapping("/userInfo")public int updateUser(@RequestBody UserInfo userInfo, HttpServletRequest request) {return userDetailMapper.updateById(userInfo);}
}
2. Sentinel限流
- 引入依赖
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId><version>2021.0.4.0</version></dependency>
- SentinelConfig配置类:定义限流规则
@Configuration
public class SentinelConfig {@Beanpublic SentinelResourceAspect sentinelResourceAspect() {return new SentinelResourceAspect();}@PostConstructpublic void init() {// 定义流控规则initFlowRules();}private void initFlowRules() {List<FlowRule> rules = new ArrayList<>();// 为/user/getUserById接口设置流控规则FlowRule userRule = new FlowRule();userRule.setResource("/userDetail/getUserById");userRule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 基于QPS限流userRule.setCount(10); // 每秒允许10个请求rules.add(userRule);// 为/api/order接口设置流控规则FlowRule orderRule = new FlowRule();orderRule.setResource("/api/order");orderRule.setGrade(RuleConstant.FLOW_GRADE_QPS);orderRule.setCount(5); // 每秒允许5个请求orderRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP); // 预热模式orderRule.setWarmUpPeriodSec(10); // 10秒预热期rules.add(orderRule);// 加载规则FlowRuleManager.loadRules(rules);}
}
- 全局异常处理器
@RestControllerAdvice
public class SentinelExceptionHandler {@ExceptionHandler(BlockException.class)public String handleBlockException(BlockException e) {String message = "请求过于频繁,请稍后再试";if (e instanceof FlowException) {message = "接口限流:" + message;} else if (e instanceof DegradeException) {message = "服务降级:系统繁忙,请稍后再试";} else if (e instanceof ParamFlowException) {message = "热点参数限流:请求过于频繁";} else if (e instanceof SystemBlockException) {message = "系统保护:系统资源不足";} else if (e instanceof AuthorityException) {message = "授权控制:没有访问权限";}return 429 + message;}
}
- UrlCleaner 类:自定义URL清理逻辑
@Component
public class UrlCleaner implements RequestOriginParser {@Overridepublic String parseOrigin(HttpServletRequest request) {// 获取请求的URL路径String path = request.getRequestURI();// 可以添加更复杂的解析逻辑,例如:// 1. 去除路径变量:/api/user/123 -> /api/user/{id}// 2. 添加请求方法前缀:GET:/api/userreturn path;}
}
- UserDetailController
@RestController
@Slf4j
@RequestMapping("/userDetail")
public class UserDetailController {@Autowiredprivate IUserDetailService userDetailService;@Autowiredprivate UserDetailMapper userDetailMapper;@GetMapping("/getUserById")// 使用资源名定义限流资源@SentinelResource(value = "/userDetail/getUserById", blockHandler = "getUserBlockHandler",fallback = "getUserFallback") public UserDetail getUserById(@RequestParam String id) {return userDetailService.getBaseMapper().selectById(id);}// 限流处理方法public UserDetail getUserBlockHandler(String id, BlockException e) {log.warn("Get user request blocked: {}", id, e);throw new RuntimeException("请求频率过高,请稍后再试");}// 异常回退方法public UserDetail getUserFallback(String id, Throwable t) {log.error("Get user failed: {}", id, t);UserDetail fallbackUser = new UserDetail();fallbackUser.setId(id);fallbackUser.setName("Unknown");return fallbackUser;}
}
若@SentinelResource
注解指定了blockHandler
和fallback
,则走相应的限流处理逻辑和异常回退逻辑;若没有指定,则会触发全局异常处理器