洋蔥

贪婪,找不到比这更好的词了,是件好事。

能够生存下来的物种,并不是那些最强壮的,也不是那些最聪明的,而是那些对变化作出快速反应的。—— 达尔文

投资股票的第一原则:不要赔钱;第二原则:永远不要忘记第一原则。—— 巴菲特

阅读全文 »

Github搜索Java、微服务等关键字,选择语言并按stars排序,会有很对优质资源,比如面试题、源码分析、优秀项目等。

阅读全文 »

推荐链接:

面试必备:四种经典限流算法讲解

5种限流算法,7种限流方式,挡住突发流量?

固定窗口计数器

固定窗口计数器(Fixed Window Counter)是一种简单的限流算法。它将时间分成固定长度的窗口(如1秒或1分钟),在每个窗口中统计请求数。当某个窗口内的请求计数超过限流阀值时,该窗口剩余时间内的请求都被拒绝;窗口结束后计数清零,重新开始统计。该算法实现非常简单,仅需一个支持原子加减的计数器即可,但存在明显的窗口边界效应。例如,如果限制1秒内最多5个请求,1秒钟末尾来了5个请求,下一个窗口开始瞬间又来了5个请求,总共10个请求瞬间涌入就会突破限流阀值。

  • 优点:实现简单、高效,开销低。适用于流量比较平稳、对延迟容忍度高的场景,如控制接口的粗粒度访问频率。
  • 缺点:存在“临界点”问题(窗口边界效应),无法平滑处理突发的临界流量。在极端突发情况下,可能短时间内超过系统承载。由于只按固定周期限制,实时性不够精细。

滑动窗口计数器

滑动窗口计数器(Sliding Window Counter)通过将固定窗口进一步细分来平滑限流。它将整个统计周期(如1秒)划分为多个更小的时间片(如每0.2秒一个),在每个小时间片中单独计数。每次请求到来时,累加当前时间所在小片的计数;然后将所有在整个滑动窗口范围内的小片计数相加,与阈值比较。随着时间流逝,窗口以微小步长向前滑动,自动丢弃过期片段中的请求。这样可以一定程度上解决固定窗口在边界时出现的“大脉冲”问题。

  • 优点:精度较高,可根据划分粒度调节限流效果。实现相对简单,易于理解和扩展。相比固定窗口,能部分缓解边界突增问题。
  • 缺点:依然不擅长处理突发高并发流量;一旦达到阈值,超出的请求会被直接拒绝。实现复杂度和内存开销略高于固定窗口(需维护多个小窗口计数器)。适用于流量变化一般、需要比固定窗口更平滑限流的场景,但若业务有频繁突发的请求浪涌,可能仍不足够灵活。

滑动日志算法

滑动日志算法(Sliding Log)是一种精确度更高的限流方法。它不使用固定窗口,而是为每个请求都记录时间戳,并将所有请求按时间排序保存。每次新请求到来时,只需清理日志中超出时间范围的旧记录,然后统计当前时间窗口内(例如过去1秒)的请求总数,与阈值比较。如果超过限制,则拒绝该请求。这种方法没有固定窗口边界的问题,限流非常精准和公平,不会出现窗口交界处被意外放行或拒绝的现象。但缺点是内存开销大:需要保存大量请求时间戳记录,随着请求量增长资源消耗显著。

  • 优点:高精度、实时性好,不会出现固定窗口的临界效应,使得限流更加平滑和公平。能够精确控制单位时间内的请求数,无突发漏洞。
  • 缺点:需要记录所有请求时间,内存消耗大。实现复杂度较高,通常需要借助哈希表、排序数据结构等来高效维护时间戳。适用于对准确度要求极高、且流量规模相对可控的场景,如关键业务的精细限流。对于海量突发流量场景,因为记录每次请求会消耗大量资源,可能不够实用。

漏桶算法

