首页 > 代码库 > Redis学习之海量小数据的存储详解
Redis学习之海量小数据的存储详解
最近有个需求,需要存储海量小数据,大概几十亿的规模,每个数据是6位的数字加一个32位的md5(16进制显示)。因为数据很小,数据总量并不算大,我们计划根据md5做分片,存储到多个redis中,每个redis大概存储1亿的数据,纯数据大概 (6+32)*10^9 = 3.8G ,这是redis数据库很擅长存的量。
1 快速加载数据到redis
redis已经非常快了,高达 10w/s ,但面对亿级别的数据,也需要将近20分钟。如果使用pipeline的话,redis还可以更快,达到 40w/s ,5分钟就可以轻松写入1亿数据。
redis自带的 redis-cli 的 --pipe 参数可以实现快速加载数据,但是需要我们把数据转成redis协议。 --pipe-timeout 参数设置为0,防止redis响应太晚redis-cli过早退出。下面例子中的pl脚本就是拼redis协议的。但是pl的性能稍弱,还没到redis的吞吐量瓶颈,自己CPU先100%了,为此,使用20个进程,每个进程500万数据,这样redis的CPU使用率到了100%,数据加载可以在5分钟内完成。
我们用 ps -eo ’pid rss pmem cmd’ | grep redis 和redis的 info 查看redis的内存使用。
2 最直观的存储方式
time head -n 5000000 data | ./redis-pipe-1.pl | redis-cli --pipe --pipe-timeout 0 redis-pipe-1.pl 最核心的是 print join("\r\n", "*3", ’$3’, "SET", ’$’.$keylen, $key, ’$1’, 1), "\r\n"; 其中key是6位数字加32位md5串,共38位。
内存使用情况
5490 9033980 6.8 redis-server *:6379
used_memory_human:8.45Gdb0:keys=100000000,expires=0,avg_ttl=0
从内存使用上看,8G左右,是预估3.8G的2倍多。因为redis的内部数据结构,1个指针就是8位,在加上小value,slab内存分配策略,2倍也没有特别不正常。
3 使用二进制存储
md5 本身是 16 位的unsigned char,为了转成可见字符用了16进制显示,变成了32位。本来想用base64 24位就可以了,后来觉得redis支持二进制,为啥不直接存16位的unsigned char。 在./redis-pipe-2.pl里面把32位的16进制显示改成了16位的数据
my @chars = ();my $hex = "";foreach (split //, $md5) {
$hex .= $_;
if (length($hex) == 2) {
push(@chars, chr(hex($hex)));
$hex = "";
}
}
$key = $appid . join("", @chars);
$keylen = length($key);
内存使用情况
12343 7437316 5.6 redis-server *:6379used_memory_human:6.96Gdb0:keys=100000000,expires=0,avg_ttl=0
从内存使用上看,减少1.5G左右,和预期差不多 16*10^9 = 1.6G
到现在为止,是从数据本身来减少内存使用。而根据分析redis自身的数据结构消耗占了一半左右,怎么减少redis数据结构的消耗呢?
4 使用SET和HSET混合的数据组织方式
先看两个很有意思的配置,是专门为小Hash做准备(使用HSET),当Hash中的条目小于512,并且每个value小于64个字节时,Redis内部采用特殊的编码方式,可以使内存平均节省5倍。
hash-max-ziplist-entries 512hash-max-ziplist-value 64
我们可以把key-value的结构拆解成key-smallhash这样的结构来降低内存的使用
my ($appid, $md5) = split /\s/, $line;my @chars = ();my $hex = "";foreach (split //, $md5) {
$hex .= $_;
if (length($hex) == 2) {
push(@chars, chr(hex($hex)));
$hex = "";
}
}
my $hash = $appid . join("", @chars[0 .. 2]);my $hashlen = length($hash);
my $key = join("", @chars[3 .. @chars-1]);my $keylen = length("$key");
print join("\r\n", "*4", ’$4’, "HSET", ’$’.$hashlen, $hash, ’$’.$keylen, $key, ’$1’, 1), "\r\n";
三个unsigned char大概是 2^24 = 16777216 如果有1亿记录的话,每个hash自身平均6个key-value
内存使用情况
8593 4052120 3.0 redis-server *:6379
used_memory_human:3.31Gdb0:keys=16733972,expires=0,avg_ttl=0
内存使用3.31G,比裸数据 (6+16)*10^9 = 2.2G 只多了50%左右。不仅是省内存,这种方式还有个优势,内存占用 不会随着条目数线性增长 。因为最多16777216个条目,就算数据导了2亿,也只是每个hash到平均12个左右。
5 额外需要关注的问题
1. 本来准备把6位的数字转成4位的整数存储,可以额外节省200M,后来放弃了,因为数字转成int,各个语言的互操作性有隐患。
2. 我们的redis读不是特别多,需要测试hash的压缩存储对性能的影响,但我估计没影响,因为默认是开的。
来源:跬步blog
Redis学习之海量小数据的存储详解