一、为什么要存 IP 地址?
存储 IP 地址是系统开发中的常见需求,主要用途包括:
- 安全审计与防护:记录用户访问 IP,识别恶意攻击、异常登录,建立 IP 黑/白名单实现访问控制。
- 用户分析与画像:结合 IP 地理信息库定位用户大致地理位置,用于地域化内容推荐、精准广告投放、市场分析及反欺诈。
- 网络与设备管理:存储网络设备(路由器、服务器等)的 IP 地址,是设备统一管理、监控和运维的基础。
- 日志分析与追踪:在系统日志中存储 IP 地址,是排查线上问题、追踪用户请求链路的必要信息。
- 合规与风控:许多行业法规要求记录用户操作行为及 IP 地址,以满足合规审计和风险控制要求。
二、两种存储方式对比
IPv4 地址本质是一个 32 位二进制数 ,通常以点分十进制呈现,如 192.168.1.1。
在数据库中存储 IP 地址,主要有两种方式:
2.1 字符串存储(VARCHAR)
直接将 IP 地址作为字符串存储,IPv4 常用 VARCHAR(15)。
sql
CREATE TABLE ip_records (
id INT AUTO_INCREMENT PRIMARY KEY,
ip_address VARCHAR(15)
);
INSERT INTO ip_records (ip_address) VALUES ('192.168.1.1');
| 维度 | 说明 |
|---|---|
| ✅ 优点 | 直观易懂,直接插入、查询和显示,无需额外转换 |
| ❌ 缺点 | 占用存储空间较大;字符串比较性能较低;不利于范围查询 |
2.2 整数存储(INT UNSIGNED)
将 IPv4 地址转换为 32 位无符号整数,使用 INT UNSIGNED 存储。
sql
CREATE TABLE ip_records (
id INT AUTO_INCREMENT PRIMARY KEY,
ip_address INT UNSIGNED
);
-- 插入时转换
INSERT INTO ip_records (ip_address) VALUES (INET_ATON('192.168.1.1'));
-- 查询时还原
SELECT INET_NTOA(ip_address) AS ip FROM ip_records;
| 维度 | 说明 |
|---|---|
| ✅ 优点 | 仅占 4 字节 ,空间小;整数比较性能高;天然支持范围查询 (BETWEEN) |
| ❌ 缺点 | 需要额外转换,不够直观,增加开发复杂度 |
2.3 综合对比
| 对比维度 | VARCHAR(15) | INT UNSIGNED |
|---|---|---|
| 存储空间 | ~15 字节 | 4 字节 |
| 索引效率 | 较低 | 较高 |
| 范围查询 | 不友好 | 天然支持 BETWEEN |
| 可读性 | 直接可读 | 需 INET_NTOA() 转换 |
| 开发复杂度 | 低 | 中 |
推荐:对性能有要求、需要范围查询(如 IP 段匹配)的场景,优先使用整数存储。
三、MySQL 内置函数
3.1 IPv4
| 函数 | 方向 | 示例 |
|---|---|---|
INET_ATON() |
字符串 → 整数 | INET_ATON('192.168.1.1') → 3232235777 |
INET_NTOA() |
整数 → 字符串 | INET_NTOA(3232235777) → '192.168.1.1' |
3.2 IPv6
IPv6 地址为 128 位 ,无法用 INT UNSIGNED 存储,需使用 VARBINARY(16)。
| 函数 | 方向 | 示例 |
|---|---|---|
INET6_ATON() |
字符串 → 二进制 | INET6_ATON('2001:db8::1') → VARBINARY(16) |
INET6_NTOA() |
二进制 → 字符串 | INET6_NTOA(...) → '2001:db8::1' |
sql
-- IPv6 存储示例
CREATE TABLE ip_records_v6 (
id INT AUTO_INCREMENT PRIMARY KEY,
ip_address VARBINARY(16)
);
INSERT INTO ip_records_v6 (ip_address) VALUES (INET6_ATON('2001:db8::1'));
SELECT INET6_NTOA(ip_address) AS ip FROM ip_records_v6;
3.3 转换原理
IPv4
IPv4 是 32 位二进制数,分为 4 个字节(Octet),每个字节对应点分十进制中的一段。
计算方式:每段 × 256 的幂次,然后求和(等价于将 4 个字节拼成一个 32 位无符号整数)。
192.168.1.1
192 × 256³ = 192 × 16,777,216 = 3,221,225,472
168 × 256² = 168 × 65,536 = 11,010,048
1 × 256¹ = 1 × 256 = 256
1 × 256⁰ = 1 × 1 = 1
───────────────────────────────────────────────
总和 = 3,232,235,777
从二进制视角看更直观:
192 → 11000000
168 → 10101000
1 → 00000001
1 → 00000001
拼接为 32 位:11000000 10101000 00000001 00000001
转为十进制:3,232,235,777
公式 :
INET_ATON(A.B.C.D)=A × 2²⁴ + B × 2¹⁶ + C × 2⁸ + D
反向转换 INET_NTOA() 就是将整数按每 8 位拆开,转回点分十进制。
IPv6
IPv6 是 128 位,分为 8 组,每组 16 位(2 字节),用冒号分隔的十六进制表示。
:: 是 零压缩(zero compression),表示中间全是 0。先展开:
2001:db8::1
↓ 展开 ::
2001:0db8:0000:0000:0000:0000:0000:0001
每组是 16 位(2 字节),8 组 × 2 字节 = 16 字节 ,所以用 VARBINARY(16) 存储:
组1: 2001 → 0x20 0x01
组2: 0db8 → 0x0d 0xb8
组3: 0000 → 0x00 0x00
组4: 0000 → 0x00 0x00
组5: 0000 → 0x00 0x00
组6: 0000 → 0x00 0x00
组7: 0000 → 0x00 0x00
组8: 0001 → 0x00 0x01
───────────────────────
最终 16 字节(十六进制):
0x20 0x01 0x0D 0xB8 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01
要点 :IPv6 没有"转成一个巨大整数"的做法,因为 128 位超出了 MySQL 整型的最大范围(
BIGINT UNSIGNED也只有 64 位)。所以直接用原始二进制VARBINARY(16)存储,不做数值运算,只做字节序列比较。
四、实战:IP 范围查询
整数存储的一大优势是范围查询非常高效:
sql
-- 查询 192.168.1.0 ~ 192.168.1.255 网段内的所有 IP
SELECT INET_NTOA(ip_address) AS ip
FROM ip_records
WHERE ip_address BETWEEN INET_ATON('192.168.1.0')
AND INET_ATON('192.168.1.255');
如果使用 VARCHAR 存储,这种范围查询几乎无法高效实现。
五、小结
- 能存整数就别存字符串 :
INT UNSIGNED(4 字节)远优于VARCHAR(15)(15 字节),且查询性能更好。 - IPv4 用
INET_ATON/INET_NTOA做转换,简单可靠。 - IPv6 用
VARBINARY(16)+INET6_ATON/INET6_NTOA,注意 IPv6 是 128 位,不能用整数类型。 - 范围查询场景(IP 段匹配、IP 库查询等)务必使用整数/二进制存储。