一、要解决的问题
一直以来,Redis 都是单线程的(参考 FAQ)。这种模型使得 Redis 简单、高效,但缺点也很明显:如果执行一个比较耗时的命令,那么在该命令执行期间,整个 Redis 服务都将被阻塞(无法并发地执行其他命令)。
大部分 Redis 命令的执行速度都很快,所以不是问题;但也有一些命令,比如 ZUNIONSTORE、LRANGE、SINTER,以及臭名昭著的 KEYS,根据处理数据集大小的不同,可能会阻塞 Redis 数秒或几分钟。
以 DEL 命令为例,当被删除的 key 是 list、set、sorted set 或 hash 类型时,时间复杂度为 O(M),其中 M 是 key 中包含的元素的个数。
二、非阻塞删除
Redis 的作者 Salvatore Sanfilippo 自己也意识到了上述问题,并提出了对应的解决方案:非阻塞删除(参考 Lazy Redis is better Redis)。简而言之,「非阻塞删除」就是将删除操作放到另外一个线程(而非 Redis 主线程)去处理。
最终「非阻塞删除」在 Redis 4.0 中得以实现(参考 Redis 4.0 release notes),从此 Redis 开启了 “多线程” 时代。
新增实现的「非阻塞删除」包括以下命令:
命令 | (原来的)阻塞版本 |
---|---|
UNLINK | DEL |
FLUSHALL ASYNC | FLUSHALL |
FLUSHDB ASYNC | FLUSHDB |
三、DEL vs UNLINK
1. 源码实现
参考 Redis 源码 可以发现,DEL 和 UNLINK 分别对应不同的处理函数:
命令 | 处理函数 |
---|---|
DEL | dbSyncDelete |
UNLINK | dbAsyncDelete |
具体的实现细节请自行研读源码。
2. 耗时对比
下面我们来实际对比一下 DEL 和 UNLINK 的耗时差异。
开启 Slow log
设置 Slow log 记录每条命令的耗时(参考 SLOWLOG):
1 | 127.0.0.1:6379> CONFIG SET slowlog-log-slower-than 0 |
创建两个大 hash
准备一个 Lua 脚本:
1 | local bulk = 1000 |
将上述脚本保存为 huge_hmset.lua
,然后借助该脚本创建两个大 hash(参考 how to load lua script from file for redis),分别为 hash1
和 hash2
,它们各自拥有 100 万个 field:
1 | $ redis-cli --eval huge_hmset.lua hash1 , 1000000 |
上述操作会在 Slow log 中产生大量 HMSET 命令,这里先清除掉:
1 | 127.0.0.1:6379> SLOWLOG RESET |
DEL hash1
1 | 127.0.0.1:6379> DEL hash1 |
UNLINK hash2
1 | 127.0.0.1:6379> UNLINK hash2 |
查看 Slow log
1 | 127.0.0.1:6379> SLOWLOG GET 2 |
耗时对比结果:
命令 | 耗时 |
---|---|
DEL hash1 | 630305 us |
UNLINK hash2 | 17 us |
值得注意的是:UNLINK 执行如此之快,并非使用了什么快速算法,而是因为它将真正的删除操作异步化了。