Skip to content

Commit 05e905d

Browse files
committed
style: update sentinel articles
1 parent e582ada commit 05e905d

File tree

3 files changed

+20
-11
lines changed

3 files changed

+20
-11
lines changed

docs/Sentinel/Sentinel底层LongAdder的计数实现.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## LongAdder 的原理
22

3-
在 LongAdder 中,底层通过多个数值进行累加来得到最后的结果。当多个线程对同一个 LongAdder 进行更新的时候,将会对这一些列的集合进行动态更新,以避免多线程之间的资源竞争。当需要得到 LongAdder 的具体的值的时候,将会将一系列的值进行求和作为最后的结果。
3+
在 LongAdder 中,底层通过多个数值进行累加来得到最后的结果。当多个线程对同一个 LongAdder 进行更新的时候,将会对这一些列的集合进行动态更新,以避免多线程之间的资源竞争。当需要得到 LongAdder 的具体的值的时候,将会将一系列的值进行求和作为最后的结果。
4+
45
在高并发的竞争下进行类似指标数据的收集的时候,LongAdder 通常会和 AtomicLong 进行比较,在低竞争的场景下,两者有着相似的性能表现。而当在高并发竞争的场景下,LongAdder 将会表现更高的性能,但是也会伴随更高的内存消耗。
56

