从Sentinel QPS限流看JUC

181 阅读4分钟

满满废话

作为一名Javaer,你是否想起第一次接触并发编程的窘境,你是否想起第一次看JUC源码时绷紧的抬头纹,你是否想起第一次看懂Doug Lea的思想的震撼,然而并发编程的实际应用局限性很大,不是谁都有机会落地生产环境。

想起笔者2年前把JUC源码(当然是靠着大神的博客啦)领悟了一番,做了满满的思维导图,非常渴望有机会应用,然而现实到情况是因为没有接触并发业务,对JUC的知识点逐渐生疏,直到我深读Sentinel源码(当然也是靠着大神的博客啦),感叹道,这就是JUC离我最近的一次实际落地。

友情提示,煽情的部分已经结束。下面是干货,全程少量代码(我懒,可能没有),大量文字阐述JUC对于CAS(Campare and sawp)的应用,拿出典型的刺头Lock(锁先生)、AtomicInteger(原子先生)、还有LongAddr(解决CAS高并发消耗资源的会分裂的分裂先生)

少一点废话,多一点干货

背景:CAS乐观锁用于解决悲观锁性能问题,但CAS在高并发场景下有个致命缺点,它会大量线程空转(什么,不知道什么叫CAS,那还不赶紧看看这篇,保证你能看懂AQS的文章),系统维护了数据一致性,却以损失性能为代价,有时候会得补偿失,这让我不禁想起一句话,也是年轻的Programer常犯的错误,不要因为技术而技术!

Lock

锁先生有两种模式,乐观锁和悲观锁,但这两个模式使用的都是CAS乐观锁,该场景下的CAS并没有解决上述高并发线程空转消耗资源的问题。

AtomicInteger

原子先生也是个刺头,一般用于并发统计,但它也没有解决困扰锁先生的高并发线程空转消耗资源的问题。

LongAddr

上面两位老大哥都没能解决问题,好在长江后浪推前浪,分裂先生带头冲锋。为什么叫LongAddr做分裂先生,因为LongAddr由一个long类型的base和一个Cell数组组成,在没有锁竞争的时候,操作base,有锁竞争的时候,将线程hash到Cell数组。假设数组长度为64,此时1000个请求竞争锁,如果使用的是锁先生(Lock),只有一个线程能够执行业务操作,其他999个线程空转继续抢锁。如果使用的是分裂先生(LongAddr),它会把1000个现场hash到64个地方,如果能平均分配的话,16个线程争夺一个锁,同一时期可以有64个线程执行业务操作。

求和的时候LongAddr将base和Cell数组的数据加起来。有点ConcurrentHashMap那股味道。

废话总结

说好的不说废话呢,还是说了这么多。

Sentinel有两种限流方法,限制线程数量和窗口时间限制QPS控制。限流方法知道了,那么怎么知道有没有达到限流阈值呢,换句话说,怎么统计窗口时间线程数量和QPS呢?答案是LongAddr。

线程数量

接下来需要一定的Sentinel基础,不然你可能读不明白笔者讲的内容。大家都知道Sentinel基于责任链模式,流量统计直接跳到StatisticSolt。

截屏2022-03-20 01.12.24.png 第一行是统计请求通过线程数量,一直点进去到StatisticNode,一个分裂先生躺在那里,结果就显而易见啦。基本思路是请求进来+1,请求结束-1,有分裂先生在,即使高并发也不会损失多少性能(笔者没有相关经验,纯属“瞎猜”😭)

截屏2022-03-20 01.15.22.png

截屏2022-03-20 01.15.36.png

QPS

QPS相对线程数量复杂点,需要一个统计当前时间窗口的容器MetricBucket。

public class MetricBucket {

    // 存储各事件的计数,比如异常总数、请求总数等
    private final LongAdder[] counters;

    // 窗口时间最小耗时
    private volatile long minRt;
}

和一个计算窗口方法的函数。。。详情

结束语

Sentinel主要是用了LongAddr分段锁思想,话说ConcurrentHashMap也是分段锁,不过两者有一定的区别。学会了分段锁,阅读Sentinel源码会轻松很多,好了,今天的分享就到这儿。

终于写完了,夜也深了,祝大家好梦🥱!