go.uber.org/ratelimit 源码分析

go.uber.org/ratelimit 源码分析

go 提供了一用来接口限流的包。其中"go.uber.org/ratelimit" 包正是基于漏桶算法实现的。

使用方式:

  1. 通过 ratelimit.New 创建限流器对象,参数为每秒允许的请求数(RPS)。
  2. 使用 Take() 方法来获取限流许可,该方法会阻塞请求知道满足限速要求。

官方示例:

import (
	"fmt"
	"time"

	"go.uber.org/ratelimit"
)

func main() {
    rl := ratelimit.New(100) // 每秒多少次

    prev := time.Now()
    for i := 0; i < 10; i++ {
        now := rl.Take()	// 平均时间
        fmt.Println(i, now.Sub(prev))
        prev = now
    }

    // Output:
    // 0 0
    // 1 10ms
    // 2 10ms
    // 3 10ms
    // 4 10ms
    // 5 10ms
    // 6 10ms
    // 7 10ms
    // 8 10ms
    // 9 10ms
}

ratelimit.New()指的是每秒平均多少次,在运行程序后,并不会严格按照官方给的样例输出。

源码分析

不仅知其然,还要知其所以然。

最大松弛量

传统的漏桶算法每隔请求的间隔是固定的,然而在实际上的互连网应用中,流量经常是突发性的。对于这种情况,uber引入了最大松弛量的概念。

假如我们要求每秒限定100个请求,平均每个请求间隔 10ms。但是实际情况下,有些间隔比较长,有些间隔比较短。如下图所示:

在这里插入图片描述

请求 1 完成后,15ms 后,请求 2 才到来,可以对请求 2 立即处理。请求 2 完成后,5ms 后,请求 3 到来,这个时候距离上次请求还不足 10ms,因此还需要等待 5ms。

但是,对于这种情况,实际上三个请求一共消耗了 25ms 才完成,并不是预期的 20ms。在 uber-go 实现的 ratelimit 中,可以把之前间隔比较长的请求的时间,匀给后面的使用,保证每秒请求数 (RPS) 即可。

了解完这个前缀知识就可以查看源码了。

New()

ratelimit.New() 内部调用的是 newAtomicInt64Based 方法。

type atomicInt64Limiter struct {
	prepadding [64]byte // 填充字节,确保state独占一个缓存行
	state      int64    // 最后一次权限发送的纳秒时间戳,用于控制请求的速度
	postpadding [56]byte // 填充字节,确保state独占一个缓存行

	perRequest time.Duration	// 限流器放行周期,用于计算下一个权限发送的state的值
	maxSlack   time.Duration	// 最大松弛量
	clock      Clock	// 指向当前时间获取函数的指针
}

// newAtomicBased返回一个新的基于原子的限制器。
func newAtomicInt64Based(rate int, opts ...Option) *atomicInt64Limiter {
	config := buildConfig(opts) // 加载配置,config.per 默认为 1s,config.slack 默认为 10
	perRequest := config.per / time.Duration(rate)
	l := &atomicInt64Limiter{
		perRequest: perRequest,
		maxSlack:   time.Duration(config.slack) * perRequest,	// 默认maxSlack为perRequest 10倍
		clock:      config.clock,
	}
	atomic.StoreInt64(&l.state, 0)	
	return l
}

Take()

// Take blocks to ensure that the time spent between multiple
// Take calls is on average time.Second/rate.
func (t *atomicInt64Limiter) Take() time.Time {
   var (
      newTimeOfNextPermissionIssue int64	// 下一次允许请求的时间
      now                          int64	// 当前时间
   )
   for {
      now = t.clock.Now().UnixNano()	
      timeOfNextPermissionIssue := atomic.LoadInt64(&t.state) // 上一次允许请求时间

      switch {
      case timeOfNextPermissionIssue == 0 || (t.maxSlack == 0 && now-timeOfNextPermissionIssue > int64(t.perRequest)):
        // if this is our first call or t.maxSlack == 0 we need to shrink issue time to now
         newTimeOfNextPermissionIssue = now
      case t.maxSlack > 0 && now-timeOfNextPermissionIssue > int64(t.maxSlack)+int64(t.perRequest):
         // a lot of nanoseconds passed since the last Take call
         // we will limit max accumulated time to maxSlack
         newTimeOfNextPermissionIssue = now - int64(t.maxSlack)
      default:
         // calculate the time at which our permission was issued
         newTimeOfNextPermissionIssue = timeOfNextPermissionIssue + int64(t.perRequest)
      }

      if atomic.CompareAndSwapInt64(&t.state, timeOfNextPermissionIssue, newTimeOfNextPermissionIssue) {
         break
      }
   }

   sleepDuration := time.Duration(newTimeOfNextPermissionIssue - now)
   if sleepDuration > 0 {
      t.clock.Sleep(sleepDuration)
      return time.Unix(0, newTimeOfNextPermissionIssue)
   }
   // return now if we don't sleep as atomicLimiter does
   return time.Unix(0, now)
}