漏桶算法(Leaky Bucket)将请求看作“水滴”注入一个固定容量的桶中,桶底有一个以恒定速率漏水的小孔。每个新请求到来时都加入桶中;如果桶满了(超出容量),多余的请求就被丢弃。而桶会持续以恒定的速率处理(漏出)请求,即使瞬间来了大量请求,出桶速度保持不变,保证系统稳定处理。该算法本质上把不规则的输入流平滑为固定输出流,对防止突发洪峰非常有效。

  • 优点:能够平滑请求流量,避免瞬间高并发导致系统崩溃;输出速率可控,通过调整漏水速度和桶容量灵活适应不同需求。适用于对输出速度要求恒定的场景,如后台任务消费、生产者-消费者模型,保证系统按固定速率服务请求。
  • 缺点:不能应对突发的流量骤增;即使系统空闲,桶也只以固定速率“漏水”,突发请求只能排队等候。实现上需要维护一个队列或计数器来缓存请求,会占用额外内存。对流量波动大的场景,需要仔细调参,否则要么延迟过大要么丢弃过多请求。

令牌桶算法

令牌桶算法(Token Bucket)维护一个“令牌桶”,桶有固定容量,并以固定速率向其中添加令牌。当请求到来时,如果桶中有令牌,则取出一个令牌放行该请求,否则拒绝。桶中的令牌最多累积到满;在处理空闲时刻,积累的令牌可用于应对突发流量。与漏桶不同,令牌桶允许一定量的突发流量,因为空闲时累计的令牌可以在瞬时释放,突发请求只要桶中有令牌就能继续处理。

  • 优点:能够平滑控制请求速率,同时允许突发流量。当流量突然增多时,可以利用桶中剩余的令牌快速响应,而不是像漏桶那样一律排队等待。算法稳定性高,可通过调整生成令牌速率和桶容量适应不同需求;如Guava的RateLimiter即基于该算法。
  • 缺点:实现复杂度比固定窗口等高,需要维护令牌的生成和消耗逻辑。如果短时间内有大量请求而积累的令牌用完,新请求仍会被拒绝,需要对参数(速率、容量)精心设置。对时间精度要求高,如果时钟不同步或者延迟误差,可能影响限流效果。

Redis 分布式限流

分布式系统中,为了让多个实例共享限流状态,常采用Redis作为集中式存储,并借助原子操作保证多个实例间的一致性。通常做法是:每个实例对同一资源发送限流请求时,都访问同一个Redis键。例如可以使用固定窗口策略——为每个时间窗口创建一个Redis键(如apiKey:当前分钟),调用 INCR 将计数+1,并在键初次创建时设置过期时间;当计数超过阈值时拒绝请求。Redis的单线程特性和Lua脚本支持使得这种操作可以原子执行,避免了并发冲突。例如,使用MULTI/EXEC或Lua脚本将INCREXPIRE打包成单次操作,就可以保证即使同时多个实例发起请求,也不会出现数据竞争。此外,对于滑动窗口和滑动日志限流,也可以利用Redis的有序集合(ZSET)存储各个请求的时间戳,并在每次请求时原子地新增和删除过期成员,从而实现精确的滑动计数。

  • 一致性保障:关键在于将限流逻辑交由Redis单点原子执行。借助Lua脚本或事务,可以让INCRHINCRBY等更新操作变成原子操作,从而确保并发情况下计数正确。只要所有服务实例都指向同一个Redis key,就能做到全局限流。
  • 挑战与方案:Redis自身单点可能成为瓶颈,可通过Redis集群或分片方案分担负载。但使用Redis Cluster时需注意:如果一个Lua脚本要操作多个键,这些键必须在同一槽位(Hash Tag)上,否则会报错pandaychen.github.io。因此,往往会通过设计让所有限流相关的键使用相同的Hash Tag,或直接使用单节点Redis(依赖外部HA)。另一个挑战是可用性:如果Redis宕机或网络抖动,可能导致短暂限流规则失效。对此可以考虑启用持久化(AOF)、哨兵自动故障转移等保证Redis可靠性。总的来说,Redis分布式限流的核心思路是共享限流状态,让所有实例“看到”同一套计数数据,从而在集群内统一应用限流决策。

算法对比

算法 是否支持突发流量 平滑程度 实现复杂度 资源(内存/计算) 典型场景
固定窗口计数器 较低(有边界效应) 简单的QPS限制,对准确性要求不高
滑动窗口计数器 较高(可细化) 需要比固定窗口更平滑的流量控制
滑动日志算法 很高(精确) 高(存储时间戳) 追求精确限流、请求量适中时
漏桶算法 否(限速稳定) 很高(输出恒定) 限制输出速率,防止请求尖峰
令牌桶算法 高(允许突发) 低/中 需要平滑输出且允许突发场景
Redis分布式 视具体算法而定 视具体算法而定 较高 中/高 分布式环境下全局限流

