基于Redis实现范围查询的IP库缓存设计方案( 二 )



3756871424<=3756871650<=3756871679
 
因为会存在这种情况,该区间与前一个区间并不是连续的,比如 。
(1)3756870911=>3756870656~3756870911(2)3756871679=>3756871424~3756871679 
明显,这两个区间之间存在断层 。但可以肯定的是,如果不落在区间(2)中,也不会落在区间(1) 。所以不满足就可以直接返回null了 。
 
基于Redis实现范围查询的IP库缓存设计方案

文章插图
 
Ip库用hash类型存储,field取ip_from或者ip_from&ip_to,value当然就是存完整的一行记录了 。最后就可以根据拿到的区间信息获取到对应记录的field,使用hget命令就能获取到最终的一行ip信息记录 。
hget ip-country-city-locations 3756871424这并不难实现,但是,耗时却非常严重,可以看下官方文档介绍的Sorted Set的zrangebyscore的时间复杂度 。IP库保守估计百万条记录,如果就这样上线,百分百又是服务雪崩 。
 
改进思路:区间+Sorted Set
 
由于Sorted Set有序集合的查询时间复杂度是O(log(n)+m),其中n是总记录数,m是此次查询获取的记录数,在limit 0 1的情况下是O(log(n)),所以一个有序集合的元素个数越多,它的查询时间耗时越长 。对于一般的应用场景,如排行榜,都是只有极少数的几百条记录 。而如果用于ip库的区间查询实现,记录上百万条,而且还是用于高并发场景,不把服务搞垮才怪了 。
 
既然我们要用Sorted Set,但又不能让集合的元素过大,那么我们可以分n/m个区间存储啊 。假设有一百万条记录,每个Sorted Set存储1000条,那就用1000个Sorted Set集合来存储 。hash的查询时间复杂度是接近O(1)的,增加1000个key在分槽位的分布式集群下根本不算什么 。
 
按照上面的思路改进后,获取一个ip的国家城市信息就变成如下步骤:
1、根据ip计算出一个number值
比如:37568716502、根据区间大小(这一步的区间指的是每个Sorted Set的最大大小),计算出该number所在的集合的key

比如:ip-country-city-locations-range-375
3、根据这个key,去Sorted Set查询number所属的区间 。
比如:zrangebyscore ip-country-city-locations-range 3756871650 +inf 0 1 
5、拿到区间信息后,从区间信息获取ip范围位置信息的 field(hash类型存储)
比如查询结果区间信息为:3756871424~3756871679拿到field就是:3756871424 
6、根据key和field拿到目标记录 。
hget ip-country-city-locations 3756871424 
编码实现
 
我将实现封装成一个组件,目的是对外提供更简单的使用方式,封装复杂的实现逻辑,同时,内部的改动对使用者无感知 。通过SPI+分层设计,利用静态代理模式等,使得组件具有极强的扩展性,如果某天想改成使用mongodb或者内存缓存,只需要实现几个接口就可以了 。
 
基于Redis实现范围查询的IP库缓存设计方案

文章插图
 
 
下面是README.MD的内容
 
关于数据源表的初始化
使用需要配置update启动参数:
-Dip.database.table.update=true如:
java -Xss256k-jar -Dip.database.table.update=true xxx-1.0.0.jartrue: 首次启动就会从指定的url文件读取解析记录,插入数据表 false: 表示已经确认表存在记录了,不需要再更新 。(也不会去解析文件) 默认:false
解析记录与插入表是异步的,后台开启一个线程执行 。耗时根据文件大小决定,我测的是86s
配置使用表
使用了java的SPI
需要指定使用哪个文件解析器,也就对应使用哪种类型的表
配置redis操作实现类
使用了java的SPI
如果解析配置使用了
com.chestnut.ip.database.parser.RedisIP2LocationFileParser则需要自己实现RedisOperation,并在
com.chestnut.ip.database.suppor.IP2LocationRedisOperation文件中配置redis操作的实现类
缓存的key
如果使用redis存储数据,则key固定为
ip-country-city-locations // 存储真实记录ip-country-city-locations-range-* // 存储范围与真实记录的key的映射其中ip-country-city-locations-range-后面的代表的分区索引

【基于Redis实现范围查询的IP库缓存设计方案】


推荐阅读