一文带你彻底理清 Redisson RateLimiter tryAcquire 分布式限流的底层原理!有图有真相,挑战全网最详细

做完鱼皮的BI项目的同学,或者比较好奇框架采用的限流底层原理的同学,可以来看看我这篇文章,主要讲Redisson RateLimiter tryAcquire 的底层原理,保证保姆级,超级详细!如果看完觉得还可以的话欢迎来支持下博客:https://blog.csdn.net/wyx1473343124/article/details/146583113?spm=1001.2014.3001.5501

1.一个简单的 Redisson 限流需要用到哪些方法?

下面是一个最简单的 Redisson 限流实践:

java
复制代码
@Test void test1(){ RRateLimiter rateLimiter = redissonClient.getRateLimiter("limit:user:1"); rateLimiter.trySetRate(RateType.OVERALL,3,2, RateIntervalUnit.SECONDS); System.out.println(rateLimiter.tryAcquire(1)); System.out.println(rateLimiter.tryAcquire(3)); }

注意,tryAcquire()等同于 tryAcquire(1)

这个实践的含义是对用户1(user:1)进行限流,大致分为三个流程:

=== getRateLimiter 获取限流器对象

==> trySetRate 设置对于同一个名称的限流器(limit:user:1), 2秒 内最多允许 3 次请求(准确一点说,是3个许可证,一个请求可以获取多个许可证)

==> tryAcquire (num) 代表,尝试获取 num 个许可证,成功获取返回 true,代表通过限流

具体来讲,执行上面的 测试方法,两秒内两次 tryAcquire,由于两秒内 1+3 = 4 > 3,因此第二次 tryAcquire 会失败。

相信来了解原理的读者大多都了解上面三个 API 的用法,那么现在的问题是:这三个 API 究竟干了些什么?

  1. getRateLimiter 和 trySetRate 干了什么?

2.1 getRateLimiter 没有对 Redis 进行任何操作,只是初始化限流器对象,并保存你传入的 name 参数

追下源码

追进构造器

可以看到,实际上就是用 name 进行了 RateLimiter 对象的初始化,而看不到任何 Lua 脚本

2.2 trySetRate 实际操作 Redis,将限流参数设置到 Redis hash 结构中

为了直观理解 trySetRate 如何操作 Redis ,可以利用 Redis 可视化客户端(我是用的是 RESP)来查看 Redis 中 key 的变化

在上文提到的 Test 测试方法中打断点运行,我们会发现,在执行 getRateLimiter 后,刷新 RESP,key 没有任何变化,状态如下图

编辑

而当我们打断点执行完 trySetRate 的一行之后,刷新 RESP,就可以看到 db1 中多了一个 key

编辑

这个 key 有点眼熟?

java
复制代码
@Test void test1(){ RRateLimiter rateLimiter = redissonClient.getRateLimiter("limit:user:1"); rateLimiter.trySetRate(RateType.OVERALL,3,2, RateIntervalUnit.SECONDS); System.out.println(rateLimiter.tryAcquire(1)); System.out.println(rateLimiter.tryAcquire(3)); }

仔细一看,就是之前你在 getRateLimiter 里传入的 name 参数,rateLimiter 对象中保存了这个 name 参数,并在 trySetRate 时将该参数作为 key ,set 到 Redis 中

那么这个 key 的内容是什么呢?

可以看到,就是一个 hash 结构,将我们 trySetRate 时传入的 rate, rateInterval,type 等,作为 field - value 设置到了其中!interval 单位为毫秒。

通过可视化,我们了解了 trySetRate 的大致行为,接下来我们追入 Lua 脚本源码

看到绿色字符串的时候,我们知道,这波稳了(狗头)。显然,和上面说的一样,不过细节是 Lua 脚本采用 hsetnx,nx 保证了只有在不存在该 field 的情况下才能设置,所以你在代码里重复 trySetRate 是不会报错的,不过这样不太规范(我们下面会讲)

  1. tryAcquire (long permits) 执行前后 Redis 状态对比

