redis~有序集合處理ip範圍的查詢問題

张占岭發表於2024-11-25

目前有兩種方式對 IP 以及歸屬地資訊進行快取:

  • 第一種是將起始 IP,結束 IP 以及中間所有 IP 轉換成整型,然後以字串方式,用轉換後的 IP 作為 key,歸屬地資訊作為 value 存入 Redis;
  • 第二種是採用有序集合和雜湊方式,首先將起始 IP 和結束 IP 新增到有序集合 ip2cityid,城市 ID 作為成員,轉換後的 IP 作為分值,然後再將城市 ID 和歸屬地資訊新增到雜湊 cityid2city,城市 ID 作為 key,歸屬地資訊作為 value。

第一種方式就不多做介紹了,簡單粗暴,非常不推薦。查詢速度當然很快,毫秒級別,但缺點也十分明顯,我用 1000 條資料做了測試,快取時間長,大概 20 分鐘,佔用空間大,將近 1G。

下面介紹第二種方式直接看程式碼

python語言實現

# generate_to_redis.py
# -*- coding:utf-8 -*-
import time
import json
from redis import Redis
def ip_to_num(x):
    return sum([256 ** j * int(i) for j, i in enumerate(x.split('.')[::-1])])
# 連線 Redis
conn = Redis(host='127.0.0.1', port=6379, db=10)
start_time = time.time()
# 檔案格式
# 1.0.0.0|1.0.0.255|澳大利亞|0|0|0|0
# 1.0.1.0|1.0.3.255|中國|0|福建省|福州市|電信
with open('./ip.merge.txt', 'r') as f:
    i = 1
    for line in f.readlines():
        item = line.strip().split('|')
        # 將起始 IP 和結束 IP 新增到有序集合 ip2cityid
        # 成員分別是城市 ID 和 ID + #, 分值是根據 IP 計算的整數值
        conn.zadd('ip2cityid', str(i), ip_to_num(item[0]), str(i) + '#', ip_to_num(item[1]) + 1)
        # 將城市資訊新增到雜湊 cityid2city,key 是城市 ID,值是城市資訊的 json 序列
        conn.hset('cityid2city', str(i), json.dumps([item[2], item[3], item[4], item[5]]))
        i += 1
end_time = time.time()
print 'start_time: ' + str(start_time) + ', end_time: ' + str(end_time) + ', cost time: ' + str(end_time - start_time)

java語言實現

以下是透過 Java 來實現將 IP 起始值和結束值透過有序集合儲存,並根據傳入的 IP 返回這個 IP 是否在 IP 範圍集合中存在的示例程式碼:

  • redis中儲存的圖
  • java程式碼如下
package com.lind.redis;

import com.lind.redis.config.LettuceRedisAutoConfigure;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * 關於ip地址範圍檢索的測試
 *
 * @author lind
 * @date 2024/3/6 11:14
 * @since 1.0.0
 */
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = { LettuceConnectionFactory.class, LettuceRedisAutoConfigure.class })
public class IpRangeTest {

    private static final String IP_RANGE_KEY = "ip_ranges";

    @Autowired
    RedisTemplate redisTemplate;

    // 將 IP 地址轉換為 long 型別的分數值
    public static double convertIPToScore(String ip) {
       String[] ipParts = ip.split("\\.");
       if (ipParts.length != 4) {
          throw new IllegalArgumentException("Invalid IP address format");
       }

       long score = 0;
       for (int i = 0; i < 4; i++) {
          long partValue = Long.parseLong(ipParts[i]);
          score = (score << 8) + partValue; // 將每個部分的值左移8位並相加
       }

       return score;
    }

    @Test
    public void testRangeIp() {
       // 儲存 IP 範圍到 Redis
       storeIPRangeToRedis("103.159.125.66", "103.159.125.177");
       storeIPRangeToRedis("114.250.19.130", "114.250.19.255");

       // 檢查使用者真實 IP 是否在 IP 集合中
       String userIP = "103.159.125.178";
       boolean inRange = checkIPInRange(userIP);
       if (inRange) {
          System.out.println(userIP + " is in the IP range set.");
       }
       else {
          System.out.println(userIP + " is not in the IP range set.");
       }
    }

    public void storeIPRangeToRedis(String startIP, String endIP) {
       redisTemplate.opsForZSet().add(IP_RANGE_KEY, startIP, convertIPToScore(startIP));
       redisTemplate.opsForZSet().add(IP_RANGE_KEY, endIP, convertIPToScore(endIP));
    }

    public boolean checkIPInRange(String userIP) {
       Long count = redisTemplate.opsForZSet().count(IP_RANGE_KEY, convertIPToScore(userIP), convertIPToScore(userIP));
       return count > 0;
    }

}

相關文章