zoukankan      html  css  js  c++  java
  • Redis实现访问控制频率

    为什么限制访问频率

    做服务接口时通常需要用到请求频率限制 Rate limiting,例如限制一个用户1分钟内最多可以范围100次

    主要用来保证服务性能和保护数据安全

    因为如果不进行限制,服务调用者可以随意访问,想调几次就调几次,会给服务造成很大的压力,降低性能,再比如有的接口需要验证调用者身份,如果不进行访问限制,调用者可以进行暴力尝试

    redis如何解决“单位时间内只能n次操作”这样的问题?

    假定要限制每分钟每个用户最多只能访问100个页面。

    方案一:string
    通过为用户使用一个名为 rate.limiting:userId 的字符串类型键,每次访问都使用 INCR命令递增该键的键值。
    如果递增后的值为 1(第一次访问),则要为键设置过期时间 60秒。
    这样每次用户访问都读取该键值,当键值超过100时,说明访问频率超过了限制,需要稍后访问。
    该键过期后会自动删除,所以下一分钟用户访问次数又会重新计算。

    伪代码如下:
        $isKeyExists = EXISTS rate.limiting:$userId    // 存在返回 1,不存在返回 0
        if $isKeyExists is 1
            $times = INCR rate.limiting:$userId
            if $times > 100        // 第100次访问会增加到101
                print 访问频率超过限制,请稍后再试 
                exit
        else
           MULTI       //此处,如果不加事务,竞态条件可能出现
        INCR rate.limiting:$userId
        EXPIRE
    $keyName, 60
    EXEC

    上面为什么要用MULTI,那是因为如果在执行完INCR rate.limiting:$userId之后,如果(出现故障)没有设置过期时间,那么该键将永远存在,所以需要加上事务。

    方案二:list

    事实上,方案一有个问题。如果一个用户在第一分钟的最后一秒访问了99次,在下一分钟的第一秒访问了100次,相当于在两秒访问了199次,
    与一分钟内最多只能访问100次相比还是差距比较大,尽管这种情况比较极端,但是依然存在。如果要实现粒度更小的控制方式,精确的保证每分钟最多访问100次,就需要使用第二种方案。

    第二种方案需要记录用户每次的访问时间,因此对于每个用户,用列表类型的键记录他最近100次访问的时间。
    如果键中的元素超过100个,就判断时间最早的元素距离现在的时间是否小于1分钟,如果是,则表示用户最近1分钟的访问次数超过100次,如果不是就将当前时间加入列表中,同时把最早的元素删除

    伪代码如下:
        $limitLength = LLEN rate.limiting:$userId
        if $limitLength < 100
            LPUSH rate.limiting:$userId, now()
        else 
            $time = LINDEX rate.limiting:$userId, -1   // 取最后一个元素
            if now() - $time < 60
                print 访问频率超过限制,请稍后再试
            else
                LPUSH rate.limiting:$userId, now()
                LTRIM rate.limiting:$userId, 0, 99     // 删除[0~99]以外的元素

    这种方式 now() 的功能是获得当前的 Unix时间,由于要记录当前访问时间,所以当要限制 “A时间最多访问B次” 时,如果”B”比较大,会占用较多内存,
    实际使用时要去权衡。而且这种方法会出现就竞态条件,可以通过脚本避免。

    但是在高并发的缓存系统中,大量使用事务是非常糟糕的,可以用redis自带的lua脚本功能实现多个操作的“原子性”

    方案三:使用lua脚本实现频率限制

    思路

    把限制逻辑封装到一个Lua脚本中,调用时只需传入:key、限制数量、过期时间,调用结果就会指明是否运行访问

    local notexists = redis.call("set", KEYS[1], 1, "NX", "EX", tonumber(ARGV[2]))
    if (notexists) then
      return 1
    end
    local current = tonumber(redis.call("get", KEYS[1]))
    if (current == nil) then
      local result = redis.call("incr", KEYS[1])
      redis.call("expire", KEYS[1], tonumber(ARGV[2]))
      return result
    end
    if (current >= tonumber(ARGV[1])) then
      error("too many requests")
    end
    local result = redis.call("incr", KEYS[1])
    return result

    使用 eval 调用

    eval 脚本 1 key 参数-允许的最大次数 参数-过期时间
  • 相关阅读:
    线索二叉树
    正则表达式之后向引用
    进步的阶梯
    树和二叉树
    java 执行 exe 文件
    Electron + Vue如何实现不同窗口之间的通信(项目总结 第一个)
    liunx 修改 ip 地址
    桌面快捷工具
    微信小程序长列表组件 recycle-view 修改,使其可以下拉刷新
    微信小程序 textarea 文本滚动不了的bug
  • 原文地址:https://www.cnblogs.com/niuben/p/10812369.html
Copyright © 2011-2022 走看看