个人的学习经验是,学习某项技术不能一开始就去看源码,也不能一开始就问 AI,我们首先需要通过可视化工具让自己对代码执行的效果建立起一个直观的理解,之后再去看源码比较合适。所以这里就先给大家展示下 tryAcquire 后 Redis 的变化,建立起一个基础理解后再去看 Lua 脚本。

首先,在 trySetRate 将指定限流配置设置到 Redis 中后,接下来我们 tryAcquire 获取许可证,看看 Redis 中发生了什么。

这是我们在 tryAcquire 之前的 Redis 的状态

接下来 debug 继续,执行 tryAcquire(1),可以看到执行完成之后,db1多出了两个key!这两个key的全名是:{limit:user:1}:permits,{limit:user:1}:value,这两个 key 观察一下,发现就是在原来的限流 key 上拼接了后缀形成新的 key

3.1 tryAcquire 后限流key (limit:user:1) 没有任何变化,可以将它理解为只读的配置 key

我们这时去观察原来的 limit:user:1 的限流key,发现 hash 结构中 的值没有任何变化!这表明这个 key 只负责存储我们指定的限流配置,而不会在 tryAcquire 时去进行写操作等改变 field 的值

那么我既然通过了限流,总有一些值需要改变吧,是哪里触发了写操作呢?这就要说到刚刚多出来的两个 key : {limit:user:1}:permits,{limit:user:1}:value ,我们去看一下它们现在是什么值

3.2 初步理解限流 key 拼接 “value”、限流 key 拼接 "permits" 所产生的两个key

首先我们看 {limit:user:1}:value 这个 key,发现它为 2。2和前面有什么关联?对了,设置的 rate 为 3,而我们 tryAcquire(1)获取一个许可证,这不就剩下了 2 吗?看到这里你应该已经对 RateLimiter 的原理有了一些初步的猜测,**联想到,trySetRate 后初始就有 3 个许可证,和设置的 rate 相对应;tryAcquire 会使许可证扣减,而 {limit:user:1}:value 则代表扣减后剩下的许可证数量。**接下来在 Lua 脚本的源码中,你会更清晰地理解它和限流的 interval 有什么关系,到底是什么过程。

拼接 "value" 的 key 说完了,拼接"permits"的 key 呢?

可以看到 {limit:user:1}:permits 是一个 ZSET 类型,也就是 SortedSet。 value 不知道在写什么,score 里又是什么玩意?好,我们接下来开始看 Lua 脚本源码

  1. tryAcquire(long permits) Lua 脚本源码详细剖析

4.1 理解 Lua 脚本中的 KEYS,ARGV 数组的各元素的含义

追进 tryAcquire

追进 tryAcquireAsync,可以发现到了 Lua 脚本源码部分

这里为了方便展示,我去除了 + 号等,得到了如下代码:

java
复制代码
return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, " local rate = redis.call('hget', KEYS[1], 'rate'); local interval = redis.call('hget', KEYS[1], 'interval'); local type = redis.call('hget', KEYS[1], 'type'); assert(rate ~= false and interval ~= false and type ~= false, 'RateLimiter is not initialized') local valueName = KEYS[2]; local permitsName = KEYS[4]; if type == '1' then valueName = KEYS[3]; permitsName = KEYS[5]; end; local currentValue = redis.call('get', valueName); if currentValue ~= false then local expiredValues = redis.call('zrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval); local released = 0; for i, v in ipairs(expiredValues) do local random, permits = struct.unpack('fI', v); released = released + permits; end; if released > 0 then redis.call('zrem', permitsName, unpack(expiredValues)); currentValue = tonumber(currentValue) + released; redis.call('set', valueName, currentValue); end; if tonumber(currentValue) < tonumber(ARGV[1]) then local nearest = redis.call('zrangebyscore', permitsName, '(' .. (tonumber(ARGV[2]) - interval), tonumber(ARGV[2]), 'withscores', 'limit', 0, 1); local random, permits = struct.unpack('fI', nearest[1]); return tonumber(nearest[2]) - (tonumber(ARGV[2]) - interval); else redis.call('zadd', permitsName, ARGV[2], struct.pack('fI', ARGV[3], ARGV[1])); redis.call('decrby', valueName, ARGV[1]); return nil; end; else assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount could not exceed defined rate'); redis.call('set', valueName, rate); redis.call('zadd', permitsName, ARGV[2], struct.pack('fI', ARGV[3], ARGV[1])); redis.call('decrby', valueName, ARGV[1]); return nil; end; ", Arrays.asList(this.getName(), this.getValueName(), this.getClientValueName(), this.getPermitsName(), this.getClientPermitsName()), new Object[]{value, System.currentTimeMillis(), ThreadLocalRandom.current().nextLong()});