67
## LongAdder 的代码实现
@@ -10,7 +11,7 @@ transient volatile Cell[] cells;
1011
transient volatile long base;
1112
```
1213

13-
cells 是一个简单的 Cell 数组,当比如通过 LongAdder 的 add()方法进行 LongAdder 内部的数据的更新的时候,将会根据每个线程的一个 hash 值与 cells 数组的长度进行取模而定位,并在定位上的位置进行数据更新。而 base 则是当针对 LongAdder 的数据的更新时,并没有线程竞争的时候,将会直接更新在 base 上,而不需要前面提到的 hash 再定位过程,当 LongAdder 的 sum()方法被调用的时候,将会对 cells 的所有数据进行累加在加上 sum 的值进行返回。
14+
cells 是一个简单的 Cell 数组,当比如通过 LongAdder 的 `add()` 方法进行 LongAdder 内部的数据的更新的时候,将会根据每个线程的一个 hash 值与 cells 数组的长度进行取模而定位,并在定位上的位置进行数据更新。而 base 则是当针对 LongAdder 的数据的更新时,并没有线程竞争的时候,将会直接更新在 base 上,而不需要前面提到的 hash 再定位过程,当 LongAdder 的 `sum()` 方法被调用的时候,将会对 cells 的所有数据进行累加在加上 sum 的值进行返回。
1415

1516
```java
1617
public long sum() {
@@ -27,7 +28,7 @@ public long sum() {
2728
}
2829
```
2930

30-
相比 sum()方法,LongAdder 的 add()方法要复杂得多。
31+
相比 `sum()` 方法,LongAdder 的 `add()` 方法要复杂得多。
3132

3233
```java
3334
public void add(long x) {
@@ -46,10 +47,15 @@ public void add(long x) {
4647
}
4748
```
4849

49-
在 add()方法的一开始,将会观察 cells 数组是否存在,如果不存在,将会尝试直接通过 casBase()方法在 base 上通过 cas 更新,这是在低并发竞争下的 add()流程,这一流程的前提是对于 LongAdder 的更新并没有遭遇别的线程的并发修改。
50-
在当 cells 已经存在,而或者对于 base 的 cas 更新失败,都将会将数据的更新落在 cells 数组之上。首先,每个线程都会在其 ThreadLocal 中生成一个线程专有的随机数,并根据这个随机数与 cells 进行取模,定位到的位置进行 cas 修改。在这个流程下,由于根据线程专有的随机数进行 hash 而定位的流程,尽可能的避免了线程间的资源竞争。但是仍旧可能存在 hash 碰撞而导致两个线程定位到了同一个 cells 槽位的情况,这里就需要通过 retryUpdate()方法进行进一步的解决。
51-
retryUpdate()方法的代码很长,但是逻辑很清晰,主要分为一下几个流程,其中的主流程是一个死循环,进入 retryUpdate()方法后,将会不断尝试执行主要逻辑,直到对应的逻辑执行完毕:
52-
1.当进入 retryUpdate()的时候,cells 数组还没有创建,将会尝试获取锁并初始化 cells 数组并直接在 cells 数组上进行修改,而别的线程在没创建的情况下进入并获取锁失败,将会直接尝试在 base 上进行更行。 2.当进入 retryUpdate()的时候,cells 数组已经创建,但是分配给其的数组槽位的 Cells 还没有进行初始化,那么将会尝试获取锁并对该槽位进行初始化。 3.当进入 retryUpdate()的时候,cells 数组已经创建,分配给其的槽位的 Cell 也已经完成了初始化,而是因为所定位到的槽位与别的线程发生了 hash 碰撞,那么将会加锁并扩容 cells 数组,之后对该线程持有的 hash 进行 rehash,在下一轮循环中对新定位的槽位数据进行更新。而别的线程在尝试扩容并获取锁失败的时候,将会直接对自己 rehash 并在下一轮的循环中重新在新的 cells 数组中进行定位更新。
50+
`add()` 方法的一开始,将会观察 cells 数组是否存在,如果不存在,将会尝试直接通过 `casBase()` 方法在 base 上通过 cas 更新,这是在低并发竞争下的 `add()` 流程,这一流程的前提是对于 LongAdder 的更新并没有遭遇别的线程的并发修改。
51+
52+
在当 cells 已经存在,而或者对于 base 的 cas 更新失败,都将会将数据的更新落在 cells 数组之上。首先,每个线程都会在其 ThreadLocal 中生成一个线程专有的随机数,并根据这个随机数与 cells 进行取模,定位到的位置进行 cas 修改。在这个流程下,由于根据线程专有的随机数进行 hash 而定位的流程,尽可能的避免了线程间的资源竞争。但是仍旧可能存在 hash 碰撞而导致两个线程定位到了同一个 cells 槽位的情况,这里就需要通过 `retryUpdate()` 方法进行进一步的解决。
53+
54+
`retryUpdate()` 方法的代码很长,但是逻辑很清晰,主要分为一下几个流程,其中的主流程是一个死循环,进入 `retryUpdate()` 方法后,将会不断尝试执行主要逻辑,直到对应的逻辑执行完毕:
55+
56+
1. 当进入 `retryUpdate()` 的时候,cells 数组还没有创建,将会尝试获取锁并初始化 cells 数组并直接在 cells 数组上进行修改,而别的线程在没创建的情况下进入并获取锁失败,将会直接尝试在 base 上进行更行。
57+
2. 当进入 `retryUpdate()` 的时候,cells 数组已经创建,但是分配给其的数组槽位的 Cells 还没有进行初始化,那么将会尝试获取锁并对该槽位进行初始化。
58+
3. 当进入 `retryUpdate()` 的时候,cells 数组已经创建,分配给其的槽位的 Cell 也已经完成了初始化,而是因为所定位到的槽位与别的线程发生了 hash 碰撞,那么将会加锁并扩容 cells 数组,之后对该线程持有的 hash 进行 rehash,在下一轮循环中对新定位的槽位数据进行更新。而别的线程在尝试扩容并获取锁失败的时候,将会直接对自己 rehash 并在下一轮的循环中重新在新的 cells 数组中进行定位更新。
5359

5460
## Cell 本身的内存填充
5561

docs/Sentinel/Sentinel时间窗口的实现.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,11 @@ private final ReentrantLock updateLock = new ReentrantLock();
2828

2929
## 以秒级别的时间窗口举个例子
3030

31-
在 sentinel 默认的秒级别时间窗口中,array 的大小为 2,也就是每 500ms 为一个时间窗口的大小。
32-
因此当一个线程试图获取一个时间窗口来记录指标数据的时候,将会根据单个时间窗口的时间跨度进行取模,来得到 array 上对应的时间窗口的下标,在这个情况下,将为 0 或者 1,之后计算当前线程时间指标所属的时间窗口的起始时间,以此为依据来判断如果在后面如果获取到的时间窗口是过期还是正好所需要的。
33-
最后,将会不断循环从 array 尝试获取之前计算得到下标位置处的时间窗口,可能发生的 4 种情况如上所示。在这个情况,如果 cas 失败或事没有尝试获取到更新锁,都不会阻塞或是挂起,而是通过 yield 重新回到就绪态等待下一次循环获取。
31+
在 sentinel 默认的秒级别时间窗口中,array 的大小为 2,也就是每 500ms 为一个时间窗口的大小。
32+
33+
因此当一个线程试图获取一个时间窗口来记录指标数据的时候,将会根据单个时间窗口的时间跨度进行取模,来得到 array 上对应的时间窗口的下标,在这个情况下,将为 0 或者 1,之后计算当前线程时间指标所属的时间窗口的起始时间,以此为依据来判断获取到的时间窗口是过期还是正好所需要的。
34+
35+
最后,将会不断循环从 array 尝试获取之前计算得到下标位置处的时间窗口,可能发生的 4 种情况如上所示。在这个情况,如果 cas 失败或是没有尝试获取到更新锁,都不会阻塞或是挂起,而是通过 yield 重新回到就绪态等待下一次循环获取。
3436

3537
## 时间窗口本身的线程安全指标更新
3638

docs/Sentinel/Sentinel限流算法的实现.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFacto
6868
slope = (coldFactor - 1.0) / count / (maxToken - warningToken);
6969
```
7070

71-
其中 count 是当前 qps 的阈值。coldFactor 则为冷却因子,warningToken 则为警戒的令牌数量,warningToken 的值为(热身时间长度 _ 每秒令牌的数量) / (冷却因子 - 1)。maxToken 则是最大令牌数量,具体的值为 warningToken 的值加上 (2 _ 热身时间长度 _ 每秒令牌数量) / (冷却因子 + 1)。当当前系统处于热身时间内,其允许通过的最大 qps 为 1 / (超过警戒数的令牌数 _ 斜率 slope + 1 / count),而斜率的值为(冷却因子 - 1) / count / (最大令牌数 - 警戒令牌数)。
71+
其中 count 是当前 qps 的阈值。coldFactor 则为冷却因子,warningToken 则为警戒的令牌数量,warningToken 的值为`(热身时间长度 * 每秒令牌的数量) / (冷却因子 - 1)`。maxToken 则是最大令牌数量,具体的值为 `warningToken + (2 * 热身时间长度 * 每秒令牌数量) / (冷却因子 + 1)`。当当前系统处于热身时间内,其允许通过的最大 qps 为 `1 / (超过警戒数的令牌数 * 斜率 slope + 1 / count)`,而斜率的值为`(冷却因子 - 1) / count / (最大令牌数 - 警戒令牌数)`
72+
7273
举个例子: count = 3, coldFactor = 3,热身时间为 4 的时候,警戒令牌数为 6,最大令牌数为 12,当剩余令牌处于 6 和 12 之间的时候,其 slope 斜率为 1 / 9。 那么当剩余令牌数为 9 的时候的允许 qps 为 1.5。其 qps 将会随着剩余令牌数的不断减少而直到增加到 count 的值。
7374

7475
```java

0 commit comments

Comments
 (0)