文档章节

基于Redis实现分布式应用限流

冷冷gg
 冷冷gg
发布于 2017/08/30 09:45
字数 863
阅读 8145
收藏 323

限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务。

前几天在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个线程: image
当限制请求信号量为5,并发10个线程:
image

资料

基于reids + lua的实现

张开涛-聊聊高并发系统之限流特技-1

总结

  1. 对于信号量的操作,使用事务操作。
  2. 不要使用时间戳作为信号量的排序分数,因为在分布式环境中,各个节点的时间差的原因,会出现不公平信号量的现象。
  3. 可以使用把这块代码抽成@rateLimiter注解,然后再方法上使用就会很方便啦
  4. 不同接口的流控,可以参考源码的里面RedisRateLimiterPlus,无非是每个接口生成一个监控参数
  5. 源码http://git.oschina.net/boding1/pig-cloud

© 著作权归作者所有

共有 人打赏支持
冷冷gg
粉丝 385
博文 110
码字总数 49926
作品 1
潍坊
UI设计师
加载中

评论(9)

冷冷gg
冷冷gg

引用来自“DRSoul”的评论

考虑一个问题,当环境中各个节点的时间差比较大的时候,每次交集之前,删除时间戳的zset中过期元素将会导致交集操作时丢失一些正持有信号量的节点信息,这个和超时时间,各节点系统时钟,获取信号量后进行业务处理的所需时间挂钩,特别当各个节点时钟相差过大,导致实际持有信号量节点大于信号量,超时时间过小时同样。
嗯 这种节点时间差比较大的问题 很头疼,这种在应用网关进行限流处理精确度会好一点,
DRSoul
DRSoul
考虑一个问题,当环境中各个节点的时间差比较大的时候,每次交集之前,删除时间戳的zset中过期元素将会导致交集操作时丢失一些正持有信号量的节点信息,这个和超时时间,各节点系统时钟,获取信号量后进行业务处理的所需时间挂钩,特别当各个节点时钟相差过大,导致实际持有信号量节点大于信号量,超时时间过小时同样。
每周精粹
每周精粹
RedisRateLimiter 的逻辑是什么
冷冷gg
冷冷gg

引用来自“瘦-马”的评论

直接 lua+redis 好了;
用 java 多次一举;

回复@瘦-马 : 并不是所有人都和大神你是的 会写lua脚本呀
瘦-马
瘦-马
直接 lua+redis 好了;
用 java 多次一举;
冷冷gg
冷冷gg

引用来自“douglaswei”的评论

redis连接资源不用还给连接池吗?
:smile: 上边还有 优化代码给漏了
douglaswei
douglaswei
redis连接资源不用还给连接池吗?
冷冷gg
冷冷gg

引用来自“1363435084”的评论

看到开涛老师就放心了
:unamused: 啥意思嘛
1363435084
1363435084
看到开涛老师就放心了
《亿级流量网站架构核心技术》目录一览

举报   在2011年年底的时候笔者就曾规划写一本Spring的书,但是因为是Spring入门类型的书,框架的内容更新太快,觉得还是写博客好一些,因此就把写完的书稿放到了博客(jinnianshilongnia...

jinjiang2009
2017/03/14
0
0
百度、阿里、腾讯、京东、大型互联网分布式架构必备技能

分布式架构 迎接高并发大数据的挑战,从深度到广度完善知识体系,成为下一个互联网高薪人才。 理论结合实战,透彻理解分布式架构及其解决方案。 面向人群 1、工作1-5年需要突破瓶颈; 2、传统...

Java高级架构
2017/12/21
0
0
小柒2012/spring-boot-seckill

分布式秒杀系统 开发环境 JDK1.7、Maven、Mysql、Eclipse、SpringBoot1.5.10、zookeeper3.4.6、kafka_2.11、redis-2.8.4、curator-2.10.0 友情提示 由于工作原因,项目正在完善中(仅供参考)...

小柒2012
05/19
0
0
Spring Cloud限流详解(内含源码)

原文:http://www.itmuch.com/spring-cloud-sum/spring-cloud-ratelimit/ 转载声明:本博客由周立创作,采用CC BY 3.0 CN许可协议。可自由转载、引用,但需署名作者且注明文章出处。如转载至...

周立_itmuch
2017/12/01
0
0
亿级流量电商详情页系统的大型高并发与高可用缓存架构实战

对于高并发的场景来说,比如电商类,o2o,门户,等等互联网类的项目,缓存技术是Java项目中最常见的一种应用技术。然而,行业里很多朋友对缓存技术的了解与掌握,仅仅停留在掌握redis/memca...

登录404
2017/06/05
1K
0

没有更多内容

加载失败,请刷新页面

加载更多

MySQL 到底支不支持事务嵌套?

最近开发中遇到了使用MySQL,多次开启事务,出现了数据错乱问题,伪代码如下: begin; # 操作1 begin; # 操作2 rollback; 执行完后出现了操作1的数据真正写入,只有操作2的数据回滚...

宇润
55分钟前
7
0
fastDfs应用(安装过程待写)

1.效果 2.安装 2.1 导入已经安装好fastDFS的镜像 2.1.1 导入镜像 2.1.2 更改系统兼容性 2.1.3 开机 2.1.4 修改 一下内容 2.1.4.1 修改系统的ip 原来系统ip...

Lucky_Me
58分钟前
5
0
5. Python3源码—字符串(str)对象

5.1. 字符串对象 字符串对象是“变长对象”。 5.1.1. Python中的创建 Python中字符串(strs)对象最重要的创建方法为PyUnicode_DecodeUTF8Stateful,如下Python语句最终会调用到PyUnicode_D...

Mr_zebra
今天
4
0
第十章:路由网关(Zuul)进阶:过滤器、异常处理

第十章:路由网关(Zuul)进阶:过滤器、异常处理 简单介绍了关于Zuul的一些简单使用以及一些路由规则的简单说明。而对于一个统一网关而言,需要处理各种各类的请求,对不同的url进行拦截,或者...

DemonsI
今天
4
0
nginx屏蔽指定接口(URL)

Step1:需求 web平台上线后,需要屏蔽某个服务接口,但又不想重新上线,可以采用nginx屏蔽指定平台接口的办法 Step2:具体操作 location /dist/views/landing/UNIQUE_BEACON_URL { re...

Linux_Anna
今天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部