要看懂这个 Lua 脚本,最先需要做的事是:理解 KEYS 和 ARGV 的各元素分别代表什么。

首先要知道,这段代码开头的 evalWrite 会将这段代码结尾的

Arrays.asList(this.getName(), this.getValueName(), this.getClientValueName(), this.getPermitsName(), this.getClientPermitsName()) 作为 Lua 脚本的 KEYS ,

将 new Object[]{value, System.currentTimeMillis(), ThreadLocalRandom.current().nextLong()} 作为 Lua 脚本的 ARGV,Lua 脚本中的索引从1开始。

那么 KEYS 大小为5,其中看到第三个参数 this.getClientValueName() 和第五个参数 this.getClientPermitsName() 带有 Client ,这主要运用于单客户端限流,而我们之前在 trySetRate 中已经指定过 RateType.OVERALL 全体限流,因此这两个参数我们不用管;那么第一,第二,第四个参数分别代表什么?this.getName,getValueName,getPermitsName,看到这些单词,是不是熟悉起来了?

getName 获取到的就是 getRateLimiter() 里传入的 name 参数,代表限流器配置key “limit:user:1”,也就是说, KEYS[1] = "limit:user:1";

getValueName 获取到的就是之前看到的 {limit:user:1}:value , 也就是说, KEYS[2] = "{limit:user:1}:value";

getPermitsName 获取到的就是之前看到的 {limit:user:1}:permits, 也就是说, KEYS[4] = "{limit:user:1}:permits"

ARGV 就比较容易理解了

ARGV[1] = value,也就是你 tryAcquire(permits)的 permits 值;

ARGV[2] = 当前毫秒时间;

ARGV[3] 则是一个随机数

4.2 Lua 脚本分段解读

首先看下 Lua 脚本第一段,显然就是读取到了 KEYS[1],也就是限流器配置 key 里的各项配置 rate,interval,type;然后将 KEYS[2],KEYS[4] ,也就是另外两个 key 赋值给 valueName 和 permitsName,type == 1 代表客户端限流而非全局限流,不考虑

java
复制代码
local rate = redis.call('hget', KEYS[1], 'rate'); local interval = redis.call('hget', KEYS[1], 'interval'); local type = redis.call('hget', KEYS[1], 'type'); assert(rate ~= false and interval ~= false and type ~= false, 'RateLimiter is not initialized') local valueName = KEYS[2]; local permitsName = KEYS[4]; if type == '1' then valueName = KEYS[3]; permitsName = KEYS[5]; end;

接着看 Lua 脚本第二段,也就是从 redis.call('get', valueName) 开始的这一段。

首先回顾我们上面提到的示例,在 trySetRate 后,第一次 tryAcquire 使 Redis 中多出了两个 key,我们首先到源码中看一下这多出来的两个 key 走的是什么逻辑。

redis.call('get', valueName) 获取 {limit:user:1}:value 对应的值,可是上面可视化也看到,我们 trySetRate 后是没有{limit:user:1}:value 这个 key 的,因此第一次 tryAcquire,这里 Lua脚本中得到的 currentValue 值为 null;

可以看到下面的 if currentValue ~= false (也就是 != null) 做了currentValue 是否为空的判断,由于此时 currentValue 为空,所以应该走 else 逻辑

编辑

else 逻辑干了什么?

