命令行下批量清理redis中的错误数据

问题

程序开发过程中,因为错误代码导致在redis中写入了错误的数据,可能使得读取的时候报错。如果没有设置ttl自动过期,将长期留存在redis中。

例如,想在set的同时指定ttl,但是少给了一个TimeUnit参数:

1
redisTpl.opsForValue().set(key, siteId, expire);

使得调用方法:

1
2
3
4
// 从
void set(K key, V value, long timeout, TimeUnit unit);
// 变成
void set(K key, V value, long offset);

从而产生了垃圾数据,还不能自动过期。

KEY前缀

项目中使用redis的时候,需要严格规划KEY的前缀,这样:

  • 一来可以避免KEY冲突。
  • 二来可以按业务范围查询维护相关的KEY。

上边代码中,我们错误设置的KEY的prefix=org_site_。

清理

有了统一的KEY浅醉,我们可以用keys命令查询出所有相关的key。

1
2
3
4
5
6
7
8
redis.crazy1984.com:6379[2]> keys org_site_*
1) "org_site_12_0"
2) "org_site_12_174"
3) "org_site_12_1"
4) "org_site_344_174"
5) "org_site_344_3182"
6) "org_site_345_0"
7) "org_site_344_3178"

暴力清理

对于查出的key,如果可以全部清理,那么可以用以前介绍过的awk命令,自动生成del命令(将下边的get替换成del即可),然后执行。

1
2
3
4
5
6
7
8
9
10
$ echo "keys org_site_*" |redis-cli -h redis.crazy1984.com -n 2 --raw | awk '{print "get "$1}' | redis-cli -h redis.crazy1984.com -n 2 --raw 
12
12
1
12
12
344
1003181
345
344

条件清理

如果线上环境不允许我们暴力清理,只能定向清理错误的数据。那么如何处理呢?这时可以用上redis支持执行lua脚本的功能了。

以我们的情况为例,我们的value本想存入一个int,但是因为错误的设置,使得value的数据变长了。

比如:原来的value=123,因为错误的set把ttl作为了offset,使得实际的数据可能变成

1
2
3
4
redis.crazy1984.com:6379[2]> setrange org_site_123 100 123
(integer) 103
redis.crazy1984.com:6379[2]> get org_site_123
"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00123"

正常情况下:value应该是个int,长度不会超过32 bytes。

错误情况下:因为ttl(本处使用的单位是秒)至少都是60以上,所以可以通过判断数据的长度来判断是否是错误的数据。lua脚本的参数以KEYS全局变量方式传入。

The arguments can be accessed by Lua using the KEYS global variable in the form of a one-based array (so KEYS[1], KEYS[2], …).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local del_keys = {};
local used_keys = {};
-- lua脚本的参数以KEYS传入,是个map
for i,k in ipairs(KEYS) do
-- 按字符串处理获取value的长度
local ktest = redis.call("STRLEN", k)
local kval = redis.call("GET", k)
-- 正常情况下value是个int,长度不会超过10(max=2,147,483,647)
if ktest > 10 then
-- 删除错误的数据
redis.call("DEL", k)
local stest = string.format("DEL %s %d", k, ktest)
del_keys[#del_keys+1] = {stest, kval};
else
local stest = string.format("GET %s %d", k, ktest)
used_keys[#used_keys+1] = {stest, kval};
end
end
return del_keys;

把这段脚本保存为文件 redis_clean.lua

然后再结合redis的keys命令,把所有以org_site_开头的key作为参数传给lua处理。

1
2
3
$ echo "keys org_site_*" |redis-cli -h redis.crazy1984.com -n 2 --raw | xargs redis-cli -h redis.crazy1984.comm -n 2 --eval redis_clean.lua
1) 1) "DEL org_site_123 103"
2) "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00123"

问题解决。