首页 > 其他分享 >并发设计类分析(Guava/Netty)

并发设计类分析(Guava/Netty)

时间:2023-12-06 21:57:00浏览次数:38  
标签:Netty 令牌 long 并发 算法 限流 线程 Test Guava

1. 限流器

1.1 限流器

常见限流算法:

  • 计数器算法
    计数器算法是一种简单的限流方法,通过对请求进行计数,当请求达到一定的阈值时,进行限制。这种方法适用于简单场景,但不够灵活。容易出现临界时间点限流失效问题。

  • 滑动窗口算法
    滑动窗口算法维护一个时间窗口内的请求数量,通过动态调整窗口大小,可以更灵活地适应流量的变化。其实就是计数器算法的优化版本,将计数器算法中的单位时间切割成了多块,但也没有完全解决临界时间点限流失效问题。

  • 漏桶算法(Leaky Bucket)
    漏桶算法与令牌桶算法类似,但是它是按照固定速率漏水,而不是放入令牌。请求被处理的速度是固定的,当请求到来时,如果漏桶未满,则请求被处理,否则被丢弃或等待。

  • 令牌桶算法(Token Bucket)
    令牌桶算法是漏桶算法的优化版,支持突发流量。在令牌桶中,令牌以固定的速率被放入到桶中,而请求要想通过,必须获取到一个令牌。如果桶中没有足够的令牌,请求就会被限制。

1.2 Guava实现原理

Guava就是令牌桶算法的实现。

需要实现的功能点:

  • 令牌以固定的速率添加到令牌桶中,假设限流的速率是 r/ 秒,则令牌每 1/r 秒会添加一个;
  • 假设令牌桶的容量是 b ,如果令牌桶已满,则新的令牌会被丢弃;
  • 请求能够通过限流器的前提是令牌桶中有令牌。

很容易想到令牌桶的一个实现是:工作线程执行操作前去申请令牌,后台再开启一个子线程定时地添加令牌。但是Guava的RateLimiter限流器并不是这个原理。

RateLimiter的实现是:通过计算下个令牌颁发时间点,并让申请线程休眠对应时长。

假设每次只申请1个令牌(实际上可以申请n个),主要流程如下图所示。

线程执行调用acquire()方法请求令牌,acquire()会调用同步reserve(long time)方法计算下个令牌颁发时间点并返回,线程计算休眠时长后,调用sleep()方法直接休眠对应时长。

image

1.3 简易限流器代码实现

class RateLimiter {
    // 单个令牌颁发间隔时长,单位纳秒
    private final long INTERVAL = 1000_000_000;
    // 最大令牌数,额外的突发量
    private final long maxPermits;
    // 当前令牌数
    private long storePermits;
    // 下个令牌颁发时间点
    private long next;

    /**
     * @param initPermits 初始令牌数
     * @param maxPermits 最大令牌数
     */
    public RateLimiter(long initPermits, long maxPermits) {
        // 省略合法性检查
        this.storePermits = initPermits;
        this.maxPermits = maxPermits;
    }

    /**
     * 获取令牌,没有令牌则等待
     */
    @SneakyThrows
    public void acquire() {
        // 获取当前时间
        long now = System.nanoTime();
        // 预占令牌,计算可执行时间点
        long timePoint = reverse(now);
        // 计算等待时长, 避免异常负数最小置为0
        long wait = Math.max(timePoint - now, 0);
        // 如果wait大于0,则休眠对应时长
        if (wait > 0) {
            TimeUnit.NANOSECONDS.sleep(wait);
        }
    }

    /**
     * 预占令牌,并返回线程可执行时间点
     * 加锁为了保护共享变量storePermits及next
     *
     * @param now 线程申请令牌时间点
     * @return 可执行时间点
     */
    synchronized private long reverse(long now) {
        // 更新令牌数及下个令牌颁发时间点
        resync(now);
        // 获取下一个令牌颁发时间
        long timePoint = next;
        // 判断是否有令牌
        if (storePermits > 0) {
            // 有令牌,令牌数-1
            storePermits--;
        } else {
            // 没有令牌,下一个令牌颁发时间需要增加一个间隔时长
            next += INTERVAL;
        }
        return timePoint;
    }

    /**
     * 更新令牌数及下个令牌颁发时间点
     * @param now 线程申请令牌时间点
     */
    private void resync(long now) {
        // 如果线程申请令牌时间点比下个令牌颁发时间点还早,那么不需要新增令牌数及更新下个令牌颁发时间点
        if (now <= next) {
            return;
        }
        // 计算新增令牌数, 实际上Guava限流器实现用double更精准
        long newPermits = (now - next) / INTERVAL;
        // 更新令牌数
        storePermits = Math.min(storePermits + newPermits, maxPermits);
        // 更新下次颁发令牌时间点
        next = now;
    }

}

@Slf4j
public class Test {
    @SneakyThrows
    public static void main(String[] args) {
        // 创建固定6个线程的线程池
        ExecutorService executor = Executors.newFixedThreadPool(6);
        // 创建一个初始令牌3个,最大令牌数为3的限流器
        RateLimiter simpleLimiter = new RateLimiter(3, 3);
        // 丢9个数字打印观察
        for (int i = 1; i <= 9; i++) {
            int finalI = i;
            executor.submit(() -> {
                simpleLimiter.acquire();
                log.info(String.valueOf(finalI));
            });
        }
        executor.shutdown();
    }
}

