不能直接用 map[string]struct{} 或 sync.map 做高频 ip 黑名单:前者引发 gc 压力和锁竞争,后者 read map 频繁升级、不支持 ttl 且内存浪费;ipv4/ipv6 混合下哈希冲突致查找退化,单机不同实例不同步。应改用 redis bitmap + 本地 arc 缓存 + ip 归一化处理。

直接用 map[string]struct{} 做内存黑名单,在万级 QPS 下会因锁竞争或 GC 压力导致延迟毛刺,必须换方案。
为什么不能直接用 sync.Map 或普通 map 存 IP 黑名单
高频写入(如实时封禁)+ 高频读取(每个请求都要查)时,sync.Map 的 read map 会频繁升级、引发写放大;而普通 map 加 sync.RWMutex 在 5000+ RPS 下,锁争用会让平均延迟从 0.2ms 涨到 8ms+。更关键的是:内存中存全量字符串 IP,对 IPv4/IPv6 混合场景内存浪费严重(一个 "2001:db8::1" 占 19 字节,但实际只需 16 字节二进制表示)。
- 黑名单条目超 10 万后,
map查找平均时间不再是 O(1),哈希冲突会使部分 key 查找退化为 O(n) -
sync.Map不支持遍历删除过期项,无法做 TTL 清理 - 单机部署时,不同 Gin 实例间黑名单不同步,攻击者可轮询绕过
用 Redis + Bitmap 做分布式黑名单检索
把 IP 转成固定长度整数(IPv4 → uint32,IPv6 → uint128 分段哈希),再映射到 Redis 的 bitmap 位上。一个 100 万 IP 的黑名单,只占约 125KB 内存,且 GETBIT 是 O(1) 原子操作。
- IPv4 直接用
net.ParseIP(ip).To4()转[]byte,再转uint32 - IPv6 用
sha256.Sum256哈希后取低 64 位,避免高位全零导致碰撞 - Redis key 命名建议带业务前缀和日期,例如
blacklist:ip:20260818,方便按天清理 - Go 客户端用
redis.UniversalClient,启用连接池(MinIdleConns: 10)防连接耗尽
中间件代码片段:
func BlacklistMiddleware(client redis.UniversalClient, key string) gin.HandlerFunc {
return func(c *gin.Context) {
ip := c.ClientIP()
offset := ipToBitmapOffset(ip) // 实现见上文转换逻辑
bit, err := client.GetBit(context.Background(), key, offset).Result()
if err == nil && bit == 1 {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "blocked by ip blacklist"})
return
}
c.Next()
}
}
本地缓存 + 异步更新的两级防护
纯 Redis 查询在 10k+ QPS 下仍有网络 RTT 开销(即使内网也常达 0.3–0.8ms)。应加一层 LRU 本地缓存,只对「未命中」且「Redis 返回 1」的 IP 做写入,避免缓存污染。
- 用
golang-lru/v2的lru.NewARC(10000),ARC 比 simple LRU 更适应黑名单访问模式(热点 IP 少,但需快速失效) - 缓存 value 类型为
struct{ blocked bool; ts time.Time },带时间戳用于被动淘汰(比如 5 分钟无访问自动剔除) - 封禁操作走异步 goroutine 写 Redis + 清本地缓存,不阻塞主流程
- 注意:本地缓存不存「放行」结果,只存「已确认封禁」,避免漏拦
容易被忽略的边界点
真实攻击流量里,X-Forwarded-For 可能伪造、CDN 回源 IP 会覆盖 ClientIP、IPv6 地址格式不统一(含缩写、大写、双冒号)。不做归一化,同一攻击者换写法就能绕过。
- 务必调用
c.Request.Header.Get("X-Forwarded-For")并取第一个非私有地址(IsPrivate()判断) - 用
net.ParseIP(strings.TrimSpace(ip)).To16()统一转为 16 字节,再做后续哈希或截断 - Redis bitmap 不支持动态扩容,预估最大 offset 后用
SETBIT key max_offset 0初始化,否则首次SETBIT可能触发 Redis 内存重分配卡顿











