Posted in

Redis如何用最少的内存存储IP地址

在工作中遇到这样一个需求:有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())
    }
}

发表回复