打印如下。由于设置了初始令牌数为3,可以看到刚开始4个线程并发打印。后续则每秒只有1个线程打印。

21:43:31.319 [pool-1-thread-2] INFO com.huaxiaogou.MDog.myguava.Test - 2
21:43:31.319 [pool-1-thread-3] INFO com.huaxiaogou.MDog.myguava.Test - 3
21:43:31.319 [pool-1-thread-4] INFO com.huaxiaogou.MDog.myguava.Test - 4
21:43:31.319 [pool-1-thread-1] INFO com.huaxiaogou.MDog.myguava.Test - 1
21:43:32.323 [pool-1-thread-5] INFO com.huaxiaogou.MDog.myguava.Test - 5
21:43:33.323 [pool-1-thread-6] INFO com.huaxiaogou.MDog.myguava.Test - 6
21:43:34.323 [pool-1-thread-2] INFO com.huaxiaogou.MDog.myguava.Test - 7
21:43:35.323 [pool-1-thread-4] INFO com.huaxiaogou.MDog.myguava.Test - 8
21:43:36.323 [pool-1-thread-3] INFO com.huaxiaogou.MDog.myguava.Test - 9

2. 高性能网络框架Netty

标签:Netty,令牌,long,并发,算法,限流,线程,Test,Guava
From: https://www.cnblogs.com/kiper/p/17880463.html

相关文章

  • 【揭秘】并发情况下如何巧妙加锁
    单体架构下锁的实现方案ReentrantLock全局锁ReentrantLock(可重入锁),指的是一个线程再次对已持有的锁保护的临界资源时,重入请求将会成功。简单的与我们常用的Synchronized进行比较:ReentrantLockSynchronized锁实现机制依赖AQS监视器模式灵活性支持响应超时、中断......
  • 使用 maven 自动将源码打包并发布
    maven-source-plugin作用:在构建过程中将项目的源代码进行打包,并作为一个jar文件附着在主构件上,在pom.xml中添加如下内容,使用maven生成jar的同时生成sources包在pom中配置如下:<build><plugins><plugin><groupId>org.apache.maven.plugins</g......
  • Guava中的函数式编程
    第1章:引言大家好!今天小黑要和咱们聊聊,在Java中使用Guava来进行函数式编程。首先,让我们来聊聊什么是函数式编程。简单来说,函数式编程是一种编程范式,它将计算视为函数的评估,避免使用程序状态和可变数据。在函数式编程中,函数是“一等公民”,意味着它们可以像任何其他数据一样被传递......
  • Java并发编程进阶
    并发编程是现代软件开发中的一个关键技能。在Java中,java.util.concurrent包提供了一系列构建块,可以帮助开发者编写并发代码。这篇文章将深入探讨线程池、并发集合、同步器,以及Java内存模型。线程池线程池是一种资源池,它管理着一组可复用的线程。使用线程池可以减少在创建和销毁线程......
  • Guava Preconditions类的各种用法
    公众号「架构成长指南」,专注于生产实践、云原生、分布式系统、大数据技术分享。GuavaPreconditions类提供静态方法列表,用于检查是否使用有效参数值调用方法或构造函数。如果前提条件失败,则会抛出指定的异常。前置依赖引入pom<dependency><groupId>com.google.guava</g......
  • 基于DotNetty实现自动发布 - 项目的配置与发现
    前言上一篇,我们实现了基于DotNetty的通信基础模块的搭建,本篇,主要实现待发布Web项目的集成。创建待发布项目为了测试,我创建了一个基于.NET4.8的Web项目OpenDeploy.TestWebProject我本机的代码仓储路径是:D:\Projects\Back\dotnet\Study\OpenDeploy.TestW......
  • 深入探讨Guava的缓存机制
    第1章:引言大家好,我是小黑,今天咱们聊聊GoogleGuava的缓存机制。缓存在现代编程中的作用非常大,它能提高应用性能,减少数据库压力,简直就是性能优化的利器。而Guava提供的缓存功能,不仅强大而且使用起来非常灵活。在咱们深入挖掘之前,先简单说说缓存。缓存,其实就是一种保存数据的手段......
  • 使用 Guava Retry 优雅的实现重试机制
    王有志,一个分享硬核Java技术的互金摸鱼侠加入Java人的提桶跑路群:共同富裕的Java人大家好,我是王有志。今天我会通过一个真实的项目改造案例和大家聊一聊如何优雅的实现Java中常用的的重试机制。业务背景在我们的系统中当客户完成支付后,保单管理系统会通过MQ推送出一条包......
  • 亿级访问高并发下如何进行估算和调优
    Java全能学习+面试指南:https://javaxiaobear.cn这篇主要讲解如何在大流量高并发场景下进行估算和调优。我们知道,垃圾回收器一般使用默认参数,就可以比较好的运行。但如果用错了某些参数,那么后果可能会比较严重,我不只一次看到有同学想要验证某个刚刚学到的优化参数,结果引起了线上G......
  • 并发情况如何实现加锁来保证数据一致性?
    单体架构下锁的实现方案1.ReentrantLock全局锁ReentrantLock(可重入锁),指的是一个线程再次对已持有的锁保护的临界资源时,重入请求将会成功。简单的与我们常用的Synchronized进行比较:ReentrantLockSynchronized锁实现机制依赖AQS监视器模式灵活性支持响应超时、......