基于 PHP + Redis 的 ip 行为分析及封禁
基于PHP+Redis 的ip行为分析及封禁
这个任务的需求来源是因为现在网站的一些接口,比如登录、注册、粉丝、关注等相关功能接口以及部分其他接口存在被一些黑客大量访问、攻击、刷接口以达到某些非法目的情况。对于这一状况,我们需要按照一定的接口频率上限规则,对非法访问接口的 ip
进行行为分析,初步阶段只进行 ip
访问接口频率的判断,并且数据库中存有相关接口要求的 单ip/min
的访问量上限。
那么这件事情就挺好解决了,在数据库中存取某些行为对应的单ip每分钟的访问量上限,接口调度的时候通过请求,将客户端ip和端口限制的行为ID传过来,接着继续执行自身的业务, ip
记录接口保存 ip
和行为 ID
组合在一起的键值对在缓存中值 inc
一次, ip
行为分析工具没分钟自动检索缓存中超过上限 ip
,对其进行封禁即可。
但是海哥告诉我, Redis
中的数据类型有很多种,它不同的数据类型存储、查询等操作,都会有不同的时间复杂度,我们做这个ip行为分析工具,面对的是高并发的状况,不仅要将这个功能做出来,还要考虑到代码的性能以及可用性。于是我就去查阅了一些与Redis命令相关的资料,再进行一些设计上的比较,得到了两种较为合理实现方案。
Sorted Set 进行存储,每个
[时间:行为]
作为一个存储Key
,IP
作为Sorted Set
中的Value
,ip
每次访问就对该存储Key进行分数增加,利用Sorted Set
的分数score
,取区间分数内的Value
,也就是ip
集合,对该集合进行封禁。利用 Hash 进行存储,每个
[时间:行为]
作为Hash
的一个存储Key
,IP
作为Hash
的field
,每个field
的值为ip
->[时间:行为]
的访问次数。通过HINCRBY
增加field
的值并取回当前值,判断是否超过行为的上限,如果超过上限,则将ip
添加到[时间:行为]
willBan
(一个名为willBan
的Set
集合) 中,分析机每分钟进行缓存分析,使用SMEMBERS
将willBan Set
中的元素取出直接封禁即可,我们这个项目还要获取ip在当前分钟内的访问量,所以我们通过HMGET
将Hash
中willBan Set
对应的VALUE
取出。
这两个方法都可以,但是时间复杂度上有一些差别,一个是程序上的时间复杂度,一个是缓存中的时间复杂度。
Sorted Set 进行存储,虽然可以直接得到分数值score,代表着IP在某一分钟内的访问量,但是Sorted Set 的 ZINCRBY 时间复杂度是 O( log(N) )的,并且, ZINCRBY的操作是非常频繁的,在高并发的情况下,Sorted Set 被进行 ZINCRBY 的次数是非常高的,相比之下行为分析机器所占用的缓存资源几乎可以忽略不计。假设当前每分钟的访问量是 v ,那么这个方案,每一次访问在缓存中的时间复杂度近似于 O( log(v) ),在程序中的复杂度近似于 O(1)。
Hash进行存储,再判断 HINCRBY 的返回值是否大于行为上限,如果大于再将IP记录在 SET 中。在缓存中,HINCRBY的时间复杂度是 O(1) ,SADD的时间复杂度是 O(N),由于只插入一个ip,所以时间复杂度是 O(1) 。在程序中的时间复杂度为 存储的时间复杂度+判断的时间复杂度+if(需要SADD) 的时间复杂度,也就是 3 * O(1),几乎可以忽略不计,这个方案中占用等级到N的,的是每分钟的ip行为分析机,通过SMEMBER得到成员 O(1) ,再HMGET得到被封禁的ip值,时间复杂度是( O(N) ),N是被即将要被封禁的数量,而被封禁的ip数量N,相对于全局来说,真的太小了(毕竟这个世界上还是好人多?)。
而目前来说,针对目前高并发,并且接口请求及其频繁的情况下,采用方案2,是比较好的。 当然, 这个是在PHP上实现的,还有一些其他的方式,比如 nginx + lua + redis ,在服务器(Nginx)端就进行封禁。比如 Nginx + Lua + OpenResty 的高可用性应用开发,可以直接在Nginx端就进行处理,这样速度很快,而且由于Nginx的轻量性,可以很好的处理高并发的情况,在本篇,就不展开来说了。
[注] 1:fastcgi_finish_request();
//可以直接终止请求避免请求方端等待时间过长,但依旧可以继续处理请求,但是仅限 php
的 fast-cgi
运行模式。
[注] 2:inc
是一个统称,代表将缓存中的值增加或减少一个数值,具体的实现方式有所不同,在 Redis
中有 INCRBY
HINCRBY
ZINCRBY
等,分别针对不同的存储结构。
附加:
- 如果在程序运行过程中,想删除 key,但是key是hash、zset等数据结构,存储的数据量很大,建议通过设置key的过期时间为1,否则通过 $redis->删除,需要等待redis进行删除操作完成后才能得到相应结果,这样程序的执行时间就会变长。而设置了key的过期时间,可以让redis自动删除,虽然过期时,Redis进行keyd的删除时间也长,但并不会让程序进入等待。