Redis SISMEMBER命令用于判断指定成员是否为集合的成员,返回1(是)或0(否),key不存在时也返回0;在Lua脚本中须用数字比较(如==1),不可当布尔值使用,以保障原子性。
直接用 redis.call('SISMEMBER', 'blacklist', KEYS[1]) 就行,但必须注意:返回值是整数 1 或 0,不是布尔值。Lua 脚本里别写 if redis.call(...) == true —— 这会永远走 false 分支。
常见错误是把返回值当 Lua 布尔用,导致拦截逻辑失效。正确写法是显式比对数字:
local is_blocked = redis.call('SISMEMBER', 'blacklist', KEYS[1]) == 1if is_blocked then return 1else return 0end
因为网络往返 + 客户端条件判断会破坏原子性:A 请求查到不在黑名单,B 在此时把 A 的 ID 加进去,A 接着就通过了。Lua 脚本在 Redis 单线程内执行,SISMEMBER 和后续动作(比如记录日志、限流计数)能包进同一原子操作里。
使用场景典型如 API 网关层的实时拦截,要求“查+拦”不可分割。如果只是读取状态且允许短暂窗口期,那用客户端逻辑也行,但不符合“实时拦截”需求。
要点:
EVAL 脚本必须把 key 名作为 KEYS[1] 传入,不能硬编码,否则无法被 Redis Cluster 路由EVAL script 1 blacklist_key user_id,第一个数字是 key 的数量OpenResty 的 access_by_lua* 阶段需要明确返回状态码或中断请求,不能只靠 Lua 脚本返回 1/0。得配合 redis.call + ngx.exit 使用:
local redis = require "resty.redis"local red = redis:new()red:set_timeout(100)red:connect("127.0.0.1", 6379)local ok, err = red:eval( "return redis.call('SISMEMBER', KEYS[1], ARGV[1]) == 1 and 1 or 0", 1, "blacklist", ngx.var.arg_uid)if ok == 1 then ngx.exit(403)end
注意这里用了 eval 方法而非 evalsha,首次调用会自动缓存 SHA1;但若脚本变更,需清空 Redis 的脚本缓存(SCRIPT FLUSH),否则旧逻辑仍在运行。
容易踩的坑:
set_timeout,超时默认是 60s,会卡住整个请求生命周期connect 返回值,连接失败时 eval 报错,Nginx 日志里是 nil 调用错误ARGV[1] 是字符串,如果传的是数字型 user_id,Lua 里比较仍安全(Redis 自动转),但建议统一用字符串避免歧义别用一个大集合存所有黑名单用户,应该按业务维度拆分,比如 blacklist:api_v1、blacklist:payment。这样既能隔离影响面,又方便单独清理。
如果黑名单项有自然过期时间(如封禁 1 小时),不要依赖 EXPIRE 整个 key —— 集合没法给单个成员设 TTL。正确做法是用 ZSET 存时间戳,再用 ZCOUNT + ZREMRANGEBYSCORE 清理过期项;但若只是静态封禁(永久或手动解封),SET + SISMEMBER 最轻量。
性能提示:
SISMEMBER 平均时间复杂度 O(1),但最坏是 O(N),当集合元素极多(千万级)且哈希冲突严重时可能变慢,这时应考虑分片(如对 user_id 取模分 16 个 key)io-threads 对 EVAL 性能无提升,因为 Lua 脚本强制单线程执行lua_calls 和 lua_scriptcachemisses 指标,高频 miss 说明脚本没复用 SHA1真正难的不是写对那一行 SISMEMBER,而是确保 key 命名可运维、过期机制可收敛、错误路径不静默失败——这些地方一漏,线上就只能靠日志倒查。