在工作中遇到这样一个需求:有5亿个IP地址,需要存储IP的md5值到ip原值的映射,在满足需求的情况下,要求占用的内存尽可能的小。
数据占用
首先是key,如果存md5字符串,一共32个字符,那么需要占用32B,但是如果存二进制字符的话就只需要占用16B了,相比之下少了一半;
其次是value,如果存原始字符串的话,对于IPv4,需要占用7-15B,而IPv6需要占用39B,这个内存一下就上去了。我们可以继续按照上面的套路,将其都转为二进制字符串,这样的话IPv4可以转为uint32,int类型在Redis中只需要占用8B,而IPv6转为二进制字符串的话只需要占用16B,这样一操作,空间占用就直接少了一大半。
弄个表格对比一下更直观:
Key 存法 | Value 存法 | 单条数据大小(不算元信息) |
---|---|---|
MD5 hex (32B) | IPv4 string (~7-15B) | 40~47B |
MD5 hex (32B) | IPv4 uint32 (8B) | 40B |
MD5 raw (16B) | IPv4 uint32 (8B) | 24B ✅ |
MD5 raw (16B) | IPv6 binary (16B) | 32B ✅ |
元信息占用
- 如果采用HEX MD5 + 字符串 IP(常见低效写法)
dictEntry: ~24B
redisObject: ~16B
SDS header (key): ~8B
SDS header (value): ~8B
单纯元信息开销:
IPv4: 24 + 16 + 8 + 8 = 56B
IPv6: 24 + 16 + 8 + 8 = 56B(和 IPv4 差不多)
- 如果采用二进制字符串存储的方式
dictEntry: ~24B
redisObject: ~16B
SDS header (key): ~8B(短字符串 ≤ 2^8 可用 SDS5/SDS8,通常 8B 起步)
SDS header (value):
IPv4 存为整数 → 0B(无 SDS,直接 int 编码)
IPv6 二进制 16B → SDS header ~8B
单纯元信息开销:
IPv4: 24 + 16 + 8 = 48B
IPv6: 24 + 16 + 8 + 8 = 56B
合计
从上面的对比中可以确定,选择二进制存储的方式肯定是占用内存更小的方式,那么5亿条数据需要占用:
IPv4 (2.5 亿条):
72B × 2.5e8 = 18,000,000,000 B ≈ 16.8 GiB
IPv6 (2.5 亿条):
88B × 2.5e8 = 22,000,000,000 B ≈ 20.5 GiB
合计约40G
代码实现
这里用Golang写一下代码示例
package main
import (
"context"
"crypto/md5"
"encoding/binary"
"fmt"
"net"
"github.com/redis/go-redis/v9"
)
var ctx = context.Background()
func ipToBinary(ipStr string) ([]byte, error) {
ip := net.ParseIP(ipStr)
if ip == nil {
return nil, fmt.Errorf("invalid ip: %s", ipStr)
}
if ipv4 := ip.To4(); ipv4 != nil {
// IPv4: 4字节
buf := make([]byte, 4)
binary.BigEndian.PutUint32(buf, binary.BigEndian.Uint32(ipv4))
return buf, nil
}
// IPv6: 16字节
return ip.To16(), nil
}
func main() {
// 连接 Redis
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
DB: 0,
})
// 示例 IP
ipStr := "192.168.1.1"
// ipStr := "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
// 1. 计算 MD5
md5Key := md5.Sum([]byte(ipStr)) // [16]byte
// 2. 转换为二进制 IP
ipBin, err := ipToBinary(ipStr)
if err != nil {
panic(err)
}
// 3. 存入 Redis (key=16B MD5原始二进制, value=IP二进制)
err = rdb.Set(ctx, string(md5Key[:]), ipBin, 0).Err()
if err != nil {
panic(err)
}
fmt.Println("存储完成:", ipStr, "=> MD5(key)", md5Key)
// 4. 从 Redis 读取并还原 IP
val, err := rdb.Get(ctx, string(md5Key[:])).Bytes()
if err != nil {
panic(err)
}
if len(val) == 4 { // IPv4
ip := net.IPv4(val[0], val[1], val[2], val[3])
fmt.Println("读取到的 IP:", ip.String())
} else if len(val) == 16 { // IPv6
ip := net.IP(val)
fmt.Println("读取到的 IP:", ip.String())
}
}