总结

固定窗口和滑动窗口算法简单、开销低,适用于快速实现限流策略;其中滑动窗口能稍微缓解窗口边界的突发效应,但对极端突发流量仍不够友好。

滑动日志算法在准确性和公平性上优势最大,但需要记录每次请求时间,资源消耗最高。

漏桶和令牌桶算法都能有效平滑输出流量,但侧重点不同:漏桶以恒定速率处理请求(不支持突发),适合输出受限的场景;令牌桶允许短时突发,但算法实现稍复杂,可根据空闲时段积累令牌平滑应对流量峰值。

分布式限流(如基于Redis的方案)则将上述任意算法扩展到多实例环境,关键在于通过Redis的原子操作来共享计数状态并保证一致性。

总体而言,选用哪种限流算法需综合考虑系统对突发流量的容忍度、实现成本和资源消耗:对极端突发容忍度高的场景,可优先考虑令牌桶;对精确性要求极高的,可采用滑动日志;而对于大部分场景,固定窗口或滑动窗口配合简单的分布式计数即可满足需求。

推荐链接:

所有版本:https://go.dev/dl/

发布历史:https://go.dev/doc/devel/release

搜 索 库:https://pkg.go.dev/

标 准 库:https://pkg.go.dev/std

Go 命 令:https://pkg.go.dev/cmd/go

用户手册:https://go.dev/doc/

Go 语言高效编程:https://go.dev/doc/effective_go

Go 编程语言规范:https://go.dev/ref/spec

Go by Example:https://gobyexample.com/https://gobyexample-cn.github.io/

Go 菜鸟教程:https://www.runoob.com/go/go-tutorial.html

GoLand 下载:https://www.jetbrains.com/go/download/other.html

安装

下载解压版:go1.24.2.windows-amd64.zip

GUI配置环境变量:

1
2
3
4
5
6
7
GOROOT=D:\Program\go\
GOBIN=%USERPROFILE%\go\bin # 后续使用 go install 安装的二进制程序都存放在 GOBIN 里
PATH=%GOROOT%\bin\ # 新建。让 go 的二进制程序可以直接使用。
PATH=%GOBIN% # 新建。让 GOBIN 里的二进制程序可以直接使用。

# 查看版本
go version

Go环境变量

