Redis 4.0 非阻塞删除

一、要解决的问题

一直以来,Redis 都是单线程的(参考 FAQ)。这种模型使得 Redis 简单、高效,但缺点也很明显:如果执行一个比较耗时的命令,那么在该命令执行期间,整个 Redis 服务都将被阻塞(无法并发地执行其他命令)。

大部分 Redis 命令的执行速度都很快,所以不是问题;但也有一些命令,比如 ZUNIONSTORELRANGESINTER,以及臭名昭著的 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

1. 源码实现

参考 Redis 源码 可以发现,DELUNLINK 分别对应不同的处理函数:

命令 处理函数
DEL dbSyncDelete
UNLINK dbAsyncDelete

具体的实现细节请自行研读源码。

2. 耗时对比

下面我们来实际对比一下 DELUNLINK 的耗时差异。

开启 Slow log

设置 Slow log 记录每条命令的耗时(参考 SLOWLOG):

1
2
3
4
5
127.0.0.1:6379> CONFIG SET slowlog-log-slower-than 0
OK
127.0.0.1:6379> CONFIG GET slowlog-log-slower-than
1) "slowlog-log-slower-than"
2) "0"

创建两个大 hash

准备一个 Lua 脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local bulk = 1000
local fvs = {}
local j
for i = 1, ARGV[1] do
j = i % bulk
if j == 0 then
fvs[2 * bulk - 1] = "field" .. i
fvs[2 * bulk] = "value" .. i
redis.call("HMSET", KEYS[1], unpack(fvs))
fvs = {}
else
fvs[2 * j - 1] = "field" .. i
fvs[2 * j] = "value" .. i
end
end
if #fvs > 0 then
redis.call("HMSET", KEYS[1], unpack(fvs))
end
return "OK"

将上述脚本保存为 huge_hmset.lua,然后借助该脚本创建两个大 hash(参考 how to load lua script from file for redis),分别为 hash1hash2,它们各自拥有 100 万个 field:

1
2
3
4
$ redis-cli --eval huge_hmset.lua hash1 , 1000000
"OK"
$ redis-cli --eval huge_hmset.lua hash2 , 1000000
"OK"

上述操作会在 Slow log 中产生大量 HMSET 命令,这里先清除掉:

1
2
127.0.0.1:6379> SLOWLOG RESET
OK

DEL hash1

1
2
3
127.0.0.1:6379> DEL hash1
(integer) 1
(0.63s)
1
2
127.0.0.1:6379> UNLINK hash2
(integer) 1

查看 Slow log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6379> SLOWLOG GET 2
1) 1) (integer) 5089
2) (integer) 1534653951
3) (integer) 17
4) 1) "UNLINK"
2) "hash2"
5) "127.0.0.1:56560"
6) ""
2) 1) (integer) 5088
2) (integer) 1534653948
3) (integer) 630305
4) 1) "DEL"
2) "hash1"
5) "127.0.0.1:56560"
6) ""

耗时对比结果:

命令 耗时
DEL hash1 630305 us
UNLINK hash2 17 us

值得注意的是:UNLINK 执行如此之快,并非使用了什么快速算法,而是因为它将真正的删除操作异步化了。

四、相关阅读