系统存在服务上限,流量超过服务上限会导致系统卡死、崩溃。
限流:为了在高并发时系统稳定可用,牺牲或延迟部分请求流量以保证系统整体服务可用。
限流算法
漏桶和令牌桶对比
相关阅读:
- 利用 Spring 拦截器实现
- 使用方式:Controller 方法或类加上限流注解,请求到达拦截器时进行拦截处理
- 使用 Redis 记录数据,Lua 保证多个命令原子性执行。
使用示例
@RestController
@RequestMapping("/ratelimit/custom")
@RateLimit(threshold = 10, rateLimiter = RateLimiterEnum.FIXED_WINDOW, time = 10, timeUnit = TimeUnit.SECONDS)
public class RateLimitController {
@GetMapping("/fixed/window")
@RateLimit(threshold = 10, rateLimiter = RateLimiterEnum.FIXED_WINDOW, time = 10, timeUnit = TimeUnit.SECONDS)
public ResponseResult<String> fixedWindow(Long id) {
id += RandomUtil.randomLong();
log.info("custom:fixedWindow:{}", id);
return ResponseResult.success("custom:fixedWindow:" + id);
}
}
限流注解 RateLimit.java
key() + keyType()rateLimiter()threshold()time() + timeUnit()@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = ((HandlerMethod) handler);
// 从方法和类上获取注解
RateLimit annotation = AspectUtil.findMethodOrClassAnnotation(handlerMethod.getMethod(),
RateLimit.class);
if (annotation == null) {
return true;
}
AspectKeyTypeEnum.KeyTypeData data = AspectKeyTypeEnum.KeyTypeData.builder()
.prefix("rate:limit").key(annotation.key()).build();
String limitKey = annotation.keyType()
.obtainTypeKey(handlerMethod.getMethod(), handlerMethod.getMethodParameters(), data);
RateLimiterEnum limiterEnum = annotation.rateLimiter();
// 执行限流脚本
Long isLimit = redisUtil.execute(limiterEnum.obtainScript(),
Lists.newArrayList(limitKey), limiterEnum.obtainArgvs(annotation).toArray());
if (isLimit != null && isLimit != 0L) {
return true;
}
throw new ResponseException(ResponseEnum.RATE_LIMITED);
}
限流算法 lua 脚本
```lua
-- 限流key ,string 保存调用限流的次数
local key = KEYS[1];
-- 最大访问量
local capacity = tonumber(ARGV[1]);
-- 限流时长(毫秒)
local ttl = tonumber(ARGV[2]);
local count = redis.call('INCR', key);
if (count == 1) then
-- 首次访问设置过期时间
redis.call('PEXPIRE', key, ttl);
end
local res = 0;
if (count <= capacity) then
res = 1;
end
-- 被限流返回0,未被限流返回1
return res;
```
```lua
-- 限流 key , zset 保存未被限流的 id 与时间戳
local key = KEYS[1];
-- 最大访问量
local capacity = tonumber(ARGV[1]);
-- 限流时长(毫秒)
local ttl = tonumber(ARGV[2]);
-- 当前时间戳(毫秒)
local now = tonumber(ARGV[3]);
-- 唯一ID
local ukid = ARGV[4];
-- 清除过期的数据
redis.call('ZREMRANGEBYSCORE', key, 0, now - ttl);
local count = redis.call('ZCARD', key);
local res = 0;
if (count < capacity) then
-- 往 zset 中添加一个值、得分均为当前时间戳的元素,[value,score]
redis.call("ZADD", key, now, ukid);
-- 重置 zset 的过期时间,单位毫秒
redis.call("PEXPIRE", key, ttl);
res = 1;
end
-- 被限流返回0,未被限流返回1
return res;
```
```lua
-- 限流 key , hash 保存限流相关信息
local key = KEYS[1];
-- 最大访问量
local capacity = tonumber(ARGV[1]);
-- 限流时长(毫秒)
local ttl = tonumber(ARGV[2]);
-- 当前时间戳(毫秒)
local now = tonumber(ARGV[3]);
-- 水流出速率(每毫秒)
local rate = tonumber(ARGV[4]);
-- 限流信息
local info = redis.call("HMGET", key, "last_time", "stored_water");
-- 上次处理时间
local last_time = tonumber(info[1]);
-- 当前存储的水量,默认为0,存在保存值使用保存值
local stored_water = tonumber(info[2]);
if (stored_water == nil) then
stored_water = 0;
end
if (last_time ~= nil) then
-- 根据上次处理时间和当前时间差,计算流出后的水量
local leaked_water = math.floor((now - last_time) * rate);
stored_water = math.max(stored_water - leaked_water, 0);
if (leaked_water > 0) then
last_time = nil;
end
end
-- 首次访问、泄露了水 设置上次处理时间
if (last_time == nil) then
redis.call("HSET", key, "last_time", now);
end
-- 被限流返回0,未被限流返回1
local res = 0;
if (capacity > stored_water) then
redis.call("HSET", key, "stored_water", stored_water + 1);
res = 1;
end
redis.call("PEXPIRE", key, ttl);
return res;
```
```lua
-- 限流 key , hash 保存限流相关信息
local key = KEYS[1];
-- 最大访问量
local capacity = tonumber(ARGV[1]);
-- 限流时长(毫秒)
local ttl = tonumber(ARGV[2]);
-- 当前时间戳(毫秒)
local now = tonumber(ARGV[3]);
-- 生成令牌速率(每毫秒)
local rate = tonumber(ARGV[4]);
-- 限流信息
local info = redis.call("HMGET", key, "last_time", "stored_tokens");
-- 上次处理时间
local last_time = tonumber(info[1]);
-- 令牌数量,默认为最大访问量,存在保存值使用保存值
local stored_tokens = tonumber(info[2]);
if (stored_tokens == nil) then
stored_tokens = capacity;
end
if (last_time ~= nil) then
-- 根据上次处理时间和当前时间差,触发式往桶里添加令牌
local add_tokens = math.floor((now - last_time) * rate);
stored_tokens = math.min(add_tokens + stored_tokens, capacity);
if (add_tokens > 0) then
last_time = nil;
end
end
-- 首次访问、添加了令牌 设置上次处理时间
if (last_time == nil) then
redis.call("HSET", key, "last_time", now);
end
-- 被限流返回0,未被限流返回1
local res = 0;
if (stored_tokens > 0) then
redis.call("HSET", key, "stored_tokens", stored_tokens - 1);
res = 1;
end
redis.call("PEXPIRE", key, ttl);
return res;
```
demo 地址:https://github.com/EastX/java-practice-demos/tree/main/demo-ratelimit
推荐阅读:
运行有问题或需要源码请点赞关注收藏后评论区留言一、利用ContentResolver读写联系人在实际开发中,普通App很少会开放数据接口给其他应用访问。内容组件能够派上用场的情况往往是App想要访问系统应用的通讯数据,比如查看联系人,短信,通话记录等等,以及对这些通讯数据及逆行增删改查。首先要给AndroidMaifest.xml中添加响应的权限配置 下面是往手机通讯录添加联系人信息的例子效果如下分成三个步骤先查出联系人的基本信息,然后查询联系人号码,再查询联系人邮箱代码 ContactAddActivity类packagecom.example.chapter07;importandroid
我正在开发一个包含大约10个不同功能组件的Sinatra应用程序。我们希望能够将这些组件混合并匹配到应用程序的单独实例中,完全从config.yaml文件配置,如下所示:components:-route:'/chunky'component_type:FoodListercomponent_settings:food_type:baconmax_items:400-route:'places/paris'component_type:Mappercomponent_settings:latitude:48.85387273165654longitude:2.340087890625-
我正在为需要有条件地设置cookie的Rails应用编写Rack中间件组件。我目前正在尝试设置cookie。通过谷歌搜索,这似乎应该可行:classRackAppdefinitialize(app)@app=appenddefcall(env)@status,@headers,@response=@app.call(env)@response.set_cookie("foo",{:value=>"bar",:path=>"/",:expires=>Time.now+24*60*60})[@status,@headers,@response]endend它不会给出错误,但也不会设置coo
我正在linux机器上学习rubyonrails并磨练我的VIM技能(skillz?)。当我在使用C++的时候开始使用VIM时,我有一个friend有一个很棒的vimfiles文件夹,里面有很多东西可以开始使用。从头开始,vim很棒,但感觉它还可以做得更好。我目前有:vim-rubybufferexplorerxml-edit(虽然我目前没有它可以处理erb文件)我知道这只是一些更有经验的vim/ruby开发人员所拥有的东西的皮毛(包括vim.rc文件中的一次性)。在某个地方是否有一个列表(或者我们可以创建一个)使ruby(和rails)编程更有趣所需的一堆标准vim配置?是否有一
前提:当我们要修改vant组件库中Tabbar图标大小的样式(原图标是字体图标,大小由font-size控制)。 字体图标字体大小由css变量(--van-tabbar-item-icon-size)控制, 1.插槽方法结论:当你想要自定义使用插槽时,插入自己的元素,那么可以直接在当前作用域直接修改元素的样式。自定义img{height:28px}传入图片,用height属性控制图片大小,达到与字体图标相同效果2.全局定义变量结论:全局定义一个变量,覆盖它默认变量的值定义变量缺点:全局修改。 :root{--van-tabbar-item-icon-size:30px!important;/
记个笔记以免遗忘,建议还是查看Element-UI提供的官方文档学习,自己摸索比较难受官方文档:Element-UI组件TableElement-UI官网提供了许多Table格式,这里以一个带有筛选器的表格为例表格的官网显示效果:直接将官方提供的示例代码贴入.vue文件中即可使用显示的数据是通过data()方法提供的假数据。方法见下:data(){return{tableData:[{date:'2016-05-02',name:'王小虎',address:'上海市普陀区金沙江路1518弄'},{date:'2016-05-04',name:'王小虎',address:'上海市普陀区金沙江路1
先给大家看看最终效果首先我们来定义数据data(){ return{ lsit:[ 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic118.nipic.com%2Ffile%2F20161216%2F24271963_122609717000_2.jpg&refer=http%3A%2F%2Fpic118.nipic.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1656923017&t=183ece148b13b64e9dd503afd1b15c91'
有些奇怪的事情发生了,我一直在阅读React文档,他们讨论了生命周期以及如何在渲染组件之前做一些事情。我正在尝试,但我尝试的一切都失败了,总是组件首先进行渲染,然后调用componenWillMount、..didMount等。在调用这些函数之后,渲染再次发生。我需要先加载数据以填充状态,因为我不希望初始状态为null,我希望它包含自初始呈现以来的数据。我正在使用Flux和Alt,这是Action@createActions(flux)classGetDealersActions{constructor(){this.generateActions('dealerDataSuccess
在ember中为组件类指定位置参数时,您必须重新打开该类(如下所示),这样它才能工作,您不能将它包含在初始声明中(至少从我所看到的示例和我自己的经验)。importEmberfrom'ember';constcomponent=Ember.Component.extend({});component.reopenClass({positionalParams:['post'],});exportdefaultcomponent;如果你在单个声明中这样做(如下所示)它将不起作用importEmberfrom'ember';exportdefaultEmber.Component.exte
调试React网站时,我可以使用ReactDeveloperTools查看组件层次结构:我如何在ReactNative中做同样的事情?rageshake菜单包含一个“检查器”,但它似乎只能让我通过点击它来检查单个元素-我看不到任何浏览完整组件层次结构的方法。 最佳答案 不幸的是,从react-native0.12版本开始,Devtools的“React”选项卡不再起作用。这是aknownissue.有一个quiteactivediscussiononGithub已经开放了一段时间,但还没有解决方案。更新Devtools“React”