redis set 本身不保证去重的原子性,sadd仅返回本次新增数量,无法防止并发重复写入;必须用lua脚本将sismember与sadd合并为原子操作才能实现可靠分布式去重。

Redis Set 能做分布式去重,但直接用 sadd + sismember 在高并发下会漏判、误判,根本原因不是命令本身错,而是「判断+写入」没原子性。
为什么 sadd 单独用不等于去重判断?
sadd 返回值确实能告诉你“这次有没有新增”,但它只反映本次操作结果,不保证你之前没被其他节点写入过。比如两个请求同时查 sismember 返回 false,接着都执行 sadd —— 两次都成功,重复就进来了。
- 典型场景:秒杀库存扣减前校验用户是否已参与,或日志系统过滤重复上报 ID
- 关键陷阱:把
sadd当作“存在性判断”,其实它只是“插入并返回是否新增” - 真正安全的判断必须和插入合并为一个原子操作,不能拆成两步
用 EVAL 脚本保证原子性
把“判断是否存在 + 不存在则添加”封装进 Lua 脚本,由 Redis 单线程执行,彻底避免竞态。脚本返回 1 表示首次写入,0 表示已存在或写入失败。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
local exists = redis.call('sismember', KEYS[1], ARGV[1])
if exists == 1 then
return 0
else
redis.call('sadd', KEYS[1], ARGV[1])
return 1
end
- 调用时用
redis.eval(script, 1, 'dedup_set', 'user:1001'),其中1是 key 个数,'dedup_set'是集合名,'user:1001'是待去重的值 - 注意:Lua 脚本里不能用
redis.call('exists', ...)替代sismember,因为exists判断的是 key 是否存在,不是 member 是否在 set 中 - 如果业务需要 TTL,得额外加
expire,且必须放在sadd后面,否则可能 set 写入了但 expire 失败,导致脏数据长期残留
大规模数据下 Set 的内存代价和替代方案
当去重量级上千万甚至上亿,Set 的内存占用会迅速飙升(每个字符串至少几十字节开销),此时 Set 不是“能不能用”,而是“值不值得用”。
-
BitMap:适合整型 ID 映射,如用户 ID 从 1 开始连续,用setbit dedup_bitmap user_id 1,空间压缩比极高,但无法处理字符串或稀疏 ID -
HyperLogLog:只适合统计基数(pfadd+pfcount),不支持查询某个具体元素是否存在,不能用于精确去重逻辑 -
BloomFilter(如 RedisBloom 模块):支持bf.add/bf.exists,有可控误判率(通常 Set 低 10 倍以上,但需接受极小概率的“假阳性”(误判存在)
真正要落地,得先明确:你的数据规模、ID 类型、是否允许误判、能否接受 TTL 管理成本。别一上来就写 sadd,也别一看到“亿级”就立刻上布隆——中间往往有更轻量的折中点,比如分片 Set 名(dedup:{hash(user_id)%16})配合定时清理。










