基于Redis的分布式限流详解
前言
Redis除了能用作缓存外,还有很多其他用途,比如分布式锁,分布式限流,分布式唯一主键等,本文将和大家分享下基于Redis分布式限流的各种实现方案。
一、为什么需要限流
用最简单的话来说:外部请求是不可控的,而我们系统的负载是有限的,如果没有限流机制,一旦外部请求超过系统承载的压力,就会出现系统宕机等严重问题。加入限流正是为了保证系统负载在可以承受的范围内
。
比如春节的秒杀环节。我们在上线前预估了能应对的秒杀 qps 是 1kw/s,但是实际可能达到了1亿/s,这种情况下这多出来的9kw请求很可能压垮我们的数据库,进而影响到接下来所有的用户正常访问。
补充:
微服务保证稳定性的几个利器:缓存、熔断、降级、限流
。
- 缓存的目的是为了降低系统的访问延迟,提高系统能力,给用户更好的体验
- 熔断的目的是为了在发现某个服务故障熔断对下游依赖的请求,减少不必要的损耗
- 降级的目的是为了在系统在某个环节故障(比如某个下游故障)不影响整体核心链路,比如返回作者列表,关注服务故障了获取不了关注真实的关注情况,这种情况可以考虑降级关注按钮,全部显示为未关注
- 限流的目的是为了保证系统处理的请求量在可以承受的范围内,防止突发流量压垮系统,保证系统稳定性。
二、单机限流和分布式限流
1、单机限流的瓶颈
单机限流有个主要的缺陷就是不够精确,我们可能有1000个实例,但是下游存储只有一套,即多对一的关系,如果单纯的以单机限流作为衡量指标,很可能把下游打挂。如果以下游(数据库)的总请求量均衡到每台机器上,由于每个机器请求数据库量级可能不同,导致部分机器被限流严重,而部分机器压根没什么请求,造成误伤。
即不好根据下游数据库服务的负载压力,来评估上游应用服务器每台的负载压力是多少。
采用根据总负载来均摊负载的方式显然并不精确,并且不能充分发挥上游服务器的处理性能,极大的限制了系统的负载能力。
比如数据库负载为limit=300,由于采用上游采用负载均摊的方式每台应用服务器限制为limit=100,如果来个固定IP或固定用户的请求一直落在服务器Server1上,那么系统的实际并发就变成了100,显然这样处理并不科学,也降低了单台应用服务器的处理能力
。
单机限流也有好处:可以根据服务器配置的不同进行不同权重的限流配置
。
比如:4核CPU,16G内存的服务器上限流为1000;2核 8G内存的服务器上限流为500.
2、分布式限流
为了解决单机限流的瓶颈,需要引入分布式限流,即我们不应为每个单机设置限流阈值,而是根据下游的负载情况设置一个全局的阈值
。目前主流的做法主要是通过 redis 或者 zookeeper 来实现。zookeeper 使用、运维都比较复杂,所以大部分是使用 redis + lua 脚本来实现
。
分布式限流器和单机限流的实现类似,只是计数存储换成了一些分布式存储而不是在单机内存中。
我们可以使用 redis + lua 脚本实现令牌桶的算法,因为 lua 的执行可以做到事务,要么全部成功要么全部失败。所以可以很简单地实现分布式的令牌桶逻辑,并且可以实现精确的限流。但是这种实现的缺陷是如果请求 qps很高,所有的请求都要和redis交互,redis 不一定能承载得住。
3、小结
- 单机限流更适合对单机精确限流,比如针对单个mysql数据库的请求限流,某低配的应用服务器只能处理100并发的限流。
- 分布式限流更适合分布式场景的总体流控,比如知道请求链路中下游的负载能力出现瓶颈,那么上游就需要根据下游瓶颈进行整体流量控制,如数据库的负载是limit=500,这时就可以采用分布式限流器来控制落在应用服务器上整体的请求数。
三、4种主流的限流算法
1、固定时间窗口
固定窗口算法又叫计数器
算法,是一种简单方便的限流算法。主要通过一个支持原子操作的计数器来累计 1 秒内的请求次数,当 1 秒内计数达到限流阈值时触发拒绝策略。每过 1 秒,计数器重置为 0 开始重新计数。
- 优点:时间窗口固定,实现简单,性能较高
- 缺点:
无法应对两个时间窗口临界时间内的突发流量
,如上图所示,我们虽然通过限流器限制了单个时间窗口内只能有2个请求(即QPS=2),但是在两个1秒时间窗口中间1秒的时间窗口内却发生了4次请求。
2、滑动时间窗口
我们已经知道固定窗口算法的实现方式以及它所存在的问题,而滑动窗口算法是对固定窗口算法的改进。既然固定窗口算法在遇到时间窗口的临界突变时会有问题,那么我们在遇到下一个时间窗口前也调整时间窗口不就可以了吗?
下面是滑动窗口的示意图:
上图的示例中,每 500ms 滑动一次窗口,可以发现窗口滑动的间隔越短,时间窗口的临界突变问题发生的概率也就越小
,不过只要有时间窗口的存在,还是有可能发生时间窗口的临界突变问题。
那么有没有什么办法更精确的统计时间窗口内的请求数呢?
答案是有的,就是记录下所有的请求时间点,新请求到来时以请求时间作为时间窗口的结尾时间统计时间窗口范围内的请求数量是否超过指定阈值
,由此来确定是否达到限流,这种方式没有了时间窗口突变的问题,限流比较准确,但是因为要记录下每次请求的时间点,所以占用的内存较多
。
该方法又叫做滑动日志算法
。不过其算法的本质还是滑动时间窗口那套,区别在于滑动时间窗口是固定时间间隔滑动时间窗口,而滑动日志算法由于保存了每个请求的时间戳,可以根据最新请求的时间戳计算出当前时间窗口内的请求数。
- 优点:优化了固定时间窗口的临界问题,
限流更加精准
- 缺点:实现较为复杂,会占用较多内存,每个请求都需要重新统计最新时间窗口内的请求数,性能较低。
3、漏桶
漏桶算法中将限流器比作一个漏斗,每一个请求到来就会向桶中添加一定的水量,桶底有一个孔,以恒定速度不断的漏出水;当一个请求过来需要向加水时,如果漏桶剩余容积不足以容纳添加的水量,就会触发拒绝策略。
假设限流要求是每分钟允许30个请求,可以将漏桶的容积看作30,每次请求加水量为1,漏桶中水的流出速度为30/时间周期1分钟。
当出现最大30个并发时,漏桶会被瞬间注满水,后续请求都会被拒绝。只有随着水量以固定速度流出后,漏斗中有剩余空间容纳新的水量,系统才能接受的新的请求。
注意:
1、漏桶算法模型中并没有队列的概念,每个请求到来时向漏斗中加水并不是加入队列等待被消费,所以漏斗并不能像消息队列那样削峰填谷、缓解突发的请求压力,限流器只负责判断当前请求是被允许还是需要拒绝。只要容器中能继续加水,请求就被允许,否则拒绝请求。
2、漏桶中水的流出速度并不等于请求的并发量
,往漏桶中加水的速度才是当前系统的实际并发量。水的流出可以等同于令牌桶算法中向桶中投放令牌
,水流出的速度越快,漏桶中就越快腾出空余空间用来存放新的请求到来需要添加的水量。所以不要简单认为水的流出速度恒定,就能控制当前系统的并发量保持均衡,两者并不是一个概念
。
3、桶的容积决定了限流器允许的最大并发
。当漏桶中没有水时,允许出现最大的并发流量。
很多地方对漏桶的说法都是可以缓冲请求,对此我有不同的看法,所有的限流器不管是采用何种方法实现,都仅仅只是做请求是否被允许的判定器,请求要么被允许通过,要么被直接拒绝,其本身并不提供缓冲请求的功能。漏桶模型中的桶的容量并不会像消息队列那样缓冲请求,不会存放真实的请求信息,等待被处理消费。
-
优点:能够起到一定的
平滑突发流量
的作用。水的恒定流出速度,可以等价于固定速度的投放令牌,不会出现一下子投放大量令牌,立刻被抢空,导致突发流量的问题。 -
缺点:
1、资源利用率低:漏桶并不能高效地利用可用的资源。因为它只在固定的时间间隔放行请求,所以在很多情况下,流量非常低,即使不存在资源争用,也无法有效地消耗资源。
2、饥饿问题:当短时间内有大量突发请求,即使服务器没有任何负载,由于漏桶中的水还没有流出,请求会大量被拒绝。
3、实现复杂,性能较低,会占用较多内存
4、令牌桶
漏桶是看处理效率和生产效率来控制流速,但是这个流速是静态的,很可能无法充分利用机器的性能。比如,服务器能处理的速率是100qps,但是我们配置的恒定流速只有50qps,这个时候服务器资源还非常地冗余。
令牌桶算法能比较灵活的调整以最大化利用资源:系统每接受到一个请求时,都要求有一个令牌,如果拿到令牌就处理,否则就拒绝,处理完以后把令牌丢弃。
桶中能存放的最大令牌数决定了令牌桶算法的最大并发,当桶中放满令牌时,允许达到最大并发。
令牌桶限流算法的核心就在于如何控制令牌的发放策略
。这样可以做的很动态,例如利用系统的负载、业务高峰情况等,高峰时且负载允许加快令牌发放。
从本质上来说,令牌桶中以恒定速度生成令牌和漏桶算法中以恒定速度控制水的流出速度是等同的,都可以实现相同的限流效果
。
漏桶 vs 令牌桶
网上的大多数说法:
令牌桶相比漏桶会更加灵活,可以根据业务诉求配置灵活的发放策略。比如可以任意时间放入令牌,或者依据机器负载放入多个令牌,但是漏桶的整形效果会更好,对下游服务会更加友好,因为不容易出现突刺。
个人理解:
两者本质上只是模型不同,但都能实现相同的限流效果。令牌桶能调整令牌的发放策略,漏桶也可以改进水流出的速率。只是我们一般的默认理解是令牌桶是定时投放一定数量的令牌,而漏桶中的水是恒定速度流出,这样的前提下,漏桶相较之下更能起到平滑突发流量
的作用,不会像令牌桶那样出现令牌投放后立刻被抢空的情况,而令牌桶则是能更好的适应突发流量。
-
优点:
可以适应流量突发
,N 个请求到来只需要从桶中获取 N 个令牌就可以继续处理。当然在漏桶算法中只要桶中剩余空间够大,也能够应付突发的流量。 -
缺点:实现复杂,性能较低,会占用较多内存
四、基于Redis的Lua分布式限流实战
基于Redis实现分布式限流一般都会采用Lua实现,以保证操作的原子性。
1、固定时间窗口
fixedWindow-rateLimit.lua脚本内容:
--KEYS[1]: 限流 key
--ARGV[1]: 阈值
--ARGV[2]: 时间窗口,计数器的过期时间
local rateLimitKey = KEYS[1];
local rate = tonumber(ARGV[1]);
local rateInterval = tonumber(ARGV[2]);local allowed = 1;
-- 每次调用,计数器rateLimitKey的值都会加1
local currValue = redis.call('incr', rateLimitKey);if (currValue == 1) then
-- 初次调用时,通过给计数器rateLimitKey设置过期时间rateInterval达到固定时间窗口的目的redis.call('expire', rateLimitKey, rateInterval);allowed = 1;
else
-- 当计数器的值(固定时间窗口内) 大于频度rate时,返回0,不允许访问if (currValue > rate) thenallowed = 0;end
end
return allowed
固定时间窗口限流器实现FixedWindowRateLimiter:
public class FixedWindowRateLimiter implements RateLimiter{private final static String PREFIX = "fixedWindowRateLimiter";private final RScript rScript;public FixedWindowRateLimiter(RedissonClient client) {//获取RScript时一定要指定:LongCodec.INSTANCE,不然Lua中获取参数值是可能会失败this.rScript = client.getScript(LongCodec.INSTANCE);}@Overridepublic boolean isAllowed(Rule rule) {String keys = String.format("%s.{%s}",PREFIX,rule.getKey());String script = LuaScript.getFixedWindowRateLimiterScript();long timeoutInMillis = -1L;if (rule.getRateInterval() >= 0L) {//由于通过expire命令设置过期时长的单位是秒s,这里需要将时间转为秒timeoutInMillis = rule.getTimeUnit().toSeconds(rule.getRateInterval());}Long result = rScript.eval(RScript.Mode.READ_WRITE, script, RScript.ReturnType.VALUE, Collections.singletonList(keys), rule.getRate(), timeoutInMillis);return result == 1L;}
}
单元测试:
@Testvoid fixedWindowRateLimitServiceTest() throws InterruptedException, ExecutionException {//创建名称为laowan.fixedWindow的固定窗口限流器,每60s允许30个请求Rule rule = new Rule("laowan.fixedWindow",30,60,TimeUnit.SECONDS, Mode.FIXED_WINDOW);while (true){boolean result = rateLimiterService.isAllowed(rule);Thread.sleep(1000L);if(result){log.info("请求正常");}else{log.info("请求太多频繁,请稍后再试");}}}
2、滑动时间窗口
slidingWindow-rateLimit.lua内容:
--KEYS[1]: 限流器的 key
--ARGV[1]: 当前时间窗口的开始时间
--ARGV[2]: 请求的时间戳(也作为score)
--ARGV[3]: 阈值
-- 1. 移除时间窗口之前的数据
redis.call('zremrangeByScore', KEYS[1], 0, ARGV[1])
-- 2. 统计当前元素数量
local res = redis.call('zcard', KEYS[1])
-- 3. 是否超过阈值
if (res == nil) or (res < tonumber(ARGV[3])) then-- 4、保存每个请求的时间搓redis.call('zadd', KEYS[1], ARGV[2], ARGV[2])return 1
elsereturn 0
end
滑动窗口限流器实现SlidingWindowRateLimiter:
public class SlidingWindowRateLimiter implements RateLimiter{private final RScript rScript;private final static String PREFIX = "slidingWindowRateLimiter";public SlidingWindowRateLimiter(RedissonClient client) {this.rScript = client.getScript(LongCodec.INSTANCE);}@Overridepublic boolean isAllowed(Rule rule) {String keys = String.format("%s.{%s}",PREFIX,rule.getKey());String script = LuaScript.getSlidingWindowRateLimiterScript();long interval = -1L;if (rule.getRateInterval() >= 0L) {interval = rule.getTimeUnit().toMillis(rule.getRateInterval());}//新窗口的结束时间long now = System.currentTimeMillis();//新窗口的起始时间long windowStartTime = now - interval;Long result = rScript.eval(RScript.Mode.READ_WRITE, script, RScript.ReturnType.INTEGER,Collections.singletonList(keys),windowStartTime,now, rule.getRate());return result == 1L;}
}
3、漏斗
leakyBucket-rateLimit.lua内容:
--参数说明:
--KEYS[1]: 限流器的 key
--ARGV[1]: 容量,决定最大的并发量
--ARGV[2]: 漏水速率,决定平均的并发量
--ARGV[3]: 一次请求的加水量
--ARGV[4]: 时间戳
local limitInfo = redis.call('hmget', KEYS[1], 'capacity', 'passRate', 'addWater','water', 'lastTs')
local capacity = limitInfo[1]
local passRate = limitInfo[2]
local addWater= limitInfo[3]
local water = limitInfo[4]
local lastTs = limitInfo[5]--初始化漏斗
if capacity == false thencapacity = tonumber(ARGV[1])passRate = tonumber(ARGV[2])--请求一次所要加的水量,一定不能大于容量值的addWater=tonumber(ARGV[3])--当前储水量,初始水位一般为0water = tonumber(ARGV[1])lastTs = tonumber(ARGV[4])redis.call('hmset', KEYS[1], 'capacity', capacity, 'passRate', passRate,'addWater',addWater,'water', water, 'lastTs', lastTs)return 1
elselocal nowTs = tonumber(ARGV[4])--计算距离上一次请求到现在的漏水量 = 流水速度 * (nowTs - lastTs)local waterPass = tonumber(ARGV[2] * (nowTs - lastTs))--计算当前剩余水量 = 上次水量 - 时间间隔中流失的水量water = math.max(tonumber(0), tonumber(water - waterPass))--设置本次请求的时间lastTs = nowTs--判断是否可以加水 (容量 - 当前水量 >= 增加水量,判断剩余容量是否能够容纳增加的水量)if capacity - water >= addWater then-- 加水water = water + addWater-- 更新增加后的当前水量和时间戳redis.call('hmset', KEYS[1], 'water', water, 'lastTs', lastTs)return 1end-- 请求失败return 0
end
漏桶实现LeakyBucketRateLimiter:
public class LeakyBucketRateLimiter implements RateLimiter{private final static String PREFIX = "leakyBucketRateLimiter";private final RScript rScript;public LeakyBucketRateLimiter(RedissonClient client) {this.rScript = client.getScript(LongCodec.INSTANCE);}@Overridepublic boolean isAllowed(Rule rule) {String keys = String.format("%s.{%s}",PREFIX,rule.getKey());long rateInterval = -1L;if (rule.getRateInterval() >= 0L) {rateInterval = rule.getTimeUnit().toMillis(rule.getRateInterval());}//容量,决定最大并发数long capacity = rule.getRate();//水流的速度,即容器中的水量在周期时间流完的速度double passRate = (double)rule.getRate()/rateInterval;//每次请求增加的水量,相当于每次请求获取的令牌数long addWater = 1L;//请求时间戳,使用毫秒long lastTs = System.currentTimeMillis();String script = LuaScript.getLeakyBucketRateLimiterScript();Long result = rScript.eval(RScript.Mode.READ_WRITE, script, RScript.ReturnType.INTEGER,Collections.singletonList(keys),capacity,passRate,addWater, lastTs);return result == 1L;}
}
4、令牌桶
令牌桶的lua脚本是参考Redisson的RRateLimiter中的lua脚本实现的,并没有开启单独的线程去添加令牌,而是每次收到新的请求时判断是否需要添加令牌。
tokenBucket-rateLimit.lua内容如下:
-- 本质上和滑动窗口类似,都是通过有序集合zset来保存缓存信息,时间戳作为score,通过时间戳维护时间窗口或令牌的有效性
-- 区别在于滑动窗口是保存每次请求的时间搓来计分,而令牌桶保存的是许可证生成的时间戳。
--参数说明:
--KEYS[1]: 限流器 key
--KEYS[2]: 桶中剩余的许可证 key
--KEYS[3]: 记录所有许可证发出的时间戳的 key--ARGV[1]: 本次请求申请的令牌数
--ARGV[2]: 请求时间搓
--ARGV[3]: 随机数
--ARGV[4]: 令牌桶容量
--ARGV[5]: 时间周期local limitInfo = redis.call('hmget', KEYS[1], 'rate', 'interval');
local rate = limitInfo[1];
local interval = limitInfo[2];
--初始化限流信息
if rate == false then-- rate表示令牌桶的最大容量rate = tonumber(ARGV[4]);-- 生成令牌的时间周期interval = tonumber(ARGV[5]);redis.call('hmset', KEYS[1], 'rate', rate, 'interval', interval);
end;
-- assert(rate ~= false and interval ~= false , 'RateLimiter is not initialized')-- 限流器名称
local valueName = KEYS[2];
local permitsName = KEYS[3];
-- 单机时才需要
-- if type == '1' then
-- valueName = KEYS[3];
-- permitsName = KEYS[5];
-- end;
assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount could not exceed defined rate');-- 获取限流器中的剩余许可数量
local currentValue = redis.call('get', valueName);
local res;
if currentValue ~= false then
-- 如果有令牌过期,就会在下次请求时,重新发放令牌,重新发放的令牌数量 releasedlocal expiredValues = redis.call('zrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval);local released = 0;for i, v in ipairs(expiredValues) do-- permits表示单次请求发放的令牌个数local random, permits = struct.unpack('Bc0I', v);released = released + permits;end;-- 更新剩余许可数量currentValueif released > 0 then-- 清除过期的许可redis.call('zremrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval);-- 如果 剩余许可数 + 发放的许可数 > 桶的大小tonumber(rate),桶已经满了if tonumber(currentValue) + released > tonumber(rate) then-- 剩余许可 = 请求许可数 - 已发放的未过期的许可数currentValue = tonumber(rate) - redis.call('zcard', permitsName);else-- 桶未满,发放新的许可数量,更新剩余可用许可:剩余许可 = 当前剩余许可 + 新发放的许可currentValue = tonumber(currentValue) + released;end;redis.call('set', valueName, currentValue);end;-- 如果剩余许可不够,需要在res中返回下个许可需要等待多长时间if tonumber(currentValue) < tonumber(ARGV[1]) then-- zrange获取有序集合下标区间 0 至 0 的成员,firstValue表示第一个许可生成的时间local firstValue = redis.call('zrange', permitsName, 0, 0, 'withscores');-- 计算下次许可生成的时间-- res = 3 + interval - (tonumber(ARGV[2]) - tonumber(firstValue[2]));res = 0;else-- 保存发放的许可:生成许可保存到permitsName中,redis.call('zadd', permitsName, ARGV[2], struct.pack('Bc0I', string.len(ARGV[3]), ARGV[3], ARGV[1]));-- 减去valueName中的许可数量redis.call('decrby', valueName, ARGV[1]);res = 1;end;
else-- 初始化限流器,struct.pack用于数据打包redis.call('set', valueName, rate);-- ARGV[2]表示分数,即时间戳, ARGV[1]为申请的令牌数redis.call('zadd', permitsName, ARGV[2], struct.pack('Bc0I', string.len(ARGV[3]), ARGV[3], ARGV[1]));redis.call('decrby', valueName, ARGV[1]);res = 1;
end;-- 刷新过期时间
-- Pttl命令以毫秒为单位返回 key 的剩余过期时间 ,如果存在过期时间,需要更新valueName和permitsName对象的过期时间
local ttl = redis.call('pttl', KEYS[1]);
if ttl > 0 thenredis.call('pexpire', valueName, ttl);redis.call('pexpire', permitsName, ttl);
end;
return res;
令牌桶限流器实现TokenBucketRateLimiter:
public class TokenBucketRateLimiter implements RateLimiter{private final static String PREFIX = "tokenBucketRateLimiter";private final RScript rScript;public TokenBucketRateLimiter(RedissonClient client) {this.rScript = client.getScript(LongCodec.INSTANCE);}@Overridepublic boolean isAllowed(Rule rule) {//keys参数组装String keyName = String.format("%s.%s",PREFIX,rule.getKey());String valueName = String.format("{%s}.value",keyName);String permitsName = String.format("{%s}.permits",keyName);long rateInterval = -1L;if (rule.getRateInterval() >= 0L) {rateInterval = rule.getTimeUnit().toMillis(rule.getRateInterval());}byte[] random = new byte[8];ThreadLocalRandom.current().nextBytes(random);String script = LuaScript.getTokenBucketRateLimiterScript();Long result = rScript.eval(RScript.Mode.READ_WRITE, script, RScript.ReturnType.INTEGER,Arrays.asList(keyName, valueName, permitsName),new Object[]{1, System.currentTimeMillis(), random,rule.getRate(),rateInterval});return result == 1L;}
}
五、怎么兼容单机限流
上面采用Redis的Lua脚本实现了经典的4种限流算法,并且都是分布式限流。那么基于Redis的限流器,可以兼容单机限流吗?
其中参考Redisson中的RRateLimiter实现,我们很容易发现,针对单机限流的实现只需要在每个限流器的Key中添加客户端标识信息即可,比如IP信息。
这样每种限流器实现就既可以支持分布式限流,也可以支持单机限流。
这里只提供思路,感兴趣的朋友可以自己实现。
六、其他限流方案
1、Guava的RateLimiter
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法实现流量限制,使用十分方便,而且十分高效。
其提供了2种令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现,但是由于限流信息都保存在本地JVM中,因此RateLimiter只能用于单机限流。
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>23.0</version>
</dependency>
使用样例:
@Service
public class GuavaRateLimiterService {/*每秒控制5个许可*/RateLimiter rateLimiter = RateLimiter.create(5.0);/*** 获取令牌** @return*/public boolean tryAcquire() {return rateLimiter.tryAcquire();}
}@Autowiredprivate GuavaRateLimiterService rateLimiterService;@ResponseBody@RequestMapping("/ratelimiter")public Result testRateLimiter(){if(rateLimiterService.tryAcquire()){return ResultUtil.success1(1001,"成功获取许可");}return ResultUtil.success1(1002,"未获取到许可");}
思考🤔?
Google Guava 工具包中的 RateLimiter 的实现是令牌桶限流,那么是怎么添加令牌的呢?
如果按照指定间隔添加令牌,那么需要开一个线程去定时添加,如果有很多个接口很多个 RateLimiter 实例,线程数会随之增加,这显然不是一个好的办法。显然 Google 也考虑到了这个问题,在 RateLimiter 中,是在每次令牌获取时才进行计算令牌是否足够的。它通过存储的下一个令牌生成的时间,和当前获取令牌的时间差,再结合阈值,去计算令牌是否足够,同时再记录下一个令牌的生成时间以便下一次调用。
2、Redisson的RRateLimiter
Redisson的RRateLimiter也是通过Lua实现的令牌桶限流,并且支持兼容分布式和单机限流,整个框架比较成熟,也是我比较推荐的在项目中使用的方案。
添加依赖:
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.20.1</version></dependency>
使用RRateLimiter限流:
@Autowired
RedissonClient redissonClient;@Testvoid limitTest() throws InterruptedException {RRateLimiter rateLimiter = redissonClient.getRateLimiter("laowan.limiter");//RateType.OVERALL 全局,RateType.PER_CLIENT每个客户端rateLimiter.trySetRate(RateType.OVERALL, 2, 10, RateIntervalUnit.SECONDS);while (true){//rateLimiter.acquire(1); // 申请1份许可,直到成功Thread.sleep(1000L);boolean res = rateLimiter.tryAcquire(1, 2, TimeUnit.SECONDS);if(res){log.info("获取许可");}else{log.info("失败");}}}
3、Redis的redis-cell限流模块
Git地址:redis-cell
Redis 4.0提供了一个限流Redis模块,名称为redis-cell,并提供原子的限流指令。
该模块只有一条指令cl.throttle,其参数和返回值比较复杂。
根据官网说明,redis-cell的好处主要是两点:
1、性能很好,非常快
2、封装了底层实现,避免自定义Lua出现幼稚写法。
但是由于作者声称redis-cell模块目前已经达到很好的性能和实现,后续已经不再积极维护。
不知道是不是我在Redis7.X版本安装该模块始终失败的原因。
感兴趣的朋友可以自己研究下。
4、Nginx 限流
在Nginx中可以通过limit_req_zone配置限流策略。
limit_req_zone
用来限制单位时间内的请求数,采用的漏桶算法 “leaky bucket”。
limit_req_zone $binary_remote_addr zone=test:10m rate=10r/s; #定义限流策略
- $binary_remote_addr 指定按 ip 限流统计。
- zone=test:10m 表示生成一个大小为 10M,名字为 one 的内存区域,用来存储访问的频次信息。
- rate=10r/s 表示允许同一个客户端的访问频次是每秒 10 次。
5、Gateway网关限流
Spring Cloud Gateway 的代码发现,RateLimiter 接口只提供了一个实现类 RedisRateLimiter:
RedisRateLimiter可以用于分布式限流,它的实现原理依然是基于令牌桶算法的,不过实现逻辑是放在一段 lua 脚本中的,我们可以在 src/main/resources/META-INF/scripts 目录下找到该脚本文件 request_rate_limiter.lua:
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil thenlast_tokens = capacity
endlocal last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil thenlast_refreshed = 0
endlocal delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed thennew_tokens = filled_tokens - requestedallowed_num = 1
endif ttl > 0 thenredis.call("setex", tokens_key, ttl, new_tokens)redis.call("setex", timestamp_key, ttl, now)
endreturn { allowed_num, new_tokens }
这段代码和上面介绍令牌桶算法时用 Java 实现的那段经典代码几乎是一样的。这里使用 lua 脚本,主要是利用了 Redis 的单线程特性,以及执行 lua 脚本的原子性
,避免了并发访问时可能出现请求量超出上限的现象。想象目前令牌桶中还剩 1 个令牌,此时有两个请求同时到来,判断令牌是否足够也是同时的,两个请求都认为还剩 1 个令牌,于是两个请求都被允许了。
有两种方式来配置 Spring Cloud Gateway 自带的限流。第一种方式是通过配置文件,比如下面所示的代码,可以对某个 route 进行限流:
spring:cloud:gateway:routes:- id: testuri: http://httpbin.org:80/getfilters:- name: RequestRateLimiterargs:key-resolver: '#{@hostAddrKeyResolver}'redis-rate-limiter.replenishRate: 1redis-rate-limiter.burstCapacity: 3
其中,key-resolver 使用 SpEL 表达式 #{@beanName} 从 Spring 容器中获取 hostAddrKeyResolver 对象,burstCapacity 表示令牌桶的大小,replenishRate 表示每秒往桶中填充多少个令牌,也就是填充速度。
第二种方式是通过下面的代码来配置:
@Beanpublic RouteLocator myRoutes(RouteLocatorBuilder builder) {return builder.routes().route(p -> p.path("/get").filters(filter -> filter.requestRateLimiter().rateLimiter(RedisRateLimiter.class, rl -> rl.setBurstCapacity(3).setReplenishRate(1)).and()).uri("http://httpbin.org:80")).build();
}
6、阿里的sentinel限流
官网:sentinel
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、流量路由、熔断降级、系统自适应过载保护、热点流量防护等多个维度保护服务的稳定性。
Sentinel 的主要特性:
可以看到Sentinel的功能十分强大,并且完美兼容目前的微服务体系,并且提供了Web控制台对限流规则实现可视化配置。
7、ratelimiter-spring-boot-starter
Git地址:ratelimiter-spring-boot-starter
介绍该方案主要是作者对基于Redis限流的封装方式比较优雅,其基于注解的限流控制使得项目中对接口进行限流十分优雅便捷。想自己封装 限流-spring-boot-starter的同学可以好好借鉴下。
<dependency><groupId>com.github.taptap</groupId><artifactId>ratelimiter-spring-boot-starter</artifactId><version>1.3</version>
</dependency>
8、信号量 Semaphore
信号量Semaphore也可以用来进行单机环境下的并发控制。
实际应用中,可以用来限制访问某种资源的数量,比如在Hystrix中就有基于Semaphore的资源隔离策略。
public class SemaphoreTest {private static ExecutorService threadPool = Executors.newFixedThreadPool(100);private static Semaphore semaphore = new Semaphore(10);public static void main(String[] args) {for (int i = 0; i < 100; i++) {threadPool.execute(new Runnable() {@Overridepublic void run() {try {semaphore.acquire();System.out.println("Request processing ...");semaphore.release();} catch (InterruptedException e) {e.printStack();}}});}threadPool.shutdown();}
}
9、Bucket4j
官方文档:Bucket4j
Bucket4j是一个基于令牌桶
算法实现的强大的限流库,它不仅支持单机限流,还支持通过诸如 Hazelcast、Ignite、Coherence、Infinispan 或其他兼容 JCache API (JSR 107) 规范的分布式缓存实现分布式限流。
和 Guava 的限流器相比,Bucket4j 的功能显然要更胜一筹
,毕竟 Guava 的目的只是用作通用工具类,而不是用于限流的。使用 Bucket4j 基本上可以满足我们的大多数要求,不仅支持单机限流和分布式限流,而且可以很好的集成监控,搭配 Prometheus 和 Grafana 简直完美。值得一提的是,有很多开源项目譬如 JHipster API Gateway 就是使用 Bucket4j 来实现限流的。
Bucket4j 唯一不足的地方是它只支持请求频率限流,不支持并发量限流
,另外还有一点,虽然 Bucket4j 支持分布式限流,但它是基于 Hazelcast 这样的分布式缓存系统实现的,不能使用 Redis
,这在很多使用 Redis 作缓存的项目中就很不爽,所以我们还需要在开源的世界里继续探索。
<dependency><groupId>com.bucket4j</groupId><artifactId>bucket4j-core</artifactId><version>8.1.1</version>
</dependency>
// define the limit 1 time per 10 minute
Bandwidth limit = Bandwidth.simple(1, Duration.ofMinutes(10));
// construct the bucket
Bucket bucket = Bucket.builder().addLimit(limit).build();
...try {executor.execute(anyRunnable);
} catch (RejectedExecutionException e) {// print stacktraces only if limit is not exceededif (bucket.tryConsume(1)) {ThreadInfo[] stackTraces = ManagementFactory.getThreadMXBean().dumpAllThreads(true, true);StacktraceUtils.print(stackTraces);}throw e;
}
10、Resilience4j
官网:Resilience4j
Resilience4j 是一款轻量级、易使用的高可用框架。用过 Spring Cloud 早期版本的同学肯定都听过 Netflix Hystrix,Resilience4j 的设计灵感就来自于它。自从 Hystrix 停止维护之后,官方也推荐大家使用 Resilience4j 来代替 Hystrix
。
Resilience4j 的底层采用 Vavr,这是一个非常轻量级的 Java 函数式库,使得 Resilience4j 非常适合函数式编程。Resilience4j 以装饰器模式提供对函数式接口或 lambda 表达式的封装,提供了一波高可用机制:重试(Retry)、熔断(Circuit Breaker)、限流(Rate Limiter)、限时(Timer Limiter)、隔离(Bulkhead)、缓存(Caceh) 和 降级(Fallback)
。我们重点关注这里的两个功能:限流(Rate Limiter) 和 隔离(Bulkhead),Rate Limiter 是请求频率限流,Bulkhead 是并发量限流。
Resilience4j 提供了两种限流的实现:SemaphoreBasedRateLimiter 和 AtomicRateLimiter。SemaphoreBasedRateLimiter 基于信号量实现,用户的每次请求都会申请一个信号量,并记录申请的时间,申请通过则允许请求,申请失败则限流,另外有一个内部线程会定期扫描过期的信号量并释放,很显然这是令牌桶的算法。AtomicRateLimiter 和上面的经典实现类似,不需要额外的线程,在处理每次请求时,根据距离上次请求的时间和生成令牌的速度自动填充。
Resilience4j 在功能特性上比 Bucket4j 强大不少,而且还支持并发量限流。不过最大的遗憾是,Resilience4j 不支持分布式限流
。
<dependency><groupId>io.github.resilience4j</groupId><artifactId>resilience4j-ratelimiter</artifactId><version>2.0.2</version>
</dependency>
通过代码配置:
//限制每秒10次调用
RateLimiterConfig config = RateLimiterConfig.custom().limitRefreshPeriod(Duration.ofSeconds(1)).limitForPeriod(10).timeoutDuration(Duration.ofMillis(25)).build();
// Create registry
RateLimiterRegistry rateLimiterRegistry = RateLimiterRegistry.of(config);// Use registry
RateLimiter rateLimiterWithDefaultConfig = rateLimiterRegistry.rateLimiter("name1");
// Decorate your call to BackendService.doSomething()
CheckedRunnable restrictedCall = RateLimiter.decorateCheckedRunnable(rateLimiterWithDefaultConfig, backendService::doSomething);
Try.run(restrictedCall).onFailure((RequestNotPermitted throwable) -> LOG.info("Wait before call it again :)"));
通过属性文件配置:
resilience4j.ratelimiter:instances:backendA:limitForPeriod: 10limitRefreshPeriod: 1stimeoutDuration: 0registerHealthIndicator: trueeventConsumerBufferSize: 100backendB:limitForPeriod: 6limitRefreshPeriod: 500mstimeoutDuration: 3s
spring注解实现流控:
@RateLimiter(name = "backendA",fallbackMethod = "fallback" )
public Mono<String> method(String param1) {return Mono.error(new NumberFormatException());
}private Mono<String> fallback(String param1, IllegalArgumentException e) {return Mono.just("test");
}private Mono<String> fallback(String param1, RuntimeException e) {return Mono.just("test");
}
总结
本文主要介绍了基于Redis通过Lua脚本实现分布式限流的几种方案。
1、4种典型的限流算法:固定时间窗口,滑动时间窗口,漏桶,令牌牌。
2、通过Lua脚本实现4种典型的分流算法。
3、其他限流实现方案介绍:Guava的RateLimiter、Redisson的RRateLimiter、Redis的redis-cell限流模块、Nginx 限流、 Spring Cloud Gateway网关限流、阿里的sentinel限流、信号量Semaphore单机并发限流、Bucket4j和Resilience4j限流。
系统设计 | 分布式限流器
5 种限流算法,7 种限流方式,挡住突发流量?
redis (3) - 各种限流方式及源码实现
超详细的Guava RateLimiter限流原理解析
ratelimiter-spring-boot-starter
Spring Cloud Gateway 限流实战,终于有人写清楚了!
相关文章:
基于Redis的分布式限流详解
前言 Redis除了能用作缓存外,还有很多其他用途,比如分布式锁,分布式限流,分布式唯一主键等,本文将和大家分享下基于Redis分布式限流的各种实现方案。 一、为什么需要限流 用最简单的话来说:外部请求是不可…...
权限提升:漏洞探针.(Linux系统)
权限提升:漏洞探针. 权限提升简称提权,由于操作系统都是多用户操作系统,用户之间都有权限控制,比如通过 Web 漏洞拿到的是 Web 进程的权限,往往 Web 服务都是以一个权限很低的账号启动的,因此通过 Webshel…...
python-11-多线程模块threading
python使用多线程实例讲解 1 进程和线程 1.1 进程和线程的概念 进程(process)和线程(thread)是操作系统的基本概念。 进程是资源分配的最小单位,线程是CPU调度的最小单位。 线程是程序中一个单一的顺序控制流程,进程内一个相对独立的、可调度的执行单…...
动态gif图片如何在线做?轻松实现图片在线生成gif
常见的jpg、png格式的静态图片想要变成gif格式的动态图片时,要怎么办呢?有没有什么简单实用的gif制作工具呢? 一、什么工具能够在线制作gif? GIF中文网作为一款专业的gif制作(https://www.gif.cn/)工具&a…...
浅谈联网汽车安全漏洞
“智能网联汽车存在内生共性问题,即软硬件的漏洞后门,基于此进行的网络攻击可以直接带来勒索、盗窃、大规模车辆恶意操控风险,还有数据泄露等网络安全事件。如果内生的漏洞后门问题不解决,系统自身难保,很难谈系统安…...
深入理解SeaTunnel:易用、高性能、支持实时流式和离线批处理的海量数据集成平台
深入理解SeaTunnel:易用、高性能、支持实时流式和离线批处理的海量数据集成平台 一、认识SeaTunnel二、SeaTunnel 系统架构、工作流程与特性三、SeaTunnel工作架构四、部署SeaTunnel1.安装Java2.下载SeaTunnel3.安装连接器 五、快速启动作业1.添加作业配置文件以定义…...
项目上线 | 兰精携手盖雅工场,数智驱动绿色转型
近年来,纺织纤维行业零碳行动如火如荼。作为低碳环保消费新时尚引领者,同时也是纤维领域隐形冠军,兰精在推进绿色发展的同时,也在不断向内探索企业数字化转型之道,以此反哺业务快速扩张。 数智转型,管理先…...
102-Linux_I/O复用方法之poll
文章目录 1.poll系统调用的作用2.poll的原型3.poll支持的事件类型4.poll实现TCP服务器(1)服务器端代码:(2)客户端代码:(3)运行结果截图: 1.poll系统调用的作用 poll 系统调用和 select 类似,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有…...
【VAR模型 | 时间序列】帮助文档:VAR模型的引入和Python实践(含源代码)
向量自回归 (VAR) 是一种随机过程模型,用于捕获多个时间序列之间的线性相互依赖性。 VAR 模型通过允许多个进化变量来概括单变量自回归模型(AR 模型)。 VAR 中的所有变量都以相同的方式进入模型:每个变量都有一个方程式ÿ…...
Spark任务提交流程
1. yarn-client Driver在任务提交的本地机器上运行,Driver启动后会和ResourceManager通讯,申请启动ApplicationMaster; 随后ResourceManager分配Container,在合适的NodeManager上启动ApplicationMaster,此时的ApplicationMaster的…...
python相对路径与绝对路径
9.1 Python 绝对路径与相对路径 - 知乎 (zhihu.com) 目录 1. 绝对路径 1.1 概念 1.2 用绝对路径打开文件 1.2 相对路径 1.3 python路径表示的斜杠问题 1. 绝对路径 1.1 概念 绝对路径 指完整的描述文件位置的路径。绝对路径就是文件或文件夹在硬盘上的完整路径。 在 Win…...
SPSS如何进行基本统计分析之案例实训?
文章目录 0.引言1.描述性分析2.频数分析3.探索分析4.列联表分析5.比率分析 0.引言 因科研等多场景需要进行数据统计分析,笔者对SPSS进行了学习,本文通过《SPSS统计分析从入门到精通》及其配套素材结合网上相关资料进行学习笔记总结,本文对基本…...
Python项目实战篇——常用验证码标注和识别(需求分析和实现思路)
前言:验证码识别和标注是现在网络安全中的一个重要任务,尤其是在一些电商平台和在线支付等场景中,验证码的安全性至关重要。本文将介绍如何使用Python实现常用的验证码标注和识别,以便为自己的项目提供参考。 一、需求分析 1、验证…...
MySQL基础(六)多表查询
多表查询,也称为关联查询,指两个或更多个表一起完成查询操作。 前提条件:这些一起查询的表之间是有关系的(一对一、一对多),它们之间一定是有关联字段,这个关联字段可能建立了外键,…...
零死角玩转stm32中级篇3-SPI总线
本篇博文目录: 一.基础知识1.什么是SPI2.SPI和IIC有什么不同3.SPI的优缺点4.SPI是怎么实现通信的5.SPI 数据传输的步骤6.SPI菊花链7.通过SPI实现数据的读和写 二.STM32F103C8T6芯片SPI协议案例代码 一.基础知识 1.什么是SPI SPI(Serial Peripheral Interface&#…...
顺序表功能实现(入手版详解)
🍉博客主页:阿博历练记 📖文章专栏:数据结构与算法 🚚代码仓库:阿博编程日记 🌹欢迎关注:欢迎友友们点赞收藏关注哦 文章目录 🍓前言✨顺序表🔍1.顺序表的整体…...
Java 中的线程是什么,如何创建和管理线程-下(十三)
书接上文 CompletableFuture CompletableFuture 是 Java 8 中新增的类,提供了更为强大的异步编程支持。它可以将多个异步任务组合成一个整体,并且可以处理异常情况。 例如,可以使用 CompletableFuture 来实现异步任务的串行执行࿱…...
为什么我的Windows 10 便签不支持更改字体?
Windows便签是一款常用的记录工具,可以帮助我们快速记录一些重要的信息。在使用Windows便签时,如果你想要更好地呈现你的信息,可以通过设置字体来达到这个效果。本文将介绍Windows便签字体设置的相关知识,希望对你有所帮助。 1、打…...
野火STM32电机系列(六)Cubemx配置ADC规则和注入通道
前文已经配置了GPIO、编码器 本节讲解CubeMXADC规则和注入通道 本文adc注入通道采用定时器触发,因此在上文定时器配置的基础上进行 常规信号(温度等)使用带DMA的常规通道连续采样 注入采样由定时器触发,采集电机三相电流&…...
预制菜,巨头们的新赛场
俗话说“民以食为天”,饮食对于大众的重要性自然是无需赘述。然而,随着生活节奏的加快,越来越多年轻人没有时间和精力去烹制菜肴,这也是外卖行业持续火热的重要原因之一。尽管如此,随着消费者健康意识的持续提升&#…...
英语语法第一章之英语语法综述
英语的任何句型基本都可以翻译成 什么怎么样 ,在这里什么就是我们常说的主语,而怎么样就是我们常说的谓语; 可能有些小伙伴会反问,不是主谓宾吗?别急等我慢慢讲解 在这里谓语也有很有多的不同的动作 可以独立完成的动作 句型&am…...
ChatGPT被淘汰了?Auto-GPT到底有多强
大家好,我是可夫小子,关注AIGC、读书和自媒体。解锁更多ChatGPT、AI绘画玩法。 说Auto-GPT淘汰了ChatGPT了,显然是营销文案里面的标题党。毕竟它还是基于ChatGPT的API,某种意义只是基于ChatGPT能力的应用。但最近,Auto…...
unity NGUI使用方法
基本用法 很多基本模块比如按钮、slider等都能从Prefab中直接拖拽到场景中实现,但都需要有一个Collider(Prefab已经自带) 因为不仅是UI,所有带有Collider的游戏物体都能接收到OnClick, OnPress这样的事件——前提是需…...
软件测试技术(五)软件测试流程
软件测试流程 软件测试流程如下: 测试计划测试设计测试执行 单元测试集成测试确认测试系统测试验收测试回归测试验证活动 测试计划 测试计划由测试负责人来编写,用于确定各个测试阶段的目标和策略。这个过程将输出测试计划,明确要完成的测…...
Redis缓存穿透和雪崩
Redis缓存穿透和雪崩 Redis缓存的使用,极大的提升了应用程序的性能和效率,特别是数据查询方面。但同时,它也带来了一些问题。其中,最要害的问题,就是数据的一致性问题,从严格意义上讲,这个问题…...
【C++】set和map的使用
对于STL容器来说,有很多相似的功能,所以这里主要将与之前不同的功能说清楚 文章目录 1.对于set与map的简单理解2. setinsert迭代器遍历countmultisetinsertfindcount 3. mapinsert与迭代器的使用统计水果次数 operator []operator[]的实现理解对整体的拆…...
大学生学java编程的就业前景怎么样?我来聊聊自己的见解
今天兴哥想跟大家分享一个话题,就是学java到底好不好找工作。因为我发现很多粉丝朋友,之前可能并不是从事IT行业的,然后想转行来做这一行,或者是有些大四即将面临毕业的老哥,可能大学没有好好学习吧,然后专…...
JavaScript全解析——canvas 入门(上)
●canvas 是 HTML5 新增的一个标签, 表示画布 ●canvas 也是 h5 的画布技术, 我们通过代码的方式在画布上描绘一个图像 canvas 标签 ●向进行 canvas 绘图, 首先我们先要了解到 canvas 标签 ●是 html5 推出的一个标签 <html> <head>... </head> <body&…...
vue 插槽的用法
Vue的插槽(Slot)是一种可以让父组件向子组件传递内容的机制。插槽可以让开发者将组件的结构和内容分离开来,从而实现更好的可维护性和复用性。 在Vue中,插槽通过 标签实现。具体用法如下: 单个插槽 在子组件中使用一…...
【C++复习2】C++编译器的工作原理
如果你是一名newbird的话,建议观看如下视频加深你的理解,再看如下内容: https://www.bilibili.com/video/BV1N24y1B7nQ?p7 The cherno会额外告诉你如何将目标文件转换成汇编代码,CPU执行指令的过程以及编译器如何通过删除冗余变…...
佛山微网站/人民网 疫情
这个例子比较大,任重而道远。理论草草看了下,光记住索引和RGB各项求最小距离了。 为简单起见,拷贝7-6过来,并把8-3封装到的引擎代码拷贝替换。 逐行来看,各个击破 先换成800*600视口 #define SCREEN_WIDTH …...
安平百度做网站/网页设计与网站建设教程
1.1 体系架构的设计<?xml:namespace prefix o ns "urn:schemas-microsoft-com:office:office" />在项目实施过程中,首先需要确定系统的总体技术体系架构。可以在规划阶段中确定的体系架构为基础,看是否需要调整,需要细…...
微信怎样制作网站/网上教育培训机构哪家好
一、主机环境 IP地址 主机名称 部署服务 硬盘(必须是裸设备硬盘,不然无法创建osd) 172.16.5.239 k8s01 ceph-deploy、mon、osd /dev/sdb 172.16.5.240 k8s02 osd /dev/sdb 172.16.5.241 k8s03 osd /dev/sdb 二、环境部署 2.1、配置yum源 cat &g…...
企业做网站需要哪些材料/seo怎么做优化
2019独角兽企业重金招聘Python工程师标准>>> 其实说。是一个付不起责任的人。 因为具体的工作安排并不能做好监督与沟通。 但是, 作为员工, 经理的安排我是应该主动的完成的。 特别是写文档这个事情。 在我认知中。 主要工作是在10月之前完成…...
网站建设推广文案/百度浏览器app下载
前言 在深入之前先了解下下ReentrantLock 和 Condition: 重入锁ReentrantLock: ReentrantLock锁在同一个时间点只能被一个线程锁持有;而可重入的意思是,ReentrantLock锁,可以被单个线程多次获取。 ReentrantLock分为“公平锁”和“非公平锁”…...
成都网站开发scwbo/搜索引擎优化的定义是什么
人工智能(AI)将会对地缘政治造成什么影响?关于这个问题的讨论大部分集中在全球实力最强的几个大国之间在国防和军事工业上的激烈竞争。不过,除了这些大国之外,人工智能在世界其他地区的发展,也可能对世界产…...