基于Redis实现分布式应用限流--转
原文地址:https://my.oschina.net/giegie/blog/1525931
摘要: 限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务。
限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务。
前几天在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,无非是每个接口生成一个监控参数
- 源码http://git.oschina.net/boding1/pig-cloud
基于Redis实现分布式应用限流--转的更多相关文章
- 基于注解的接口限流+统一session认证
		代码心得: 一个基本的做法:对于用户身份认证做到拦截器里,针对HandlerMethod进行统一拦截认证,根据方法上的注解标识,判别是否需要身份验证,并将查找出来的User实体存入ThreadLoca ... 
- 限流(三)Redis + lua分布式限流
		一.简介 1)分布式限流 如果是单实例项目,我们使用Guava这样的轻便又高性能的堆缓存来处理限流.但是当项目发展为多实例了以后呢?这时候我们就需要采用分布式限流的方式,分布式限流可以以redis + ... 
- 基于.net的分布式系统限流组件   C# DataGridView绑定List对象时,利用BindingList来实现增删查改   .net中ThreadPool与Task的认识总结  C# 排序技术研究与对比  基于.net的通用内存缓存模型组件  Scala学习笔记:重要语法特性
		基于.net的分布式系统限流组件 在互联网应用中,流量洪峰是常有的事情.在应对流量洪峰时,通用的处理模式一般有排队.限流,这样可以非常直接有效的保护系统,防止系统被打爆.另外,通过限流技术手段,可 ... 
- 基于kubernetes的分布式限流
		做为一个数据上报系统,随着接入量越来越大,由于 API 接口无法控制调用方的行为,因此当遇到瞬时请求量激增时,会导致接口占用过多服务器资源,使得其他请求响应速度降低或是超时,更有甚者可能导致服务器宕机 ... 
- Redis令牌桶限流
		一 .场景描述 在开发接口服务器的过程中,为了防止客户端对于接口的滥用,保护服务器的资源, 通常来说我们会对于服务器上的各种接口进行调用次数的限制.比如对于某个 用户,他在一个时间段(interval ... 
- 一个轻量级的基于RateLimiter的分布式限流实现
		上篇文章(限流算法与Guava RateLimiter解析)对常用的限流算法及Google Guava基于令牌桶算法的实现RateLimiter进行了介绍.RateLimiter通过线程锁控制同步,只 ... 
- 服务限流 -- 自定义注解基于RateLimiter实现接口限流
		1. 令牌桶限流算法 令牌桶会以一个恒定的速率向固定容量大小桶中放入令牌,当有浏览来时取走一个或者多个令牌,当发生高并发情况下拿到令牌的执行业务逻辑,没有获取到令牌的就会丢弃获取服务降级处理,提示一个 ... 
- 库存秒杀问题-redis解决方案- 接口限流
		<?php/** * Created by PhpStorm. * redis 销量超卖秒杀解决方案 * redis 文档:http://doc.redisfans.com/ * ab -n 1 ... 
- Redis+Lua实现限流
		相比Redis事务来说,Lua脚本有以下优点减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求, 而脚本只需一次即可, 减少网络传输;原子操作: Redis 将整个脚本作为一个原子 ... 
随机推荐
- jQuery基本选择器模块(二)
			选择器模块 1.push方法的兼容性(了解) 问题:IE8不支持aplly方法中的第二个参数是 伪数组 目标:实现 push 方法的浏览器兼容性问题 var push = [].push; try { ... 
- phpExcel导出大量数据出现内存溢出错误的解决方法
			phpExcel将读取的单元格信息保存在内存中,我们可以通过 代码如下:PHPExcel_Settings::setCacheStorageMethod() 来设置不同的缓存方式,已达到降低内存消耗的 ... 
- 服务端Linux机器日志查看命令
			常用查看命令 ps -ef | grep java 查看当前机器运行程序: tail -100f /... 查看日志,实时的 less /- 按页查看日志,空格翻页,q退出 cat /.. | gre ... 
- 插入排序InsertSort
			插入排序:从第二个数开始 一直和前面的数组比较 获得排序定位  代码 /** *插入排序 */ public class InsertSort { public static void inser ... 
- 【disconf】环境搭建【linux】
			1.搭建disconf需要安装的配置. 安装Linux:CentOS7 安装Zookeeper:zookeeper-3.4.6 安装Redis:redis-3.0.0 安装N ... 
- ajax异步上传文件和表单同步上传文件 的区别
			1. 用表单上传文件(以照片为例)-同步上传 html部分代码:这里请求地址index.php <!DOCTYPE html> <html lang="en"&g ... 
- 03.IO读写-1.IO介绍
			1 文件操作介绍 in: 输入,读入.从硬盘中读到内存 out: 输出.从内存写到硬盘 文件的作用: 数据存储 2 文件的打开与关闭 2.1 打开文件 在Python,使用open函数,可以打开一个已 ... 
- Python - def 函数
			1.def 函数 如果我们用代码实现了一个小功能,但想要在程序代码中重复使用,不能在代码中到处粘贴这些代码,因为这样做违反了软件工程中 DRY原则. Python 提供了 函数功能,可以将我们这部分功 ... 
- 酒店管理系统(功能结构图、E-R图、用例图)
			功能结构图 用例图 状态图 流程图 数据字典 er图 
- MVC笔记(一)
			1 MVC介绍 MVC是一个编程思想. 是一种设计模式 思想: 将一个功能分解成3个部分, M: Model (模型) 处理数据相关的逻辑 V: View (视图) 显示页面 C: Controlle ... 