java
复制代码
else assert(tonumber(rate) >= tonumber(ARGV[1]), 'Requested permits amount could not exceed defined rate'); redis.call('set', valueName, rate); redis.call('zadd', permitsName, ARGV[2], struct.pack('fI', ARGV[3], ARGV[1])); redis.call('decrby', valueName, ARGV[1]); return nil; end;

首先,第一行 assert 判断 限流器 rate 参数是否大于等于 ARGV[1], ARGV[1] 即 tryAcquire 传入的 permits 。如果 rate < 传入的 permits,就返回 'Request permits mount could not exceed defined rate',即 '请求的许可数不能超过限流器配置的 rate',这个也好理解,你两秒钟限3次,(限3个许可),那你一次请求4个许可显然是不行的。

接下来的三行,关键点来了!

redis.call('set', valueName, rate):

这代表如果是 trySetRate 后第一次 tryAcquire,则会去 Redis 中设置 String 类型的 {limit:user:1}:value,初始化值为限流器配置的 rate;

redis.call('zadd', permitsName, ARGV[2], struct.pack('fI', ARGV[3], ARGV[1]));

zadd 这一段采用的是 Redis sortedSet 数据结构的 zadd key score value****命令将当前毫秒时间戳 (ARGV[2]) 作为 score,并将随机数(ARGV [3] )、permits (ARGV[1])打包作为 value,存入新建的key {limit:user:1}:permits 中

这下你是否可以理解之前看到的这个图了呢?

score的这一长串就是当前毫秒时间戳,而 value 则是 struct.pack,压缩(打包) permits 和随机数后得到的一个值

而最后一行 decrby valueName ARGV[1]比较简单,既然你现在 {limit:user:1}:value 初始值为 rate,而且 assert 判断了 rate >= permits,那么这个请求获取许可证肯定是成功的;获取完后从 {limit:user:1}:value 中 扣减 permits(ARGV[1]) 个许可,然后返回 nil;

那么我们可以好好思考一下了:为什么 要有 zadd 的那一行代码,将这个成功获取到许可证的请求的 时间戳 和 打包值 存储下来?

拿我们的例子来看,rate 为3,rateInterval为2000(2秒),你想,如果按照现在提到的逻辑,每一次 tryAcquire(permits) 获取许可证都从 {limit:user:1}:value 中 扣减 permits 个许可,但是{limit:user:1}:value 初始化值就是 rate = 3, 你这样获取许可证,总有一天会获取完的,难道我们建了一个限流器,从建立开始到世界毁灭都只能提供3个许可证吗?那也太搞笑了。

为了实现一个正常运作的限流器,关键点就在于 回收 老请求的许可证,将回收的许可证数加到{limit:user:1}:value 的 key 中,{limit:user:1}:value 有减有加才能确保限流器一直正常工作;而为了回收老请求的许可证,我们需要记录每一个成功获取许可证的请求,所以用到了 zadd 记录到 SortedSet 。

我们上面说,要回收老请求。那么什么可以定义为"老"请求 ? 这下我也亢奋起来了,写了这么久,终于写到了RateLimiter最核心的点:

1)回收:第一次 tryAcquire 后,每当一个请求尝试 tryAcquire,Redisson 就会回收“当前时间戳减去 rateInterval”前的所有请求记录,并将它们的 permits 加回到 {limit:user:1}:value上;

2)判断:然后再判断现在的{limit:user:1}:value 的许可数是否 >= 当前请求的 permits:

==> 如果是,说明可以给这个请求 permits 个许可,则从 {limit:user:1}:value 中扣减对应数量的许可,并记录这个获取许可成功的请求,也就是通过 zadd, 将这个请求对应的时间戳和打包值存储到 {limit:user:1}:permits 中;

==> 如果不是,说明给不了这个请求 permits 个许可,当前请求获取许可证失败;**这个时候不会记录该请求到{limit:user:1}:permits,**也不会扣减许可证。返回给客户端一个毫秒值时间,代表还需要等待多久可以再次尝试获取。

这也就是说,老请求指的是 rateInterval 前的请求,每次有客户端尝试获取许可证,都会去回收 rateInterval 前的请求。

