基于Redis的分布式限流

遇到这种场景:要求某个接口1s最多请求10次,在分布式环境下guava的RateLimiter用不上。redis可以满足需求,于是baidu一下redis分布式限流的代码实现,总结看基本分为两种,脚本实现、非脚本实现。非脚本实现缺点明显,lua实现优势满满,肯定用lua啊啊啊啊。但是还是要看下非脚本实现的坑在哪里,lua实现的两种方式:均匀实现和非均匀实现。当然用lua的均匀实现方式是最好用的,也是推荐的。

看具体的实现之前,还是给一个场景:限定登录接口1s最多请求5次

非脚本实现

实现思路:用String结构,value存储当前登录次数,设置key的过期时间是1s。所以只要key没过期并且value<10就可以继续登录。

private boolean accessLimit(String ip, int limit, int time, Jedis jedis) {
    boolean result = true;

    String key = "rate.limit:" + ip;
    if (jedis.exists(key)) {
        long afterValue = jedis.incr(key);
        if (afterValue > limit) {
            result = false;
        }
    } else {
        Transaction transaction = jedis.multi();
        transaction.incr(key);
        transaction.expire(key, time);
        transaction.exec();
    }
    return result;
}

这段代码存在以下几个问题:

  • 可能出现竞态条件
  • 不使用pipeline的情况下,最多发送5条指令给redis,传输太多
  • 限速不均匀

下面一一来看一下这几个问题

可能出现竞态条件

redis是单线程单进程,多客户端的命令请求是串行在服务端执行的,所以在服务端不存在竞争条件,竞争条件存在于多客户端,没办法保证一个客户端的多次命令请求是一个原子操作。redis事物可以解决这个问题,redis事物可以保证一个客户端的多个命令原子执行。但是啊但是,redis事物也不是万能的,使用受限,使用的时候要考虑自己的使用场景。

redis事物使用需要注意:
1.实现乐观锁需要配合WATCH命令
2.redis事物只支持单机或者单节点。所在redis cluster环境下,需要操作多个key的情况不能使用事物。因为多个key很可能在不同的redis节点。

经过上面的分析,在redis集群环境,上面的代码是可以使用redis事物,因为事物里边只有一个key,肯定事物的作用范围也只有一个redis节点。再加上WATCH上面的代码就滴水不漏了。

但是啊但是,这个实现太麻烦,跟redis的交互也太多。

限速不均匀

上面代码实现的时间窗不平滑。
举个例子:限速每秒5个,如下的场景9个请求都能被接收,因为第一请求设置1s过期,第5个请求又设置1s过期,所以这9个请求都不会被限速拦截。但是中间的7个请求也是在1s内,已经违背了限速每秒5个。

基于Redis的分布式限流_第1张图片

脚本实现

脚本实现是指用lua+redis实现,可保证lua脚本原子执行,并且和redis服务端只交互一次。限速是否均匀要看实现方式。

脚本实现-非均匀实现

实现思路:跟上边的非脚本实现一样的思路。

lua脚本:

local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire_time = tonumber(ARGV[2])

local is_exists = redis.call("EXISTS", key)
if is_exists == 1 then

    if redis.call("INCR", key) > limit then

        return 10
    else
        return 1
    end
else
    redis.call("SET", key, 1)
    redis.call("EXPIRE", key,expire_time )
    return 1
end

顺便说一下在redis集群环境下使用lua遇到的问题:

redis.clients.jedis.exceptions.JedisClusterException: No way to dispatch this command to Redis Cluster because keys have different slots.

	at redis.clients.jedis.JedisClusterCommand.run(JedisClusterCommand.java:46)
	at redis.clients.jedis.JedisCluster.eval(JedisCluster.java:1737)

或者是这样的报错

@user_script:2: @user_script: 2: Lua script attempted to access a non local key in a cluster node

因为lua脚本中的key也必须在同一个槽中,所以必须给key加{}保证lua中的key都在同一个槽中。
基于Redis的分布式限流_第2张图片

两点说明:

  • 调用lua脚本传递的参数key就要带有{}。在lua脚本中给传递进来的key加{}是不行的。
  • 即便lua中只操作一个key,也要加{}。

脚本实现-均匀实现

实现思路:用list结构实现,保存有限个元素,(限速每秒请求5次则list最多保存5个元素),value记录请求时间。每次请求,用当前时间和list最后一个元素时间比较,判断是否限速。

链接: 参考原文.

你可能感兴趣的:(Redis)