一 、业务场景
在教学编程中做项目系统接口服务的时候,为了防止客户端对于接口的滥用、保护服务器的资源, 通常来说我们会对于服务器上的各种接口进行请求次数的限制。比如对于某个用户,他在一个时间段内,比如 1 0秒,请求服务器接口的次数不能够大于一个上限,比如说5次。如果用户调用接口的次数超过上限的话,就直接拒绝用户的请求,返回错误信息。
服务接口的流量控制策略:限流、分流、降级、熔断等。本文讨论下限流策略,虽然降低了服务接口的访问频率和并发量,却换取服务接口和业务应用系统的高可用。
二、常用的限流算法
1、漏桶算法
漏桶(Leaky Bucket)算法思路比较简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流进速度过大会直接导致桶溢出(访问频率超过接口响应速率),然后就拒绝请求,这里我们可以得出结论:漏桶算法能强行限制数据的传输速率。
漏桶算法的话一般有两个变量,一个是桶的大小,也就是你的承载量,比如流量突发增多时可以存多少的水,另一个就是你能够去正常处理的请求,水桶漏洞的大小。
因为漏桶的漏出速率是固定的,所以即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流量突发到端口。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。
2、令牌桶算法
令牌桶算法(Token Bucket)和Leaky Bucket 效果一样但方向相反的算法,随着时间流逝,系统会按你的并发量,比如恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入令牌,桶可以承载一定的令牌数,具体一个桶有多少令牌,这里是可以自己去控制的。当请求进来时,每个请求都会拿走一个Token,如果没有Token拿了就阻塞等待或者拒绝服务。
Redis令牌桶算法还有一个优点就是可以方便的改变速度. 如果你需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如10毫秒)往桶中增加一定数量的令牌, 有些算法则实时的计算应该增加的令牌的数量.
三、观摩PHP+Redis实现令牌桶算法
<?php namespace Api\Li; //令牌桶限流算法 class Rate{ private $minSecond = 20; //单个用户每分钟访问数 private $daySecond = 5000; //单个用户每天总的访问量 // public function minLi($uid) { $minSecondKey = $uid . '_minSecond'; $daySecondKey = $uid . '_daySecond'; $resMin = $this->getRedisLi($minSecondKey, $this->minSecond, 60); $resDay = $this->getRedisLi($minSecondKey, $this->minSecond, 86400); if (!$resMin['status'] || !$resDay['status']) { exit($resMin['msg'] . $resDay['msg']); } } public function getRedisLi($key, $initNum, $expire) { $nowtime = time(); $result = ['status' => true, 'msg' => '']; $redisObj = $this->di->get('redis'); $redis->watch($key); $limitVal = $redis->get($key); if ($limitVal) { $limitVal = json_decode($limitVal, true); $newNum = min($initNum, ($limitVal['num'] - 1) + (($initNum / $expire) * ($nowtime - $limitVal['time']))); if ($newNum > 0) { $redisVal = json_encode(['num' => $newNum, 'time' => time()]); } else { return ['status' => false, 'msg' => '目前令牌已经消耗完!']; } } else { $redisVal = json_encode(['num' => $initNum, 'time' => time()]); } $redis->multi(); $redis->set($key, $redisVal); $rob_result = $redis->exec(); if (!$rob_result) { $result = ['status' => false, 'msg' => '很抱歉,访问次数过于频繁!']; } return $result; }}
代码要点:
(1)定义规则
单个用户每分钟访问次数($minSecond),单个用户每天总的访问次数($daySecond)。
(2)计算速率
该代码示例以秒为最小的时间单位,速率=访问次数/时间($initNum / $expire)
(3)每次消耗令牌后需要补充的令牌个数--计算方式
获取上次请求的时间即上次存入令牌的时间,计算当前时刻与上次请求的时间差乘以速率就是此次需要补充的令牌个数,切记!补充令牌后的总令牌数不能大于初始化令牌数,以补充数和初始化数的最小值为标准。
(4)程序流程
第一次请求时初始化令牌个数($minSecond),存入Redis同时将当前的时间戳存入以便计算下次需要补充的令牌个数。第二次访问时获取剩余的令牌个数,并添加本次应该补充的令牌个数,补充后如何令牌数>0则当前访问是有效的可以访问,否则令牌使用完毕不可访问。先补充令牌再判断令牌是否>0的原因是由于还有速率这个概念即如果上次剩余的令牌为0但是本次应该补充的令牌>1那么本次依然可以访问。
(5)针对并发的处理
5.1使用乐观锁+事务解决。5.2redis无需集合+list队列。5.3redis+lua分布式锁
扫码二维码 获取免费视频学习资料
- 本文固定链接: http://phpxs.com/post/7653/
- 转载请注明:转载必须在正文中标注并保留原文链接
- 扫码: 扫上方二维码获取免费视频资料