上面这么说,可能有点抽象,通过下面的图来辅助理解

4.3 多个请求尝试获取同一限流器许可证,流程可视化图

编辑

是否能理解呢?具体来说:

1.每一个请求来获取许可证,都会先尝试回收 2000 毫秒前的请求

==> 如果SortedSet 中没有2000ms前的请求记录(没有请求,或者已经被回收过) , 就什么都不做;

==> 如果SortedSet 中有2000ms前的请求记录,则回收请求;回收请求的许可证数将加回到 {limit:user:1}:value,并将回收请求移除出 SortedSet

2.尝试回收请求后,判断当前 {limit:user:1}:value 中许可证个数是否 >= permits,如果是,则获取许可证成功,记录请求,扣减许可(对应限流放行);如果不是,获取许可证失败,不记录请求(对应限流拒绝)

理解上面的流程后,你会发现一个有趣的特性:

{limit:user:1}:value (当前可用许可证数量)再怎么变化,也不会超过 rate=3 —— 因为第一次 tryAcquire 时,指定了 {limit:user:1}:value 初始值为 rate,而后续我们没有主动增加 {limit:user:1}:value 的操作,一切许可证的增加都靠回收,可以理解成开始的 3 个 许可反复回收利用。

4.4.理解流程可视化图后,回看 Lua 脚本源码

我们之前还有一段 Lua 脚本没有读过,这段是最复杂的,但是在理解上面的流程可视化图后,你会发现,无非就是先回收,再判断!

plain
复制代码
local currentValue = redis.call('get', valueName); if currentValue ~= false then local expiredValues = redis.call('zrangebyscore', permitsName, 0, tonumber(ARGV[2]) - interval); local released = 0; for i, v in ipairs(expiredValues) do local random, permits = struct.unpack('fI', v); released = released + permits; end; if released > 0 then redis.call('zrem', permitsName, unpack(expiredValues)); currentValue = tonumber(currentValue) + released; redis.call('set', valueName, currentValue); end; if tonumber(currentValue) < tonumber(ARGV[1]) then local nearest = redis.call('zrangebyscore', permitsName, '(' .. (tonumber(ARGV[2]) - interval), tonumber(ARGV[2]), 'withscores', 'limit', 0, 1); local random, permits = struct.unpack('fI', nearest[1]); return tonumber(nearest[2]) - (tonumber(ARGV[2]) - interval); else redis.call('zadd', permitsName, ARGV[2], struct.pack('fI', ARGV[3], ARGV[1])); redis.call('decrby', valueName, ARGV[1]); return nil; end;

具体来说:

1.首先通过 zrangebyscore 获取 SortedSet 中 score 小于(ARGV[2] - interval )的记录,ARGV[2] 是当前时间戳,也就是获取时间戳在当前时间戳 interval ms 前的所有记录;

2.for 循环计算被回收的请求的许可证总数;

3.如果有请求被回收(released > 0), 那么 zrem 移除 SortedSet 中的对应记录,回加 released,即 ‘释放许可证数‘ 到 currentValue,并 set 更新 {limit:user:1}:value;

4.在 {limit:user:1}:value 被更新(或不变)后,判断 currentValue 与 ARGV1 的大小:

如果currentValue < permits,说明当前的许可证数不够它获取;那么,基于重试机制的考虑,再次 zrangebyscore 获取 "当前时间戳减去 rateInterval 到 当前时间戳" 区间内的最早的请求,返回一个时间,代表这个最早的请求还有多久被回收;

如果currentValue >= permits,说明当前许可证数可以满足它获取,那么,zadd 记录请求,decrby 扣减 {limit:user:1}:value 许可证数量。

以上就是整个 tryAcquire Lua脚本的解读。

一些细节问题:

currentValue < permits情况的详细解读?

java
复制代码
local nearest = redis.call('zrangebyscore', permitsName, '(' .. (tonumber(ARGV[2]) - interval), tonumber(ARGV[2]), 'withscores', 'limit', 0, 1); local random, permits = struct.unpack('fI', nearest[1]); return tonumber(nearest[2]) - (tonumber(ARGV[2]) - interval);

