背景:公司信息流业务需要对接字节RTA,RTA作为广告主先'选'后'买'的重要环节,对提高广告主投放质量,降低成本有较大作用,但是RTA具有极高流量(50WQps)超低延迟(60ms)海量数据(十亿级别)特质,这对于我们在接入时存在很大的挑战。
挑战
- 服务如何实现高并发(50wQPS)
- 服务接口如何实现超低延迟(60ms以下)
- 海量设备数据如何低成本、高效存储
本次主要讲的是第三点(海量设备数据如何低成本、高效存储)
Redis支持五种数据类型:string(字符串)、hash(哈希)、list(列表)、set(集合)及zset(有序集合)
我们的数据结构主要是存储设备和对应的质量分,用 JSON 格式表示 如下:
字符串长度为 214
{
"deviceId": "7371f6d7040883b214561be6811b9a15",
"score": {
"1125": 0.0,
"1126": 0.0,
"1127": 0.0,
"1128": 0.0,
"1129": 0.0
}
}
对于上述数据结构,reids可以 选择 string或者 hash 两种数据类型来进行存储,下面我们看下这两种数据类型在20亿数据下的内存占用情况
注意:reids底层会对数字类型进行编码优化、字符串直接存储对应字节
直接存储数据原始值,不做任何处理
1、string(字符串)
key: 设备ID的MD5值 value:设备对应的所有模型分的JSON字符串
上面是按照字符串结构存储时redis的数据内容,因此可以粗略计算出20亿设备占用多少redis内存
214(B) * 2000000000 / 1024(k) / 1024(M) / 1024(G) = 400G
2、hash (哈希)
key: 设备ID的MD5值 filed: rtaScore(固定字符串) value:设备对应的所有模型分的JSON字符串
类似于字符串的计算方式,使用hash结构我们还要多一些额外的属性,因此内存占用要大于字符串结构的
不直接存储原始数据,对原始数据进行编码和转化
下面我们采取hash(哈希)结构来对此进行分析
1、key 处理
将 32位的MD5字符串经过 CRC32 算法转成一个整数作为key,然后在对 key % 500w 取模 作为hash的key,这样就将key分散到500w个桶中
2、filed 处理
将 32位的MD5字符串经过 BKDR_Hash 算法转成一个非负整数 作为filed 进行存储
3、value 处理
将 质量分对象进行 ProtoBuffer 编码,生成短小、紧凑的字节。
上面这些紧紧是对代码层面的改造,还有一点就是还要对redis的参数进行设置,如下:
hash-max-ziplist-entries = 1024
hash-max-ziplist-value = 128
以上两个配置是强制 hash(哈希)底层使用ziplist结构
按照上面逻辑,将20亿数据存储到redis中,实测下来占用内存50G, 通过计算可以发现节省了80%的内存。
补充说明:
redis为了节约内存空间使用,zset和hash容器在元素个数比较小的时候,采用压缩列表(ziplist)进行存储,上面对redis的参数进行配置就是为了一点。
压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构,有点类似 java 中 ArrayList 的底层数组实现,能够很好地利用空间局部性提升内存访问效率。 另外它通过对数据的动态编码,能够很大程度节省内存使用,这对 Redis 来说非常重要。
ziplist的结构: