必须用SET而非STRING或HASH,因其SISMEMBER为O(1)查询、天然去重、无序且内存紧凑;STRING需GET+分割+遍历,HASH冗余且不支持集合运算,intset编码下内存比HASH低40%以上。

为什么必须用 SET 而不是 STRING 或 HASH
因为黑白名单本质是“成员是否存在”的判断,SET 的 SISMEMBER 是 O(1) 时间复杂度,且天然去重、无序、内存紧凑。用 STRING 存 IP 列表(如 CSV)会导致每次都要 GET + 字符串分割 + 遍历,高并发下直接拖垮 Redis;用 HASH 虽然也能 HGET,但 key-value 映射关系冗余,且无法做集合运算(比如“白名单 ∩ 黑名单”校验冲突)。更关键的是:SET 在小整数场景下自动切到 intset 编码,内存比 HASH 低 40% 以上——这对存几万 IP 的黑名单很实在。
access_by_lua_block 里查 redis:sismember 的实操要点
别在每次请求里新建 Redis 连接,否则连接池打满、TIME_WAIT 爆表。必须用 init_by_lua_block 初始化连接池,并复用:
- 在
http块里加:init_by_lua_block { local redis = require "resty.redis"; local red = redis:new(); red:set_timeout(500); red:init_pipeline(); }(注意:这里只是示例初始化逻辑,真实需存到全局变量或共享字典) - 在
access_by_lua_block中从连接池取连接:local red = redis:connect("127.0.0.1", 6379)改为local red = redis:new(); red:set_timeout(500); local ok, err = red:connect("127.0.0.1", 6379),并确保有错误兜底(ngx.log(ngx.ERR, ...)) - 获取真实 IP 不能只信
ngx.var.remote_addr,要按顺序读:ngx.var.http_x_real_ip→ngx.var.http_x_forwarded_for(取第一个非空 IPv4),否则 CDN 或 LB 后面的封禁全失效 - 查黑名单优先于白名单:先
red:sismember("blacklist", ip),命中直接ngx.exit(403);再查白名单时,建议用red:sismember("whitelist", ip),未命中才放行(即“黑名单优先+白名单兜底”)
Redis 主从+哨兵对黑白名单可用性的影响
单节点 Redis 故障=全站放行或全站拦截,不可接受。主从集群 + 三哨兵是底线配置:
- 写操作(如
SADD blacklist 1.2.3.4)必须打到 master,哨兵会自动切换 master 角色,但 Lua 脚本里redis:connect的地址不能写死 IP,得用域名或 VIP,否则 failover 后连接中断 - 读操作(
SISMEMBER)可路由到 slave,但 OpenResty 的resty.redis默认不支持读写分离,得自己封装或改用resty.rediscluster;简单方案是所有读写都走 master,靠哨兵保障 master 可用性 - TTL 必须设:用
SETEX blacklist:1.2.3.4 7200 1替代SADD,避免误加永久黑名单;但注意SET类型不支持原生命令级 TTL,所以得用带前缀的 key + 单值,或改用SET+EXPIRE两步(性能略差)
当 IP 量超 100 万时,SET 还够用吗?
够,但要注意编码退化和内存水位。100 万纯 IPv4 字符串(如 "192.168.1.1")在 hashtable 编码下约占用 120MB 内存(实测),而 intset 完全用不上(IPv4 字符串不是整数)。这时候真正卡脖子的是:
- Redis 单线程处理大量
SISMEMBER请求的 CPU 使用率,建议压测时观察INFO commandstats中sismember的calls和usec_per_call - 如果黑白名单规则本身需要组合判断(比如“来自某 ASN 且访问 /admin 的 IP”),纯
SET不够用,得上RedisBloom模块或本地布隆过滤器预筛 - 千万别用
SMEMBERS全量拉取再 in-memory 判断——这是把 Redis 当数据库使,一调就超时
最常被忽略的一点:SET 的 key 命名空间没隔离。线上环境务必用不同前缀区分环境,比如 prod:blacklist、staging:blacklist,否则测试机一刷库,生产就变裸奔。











