`

redis范围查询应用-查找IP所在城市

阅读更多


需求
根据IP找到对应的城市

原来的解决方案
oracle表(ip_country):




查询IP对应的城市:

1.把a.b.c.d这样格式的IP转为一个数字,例如为把210.21.224.34转为3524648994
2. select city from ip_country where ipstartdigital <= 3524648994 and 3524648994 <=ipenddigital

redis解决方案

我们先把上面的表简化一下:

id	city	min	max
1	P1	0	100
2	P2	101	200
3	P3	201	300
4	P4	301	400


(注意:min/max组成的range之间不能有重叠)

主要思路就是用hmset存储表的每一行,并为每一行建立一个id(作为key)
然后把ip_end按顺序从小到大存储在sorted set当中,score对应该行的id
查询时,利用redis sorted set的范围查询特性,从sorted set中查询到id,再根据id去hmget

实验
//存储表的每一行
127.0.0.1:6379> hmset {ip}:1 city P1 min 0 max 100
OK
127.0.0.1:6379> hmset {ip}:2 city P2 min 101 max 200
OK
127.0.0.1:6379> hmset {ip}:3 city P3 min 201 max 300
OK
127.0.0.1:6379> hmset {ip}:4 city P4 min 301 max 400
OK

//建立sorted set(score-member,例如score=100,member=1等等)
127.0.0.1:6379> zadd {ip}:end.asc 100 1 200 2 300 3 400 4
(integer) 4
127.0.0.1:6379> zrange {ip}:end.asc 0 -1
1) "1"
2) "2"
3) "3"
4) "4"

//查询对应的区间(score)
127.0.0.1:6379> zrangebyscore {ip}:end.asc 90 +inf LIMIT 0 1
1) "1"
127.0.0.1:6379> zrangebyscore {ip}:end.asc 123 +inf LIMIT 0 1
1) "2"
127.0.0.1:6379> zrangebyscore {ip}:end.asc 100 +inf LIMIT 0 1
1) "1"
//解释:
//zrangebyscore {ip}:end.asc 90 +inf LIMIT 0 1
//表示查找score大于等于90的第一个值。(+inf在Redis中表示正无穷大)
//该语句返回值member=1,与hmset当中的id对应,因此可以通过hmget查找城市了:

//查找城市
127.0.0.1:6379> hmget {ip}:1 city
1) "P1"

注意在设计redis key时,采用了统一的前缀:{ip}
这是为了使得这些IP相关的数据都落在同一台redis server中(我们的redis以集群形式部署且采取一致性哈希),往后数据迁移什么的会更方便。
同时要注意,如果{ip}:end.asc当中的ip是不连续的,则需要检查,例如:
数据:
id	city	min	  max
1	 P1	    0	  100
3	 P3	   201	  300
4	 P4	   301	  400

//查找150在哪个区间:
127.0.0.1:6379> zadd {ip}:end.asc:miss 100 1 300 3 400 4
(integer) 3
127.0.0.1:6379> zrange {ip}:end.asc:miss 0 -1
1) "1"
2) "3"
3) "4"
127.0.0.1:6379> zrangebyscore {ip}:end.asc:miss 150 +inf LIMIT 0 1
1) "3"
//那么返回的member就会是3,但150显然不在(201,300)这个区间,应该返回的查询结果是“无对应记录”。因此查得member后需要检查(后续代码中有做这一步)。

实操

从数据库中导出的得到的文本是这样的(选取几行为例子):
ipcountry_tab_orderby_end_asc.txt:
"IPSTART"	"IPSTARTDIGITAL"	"IPEND"	"IPENDDIGITAL"	"COUNTRY"	"CITY"	"TYPE"	"REGISTRY"	"ADRESS"	"PROVINCE"
"1.184.0.0"	28835840	"1.184.127.255"	28868607	"中国"	"广州市"	""	""	""	"广东省"
"1.184.128.0"	28868608	"1.184.255.255"	28901375	"中国"	"广州市"	""	""	""	"广东省"
"1.185.0.0"	28901376	"1.185.95.255"	28925951	"中国"	"南宁市"	""	""	""	"广西省"

1.生成批量的hmset命令及zadd命令
写个小程序来生成:
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;

public class IpCountryRedisImport {
	
	public static void main(String[] args) throws IOException {
		File file = new File("E:/doc/ipcountry_tab_orderby_end_asc.txt");
		File hmsetFile = new File("E:/doc/ip_country_redis_cmd.txt");
		File zaddFile = new File("E:/doc/ip_country_redis_zadd.txt");
		
		List<String> lines = FileUtils.readLines(file);
		int i = 0;
		StringBuilder rows = new StringBuilder();
		StringBuilder ends = new StringBuilder();
		for (String str : lines) {
			if (StringUtils.isEmpty(str)) {
				continue;
			}
			
			//skip first line
			if (i == 0) {
				i++;
				continue;
			}
			
			i++;
			
			//"IPSTART"	"IPSTARTDIGITAL"	"IPEND"	"IPENDDIGITAL"	"COUNTRY"	"CITY"	"TYPE"	"REGISTRY"	"ADRESS"	"PROVINCE"
			//0               1                2         3              4          5       6         7         8            9 
			String[] parts = str.split("\t");
			String start = parts[1];
			String end = parts[3];
			String country = parts[4];
			String city = parts[5];
			String type = parts[6];
			String registry = parts[7];
			String address = parts[8];
			String province = parts[9];
			
			//String cmd = "hmset {ip}:" + (i++) + " start " + start + " end " + end + " country " + country + " city " + city + " type " + type + " registry " + registry + " address " + address + " province " + province;
			
			rows.append("*18\r\n");
			
			rows.append(format("hmset"));
			
			rows.append(format("{ip}:" + i));
			
			rows.append(format("start"));
			rows.append(format(start));
			
			rows.append(format("end"));
			rows.append(format(end));
			
			rows.append(format("country"));
			rows.append(format(country));
			
			rows.append(format("city"));
			rows.append(format(city));
			
			rows.append(format("type"));
			rows.append(format(type));
			
			rows.append(format("registry"));
			rows.append(format(registry));
			
			rows.append(format("address"));
			rows.append(format(address));
			
			rows.append(format("province"));
			rows.append(format(province));
			
			
			//zadd {ip}:end.asc 1234 1
			ends.append("*4\r\n");
			ends.append(format("zadd"));
			ends.append(format("{ip}:end.asc"));
			ends.append(format(end));
			ends.append(format("" + i));
			
		}
		FileUtils.writeStringToFile(hmsetFile, rows.toString(), "UTF-8");
		FileUtils.writeStringToFile(zaddFile, ends.toString(), "UTF-8");
		System.out.println(1);
	}
	
	private static String format(String value) throws UnsupportedEncodingException {
		String trimValue = value.replace("\"", "");
		return "$" + trimValue.getBytes("UTF-8").length+ "\r\n" + trimValue + "\r\n";
	}

}


需要注意的是,format方法里面,值的长度不是字符串的长度,而是字符串转化为字节之后的长度

生成hmset结果举例(ip_country_redis_cmd.txt,每一行都是以\r\n结尾):
*18
$5
hmset
$8
{ip}:645
$5
start
$8
28835840
$3
end
$8
28868607
$7
country
$6
中国
$4
city
$9
广州市
$4
type
$0

$8
registry
$0

$7
address
$0

$8
province
$9
广东省

生成的zadd命令举例(ip_country_redis_zadd.txt):
*4
$4
zadd
$12
{ip}:end.asc
$8
16777471
$1
2


需要注意的是,txt文件通过SecureCRT上传到linux后,\r\n可能就只剩\n了,可以替换一下:
perl -pi -e 's/\n/\r\n/' ip_country_redis_cmd.txt 
perl -pi -e 's/\n/\r\n/' ip_country_redis_zadd.txt


2.导入redis

文件生成完毕后,执行以下命令导入:
cat ip_country_redis_cmd.txt | redis-cli –pipe
cat ip_country_redis_zadd.txt | redis-cli --pipe


40万行的数据,花费时间不到一分钟,redis的mass insertion还是很强大的

在这里要提一下的是,redis文档中关于批量导入的说明可能会有误导:
文档是这样的:
SET Key0 Value0
SET Key1 Value1
...
SET KeyN ValueN


我刚开始以为像上面那样,只要把批量redis命令写在同一个文本文件,然后直接导入就可以了:
cat cmd.txt | redis-cli –pipe

实际上不是的,要符合redis protocol才可以
protocol语法:
*<args><cr><lf>
$<len><cr><lf>
<arg0><cr><lf>
<arg1><cr><lf>
...
<argN><cr><lf>

举例:
*3<cr><lf>
$3<cr><lf>
SET<cr><lf>
$3<cr><lf>
key<cr><lf>
$5<cr><lf>
value<cr><lf>


说明:
*后面的数字表示该条redis命令有多少参数,
例如:
set ab 1234参数个数是3
hmset name google.com 1 baidu.com 2的参数个数是6
接下来就是命令的每一部分(空格分隔),先是长度,后是值:
以“set ab 1234”为例:
set的长度是3,ab的长度是2,1234的长度是4,因此最终内容为:
*3
$3
set
$2
ab
$4
1234


注意每一行都是以<cr><lf>(也就是\r\n)结尾


3.查询
使用spring redis

关键代码:
long min = ip;	//转换成数字的IP
        long max = Long.MAX_VALUE;
        long offset = 0;
        long count = 1;
        Set<String> result = redisTemplate.opsForZSet().rangeByScore(zSetName, min, max, offset, count);
        String ipIndex = null;
        if (result != null && result.size() > 0) {
            ipIndex = result.iterator().next();
        }
        
        
        if (ipIndex != null) {
            final String ipKey = redisIprowPrefix + ipIndex;
            Collection<String> fields = new ArrayList<String>();
            fields.add("ipstart");
            fields.add("ipstartdigital");
            fields.add("ipend");
            fields.add("ipenddigital");
            fields.add("country");
            fields.add("city");
            fields.add("type");
            fields.add("registry");
            fields.add("adress");
            fields.add("province");
            fields.add("latitude");
            fields.add("longitude");
            fields.add("addresstype");
            
            List<String> fieldValues = redisTemplate.<String, String>opsForHash().multiGet(ipKey, fields);
            
            
            if (fieldValues != null && fieldValues.size()==fields.size()) {
                String startDigital = fieldValues.get(1);
                if (StringUtils.isNotBlank(startDigital)) {
                    long ipStartDigital = Long.parseLong(startDigital);
					
					//检查是否确实在区间内:start <= x <= end
                    if (ipStartDigital > ip) {
                        logger.info("ip is not in this range(that is, ip < ipstartdigital), ipstartdigital={}, ip={}", ipStartDigital, ip);
                        return null;
                    }
                }
                
                String endDigital = fieldValues.get(3);
                IpcountryResp resp = new IpcountryResp();
                if (StringUtils.isNotBlank(endDigital)) {
                    resp.setIpenddigital(Long.parseLong(endDigital));
                }
                
                resp.setIpstart(fieldValues.get(0));
                resp.setIpend(fieldValues.get(2));
                
                
                resp.setCountry(fieldValues.get(4));
                resp.setCity(fieldValues.get(5));
                resp.setType(fieldValues.get(6));
                resp.setRegistry(fieldValues.get(7));
                resp.setAdress(fieldValues.get(8));
                resp.setProvince(fieldValues.get(9));
                resp.setLatitude(fieldValues.get(10));
                resp.setLongitude(fieldValues.get(11));
                resp.setAddresstype(fieldValues.get(12));
                

                

                
                return resp;
            }
            
        }
			
			


redisTemplate需要配置序列化相关的property:
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"
        p:connection-factory-ref="jedisConnFactory">
        <property name="valueSerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property>
        <property name="keySerializer">
            <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
        </property> 
</bean>




参考
http://stackoverflow.com/questions/9989023/store-ip-ranges-in-redis
  • 大小: 47.2 KB
1
1
分享到:
评论

相关推荐

    php 根据ip地址判断是那个国家

    可以考虑缓存查询结果,如使用Redis或Memcached存储最近查询过的IP和其位置信息。 6. **隐私和合规性** - 在处理用户IP地址时,要遵守相关的隐私政策和法规,如GDPR。不要在未经用户许可的情况下收集、存储和使用...

    handsome获取ip+地区工具包

    此外,为了提高性能,一些工具包可能会采用缓存策略,例如使用内存缓存或Redis等分布式缓存存储最近查询过的IP信息,减少对数据库的访问次数。 在实际应用中,这个工具包可以用于网站统计分析、用户行为追踪、广告...

    C#操作IP地址数据库源码(十分经典)

    4. 查询优化:为了提高查询效率,可以使用二分查找、哈希表或B树等算法对IP地址范围进行索引。C#的`List&lt;T&gt;`或`Dictionary, TValue&gt;`等集合类型可帮助实现这些数据结构。 5. 源码实现:在实际的源码中,可能会包含...

    基于PHP的IP地址查询程序(QQwry纯真数据库版)源码.zip

    6. **查询优化**:为了提高查询效率,可能需要实现二分查找算法来快速定位IP所在的数据库段,而不是线性遍历整个文件。 7. **错误处理**:良好的错误处理机制是任何程序不可或缺的部分。在PHP中,可以使用`try......

    基于PHP的根据IP识别所在地区天气预报v1.0源码.zip

    2. 地理位置解析:通过IP地址,系统会查找对应的地理位置,包括国家、城市等信息。 3. 天气查询:有了地理位置信息后,系统向天气API发送请求,获取该地区的天气预报数据。 4. 数据展示:最后,将天气预报数据格式化...

    Laravel开发-sypexgeo

    SypexGeo 是一个高效的 IP 地理位置数据库,它能够快速地查找并返回访问者所在的国家、城市、经纬度等信息。Laravel 作为 PHP 的流行框架,结合 SypexGeo 可以为开发者提供便利的地理定位服务。 首先,让我们了解...

Global site tag (gtag.js) - Google Analytics