刚看到你的问题,你问的很久以前,我有一个使用Redis的解决方案。
让我们首先尝试用一些基本数字(而不是 IP)对问题进行建模,看看如何解决它:
范围到国家/地区查找
Lookup | Range | Country
--------|------------+------------------
| 5 | begin:Country A
L1 >>> |
| 10 | end:Country A
| |
L2 >>> |
| |
L2.1>>> 15 | begin:Country B
| |
| 20 | end:Country B
L3 >>> |
| |
查找L1:
在[6,10](此处包含范围)之间进行号码查找。在这种情况下,结果将是end:Country A => IP 地址属于 A 国。为什么我们以6 开头将在L2 中显而易见。
查找L2:
查找范围 [11, 15] 中的数字(此处包含范围)。结果将是begin:Country B =>
查找L3:
结果将是Empty List or Set => 错误:IP 不属于任何已知范围
插入比较麻烦!
必须小心插入范围,因为新插入的范围可能会破坏现有范围。以下是插入的情况:
Insert | Range | Country
--------|------------+------------------
| 5 | begin:Country A
| |
I1 >>> 8,9 | !!! Country C !!!
| |
| 10 | end:Country A
| |
| |
I2 >>> 12,14 | Country E
| |
| |
| 15 | begin:Country B
| |
I3 >>> 17,21 | !!! Country D !!!
| |
| 20 | end:Country B
| |
I4 >>> 22,27 | Country F
| |
插入I1:
使 IP 地址为 6 和 7(在 5 和 8 之间)的地址无效。 => 实际上Country A 范围缩小到单个IP 地址10。
插入I2:
好的,没有范围相交
插入I3:
国家 B 的开始渲染无效 + 国家 D 的开始渲染(17..20)无效
插入I4:
好的
注意:在某些情况下,您可能需要引入范围分割逻辑。
基于 Redis 的解决方案
我建议为此目的使用 Redis ZSET。以下是观察结果:
除了点分十进制字符串表示之外,每个 IPv4 地址都可以表示为 32 位整数。
Redis ZSET 通过对存储成员进行额外排序来保证存储成员的唯一性
我们可以使用分数范围搜索ZSET成员,即ZRANGEBYSCORE命令。
如果我们使用数字 IP 作为 ZSET 分数,我们就完成了。通过为特定范围添加 begin: 和 end: 前缀来强制执行国家/地区的唯一性。在现实生活中,一个国家可能有多个 IP 地址范围,因此您最终可能会将范围编号编码为国家名称,例如 begin:r1:Country A 和 end:r1:Country A。您可以对此进行规范化并引入间接性。但是为了保持低查找次数,您需要对其进行非规范化并在单个数据库访问中拥有尽可能多的信息。这是因为引入新范围的频率远低于进行查找的频率,因此增加查找次数会降低性能。
Lookup | Score | Country
--------|------------+------------------
| 5 | begin:Country A
L1 >>> |
| 10 | end:Country A
| |
L2 >>> |
| |
L2.1>>> 15 | begin:Country B
| |
| 20 | end:Country B
L3 >>> |
| |
使用哪些 Redis 命令
这里只是简单的命令,没有你的逻辑来检查插入等期间的错误情况。
-
添加新范围
> ZADD ip-to-country 3232235777 "begin:Country A" 3232235876 "end:Country A"
注意: 3232235777 是 IPv4 192.168.1.1 表示为 unsigned int,同样适用于 192.168.1.100。
-
检查特定 IP 属于哪个范围
> ZRANGEBYSCORE ip-to-country 3232235778 +inf WITHSCORES LIMIT 0 1
注意:3232235778 是 IPv4 192.168.1.2,表示为 unsigned int,我们从192.168.1.8 开始(即+inf)查找一个元素(即LIMIT 0 1)。
-
检查 Lookup 2.1 查找的 IP 开始新范围
> ZSCORE ip-to-country "begin:Country A"
注意:结果将是3232235777
复杂性分析
空间复杂性:如果在最坏的情况下我们最终得到每个 IP 代表范围的开始和结束,我们将需要 O(2*N) 空间,其中 N 是 2^32。但在现实生活中,这个数字会小得多。在一些算法书籍中,您会看到2^32 被认为是一个常数因子,因此将简化为O(1)。
运行时复杂度: Redis 声明ZRANGEBYSCORE 是O(log(N)+M) 操作,其中M 是LIMIT 中的元素个数,即这里只有1。如果我们最多有2*2^32 在最坏情况下的得分比 log2(8billion) 大约是 Redis 集合实现中的 33 比较。但实际上我认为不会超过 2 或 3000 个范围,大约是 11 比较。 Redis 状态为KEYS command:
在入门级笔记本电脑上运行的 Redis 可以在 40 毫秒内扫描 100 万个密钥数据库。
总而言之,您的查找速度将非常快!