什么是限流
限流是一种控制系统访问频率的技术手段,就像高速公路的收费站控制车流量一样。
生活场景类比:
- 银行atm机:每张卡每天最多取款5次
- 手机验证码:每个手机号每分钟最多发送1条
- 网站登录:每个ip每分钟最多尝试5次
技术价值:
- 防止恶意攻击:阻止暴力破解、恶意爬虫
- 保护系统稳定:避免瞬间大量请求压垮服务器
- 提升用户体验:确保正常用户的访问质量
- 节约成本:减少不必要的资源消耗
系统架构
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 用户请求 │───→│ 限流切面 │───→│ 业务接口 │
│ (http api) │ │ (aop拦截) │ │ (controller) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ 限流服务 │
│ (核心逻辑处理) │
└─────────────────┘
│
▼
┌─────────────────┐
│ 缓存存储 │
│ (ehcache/redis) │
└─────────────────┘工作流程:
- 用户发起http请求
- spring aop切面拦截带有@ratelimiter注解的方法
- 限流服务根据注解配置生成限流键
- 从缓存中获取当前访问次数
- 判断是否超过限制,决定放行或拒绝
- 更新缓存中的计数器
核心组件详解
1. 限流注解 (@ratelimiter)
这是系统的核心注解,定义了限流的各种参数:
package cn.jbolt.config.anno.ratelimiter;
import org.springframework.core.annotation.aliasfor;
import java.lang.annotation.elementtype;
import java.lang.annotation.retention;
import java.lang.annotation.retentionpolicy;
import java.lang.annotation.target;
@retention(retentionpolicy.runtime)
@target({elementtype.type, elementtype.method})
public @interface ratelimiter {
/**
* 缓存前缀 - 用于区分不同业务的限流数据
*/
string prefix() default "jblimit:";
/**
* 时间窗口(秒) - 限流的时间范围
*/
int time() default 60;
/**
* 允许访问次数 - 时间窗口内最大访问次数
*/
@aliasfor(attribute = "count")
int value() default 12;
/**
* 限制类型 - 决定按什么维度限流
*/
ratelimittype limittype() default ratelimittype.default;
/**
* 限制提示消息 - 触发限流时返回的错误信息
*/
string msg() default "操作过于频繁,请稍后重试";
/**
* 允许访问次数 - 与value互为别名
*/
@aliasfor(attribute = "value")
int count() default 12;
/**
* 自定义键 - 当limittype为custom时使用
*/
string customkey() default "";
/**
* 是否启用 - 可用于动态开关限流功能
*/
boolean enabled() default true;
/**
* 额外的时间窗口限制(秒)
* 实现双重限流:比如1秒最多1次 + 1分钟最多10次
*/
int extratime() default -1;
/**
* 额外时间窗口内的允许访问次数
*/
int extracount() default -1;
/**
* 额外限制的提示消息
*/
string extramsg() default "";
}2. 限流类型枚举 (ratelimittype)
package cn.jbolt.config.anno.ratelimiter;
public enum ratelimittype {
/**
* 默认限制(全局)
* 所有请求共享一个计数器
*/
default,
/**
* 基于ip地址限制
* 每个ip独立计数
*/
ip,
/**
* 基于用户id限制
* 每个登录用户独立计数
*/
user,
/**
* 基于自定义key限制
* 根据业务逻辑自定义限流维度
*/
custom
}3. 限流异常类 (ratelimitexception)
package cn.jbolt.config.exception;
public class ratelimitexception extends runtimeexception {
private final string message;
private final int retryafter;
public ratelimitexception(string message) {
this(message, 0);
}
public ratelimitexception(string message, int retryafter) {
super(message);
this.message = message;
this.retryafter = retryafter;
}
@override
public string getmessage() {
return message;
}
public int getretryafter() {
return retryafter;
}
}4. 全局异常处理器 (ratelimitexceptionhandler)
package cn.jbolt.config.handler;
import cn.jbolt.config.exception.ratelimitexception;
import org.springframework.http.httpstatus;
import org.springframework.http.responseentity;
import org.springframework.web.bind.annotation.exceptionhandler;
import org.springframework.web.bind.annotation.restcontrolleradvice;
import javax.servlet.http.httpservletresponse;
import java.util.hashmap;
import java.util.map;
@restcontrolleradvice
public class ratelimitexceptionhandler {
@exceptionhandler(ratelimitexception.class)
public responseentity<map<string, object>> handleratelimitexception(
ratelimitexception e, httpservletresponse response) {
map<string, object> result = new hashmap<>();
result.put("code", httpstatus.too_many_requests.value());
result.put("message", e.getmessage());
result.put("data", null);
// 设置http响应头,告诉客户端多久后可以重试
if (e.getretryafter() > 0) {
response.setheader("retry-after", string.valueof(e.getretryafter()));
}
response.setheader("x-ratelimit-window", "60");
return responseentity.status(httpstatus.too_many_requests).body(result);
}
}5. ip工具类 (iputils)
package cn.jbolt.util;
import org.springframework.util.stringutils;
import javax.servlet.http.httpservletrequest;
public class iputils {
private static final string[] ip_header_names = {
"x-forwarded-for",
"x-real-ip",
"proxy-client-ip",
"wl-proxy-client-ip",
"http_client_ip",
"http_x_forwarded_for"
};
private static final string unknown = "unknown";
private static final string localhost_ipv4 = "127.0.0.1";
private static final string localhost_ipv6 = "0:0:0:0:0:0:0:1";
/**
* 获取客户端真实ip地址
* 处理代理服务器、负载均衡器等场景
*/
public static string getclientip(httpservletrequest request) {
if (request == null) {
return unknown;
}
string ip = null;
// 依次检查各种可能的ip头
for (string header : ip_header_names) {
ip = request.getheader(header);
if (isvalidip(ip)) {
break;
}
}
// 如果头信息中没有找到,则使用getremoteaddr
if (!isvalidip(ip)) {
ip = request.getremoteaddr();
if (localhost_ipv6.equals(ip)) {
ip = localhost_ipv4;
}
}
// 处理多个ip的情况(x-forwarded-for可能包含多个ip)
if (stringutils.hastext(ip) && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return stringutils.hastext(ip) ? ip : unknown;
}
/**
* 检查ip是否有效
*/
private static boolean isvalidip(string ip) {
return stringutils.hastext(ip) && !unknown.equalsignorecase(ip);
}
}技术实现原理
1. aop切面拦截
系统使用spring aop在方法执行前进行拦截,这是一个核心的限流切面类:
package cn.jbolt.config.aspect;
import cn.jbolt.config.anno.ratelimiter.ratelimiter;
import cn.jbolt.config.anno.ratelimiter.ratelimittype;
import cn.jbolt.config.exception.ratelimitexception;
import cn.jbolt.util.iputils;
import cn.jbolt.util.cache.ratelimitercache;
import org.aspectj.lang.proceedingjoinpoint;
import org.aspectj.lang.annotation.around;
import org.aspectj.lang.annotation.aspect;
import org.slf4j.logger;
import org.slf4j.loggerfactory;
import org.springframework.stereotype.component;
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
public class ratelimiteraspect {
private static final logger logger = loggerfactory.getlogger(ratelimiteraspect.class);
@around("@annotation(ratelimiter)")
public object around(proceedingjoinpoint point, ratelimiter ratelimiter) throws throwable {
// 检查是否启用限流
if (!ratelimiter.enabled()) {
return point.proceed();
}
// 获取http请求对象
httpservletrequest request = getcurrentrequest();
if (request == null) {
logger.warn("无法获取httpservletrequest,跳过限流检查");
return point.proceed();
}
// 生成限流键
string limitkey = generatelimitkey(point, ratelimiter, request);
// 执行主要限流检查
checkratelimit(limitkey, ratelimiter.time(), ratelimiter.count(), ratelimiter.msg());
// 执行额外限流检查(如果配置了)
if (ratelimiter.extratime() > 0 && ratelimiter.extracount() > 0) {
string extralimitkey = limitkey + ":extra";
string extramsg = ratelimiter.extramsg().isempty() ? ratelimiter.msg() : ratelimiter.extramsg();
checkratelimit(extralimitkey, ratelimiter.extratime(), ratelimiter.extracount(), extramsg);
}
// 所有限流检查通过,继续执行业务方法
return point.proceed();
}
/**
* 执行限流检查
*/
private void checkratelimit(string key, int timewindow, int maxcount, string message) {
try {
// 增加计数器并获取当前访问次数
int currentcount = ratelimitercache.incrementandget(key, timewindow, timeunit.seconds);
logger.debug("限流检查: key={}, 当前次数={}, 限制次数={}", key, currentcount, maxcount);
// 检查是否超过限制
if (currentcount > maxcount) {
long ttl = ratelimitercache.getttl(key);
logger.warn("触发限流: key={}, 当前次数={}, 限制次数={}, 剩余时间={}秒",
key, currentcount, maxcount, ttl);
throw new ratelimitexception(message, (int) ttl);
}
} catch (ratelimitexception e) {
throw e;
} catch (exception e) {
logger.error("限流检查异常: key={}", key, e);
// 限流服务异常时,选择放行而不是阻塞
}
}
/**
* 生成限流键
*/
private string generatelimitkey(proceedingjoinpoint point, ratelimiter ratelimiter, httpservletrequest request) {
stringbuilder keybuilder = new stringbuilder();
keybuilder.append(ratelimiter.prefix());
// 添加方法签名
string methodsignature = point.getsignature().toshortstring();
keybuilder.append(methodsignature);
// 根据限流类型添加不同的标识
switch (ratelimiter.limittype()) {
case ip:
keybuilder.append(":ip:").append(iputils.getclientip(request));
break;
case user:
string userid = getcurrentuserid(request);
keybuilder.append(":user:").append(userid != null ? userid : "anonymous");
break;
case custom:
keybuilder.append(":custom:").append(ratelimiter.customkey());
break;
case default:
default:
keybuilder.append(":default:global");
break;
}
// 添加时间窗口,确保不同时间窗口的限流独立
keybuilder.append(":").append(ratelimiter.time());
string finalkey = keybuilder.tostring();
logger.debug("生成限流键: {}", finalkey);
return finalkey;
}
/**
* 获取当前http请求
*/
private httpservletrequest getcurrentrequest() {
try {
servletrequestattributes attrs = (servletrequestattributes) requestcontextholder.getrequestattributes();
return attrs != null ? attrs.getrequest() : null;
} catch (exception e) {
logger.warn("获取httpservletrequest失败", e);
return null;
}
}
/**
* 获取当前用户id
* 这里需要根据实际的用户认证体系来实现
*/
private string getcurrentuserid(httpservletrequest request) {
// 方案1:从session中获取
object userid = request.getsession().getattribute("userid");
if (userid != null) {
return userid.tostring();
}
// 方案2:从jwt token中获取
string token = request.getheader("authorization");
if (token != null && token.startswith("bearer ")) {
// 解析jwt获取用户id
// return jwtutils.getuseridfromtoken(token);
}
// 方案3:从请求参数中获取
string useridparam = request.getparameter("userid");
if (useridparam != null) {
return useridparam;
}
return null;
}
}2. 缓存数据结构
系统使用一个包装类来存储缓存数据:
package cn.jbolt.util.cache;
import java.io.serializable;
import java.util.concurrent.timeunit;
public class cachewrapper implements serializable {
private static final long serialversionuid = 1l;
private object value;
private long timestamp;
private long durationmillis;
public cachewrapper() {
}
public cachewrapper(object value, long duration, timeunit unit) {
this.value = value;
this.timestamp = system.currenttimemillis();
this.durationmillis = unit.tomillis(duration);
}
/**
* 检查是否已过期
*/
public boolean isexpired() {
return system.currenttimemillis() - timestamp > durationmillis;
}
/**
* 获取剩余过期时间(毫秒)
*/
public long getremainingtime() {
long elapsed = system.currenttimemillis() - timestamp;
return math.max(0, durationmillis - elapsed);
}
// getter和setter方法
public object getvalue() {
return value;
}
public void setvalue(object value) {
this.value = value;
}
public long gettimestamp() {
return timestamp;
}
public void settimestamp(long timestamp) {
this.timestamp = timestamp;
}
public long getdurationmillis() {
return durationmillis;
}
public void setdurationmillis(long durationmillis) {
this.durationmillis = durationmillis;
}
}完整代码示例
1. 控制器示例
package cn.jbolt.controller;
import cn.jbolt.config.anno.ratelimiter.ratelimiter;
import cn.jbolt.config.anno.ratelimiter.ratelimittype;
import org.springframework.web.bind.annotation.*;
@restcontroller
@requestmapping("/api")
public class democontroller {
/**
* 登录接口 - 防止暴力破解
* 每个ip每分钟最多尝试5次
*/
@postmapping("/login")
@ratelimiter(
limittype = ratelimittype.ip,
time = 60,
count = 5,
msg = "登录尝试过于频繁,请1分钟后重试"
)
public result login(@requestbody loginrequest request) {
// 登录逻辑
if (isvaliduser(request.getusername(), request.getpassword())) {
return result.success("登录成功");
} else {
return result.error("用户名或密码错误");
}
}
/**
* 发送验证码 - 防止恶意发送
* 每个ip每分钟最多3次
*/
@postmapping("/sms/send")
@ratelimiter(
limittype = ratelimittype.ip,
time = 60,
count = 3,
msg = "验证码发送过于频繁,请稍后重试"
)
public result sendsms(@requestbody smsrequest request) {
// 发送短信逻辑
boolean success = smsservice.sendcode(request.getphone());
return success ? result.success("发送成功") : result.error("发送失败");
}
/**
* 查询接口 - 防止爬虫
* 每个ip每分钟最多100次
*/
@getmapping("/products")
@ratelimiter(
limittype = ratelimittype.ip,
time = 60,
count = 100,
msg = "查询过于频繁,请稍后重试"
)
public result getproducts(@requestparam(defaultvalue = "1") int page) {
// 查询商品逻辑
list<product> products = productservice.getproducts(page);
return result.success(products);
}
/**
* 用户操作 - 防止频繁操作
* 每个用户每分钟最多30次
*/
@postmapping("/user/update")
@ratelimiter(
limittype = ratelimittype.user,
time = 60,
count = 30,
msg = "操作过于频繁,请稍后重试"
)
public result updateuser(@requestbody userupdaterequest request) {
// 更新用户信息逻辑
boolean success = userservice.updateuser(request);
return success ? result.success("更新成功") : result.error("更新失败");
}
/**
* 关键操作 - 严格限流
* 1秒最多1次 + 1分钟最多5次
*/
@postmapping("/transfer")
@ratelimiter(
limittype = ratelimittype.user,
time = 1, count = 1, msg = "操作过于频繁,请稍后再试",
extratime = 60, extracount = 5, extramsg = "您在1分钟内的操作次数已达上限"
)
public result transfer(@requestbody transferrequest request) {
// 转账逻辑
boolean success = transferservice.transfer(request);
return success ? result.success("转账成功") : result.error("转账失败");
}
/**
* 自定义限流 - 按商品限制
* 每个商品每分钟最多下单20次
*/
@postmapping("/order/{productid}")
@ratelimiter(
limittype = ratelimittype.custom,
customkey = "product_order",
time = 60,
count = 20,
msg = "该商品下单过于频繁,请稍后重试"
)
public result createorder(@pathvariable string productid, @requestbody orderrequest request) {
// 创建订单逻辑
order order = orderservice.createorder(productid, request);
return result.success(order);
}
// 辅助方法
private boolean isvaliduser(string username, string password) {
// 实际的用户验证逻辑
return "admin".equals(username) && "123456".equals(password);
}
}2. 统一返回对象
package cn.jbolt.common;
public class result {
private int code;
private string message;
private object data;
public static result success(object data) {
result result = new result();
result.code = 200;
result.message = "success";
result.data = data;
return result;
}
public static result error(string message) {
result result = new result();
result.code = 500;
result.message = message;
result.data = null;
return result;
}
// getter和setter方法
public int getcode() {
return code;
}
public void setcode(int code) {
this.code = code;
}
public string getmessage() {
return message;
}
public void setmessage(string message) {
this.message = message;
}
public object getdata() {
return data;
}
public void setdata(object data) {
this.data = data;
}
}使用指南
1. 基本使用
// 最简单的用法 - 使用默认配置
@ratelimiter(limittype = ratelimittype.ip)
public string simpleapi() {
return "success";
}
// 自定义时间窗口和次数
@ratelimiter(
limittype = ratelimittype.ip,
time = 60, // 60秒
count = 100 // 最多100次
)
public string customapi() {
return "success";
}2. 不同场景的配置建议
// 登录接口 - 严格限制
@ratelimiter(
limittype = ratelimittype.ip,
time = 60, count = 5,
msg = "登录尝试过于频繁,请1分钟后重试"
)
// 查询接口 - 适中限制
@ratelimiter(
limittype = ratelimittype.ip,
time = 60, count = 100,
msg = "查询过于频繁,请稍后重试"
)
// 用户操作 - 按用户限制
@ratelimiter(
limittype = ratelimittype.user,
time = 60, count = 30,
msg = "操作过于频繁,请稍后重试"
)
// 全局保护 - 系统级限制
@ratelimiter(
limittype = ratelimittype.default,
time = 60, count = 200,
msg = "系统繁忙,请稍后重试"
)3. 双重限流配置
// 严格的双重限流:秒级 + 分钟级
@ratelimiter(
limittype = ratelimittype.ip,
time = 1, count = 1, msg = "请求过于频繁,请稍后再试",
extratime = 60, extracount = 10, extramsg = "您在1分钟内的请求次数已达上限"
)
// 适中的双重限流:分钟级 + 小时级
@ratelimiter(
limittype = ratelimittype.ip,
time = 60, count = 100, msg = "1分钟内请求过多",
extratime = 3600, extracount = 1000, extramsg = "1小时内请求过多"
)总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论