一、直接删除大Key的风险
DEL命令在删除单个集合类型的Key时,命令的时间复杂度是O(M),其中M是集合类型Key包含的元素个数。
DEL keyTime complexity: O(N) where N is the number of keys that will be removed. When a key to remove holds a value other than a string, the individual complexity for this key is O(M) where M is the number of elements in the list, set, sorted set or hash. Removing a single key that holds a string value is O(1).
生产环境中遇到过多次因业务删除大Key,导致Redis阻塞,出现故障切换和应用程序雪崩的故障。测试删除集合类型大Key耗时,一般每秒可清理100w~数百w个元素; 如果数千w个元素的大Key时,会导致Redis阻塞上10秒可能导致集群判断Redis已经故障,出现故障切换;或应用程序出现雪崩的情况。
说明:Redis是单线程处理。单个耗时过大命令,导致阻塞其他命令,容易引起应用程序雪崩或Redis集群发生故障切换。所以避免在生产环境中使用耗时过大命令。
Redis删除大的集合键的耗时, 测试估算,可参考;和硬件环境、Redis版本和负载等因素有关
Key类型 | Item数量 | 耗时 |
Hash | ~100万 | ~1000ms |
List | ~100万 | ~1000ms |
Set | ~100万 | ~1000ms |
Sorted Set | ~100万 | ~1000ms |
当我们发现集群中有大key时,要删除时,如何优雅地删除大Key?
二、如何优雅地删除各类大Key
从Redis2.8版本开始支持SCAN命令,通过m次时间复杂度为O(1)的方式,遍历包含n个元素的大key.这样避免单个O(n)的大命令,导致Redis阻塞。 这里删除大key操作的思想也是如此。
2.1 Delete Large Hash Key
通过hscan命令,每次获取500个字段,再用hdel命令,每次删除1个字段。Python代码:
def del_large_hash():
r = redis.StrictRedis(host='redis-host1', port=6379)
large_hash_key ="xxx"
cursor = '0'
while cursor != 0:
cursor, data = r.hscan(large_hash_key, cursor=cursor, count=500)
for item in data.items():
r.hdel(large_hash_key, item[0])
2.2 Delete Large Set Key
删除大set键,使用sscan命令,每次扫描集合中500个元素,再用srem命令每次删除一个键Python代码:
def del_large_set():
r = redis.StrictRedis(host='redis-host1', port=6379)
large_set_key = 'xxx'
cursor = '0'
while cursor != 0:
cursor, data = r.sscan(large_set_key, cursor=cursor, count=500)
for item in data:
r.srem(large_size_key, item)
2.3 Delete Large List Key
删除大的List键,未使用scan命令; 通过ltrim命令每次删除少量元素。Python代码:
def del_large_list():
r = redis.StrictRedis(host='redis-host1', port=6379)
large_list_key = 'xxx'
while r.llen(large_list_key)>0:
r.ltrim(large_list_key, 0, -101)
2.4 Delete Large Sorted set key
删除大的有序集合键,和List类似,使用sortedset自带的zremrangebyrank命令,每次删除top 100个元素。Python代码:
def del_large_sortedset():
r = redis.StrictRedis(host='large_sortedset_key', port=6379)
large_sortedset_key='xxx'
while r.zcard(large_sortedset_key)>0:
r.zremrangebyrank(large_sortedset_key,0,99)
三、Redis Lazy Free
应该从3.4版本开始,Redis会支持lazy delete free的方式,删除大键的过程不会阻塞正常请求。
【最佳实践】
核心思路是拆分,通过上面相关方法,分批获取,分批删除;
(1)关于任意类型key删除
#!/bin/bash
source /etc/profile
next_position=0
del_key_file='6381_del_key.txt'
redis_port=6381
while read line
do
while [ 1 ]
do
key_info=`redis-cli -a bfengzlgdredis2017 -p $redis_port scan ${next_position} match $line count 10000`
next_position=`echo ${key_info}|awk '{print $1}'`
echo ${key_info}|awk '{$1=null;print }'|sed 's/ /\n/g'|sed '/^$/d'|sed 's#^#del #g' >> all_${redis_port}.txt
if [ ${next_position} -eq 0 ];then
break
fi
done
done<$del_key_file
6381_del_key.txt 这个文件里存放的是需要被删除的key 信息,或通配符信息
(2)单个key前缀的所有key查找
获取 userplaylist: 开头的所有key
#!/bin/bash
source /etc/profile
next_position=0
#del_key_file='6379_del_key.txt'
redis_port=6379
while [ 1 ]
do
key_info=`redis-cli -a bfengzlgdredis2017 -p $redis_port scan ${next_position} match userplaylist:* count 10000`
next_position=`echo ${key_info}|awk '{print $1}'`
echo ${key_info}|awk '{$1=null;print }'|sed 's/ /\n/g'|sed '/^$/d'|sed 's#^#del #g' >> all_${redis_port}.txt
if [ ${next_position} -eq 0 ];then
break
fi
done
(3)关于应用删除key命令
#!/bin/bash
source /etc/profile
cd `dirname $0`
f_del_key(){
redis_port=$1
key_name=$2
all_number=`ls -l ${key_name}*|wc -l`
for i in `ls |grep ${key_name}`
do
redis-cli -a bfengzlgdredis2017 -p ${redis_port} --pipe <$i
date "+%F-%T"
echo "${all_number}-${i}"
sleep 0.3
done
}
main(){
f_del_key 6383 6383_del_key_
}
main
【清理 set 大key实践 】
直接使用 spop,再结合一个 shell 爽飞
(4)spop(随机删除):spop setkey del_count
【使用 unlink 代替 del】
Redis UNLINK 命令跟 DEL 命令十分相似:用于删除指定的 key
。
就像 DEL 一样,如果 key
不存在,则将其忽略。
但是,该命令会执行命令之外的线程中执行实际的内存回收,因此它不是阻塞,而 DEL 是阻塞的。这就是命令名称的来源:UNLINK 命令只是将键与键空间断开连接。实际的删除将稍后异步进行。
参考:redis-cli -a 12345678qwert --scan --pattern "refresh:*" | xargs -L 10 redis-cli -a 12345678qwert -n 0 unlink