直接用DEL清空再重建是最省事的:先DEL原key,再RPUSH/LPUSH去重后数据,操作快(O(1))、原子、无残留;若需保留过期时间,重建后须立即EXPIRE;不适用于无法获取全量数据的场景。

直接用 DEL 清空再重建是最省事的
如果你只是想“清掉旧 List、塞入去重后的新数据”,别在原 List 上折腾去重逻辑。直接 DEL 掉整个 key,再用 RPUSH 或 LPUSH 把去重后的元素一次性写入。它快(O(1))、原子、无残留、不阻塞 Redis 主线程。
常见误区是执着于“原地去重”——但 Redis 的 List 本身不提供去重命令,所有所谓“原地”方案都要遍历、判断、删改,本质是模拟,成本远高于重建。
- 适用前提:你有完整的去重后数据(比如来自应用层处理好的
List<String>) - 注意点:
DEL会清除 key 的过期时间(EXPIRE),如果需要保留,重建后得立刻EXPIRE新 key - 不适用场景:你无法拿到全部元素(比如 List 超大、或持续被其他客户端写入),此时才需考虑原子化在线去重
Lua 脚本实现原子化在线去重(保留顺序)
当必须在 List 不中断服务的前提下清理重复项(例如任务队列持续写入),唯一靠谱的方式是 Lua 脚本 + SET 辅助。核心思路:用 SET 记录已见元素,遍历 List 时跳过重复项,把不重复的重新 LPUSH 到临时 List,最后原子替换。
以下脚本完成三件事:LRANGE 拉全量 → 用 Lua table 去重并保序 → LPUSH 到新 key → RENAME 原子切换:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
local list = redis.call('LRANGE', KEYS[1], 0, -1)
local seen = {}
local unique = {}
for i = 1, #list do
if not seen[list[i]] then
seen[list[i]] = true
table.insert(unique, list[i])
end
end
redis.call('DEL', KEYS[1])
for i = #unique, 1, -1 do
redis.call('LPUSH', KEYS[1], unique[i])
end
return #unique
- 必须用
KEYS[1]传入 List key,不能硬编码;ARGV不参与此逻辑 - 遍历时从尾到头
LPUSH,才能还原原始顺序(因为LPUSH是头插,逆序插入即正序结果) - 该脚本会阻塞 Redis 单线程,若 List 超过 10 万元素,建议拆成小批次(如每次处理 5000 条)并由客户端轮询调用
为什么不用 LREM 配合遍历去重
LREM 看似能删重复,但它只删指定值的 N 个匹配项,无法“留一个删其余”。想靠它实现去重,得先 LINDEX 取值,再 LREM 删后续所有同值项——这需要多次往返、无法原子、且高并发下极易出错。
-
LREM key 0 "a"会删光所有"a",不是“留首删余” - 没有
LREM key 1 "a"这种“删第二个起”的语法;count=1表示只删第一个匹配项 - 若用客户端循环:先
LPOP→ 判断是否已存在 → 再决定是否LPUSH回去,网络延迟+竞态条件会让结果不可控
Set 辅助方案的陷阱与绕不开的权衡
生产中更常见的做法不是“清理旧 List”,而是“写入时防重”:用 SET 存唯一标识,List 存完整消息,靠 Lua 脚本保证 SADD 成功才 RPUSH。但这个模式有硬伤:
- 去重粒度只能是字符串;若 List 存的是 JSON,需提前提取业务主键(如
"id"字段),不能直接塞整条 JSON 到 Set - Set 和 List 的 key 名必须严格对应(如
task:list和task:set),否则原子性失效 - 若业务要求“相同内容但不同时间戳视为不同任务”,那基于内容的 Set 去重就完全错误——此时应改用 ZSET + 时间戳做分数去重
真正难的从来不是“怎么写脚本”,而是想清楚:你要去重的依据到底是什么?是字段值?是业务语义?还是时间窗口内的幂等?没定义清楚这个,所有技术方案都是空中楼阁。

