命令 作用
go env 查看所有当前 Go 环境变量
go env VAR 查看单个变量(如 go env GOPATH
go env -w VAR=value 写入配置文件,永久设置变量(Go 1.13+ 支持)。
即将变量写入本地配置文件(通常位于 $HOME/.config/go/env%USERPROFILE%\go\env)。
go env -u VAR 取消变量设置,即从配置文件中删除(恢复默认行为)
go env -json 以 JSON 格式输出所有变量,适合脚本或工具
1
2
3
4
5
6
7
8
# GOPATH 指定工作目录的根。1、存放依赖包缓存(比如在 $GOPATH/pkg/mod 下);2、存放 go install 安装后的二进制文件(在 $GOPATH/bin)。
go env -w GOPATH=%USERPROFILE%\go
# 后续使用 go install 安装的二进制程序都存放在 GOBIN 里
go env -w GOBIN=%USERPROFILE%\go\bin
# GOPROXY 多个地址之间用英文逗号分隔,Go 会按顺序尝试这些代理。direct 表示直接访问源地址拉取。
go env -w GOPROXY=https://goproxy.cn,direct
# GOPRIVATE 是 Go Modules 引入的配置项,用于声明哪些模块是私有的,不应该通过公共代理访问,也不应校验哈希(checksumdb)。
go env -w GOPRIVATE=*.zhaolq.com,internal

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
├─01
│ └─hello
│ go.mod
│ go.sum
│ hello.go

└─02
│ go.work

├─greetings
│ go.mod
│ greetings.go
│ greetings_test.go

└─hello
go.mod
hello.go

go.mod—模块定义文件(git管理)

这是 Go 模块的核心文件,描述项目的模块路径、依赖项及其版本。

主要作用:

  • 声明模块名(通常是项目的 import path)
  • 指定 Go 版本(go 1.20 等)
  • 声明依赖的第三方模块和版本(require
  • 可包含 replace 替换依赖模块路径(本地调试等)

示例内容:

1
2
3
4
5
6
7
8
9
10
module example/hello

go 1.24

require rsc.io/quote v1.5.2

require (
golang.org/x/text v0.24.0
rsc.io/sampler v1.99.99
)

go.sum—校验和文件(git管理)

记录每个依赖模块及其版本的哈希校验和,用于验证依赖包的一致性和完整性。

作用:

  • 防止被篡改的模块被编译
  • 确保团队成员或 CI 下载的是一致的依赖
  • 自动生成,不建议手动修改

示例内容:

1
2
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=

go.work—工作区文件(可选,Go 1.18+)

用于管理多个 Go 模块组成的工作区,支持跨模块开发,解决 monorepo 场景下的依赖问题。

使用场景:

  • 一个项目下有多个模块(例如微服务或工具库),需要一起开发和测试
  • 替代 replace 用于本地联调多个模块

示例内容:

1
2
3
4
5
6
go 1.24

use (
./moduleA
./moduleB
)

命令清单

官网学习中遇到的命令顺序

1
2
3
4
5
6
7
8
9
go mod init example/hello
go mod edit -replace example.com/greetings=../greetings
go mod tidy
go run .
go test .
go build
go list -f '{{.Target}}'
go env -w GOBIN=C:\Users\Administrator\go\bin
go install

初始化与模块管理

命令 说明
go mod init <模块名> 初始化模块(生成 go.mod)
go mod tidy 自动添加/删除依赖
go mod download 下载 go.mod 中声明的所有依赖到本地
go mod vendor 将依赖复制到 vendor/ 目录中
go mod verify 校验模块缓存
go list -m all 查看所有依赖模块

构建与运行

命令 说明
go run <file.go> 编译并运行 Go 源码文件
go run . 编译并运行当前目录的 main
go build 编译当前包,生成可执行文件(二进制文件)
go build -o myapp 指定输出文件名
go install 编译并安装当前模块(安装到 $GOBIN$GOPATH/bin
go clean 清除当前模块下的构建产物,包括编译生成的二进制文件和中间文件
go clean -modcache 清除全局的模块缓存(即下载的第三方依赖)

测试

命令 说明
go test 运行当前包的测试用例
go test -v 显示详细的测试输出
go test ./... 递归测试当前模块的所有包(包括子包)
go test -cover 查看测试覆盖率
go test -bench . 执行基准测试(以 Benchmark 开头的函数)

代码工具

命令 说明
go fmt ./... 格式化所有 Go 文件
go vet 静态代码分析(找潜在 bug)(推荐)
go doc <包名/函数> 查看文档,如:go doc fmt.Println
go list 列出当前模块信息
go list -m -u all 列出当前模块依赖的所有模块,以及每个模块的最新版本:
go list -m -u example.com/theirmodule 显示特定模块的最新版本
go list -f '{{.Target}}' 打印可编译包的目标文件路径
go list -json 输出包的所有结构化信息(JSON 格式)

工具命令

命令 说明
go version 查看 Go 版本
go env 查看 Go 环境变量
go env -w GOBIN=C:\path\to\your\bin 指定 Go 程序安装目录
go help <命令> 查看某个命令帮助,比如 go help mod

第三方包安装

命令 说明
go get . 添加模块中某个包的所有依赖项
go get <module> 安装或升级某个包(Go 1.17 及以前)
go install <module>@latest Go 1.18+ 推荐方式安装工具包

示例:安装一个工具(如静态分析器)

1
2
// golangci-lint 是 Go 语言中最流行的 静态代码分析工具,它相当于一个多合一的“代码质量检查器”
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

欢迎来到安全专栏的第5次加餐时间。

随着科技的快速发展,各种新的技术和概念不断出现,持续出现的新技术会不断推动安全的发展。虽然,每一个新技术都会衍生出新的安全威胁和隐患,但是,这些新的安全问题也正是安全行业保持活力的源泉。所以,对于安全人员来说,这些新技术的出现既是一种挑战,也是一种机遇。

阅读全文 »
0%