sinter 无法满足多标签组合筛选需求,因其存在主线程阻塞、不支持动态排除子集及复杂混合逻辑三大缺陷;lua 脚本通过原子执行、keys 安全传参和按需调用 smembers 等方式实现高效可维护的集合运算。

为什么不能只用 SINTER 做多标签组合筛选
直接用 SINTER 求多个标签集合交集,看似简单,但实际会卡在三个地方:一是当某个标签集合特别大(比如“男”有 500 万用户),Redis 会阻塞主线程几毫秒甚至更久;二是无法动态排除某些子集(比如“打过广告但未付费的用户”);三是不支持带权重或条件的混合逻辑(如“A 标签 AND (B 标签 OR C 标签) NOT D 标签”)。这些需求单靠原生命令根本没法表达。
用 Lua 脚本封装 Set 运算的核心要点
Lua 脚本在 Redis 中是原子执行的,能避免多次网络往返和中间状态污染。关键不是“写个脚本”,而是怎么组织它才能兼顾可读、可维护和性能:
- 把输入标签名统一通过
KEYS传入,避免在脚本里拼接 key 名(防止注入和 key 空间污染) - 用
redis.call("SMEMBERS", KEYS[i])获取小集合(SSCAN 分批处理,否则可能 OOM 或超时 - 逻辑运算优先用 Lua 表操作(
table.insert、table.concat),别反复调用 Redis 命令——每次redis.call都有开销 - 最终结果必须用
redis.call("SADD", ...)写回一个临时 key,并设置EXPIRE,避免堆积
示例片段(求 A ∩ (B ∪ C) − D):
local a = redis.call("SMEMBERS", KEYS[1])
local b = redis.call("SMEMBERS", KEYS[2])
local c = redis.call("SMEMBERS", KEYS[3])
local d = redis.call("SMEMBERS", KEYS[4])
local union_bc = {}
for _, v in ipairs(b) do union_bc[v] = true end
for _, v in ipairs(c) do union_bc[v] = true end
local result = {}
for _, v in ipairs(a) do
if union_bc[v] and not d[v] then table.insert(result, v) end
end
redis.call("SADD", KEYS[5], unpack(result))
redis.call("EXPIRE", KEYS[5], 300)
如何避免 Lua 脚本超时与内存爆炸
Redis 默认 lua-time-limit 是 5000ms,但实际中脚本卡住往往不是因为逻辑慢,而是数据规模没控住:
- 提前在应用层校验每个标签集合大小:
SCARD tag:gender:male,超过阈值(如 10 万)就拒绝执行或降级为分页扫描 - 禁止在 Lua 中使用
for i=1,#big_set do遍历百万级表——改用SSCAN+ 游标分批,在外层循环中控制总迭代次数 - 不要把中间结果全存进 Lua 表,尤其去重或排序场景,优先用 Redis 的
SUNIONSTORE/SDIFFSTORE做中间聚合 - 所有临时 key 必须带业务前缀和随机后缀(如
tmp:filter:abc123),防止并发冲突
生产环境必须加的兜底机制
再严谨的脚本也扛不住异常流量或误操作。上线前至少补上这三件事:
- 对每个脚本计算 SHA1 后预加载:
SCRIPT LOAD <script></script>,后续用EVALSHA替代EVAL,减少网络传输和解析开销 - 在应用侧做熔断:连续 3 次
BUSY错误或超时,自动切到降级策略(如返回空集或走 MySQL 回源) - 监控
lua_script_count和used_memory_lua,这两个指标突增大概率意味着脚本泄漏或死循环
最常被忽略的一点:Lua 脚本里的 redis.call 报错会整个中断,但错误信息不会透出到客户端——得在脚本末尾加 return {err = "xxx"} 并在外层解析,否则你永远不知道是 key 不存在还是逻辑分支没走到。











