Redis UNLINK 命令详细教程
UNLINK 用于删除一个或多个 Key,是 DEL 的非阻塞版本。它在主线程中只完成"从键空间解除链接"这一步,实际的内存回收交给后台线程异步执行,因此删除包含大量元素的集合类大 Key 时几乎不会阻塞 Redis。对调用方而言,返回值和语义与 DEL 一致:返回实际删除的 Key 数量。
本文基于 Redis 通用 Key 操作介绍
UNLINK。Redis 命令本身不区分大小写,因此UNLINK、unlink和Unlink的效果相同;文档统一使用大写形式。UNLINK自 Redis 4.0 起可用。
资料合集:https://pan.quark.cn/s/10e98d308913、https://pan.quark.cn/s/f56bc69c5338
一、命令概览
1. 基本语法
redis
UNLINK key [key ...]
参数说明:
| 参数 | 说明 |
|---|---|
key [key ...] |
一个或多个要删除的 Key。不存在的 Key 会被忽略,不会报错 |
返回值:
- 返回被实际删除(解除链接)的 Key 数量(整数)。
- 所有指定 Key 都不存在时返回
(integer) 0。
2. 最简单的示例
先写入两个字符串值:
redis
SET key1 "Hello"
SET key2 "World"
删除一个 Key:
redis
UNLINK key1
返回:
text
(integer) 1
删除多个 Key(其中 key3 不存在):
redis
UNLINK key1 key2 key3
返回:
text
(integer) 1
key1 在上一步已被删除,本次只删除了 key2,计数为 1。
3. 命令特性
- 适用于任意数据类型的 Key,包括 String、Hash、List、Set、Sorted Set、Stream。
- Key 一经
UNLINK,立即对后续命令不可见,无需等待后台内存回收。 - 不存在的 Key 会被忽略,不产生错误,重复执行是安全的。
- ACL 类别为
@keyspace、@write、@fast。
二、UNLINK 的返回结果
1. 删除一个存在的 Key
redis
SET greeting "Hello"
UNLINK greeting
返回:
text
(integer) 1
删除后 Key 不再存在:
redis
EXISTS greeting
返回:
text
(integer) 0
2. 删除不存在的 Key
redis
UNLINK not-exists
返回:
text
(integer) 0
3. 一次删除多个 Key
redis
SET a 1
SET b 2
SET c 3
UNLINK a b c
返回:
text
(integer) 3
4. 混合存在与不存在的 Key
redis
SET exist-1 1
UNLINK exist-1 not-exist-1 not-exist-2
返回:
text
(integer) 1
不存在的 Key 不影响计数,也不产生错误。
5. 重复删除
redis
SET once 1
UNLINK once
UNLINK once
两次 UNLINK 分别返回:
text
(integer) 1
(integer) 0
UNLINK 是幂等且安全的:重复删除同一 Key 不会报错。
6. 已过期的 Key
已经过期的 Key 在 Redis 中被视为不存在,UNLINK 对它返回 0:
redis
SET short-lived value EX 1
等待超过 1 秒后执行:
redis
UNLINK short-lived
返回:
text
(integer) 0
三、异步删除的执行原理
1. 两阶段执行
UNLINK 把删除拆成两个阶段:
- 主线程解除链接 :从键空间(dict)中移除该 Key 的引用。这一步对每个 Key 是 O(1),耗时与值的大小无关。完成后,Key 立即对
EXISTS、GET等所有命令不可见。 - 后台线程回收内存:值对象占用的内存由后台线程异步释放。对于包含大量元素的 Hash、List、Set 等,这部分开销不再占用主线程时间。
因此"删除成功"(返回计数、Key 不可见)与"内存已释放"是两个独立的时刻,后者可能稍晚完成。
2. 小对象的处理
对于元素很少的小对象,启动后台任务的开销可能高于直接释放,Redis 可能仍在当前流程中同步完成释放。是否转入后台由内部阈值决定,属于实现细节,随版本可能调整。从调用方角度看,UNLINK 的设计目标是保证主线程不被大对象的释放过程长时间阻塞。
3. 观察 lazyfree 状态
INFO 输出中的 lazyfree_pending_objects 字段反映当前等待后台线程释放的对象数量。批量删除大 Key 后,该数值可能短暂上升,随后归零:
bash
redis-cli INFO | grep lazyfree
4. 与 DEL 的阻塞对比
DEL 是同步删除,必须在主线程中完成全部内存释放。删除一个包含数百万元素的集合时,主线程可能被占用几十到几百毫秒,期间所有其他命令都被延迟;UNLINK 把这部分工作移出主线程,避免此类卡顿。
四、UNLINK 与 DEL 的区别
两者语法和返回值语义完全一致,区别只在删除方式:
| 对比项 | DEL |
UNLINK |
|---|---|---|
| 删除方式 | 同步,主线程释放全部内存 | 主线程解除链接,后台线程释放内存 |
| 大 Key 阻塞风险 | 高,可能明显阻塞 | 通常极低 |
| 每个命令的开销 | O(N),N 为所有被删值包含的元素总数 | 解除链接 O(1),回收在后台 |
| 返回值 | 实际删除数量 | 实际删除数量 |
| Key 不可见的时机 | 命令返回时 | 命令返回时 |
| ACL 类别 | @keyspace @write @slow |
@keyspace @write @fast |
| 可用版本 | 1.0+ | 4.0+ |
选择建议:
- 删除小 Key(普通字符串、小型集合)时两者表现几乎一致,用
DEL或UNLINK都可以。 - 删除大 Key(元素数多的集合、大字符串)或一次删除大量 Key 时,优先使用
UNLINK。 - 代码无法确定 Key 的规模时,默认使用
UNLINK是更稳妥的选择。
五、与相关命令的区别
1. UNLINK 与 DEL
见第四章。两者返回计数语义相同,UNLINK 是大 Key 删除场景的推荐命令。
2. UNLINK 与 FLUSHDB ASYNC / FLUSHALL ASYNC
FLUSHDB 和 FLUSHALL 也支持异步方式(Redis 4.0+):
redis
FLUSHDB ASYNC
FLUSHALL ASYNC
它们清空的是整个数据库(或所有数据库)的所有 Key,属于高危操作;UNLINK 只删除指定的 Key。整体清库时的异步能力与 UNLINK 同源,都依赖 lazy free 后台线程。
3. UNLINK 与类型内部删除命令
UNLINK 删除整个 Key,以下命令只删除数据结构内部的元素:
HDEL key field:删除 Hash 中的字段。SREM key member:删除 Set 中的成员。LREM key count element:删除 List 中的元素。ZREM key member:删除 Sorted Set 中的成员。XDEL key id:删除 Stream 中的消息。
注意:集合内部删除大量元素同样有释放开销。将大集合"瘦身"到空也会删除 Key,但过程可能是同步的;对超大的集合,直接 UNLINK 整个 Key 通常比逐个删除元素高效得多。
4. UNLINK 与 GETDEL
GETDEL 读取 String 值的同时同步删除 Key,适用于"取一次即失效"的一次性数据:
redis
GETDEL one-time-token
仅删除而不需要返回值时,使用 UNLINK(或 DEL)。
5. UNLINK 与 EXPIRE
EXPIRE 让 Key 在指定时间后自动过期,是延迟生效的删除;UNLINK 是立即删除:
redis
EXPIRE cache-key 300 # 300 秒后自动删除
UNLINK cache-key # 立即删除
过期删除是否走后台线程由 lazyfree-lazy-expire 配置决定,见下文。
6. UNLINK 与 lazyfree 系列配置
Redis 提供多个配置项控制隐式删除是否走后台线程:
| 配置项 | 作用 | 起始版本 |
|---|---|---|
lazyfree-lazy-user-del |
设为 yes 后,DEL 表现得像 UNLINK |
6.0 |
lazyfree-lazy-user-flush |
设为 yes 后,FLUSHDB/FLUSHALL 默认走后台 |
6.0 |
lazyfree-lazy-expire |
过期 Key 的删除走后台 | 4.0 |
lazyfree-lazy-eviction |
内存淘汰删除走后台 | 4.0 |
lazyfree-lazy-server-del |
隐式删除(如 RENAME 覆盖目标 Key)走后台 |
4.0 |
查看与修改:
redis
CONFIG GET lazyfree-lazy-user-del
CONFIG SET lazyfree-lazy-user-del yes
修改配置只影响后续操作,不会回溯已执行的动作。
六、事务、Pipeline 与并发场景
1. 在事务中使用
UNLINK 可以放入事务队列:
redis
MULTI
UNLINK key-a
UNLINK key-b
EXEC
EXEC 执行后返回各命令的结果,每条 UNLINK 的结果为实际删除数量。
2. 原子性与竞态
UNLINK 是单条命令,服务端一次性完成"解除链接",不存在删除一半的中间状态。但"先读后删"的多步流程不是原子操作。
例如"比较锁值后再释放锁"不能直接写成先 GET 再 UNLINK,否则可能误删其他客户端后来获得的锁。应使用 Lua 脚本将比较与删除作为原子操作:
lua
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("UNLINK", KEYS[1])
else
return 0
end
3. Pipeline 与 SCAN 分批删除
需要删除大量 Key 时,先使用 SCAN 增量遍历,再用 Pipeline 批量发送 UNLINK:
text
UNLINK key:1
UNLINK key:2
UNLINK key:3
不要用 KEYS 先查出所有 Key 再逐个删除,KEYS 本身就会阻塞服务器。每批的数量应控制在合理范围(如几百个),避免单批过大造成请求排队。
4. 持久化与复制
UNLINK 是写命令,与 DEL 一样会持久化到 AOF 并复制到从节点,最终状态与执行 DEL 相同。所有节点(含从节点)都应使用 Redis 4.0 及以上版本。
七、在常见客户端中的使用方式
1. redis-cli
bash
redis-cli SET greeting "Hello"
redis-cli UNLINK greeting
redis-cli UNLINK greeting
第二条命令返回 1,第三条返回 0。
2. Python(redis-py)
python
import redis
client = redis.Redis(host="localhost", port=6379, decode_responses=True)
client.set("greeting", "Hello")
removed = client.unlink("greeting", "not-exists")
print(removed) # 1
3. Node.js(node-redis)
javascript
import { createClient } from "redis";
const client = createClient();
await client.connect();
await client.set("greeting", "Hello");
const removed = await client.unlink(["greeting", "not-exists"]);
console.log(removed); // 1
await client.quit();
4. Java(Jedis)
java
import redis.clients.jedis.Jedis;
try (Jedis jedis = new Jedis("localhost", 6379)) {
jedis.set("greeting", "Hello");
long removed = jedis.unlink("greeting", "not-exists");
System.out.println(removed); // 1
}
不同客户端对返回值的封装可能不同,但都对应实际删除的 Key 数量。旧版客户端 SDK 可能没有封装 unlink 方法,此时可通过 execute_command("UNLINK", ...) 或升级 SDK 解决。
八、典型业务场景
1. 大 Key 清理
删除前先评估规模,再异步删除:
redis
MEMORY USAGE big:cache:key
UNLINK big:cache:key
删除包含大量元素的集合 Key 时,UNLINK 能避免主线程被内存释放过程阻塞。
2. 按模式批量删除
清理一批具有相同前缀的临时 Key:
text
SCAN 游标 MATCH tmp:batch:2026:* COUNT 500
UNLINK k1 k2 k3 ...(每批数百个)
SCAN 负责渐进式遍历,UNLINK 负责低阻塞删除,两者配合是生产环境批量清理的推荐组合。
3. 缓存主动失效
底层数据更新后删除对应缓存,等待下次访问重建:
redis
UNLINK cache:product:1001
缓存 Key 较大或数量较多时,用 UNLINK 替代 DEL 降低对线上请求的影响。
4. 会话与临时数据批量下线
活动结束、会话批量过期前主动清理:
redis
UNLINK session:abc session:def session:ghi
九、性能与使用建议
- 时间复杂度:每个 Key 的解除链接为 O(1),总命令开销 O(N),N 为 Key 数量;值的内存回收在后台线程完成,复杂度 O(M),M 为值内元素数量,不占用主线程。
- 删除大 Key 或批量删除时优先使用
UNLINK,替代DEL。 - 批量删除必须配合
SCAN增量遍历并分批执行,不要使用KEYS。 UNLINK与DEL一样会触发del类型的 keyspace 通知,大量删除时注意订阅端的消费能力。- 删除不可逆,执行前确认 Key 名称与影响范围;重要数据先备份(如
COPY)。 - 集群模式下,单条
UNLINK的多个 Key 必须位于同一哈希槽;跨槽位删除需按槽位分组多次执行,可用 hash tag 保证同槽。 - 内存释放是异步的:
used_memory可能略滞后于删除动作,进程 RSS 因内存分配器持有内存也可能不会立即下降。 - Redis 版本低于 4.0 的实例不支持
UNLINK,调用会返回 unknown command 错误。
十、常见问题排查
问题 1:UNLINK 返回 0 但 Key 看起来还在
按以下顺序检查:
- 是否连接到了正确的 Redis 实例、端口和逻辑数据库(
SELECT切换的库会影响命令可见范围)。 - Key 是否已经过期(过期 Key 会被视为不存在)。
- Key 名称是否包含不可见字符、前后空格或大小写差异。
redis
EXISTS key
TTL key
TYPE key
问题 2:执行 UNLINK 报 unknown command
text
(error) ERR unknown command 'UNLINK'
实例版本低于 4.0。升级 Redis,或在确认 Key 规模不大的前提下改用 DEL(并可结合 lazyfree-lazy-user-del 配置,前提是版本支持该配置)。
问题 3:删除后内存没有立即下降
两方面的正常现象:
UNLINK的内存回收是异步的,删除后可观察lazyfree_pending_objects是否正在回落。- 即使
used_memory已下降,进程 RSS 也可能因内存分配器(如 jemalloc)持有内存而不立即归还操作系统。
持续不下降时,再排查是否存在其他大 Key 或内存碎片问题:
redis
MEMORY USAGE key
INFO memory
问题 4:删除大 Key 期间仍有延迟抖动
可能原因:
- 实际执行的是
DEL而非UNLINK(检查代码与lazyfree-lazy-user-del配置)。 - 单批删除的 Key 数量过大,命令本身和通知产生排队。减小每批数量。
- 延迟来自遍历环节(如误用了
KEYS),改用SCAN。 - 值属于极小对象,走同步释放路径属于正常实现,通常不构成可感知的阻塞。
问题 5:集群下报 CROSSSLOT 错误
text
(error) CROSSSLOT Keys in request don't hash to the same slot
一条 UNLINK 中的多个 Key 分布在不同哈希槽。按槽位分组分别执行,或使用 hash tag(如 user:{1001}:a、user:{1001}:b)让相关 Key 落在同一槽。
十一、完整练习
下面的示例覆盖基本删除、不存在的 Key、批量删除、幂等性、集合类型和与 DEL 的对比。请在测试数据库执行;FLUSHDB 会清空当前逻辑数据库中的全部 Key。
redis
FLUSHDB
# 1. 基本用法
SET demo:a "1"
UNLINK demo:a
EXISTS demo:a
# 2. 不存在的 Key
UNLINK demo:not-exist
# 3. 批量删除
SET demo:k1 "v1"
SET demo:k2 "v2"
SET demo:k3 "v3"
UNLINK demo:k1 demo:k2 demo:not-exist
# 4. 重复删除(幂等)
SET demo:once "v"
UNLINK demo:once
UNLINK demo:once
# 5. 集合类型
HSET demo:hash f1 v1 f2 v2 f3 v3
UNLINK demo:hash
EXISTS demo:hash
TYPE demo:hash
# 6. 与 DEL 对比(小对象两者表现一致)
SET demo:del-key "v"
DEL demo:del-key
SET demo:unlink-key "v"
UNLINK demo:unlink-key
# 7. 已过期的 Key
SET demo:temp "v" EX 1
TYPE demo:temp
# 等待 1 秒以上后执行:
# UNLINK demo:temp
# 8. 观察后台回收状态
SET demo:big "value"
UNLINK demo:big
INFO lazyfree
# 9. SCAN 配合批量删除
SET demo:s1 "v1"
SET demo:s2 "v2"
SCAN 0 MATCH demo:s* COUNT 100
预期结果:
UNLINK demo:a返回(integer) 1,随后EXISTS demo:a返回(integer) 0。UNLINK demo:not-exist返回(integer) 0。UNLINK demo:k1 demo:k2 demo:not-exist返回(integer) 2。- 两次
UNLINK demo:once分别返回(integer) 1和(integer) 0。 UNLINK demo:hash返回(integer) 1,EXISTS返回0,TYPE返回none。DEL与UNLINK对小 Key 均返回(integer) 1,效果一致。demo:temp过期后UNLINK返回(integer) 0。INFO lazyfree可查看lazyfree_pending_objects(通常为 0,批量删除大 Key 后短暂大于 0)。SCAN返回游标与demo:s1、demo:s2的 Key 列表,可据此再执行UNLINK。
十二、命令速查表
| 需求 | 命令 |
|---|---|
| 异步删除一个或多个 Key | UNLINK key [key ...] |
| 同步删除一个或多个 Key | DEL key [key ...] |
| 读取并删除 String | GETDEL key |
| 判断 Key 是否存在 | EXISTS key |
| 评估 Key 内存占用 | MEMORY USAGE key |
| 增量遍历 Key | SCAN cursor [MATCH pattern] [COUNT n] |
| 异步清空当前数据库 | FLUSHDB ASYNC |
| 异步清空所有数据库 | FLUSHALL ASYNC |
| 设置过期时间(延迟删除) | EXPIRE key seconds |
| 让 DEL 默认走异步 | CONFIG SET lazyfree-lazy-user-del yes |
| 查看待回收对象数量 | INFO 中 lazyfree_pending_objects |
| 复制 Key 作为备份 | COPY source destination |
总结
UNLINK 的核心作用是按名称非阻塞地删除一个或多个 Key,并返回实际删除数量:
redis
UNLINK key [key ...]
使用时重点注意五点:
- 语义与
DEL一致:不存在的 Key 被忽略,返回计数只统计实际删除的 Key;重复执行安全幂等。 - 区别在执行方式:主线程只做 O(1) 的解除链接,内存回收在后台线程完成,Key 在命令返回时即不可见。
- 删除大 Key、批量清理、缓存主动失效等场景优先使用
UNLINK;小 Key 场景与DEL表现几乎一致。 - 异步释放意味着内存下降可能略滞后,
lazyfree_pending_objects可观察回收状态;RSS 不立即下降属内存分配器正常行为。 - "先读后删"不是原子操作,锁释放等场景需用 Lua 脚本;集群下单条命令的多个 Key 必须同槽。