switch 这块挺绕的,刚开始一直以为timeOfNextPermissionIssue 为下次放行的时间戳,这样的话当t.maxSlack = 0时,只要 now-timeOfNextPermissionIssue > 0 就应该放行。无法解释(t.maxSlack == 0 && now-timeOfNextPermissionIssue > int64(t.perRequest))

让我们对上面的三个 case 分析一下

case 1

case timeOfNextPermissionIssue == 0 || (t.maxSlack == 0 && now-timeOfNextPermissionIssue > int64(t.perRequest))

这个比较好理解,我们仍以每秒100个请求为例,平均间隔 10ms。当本次请求时间与上次放行时间 > 时间间隔时即可放行,并记录本次访问时间,如图:

在这里插入图片描述

case 2

case t.maxSlack > 0 && now-timeOfNextPermissionIssue > int64(t.maxSlack)+int64(t.perRequest)

这块比较巧妙,假如松弛量是3 ms,当我们在第二次请求时的时间戳 > 13 ms,此时 newTimeOfNextPermissionIssue= now - maxSlack = 12 ms。

maxSlack 较大且与上次请求相隔较长时,后续的大量请求会被直接放行,以弥补此次浪费的时间。

假设第一次请求时间为0, maxSlack 为 100 ms,perRequest为10 ms,在第二次请求时与第一次间隔为 111 ms ,newTimeOfNextPermissionIssue = 111 - 100 = 11 ms。而 now 为 111 ms,限流器在后面的10次take中都会经过default直接放行,直到 newTimeOfNextPermissionIssue > now

在这里插入图片描述

case 3

对于其它的请求, newTimeOfNextPermissionIssue = timeOfNextPermissionIssue + int64(t.perRequest)

假如maxSlack为 100ms,perRequest 为 10ms,当请求2在15ms访问后,state 更新为 10ms,这样在请求3在20ms访问时,不会出现拦截的情况。

在这里插入图片描述

小结

uber 对基于漏桶实现的 ratelimit 进行了一些优化,让其限流更加的平滑。主要体现在两点:

  1. 本次请求时间距离上次放行时间 > 时间间隔 + 松弛量时,后面10次的请求会根据情况直接放行
  2. 时间间隔 + 松弛量 >= 本次请求时间距离上次放行时间 > 时间间隔state = state + perRequest
