基于Redis实现分布式应用限流的方法
程序员文章站
2023-12-11 16:31:04
限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务。
前几天在dd的公众号,看了一篇关于使用 瓜娃 实现...
限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务。
前几天在dd的公众号,看了一篇关于使用 瓜娃 实现单应用限流的方案 --》原文,参考《redis in action》 实现了一个jedis版本的,都属于业务层次限制。 实际场景中常用的限流策略:
nginx接入层限流
按照一定的规则如帐号、ip、系统调用逻辑等在nginx层面做限流
业务应用系统限流
通过业务代码控制流量这个流量可以被称为信号量,可以理解成是一种锁,它可以限制一项资源最多能同时被多少进程访问。
代码实现
import redis.clients.jedis.jedis; import redis.clients.jedis.transaction; import redis.clients.jedis.zparams; import java.util.list; import java.util.uuid; /** * @email wangiegie@gmail.com * @data 2017-08 */ public class redisratelimiter { private static final string bucket = "bucket"; private static final string bucket_count = "bucket_count"; private static final string bucket_monitor = "bucket_monitor"; static string acquiretokenfrombucket( jedis jedis, int limit, long timeout) { string identifier = uuid.randomuuid().tostring(); long now = system.currenttimemillis(); transaction transaction = jedis.multi(); //删除信号量 transaction.zremrangebyscore(bucket_monitor.getbytes(), "-inf".getbytes(), string.valueof(now - timeout).getbytes()); zparams params = new zparams(); params.weightsbydouble(1.0,0.0); transaction.zinterstore(bucket, params, bucket, bucket_monitor); //计数器自增 transaction.incr(bucket_count); list<object> results = transaction.exec(); long counter = (long) results.get(results.size() - 1); transaction = jedis.multi(); transaction.zadd(bucket_monitor, now, identifier); transaction.zadd(bucket, counter, identifier); transaction.zrank(bucket, identifier); results = transaction.exec(); //获取排名,判断请求是否取得了信号量 long rank = (long) results.get(results.size() - 1); if (rank < limit) { return identifier; } else {//没有获取到信号量,清理之前放入redis 中垃圾数据 transaction = jedis.multi(); transaction.zrem(bucket_monitor, identifier); transaction.zrem(bucket, identifier); transaction.exec(); } return null; } }
调用
测试接口调用
@getmapping("/") public void index(httpservletresponse response) throws ioexception { jedis jedis = jedispool.getresource(); string token = redisratelimiter.acquiretokenfrombucket(jedis, limit, timeout); if (token == null) { response.senderror(500); }else{ //todo 你的业务逻辑 } jedispool.returnresource(jedis); }
优化
使用拦截器 + 注解优化代码
拦截器
@configuration static class webmvcconfigurer extends webmvcconfigureradapter { private logger logger = loggerfactory.getlogger(webmvcconfigurer.class); @autowired private jedispool jedispool; public void addinterceptors(interceptorregistry registry) { registry.addinterceptor(new handlerinterceptoradapter() { public boolean prehandle(httpservletrequest request, httpservletresponse response, object handler) throws exception { handlermethod handlermethod = (handlermethod) handler; method method = handlermethod.getmethod(); ratelimiter ratelimiter = method.getannotation(ratelimiter.class); if (ratelimiter != null){ int limit = ratelimiter.limit(); int timeout = ratelimiter.timeout(); jedis jedis = jedispool.getresource(); string token = redisratelimiter.acquiretokenfrombucket(jedis, limit, timeout); if (token == null) { response.senderror(500); return false; } logger.debug("token -> {}",token); jedis.close(); } return true; } }).addpathpatterns("/*"); } }
定义注解
/** * @email wangiegie@gmail.com * @data 2017-08 * 限流注解 */ @target(elementtype.method) @retention(retentionpolicy.runtime) @documented public @interface ratelimiter { int limit() default 5; int timeout() default 1000; }
使用
@ratelimiter(limit = 2, timeout = 5000) @getmapping("/test") public void test() { }
并发测试
工具:apache-jmeter-3.2
说明: 没有获取到信号量的接口返回500,status是红色,获取到信号量的接口返回200,status是绿色。
当限制请求信号量为2,并发5个线程:
当限制请求信号量为5,并发10个线程:
资料
总结
- 对于信号量的操作,使用事务操作。
- 不要使用时间戳作为信号量的排序分数,因为在分布式环境中,各个节点的时间差的原因,会出现不公平信号量的现象。
- 可以使用把这块代码抽成@ratelimiter注解,然后再方法上使用就会很方便啦
- 不同接口的流控,可以参考源码的里面redisratelimiterplus,无非是每个接口生成一个监控参数
- 源码:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。