上面说的可能不够详细。首先,重试机制指的是获取许可失败后不是立即失败,而是过一段时间再尝试。那么什么时候再尝试?总不能无脑的重复尝试,很影响性能。我们上面提到,许可证的增加只靠回收。那么是不是等到下一个请求被回收,许可证增加后,再让请求重试,会比较合理呢?

所以,**这里的 nearest 指的就是下一个将被回收的请求。**zrangebyscore 命令中 的范围 (tonumber(ARGV[2] - interval), tonumber(ARGV[2]),代表 "当前时间戳减去 rateInterval 到 当前时间戳" 区间; limit 0,1 代表获取第一条。由于这个 nearest 在 (tonumber(ARGV[2]) - interval), tonumber(ARGV[2]) 区间内,所以它不会现在就被回收,而是要等到一段时间后才被回收。等到多久之后被回收?

首先,nearest 是 SortedSet 中的一条记录,nearest[1] 代表 pack 的那个值,nearest[2] 代表它获取许可证时的时间戳。

编辑

现在拿上面那个例子举例,请求A是nearest,它对应的时间戳是nearest[2],当前回收了ARGV[2] - interval 之前的请求,那么,是不是等到黑色双箭头的时间过去,请求 A 就将被回收,许可证对应增加?那么 Redis 就返回 (nearest[2]) - (tonumber(ARGV[2]) - interval) ,就是在告诉发出请求C的客户端:你过这段时间再来,到时候就能回收请求A,说不定就有足够的许可了!

5.总结:

5.1 概括 Redisson RateLimiter 的工作流程

getRateLimiter 获取限流器

trySetRate 用 hsetnx 命令向 redis 的 限流器名称的 key 中写入限流配置 rate , rateInterval, type

tryAcquire 尝试获取许可证

tryAcquire 底层 Lua 脚本的逻辑是:首先判断是否是第一次来,如果是建立限流 key 后第一次来,那么会创建 限流 key + value, 限流 key + permits 这两个新的 key ,+value的 key 是 String结构, 用于存储剩余的许可证个数,初始值为 rate;+ permits 的key 是 sortedSet 结构,用于存储获取许可证成功的历史请求,并记录它们获取许可证的个数,以 获取许可证的毫秒时间戳作为 SortedSet 的 score。

接着 第二次开始,首先用 zrangebyscore 命令获取 当前时间 rateInterval 毫秒之前的历史请求,将它们移除出 SortedSet,并将其许可证回收,回加到 value 的 key;然后判断剩余许可证个数是否 >= 当前请求尝试获取的许可证个数,如果满足,那么扣减许可证,zadd 记录到 SortedSet 并返回 true; 否则,返回需要等待的时间,即,现在 SortedSet 中最早的历史请求将过多久被回收。

5.2 RateLimiter 采用滑动窗口实现了限流

概括完工作流程后,我们应该思考:它到底如何实现的限流?其实也好理解,**RateLimiter 采用的,相当于是将每一个尝试获取许可证的请求的当前时间戳作为滑动窗口的末尾。**它回收了滑动窗口头部之前的所有许可证,使得当前长度为 rateInterval 的滑动窗口,最多允许 rate 个许可证(因为许可证上限只有 rate 个)。如果当前请求会导致滑动窗口许可证个数多于 rate 个,就拒绝,反之通过限流。

其实网上有不少人说 RateLimiter 是令牌桶,但是我觉得并不是。令牌桶不会严格限制某个时间区段内的许可证个数,从而更灵活;而显然这里的 RateLimiter 严格限制了长度为 rateInterval 窗口内的许可证数量,所以我说是滑动窗口。不过,这点也没那么重要,见仁见智吧。

5.3 理解源码后,我们配置 RateLimiter 能做出哪些优化?

**Rate不要设置太大。**从源码中你也看出了,RateLimiter 记录了所有的获取许可证成功的记录,所以如果你设置的Rate值过大,在Redis中存储的信息 (permitsName对应的zset) 也就越多,每次执行那段lua脚本的性能也就越差,这对Redis实例也是一种压力。如果想设置较大的限流阈值,倾向于小Rate+小时间窗口的方式。比如在允许的情况下,不要设置 30秒 允许 90 个许可,而应该设置1 秒允许 3 个许可,后者这种设置方式请求也会更均匀一些。(这一段引用自华为云社区详解Redisson分布式限流的实现原理-云社区-华为云

尾声:

这一篇文章实在是呕心沥血,写了好久,花了很大气力,但最后还是感觉把这个事讲清楚了,我自己对源码的理解也更清晰了!网上同样也有其他源码解读,但是我认为没有达到看一遍就能看懂的程度,所以希望能做一些对社区真正有所贡献的事。如果觉得文章的结构、叙述顺序等有可以优化的地方,欢迎指出,也是督促我优化文章、之后写的更易懂的动力,谢谢~

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
krafft
作者分享
令牌桶算法好有意思😋 附鱼皮在 BI 项目里推荐的限流算法教程:https://juejin.cn/post/6967742960540581918 真的写的很好,尤其伪代码能让人学到不少,部分实现个人觉得有待商榷,比如有几个 long 类型变量应该换 double, 但总体的思路真的 nice, 感觉可以时常温习
5
#在校生刷八股的体会 如果你直接拿着一本八股就开始背,往往就半途而废了,因为觉得很枯燥,并且太多的题目也让你找不到重点,背完这题,其实还没完全掌握,又急着去看下一题,至少我的感受是这样背很影响学习热情。 最近找到的一个方法是,每天到牛客上刷刷面经,看面经里面有哪些你不会的题,然后就到面试鸭里面去搜,搜完还是不太懂就去问AI,让它逐步给你解释。这样就相当于每天去找一个有意思的问题,也不会有那种八股题讲的不太透彻的毛病,似懂非懂的感觉还是让人很难受的,不如我G哥(gpt) 算法也是同理,如果你单纯去刷,可能会有点枯燥,但如果想着总结一点经验发到CSDN,虽然可能会多消耗你的时间,但长线来看能让你保持热情。 虽然我也是个经常性报废的人,但还是和大家共勉吧,不要焦虑,要去做事,不专注于结果,而专注于提升。
7
api开放平台开发客户端sdk要写一个starter,本来是不难的事,结果最后打jar包的时候一直报错,说版本是Java5,这个问题以前也见过很多次,一般改了idea里的setting或者project structure就好了,结果这次,不知道是不是删了pom.xml中的plugin的原因,一直好不了,搞了两个小时差点心态崩了,idea里能改的都改了一遍,百度上搜搜搜,最后都不行 最后突然一下子冷静下来,思考:为什么明明改了idea的配置,结果还是会经常出现Java5的问题?这个问题的根源在哪?搜了一下这个想法,百度给出的回答就和之前完全不同了,最后发现是Maven配置文件的问题,照着文章改了maven的settings.xml,成功解决,文章地址: https://blog.csdn.net/Montaro2017/article/details/107375120 经验教训:遇到这种搞了很久的bug,在百度的同时一定不能太着急,比如你mvn install报错了,不要第一时间百度,还是得冷静下来,一是仔细看报错信息,二是思考,为什么总是会出现某个问题,问题的根源在哪,而不是慌忙地粘贴报错信息到百度上搜索,否则很可能竹篮打水。 #maven #经验 #Java
4
9.11 还是继续开始打卡🤪 今天刷了一道力扣,面试鸭看了两道java基础,api项目几乎没有推动,晚上状态有点差😇😇 明天计划还是一道力扣,项目把starter做出来,面试鸭看个三道以上😅
2
day24 期末周终于结束 4讲redis,想上传代码到github又记起来自己git学的一坨,又重新跟着git book看了一晚上 看了看优秀的球友,感觉自己纯飞舞 [捂脸][捂脸][捂脸] 暑假好好干吧
0
下载 APP