当前位置: 首页 > news >正文

Hyperf方案 API限流熔断

<?php/** * 案例055:API限流熔断 * 说明:基于Redis计数器实现接口限流,超出限制返回429,防止接口被刷 * 需要安装的包:hyperf/redis */declare(strict_types=1);namespaceApp\Middleware;useHyperf\Redis\Redis;usePsr\Http\Message\ResponseInterface;usePsr\Http\Message\ServerRequestInterface;usePsr\Http\Server\MiddlewareInterface;usePsr\Http\Server\RequestHandlerInterface;/** * 接口限流中间件 * 支持多维度限流:IP、用户、接口 * 算法:固定窗口计数器(简单但有临界问题) * 生产可以升级为滑动窗口(用Redis ZSET实现) */classRateLimitMiddlewareimplementsMiddlewareInterface{// 不同接口的限流规则,格式:路径前缀 => [次数, 时间窗口(秒)]privatearray$rules=['/api/v1/auth/login'=>[5,60],// 登录:1分钟最多5次,防暴力破解'/api/v1/auth/sms'=>[1,60],// 发短信:1分钟只能发1条'/api/v1/orders'=>[30,60],// 下单:1分钟30次'default'=>[200,60],// 其他接口默认:1分钟200次];publicfunction__construct(privateRedis$redis){}publicfunctionprocess(ServerRequestInterface$request,RequestHandlerInterface$handler):ResponseInterface{$path=$request->getUri()->getPath();$ip=$this->getClientIp($request);$userId=\Hyperf\Context\Context::get('user_id',0);// 查找匹配的限流规则[$maxRequests,$windowSeconds]=$this->findRule($path);// 按IP限流(未登录用IP,登录后用用户ID,防共享IP影响)$limitKey=$userId>0?"rate_limit:user:{$userId}:{$path}":"rate_limit:ip:{$ip}:{$path}";$result=$this->checkAndIncrement($limitKey,$maxRequests,$windowSeconds);if(!$result['allowed']){return$this->tooManyRequests($result['reset_at'],$maxRequests);}// 在响应头里告知客户端剩余次数,方便前端显示$response=$handler->handle($request);return$response->withHeader('X-RateLimit-Limit',(string)$maxRequests)->withHeader('X-RateLimit-Remaining',(string)$result['remaining'])->withHeader('X-RateLimit-Reset',(string)$result['reset_at']);}/** * 检查并递增计数(原子操作,用Lua脚本保证原子性) */privatefunctioncheckAndIncrement(string$key,int$max,int$window):array{// Lua脚本:原子地检查+递增,避免并发时的竞态条件$lua=<<<'LUA'local key=KEYS[1]local max=tonumber(ARGV[1])local window=tonumber(ARGV[2])local current=redis.call('GET',key)ifcurrent==falsethen--第一次请求,设置计数为1并设置过期时间 redis.call('SET',key,1,'EX',window)return{1,max-1,1}--{current,remaining,allowed}end current=tonumber(current)ifcurrent>=max then--超出限制 local ttl=redis.call('TTL',key)return{current,0,0}end--递增 redis.call('INCR',key)return{current+1,max-current-1,1}LUA;$result=$this->redis->eval($lua,[$key,$max,$window],1);$remaining=$result[1]??0;$allowed=(bool)($result[2]??0);$resetAt=time()+($allowed?$this->redis->ttl($key):0);returncompact('allowed','remaining','resetAt');}privatefunctionfindRule(string$path):array{foreach($this->rulesas$prefix=>$rule){if($prefix!=='default'&&str_starts_with($path,$prefix)){return$rule;}}return$this->rules['default'];}privatefunctiongetClientIp(ServerRequestInterface$request):string{// 有代理时从X-Forwarded-For取真实IP$forwarded=$request->getHeaderLine('X-Forwarded-For');if($forwarded){returntrim(explode(',',$forwarded)[0]);// 取第一个IP(最原始的)}$serverParams=$request->getServerParams();return$serverParams['remote_addr']??'0.0.0.0';}privatefunctiontooManyRequests(int$resetAt,int$max):ResponseInterface{$response=\Hyperf\Utils\ApplicationContext::getContainer()->get(\Hyperf\HttpServer\Contract\ResponseInterface::class);return$response->json(['code'=>429,'message'=>'请求太频繁,请稍后再试','reset_at'=>$resetAt,])->withStatus(429)->withHeader('Retry-After',(string)($resetAt-time()));}}/** * 滑动窗口限流(更精确,无临界问题,用ZSet实现) */classSlidingWindowRateLimiter{publicfunction__construct(privateRedis$redis){}/** * 检查是否超出限制(滑动窗口版本) * 用ZSet存每次请求的时间戳,定期清理窗口之外的记录 */publicfunctionisAllowed(string$key,int$max,int$windowSeconds):bool{$now=microtime(true)*1000;// 毫秒时间戳$start=$now-$windowSeconds*1000;// 窗口起始时间// 用pipeline批量执行,减少网络往返$this->redis->multi();$this->redis->zRemRangeByScore($key,'-inf',$start);// 删掉窗口外的记录$this->redis->zCard($key);// 数当前窗口内的请求数$this->redis->zAdd($key,$now,uniqid('',true));// 添加本次请求$this->redis->expire($key,$windowSeconds+1);// 设置key过期时间$results=$this->redis->exec();$count=$results[1];// zCard的结果if($count>=$max){// 超出了,把刚加进去的删掉(回滚)$this->redis->zRemRangeByScore($key,$now,$now);returnfalse;}returntrue;}}
http://www.jsqmd.com/news/620287/

相关文章:

  • 告别黑壳子DAP!用AT32F403A和CherryUSB自制高速DAPLink V2仿真器(附完整源码)
  • E2E_P01Check 函数笔记
  • 终极宝可梦随机化器ZX:重新定义七代游戏的个性化冒险体验
  • 一条命令搞定OpenClaw部署?PPClaw的便利背后,你得先看清这些代价
  • python学习笔记2
  • OpenClaw 横向评测|对比 AutoGPT、CoPaw、NanoClaw 等主流 AI Agent,谁更适合你?
  • Keyviz:终极免费工具!实时可视化你的键盘鼠标操作,让教学录制更轻松
  • Hyperf方案 API签名验证
  • 第15届省赛蓝桥杯大赛C/C++大学B组
  • C++跨平台性能监控实战:构建CPU、GPU、磁盘与网络一体化探针
  • nav标签适用场景是什么_导航栏语义化写法【方法】
  • 部署成本降60%,响应提速10倍:镜像视界AI视频孪生的工程化价值
  • mysql事务隔离级别切换注意事项_如何保证系统平滑过渡
  • 【传统图像分割算法】- 图像分割之自适应阈值(Adaptive Thresholding)完全解析
  • 嵌入式Linux实战:手把手教你为EC20 4G模块编译GobiNet驱动(附完整Makefile配置)
  • HY-Motion 1.0开源大模型部署教程:支持企业级3D数字人动作生成生产环境
  • 2279 上市公司跨国供应链【存续力】(Sustainability) 指标(2004.07-2026.02)
  • C++ vs .NET 数组原地反转实测:小数组 C++ 碾压,大数组 .NET 反杀?好
  • 蛋白靶点CD49e(整合素α5):细胞黏附机制与抗体药物研发技术解析
  • 《算法题讲解指南:动态规划算法--回文串问题》--35.回文子串,36. 最长回文子串,37.分割回文串 IV,38.分割回文串 II,39.最长回文子序列,40.让字符串成为回文串的最少插入次数
  • 一个拉胯的分库分表方案有多绝望?整个部门都在救火!
  • 茉莉花插件:5步掌握Zotero中文文献管理终极技巧
  • 【GUI-Agent】阶跃星辰 GUI-MCP 解读---()---GUI-MCP 整体架构炔
  • 【web服务】web服务之nginx详细配置上
  • IPD实战指南:如何运用SPAN工具精准定位高潜力市场并优化产品战略布局
  • 从“记录监控”到“空间决策”:镜像视界AI重新定义视频孪生
  • LLM推理微服务基准测试全链路指南,从Prompt扰动控制到P99延迟归因分析
  • Java项目Loom迁移避坑手册(2024生产环境血泪总结)
  • 从“虚短虚断”到闭环增益:深度解析理想运放负反馈放大电路
  • ruoyi前后端分离版本