<think>好的,我现在需要帮助用户解决使用exe4j将Maven项目打包成EXE后出现的NoClassDefFoundError问题。用户提到涉及JNA库和Java 8,可能需要调整pom配置。首先,我需要回忆一下这个错误的常见原因以及如何通过Maven配置来解决。 首先,NoClassDefFoundError通常意味着在运行时缺少某个类,而这个类在编译时是可用的。对于Maven项目,最常见的原因是依赖项没有正确打包到最终的JAR文件中。特别是当使用第三方JAR包时,如果这些依赖没有包含进去,就会导致这个问题。用户提到使用了JNA,所以需要确保JNA的相关依赖被正确引入并打包。 接下来,用户使用的是exe4j,这可能会涉及到如何将依赖项包含到生成的EXE中。exe4j通常需要指定类路径,包括所有依赖的JAR文件。如果这些依赖没有被正确打包或引用,就会出现类找不到的错误。 回顾用户提供的引用内容,引用[1]提到解决NoClassDefFoundError的方案之一是使用Maven的maven-assembly-plugin来创建一个包含所有依赖的fat JAR。这可能是一个关键点。另外,引用[4]中提供的pom.xml示例展示了如何配置编译器插件和依赖项,这可能对用户有帮助。 接下来,我需要考虑用户可能的配置错误。例如,是否在pom.xml中正确声明了JNA的依赖?是否使用了正确的scope(如system)来引入本地的JAR文件,或者是否应该通过Maven仓库来获取依赖?此外,Maven的打包方式是否正确,是否使用了能够包含依赖的插件? 可能的解决方案步骤包括: 1. 检查pom.xml中的依赖是否正确添加了JNA相关库,确保groupId、artifactId和version正确无误。 2. 使用maven-assembly-plugin或maven-shade-plugin创建包含所有依赖的uber JAR,这样exe4j打包时只需要包含这个JAR即可。 3. 在exe4j配置中,正确设置类路径,确保所有必要的JAR文件或目录被包含进去。 4. 确保Java 8的版本兼容性,检查是否有模块化的问题,特别是在使用jlink的情况下,可能需要包含相关模块。 另外,用户提到Java 8,所以需要注意JDK版本是否与exe4j兼容,以及是否在打包时正确绑定了JRE。引用[5]中提到了使用jlink生成定制的JRE,这可能在配置exe4j时需要指定正确的JRE路径。 可能还需要检查Maven的打包过程,是否在打包时跳过了依赖项。例如,默认的maven-jar-plugin可能不会包含依赖,因此需要使用assembly或shade插件。 总结来说,用户的问题可能出在依赖项没有正确打包,或者exe4j的类路径配置不正确。需要指导用户如何配置Maven生成包含所有依赖的JAR,并在exe4j中正确引用这个JAR和JRE。</think>### 解决方案:Maven项目通过exe4j打包EXE出现NoClassDefFoundError的解决方法 #### 问题分析 `NoClassDefFoundError`通常由以下原因导致: 1. **依赖未正确打包**:第三方依赖(如JNA)未包含在最终JAR文件中 2. **类路径配置错误**:exe4j未正确配置依赖库路径 3. **JRE版本不匹配**:运行时环境缺少必要模块(Java 8需注意模块化配置) --- #### 步骤1:验证Maven依赖配置 确保`pom.xml`中已正确声明JNA依赖: ```xml <dependencies> <dependency> <groupId>net.java.dev.jna</groupId> <artifactId>jna</artifactId> <version>5.13.0</version> <!-- 检查最新版本 --> </dependency> </dependencies> ``` *若使用本地JAR文件,需配置`system`作用域*[^4]: ```xml <dependency> <groupId>com.custom</groupId> <artifactId>jna</artifactId> <version>1.0</version> <scope>system</scope> <systemPath>${project.basedir}/lib/jna.jar</systemPath> </dependency> ``` --- #### 步骤2:生成包含依赖的Fat JAR 使用`maven-assembly-plugin`生成包含所有依赖的JAR: ```xml <build> <plugins> <plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <archive> <manifest> <mainClass>your.main.Class</mainClass> </manifest> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> <executions> <execution> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build> ``` 执行`mvn clean package`后,在`target`目录会生成`*-jar-with-dependencies.jar`文件[^1]。 --- #### 步骤3:exe4j配置要点 1. **可执行JAR配置**: - 选择生成的`*-jar-with-dependencies.jar`作为主JAR - 添加项目依赖的其他资源目录(如图片、配置文件) 2. **JRE配置**: - 指定最小Java版本为8 - 推荐绑定自定义JRE(使用`jlink`生成)[^5]: ```shell jlink --add-modules java.base,java.desktop,java.sql --output myjre ``` - 在exe4j中设置`Bundled JRE`路径为`myjre`目录 --- #### 步骤4:验证打包结果 1. 使用`jar tf your.jar`检查JAR内容是否包含`com/sun/jna`等目录 2. 执行`java -jar your.jar`验证基础功能 3. 通过exe4j生成EXE后,在命令行运行观察完整错误日志 --- #### 扩展优化 若使用Spring Boot,可改用`maven-shade-plugin`: ```xml <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> </plugin> ``` ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值