ip白名单算法(pdd活跃用户)
https://www.hellojava.com/a/76003.html
1)用ip地址字符串hash,显然太low
2)ip地址字符串转换为long(int存不了那么多,unsigned int 可以),ipv4总共2^32个(4g=2^2*2^10*2^10*2^10=4*1024^3),一个bit存一个ip地址,4g个bit=0.5g个字节
故建立长度为0.5g的byte数组,接下去要确定的是白ip所在的位置
所在数组索引=ip/8
在byte中位索引=ip%8=ip&(8-1)
/** * ipv4总共2^32个(4g) * 一个bit存一个ip地址,4g个bit=0.5g个字节 */ private static byte [] pool = new byte[512*1024*1024]; // 0-255 <子ip字符串,long> 缓存 private static ConcurrentHashMap<String, Long> cache = new ConcurrentHashMap<>(256); public static void add(String ip) { long ipLong = ipToLong(ip); long index = ipLong / 8; long pos = ipLong & (8-1);相当于ipLong % 8 pool[(int)index] |= 1 << pos; } public static boolean check(String ip) { long ipLong = ipToLong(ip); long index = ipLong / 8; long pos = ipLong & (8-1); return (pool[(int)index] & (1<<pos)) > 0; }
为什么hashmap以2的倍数作为桶的长度,同时以2作为扩容倍数
其中ip字符串转数字涉及到一个缓存,如"192"直接缓存,下一次不需要再转数字
如:
192.168.1.3
为3232235779,
byte index = 404029472.375
bit index = 3
public static void add(String ip) { long ipLong = ipToLong(ip); // System.out.println(ipLong); long index = ipLong / 8; // System.out.println(index); long pos = ipLong & (8-1); // System.out.println(1 << pos); pool[(int)index] |= 1 << pos; }
程序输出:
3232235779
404029472
8 二进制为 00001000
对该方式进行压力测试,CPU密集型,开4个线程,每个线程跑200w次
public static void test() { check("192.168.1.3"); check("192.168.1.4"); } // 4并发数 缓存3s执行完,不缓存5s,qps约2百万 // 隔壁需要6s public static void main(String [] f) throws InterruptedException { String ip = "192.168.1.3"; add(ip); System.out.println(check(ip)); System.out.println(check("192.168.2.3")); String ip1= "192.168.1.3"; IPFiliterTest.addWhiteIpAddress(ip1); final int threadCount = 4; CountDownLatch countDownLatchMain = new CountDownLatch(threadCount); CountDownLatch countDownLatchSub = new CountDownLatch(1); for(int i=0; i<threadCount; ++i) { new Thread(new Runnable() { @Override public void run() { try { countDownLatchSub.await(); } catch (InterruptedException e) { e.printStackTrace(); } for(int j=0; j<1000000; ++j) test(); //IPFiliterTest.test(); countDownLatchMain.countDown(); } }).start(); } System.out.println(System.currentTimeMillis()); countDownLatchSub.countDown(); countDownLatchMain.await(); System.out.println(System.currentTimeMillis()); }
结果:
总共800w次在4s中完成(3s为缓存,5s为不缓存),qps=200w
使用原作者的需要6s
10线程5.8s,总共2000w次调用,qps=333w
100线程40.8s,总共2亿次调用,qps=490w
使用benchmark
4线程和10线程,一次预热3次迭代都是290w*2=580w左右的qps
4thr. IpPool.test thrpt 3 2908446.729 ± 178674.949 ops/s
10 thr IpPool.test thrpt 3 2860266.544 ± 505878.277 ops/s
该算法使用内存至少512m,稍显大,可以使用外部存储集群,比如redis bit结构,程序分片
总共4g个比特,分为4片,每片1g个bit 128m内存,那么
服务器索引=x/1g
路由后的ip=x%1g=x&(2^30-1)
这种方式也可以用于活跃用户统计
(拼多多面试真题:如何用Redis统计独立用户访问量https://blog.csdn.net/weixin_38405253/article/details/92285696),
设20亿用户,每个用户一个bit,总共需要20亿/8个字节=238M内存
缺点就是如果用户稀疏,会浪费内存,对uid hash空间更小