什么是限流
限流是一种控制系统访问频率的技术手段,就像高速公路的收费站控制车流量一样。
生活场景类比:
- 银行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小时内请求过多" )
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论