您现在的位置是:首页 > 正文

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

2024-04-01 00:11:11阅读 2

原文链接:http://xiaoqiangge.com/aritcle/1513004492550.html

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

image

前几天在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

image

点评

这种方式可以实现限流功能,但是有一个很严重的问题,窗口中数据过期时间不均匀。所谓时间均匀就是确保每条数据都能遵守过期时间合约,但是上面这个代码不能完全遵守过期时间合约,如下测试,

@Test
    public void test01(){
        JedisPoolConfig config = new JedisPoolConfig();
        config.setMaxIdle(20);
        config.setMaxTotal(40);
        config.setMinIdle(10);
        JedisPool jedisPool = new JedisPool(config, "127.0.0.1", 32768, 1000);
        while(true){
            String value =   RedisRateLimiter.acquireTokenFromBucket(jedisPool.getResource(),5,10000);
            log.info(">> {}",value);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

代码中设置的限流条数是5条,限流时是10妙,意思就是说10秒内,最多流量为5条,也就是没一条都得遵守10秒过期的合约。输出的日志如下,

22:34:32.667 [main] INFO com.eju.ess.MyTest - >> b7a6da80-6e8c-489a-8892-d841974c36d6
22:34:33.680 [main] INFO com.eju.ess.MyTest - >> e85155f4-07a5-4be4-8db5-60043b9ae6af
22:34:34.682 [main] INFO com.eju.ess.MyTest - >> 5cf9aa87-9e90-4a3f-8ffa-09217159e2d1
22:34:35.683 [main] INFO com.eju.ess.MyTest - >> ce9c7106-f291-4e0a-8a19-bd20c492aa2c
22:34:36.685 [main] INFO com.eju.ess.MyTest - >> 325add3a-ba66-42a7-a741-320f48e90525
22:34:37.687 [main] INFO com.eju.ess.MyTest - >> null
22:34:38.689 [main] INFO com.eju.ess.MyTest - >> null
22:34:39.690 [main] INFO com.eju.ess.MyTest - >> null
22:34:40.691 [main] INFO com.eju.ess.MyTest - >> null
22:34:41.693 [main] INFO com.eju.ess.MyTest - >> null
22:34:42.694 [main] INFO com.eju.ess.MyTest - >> b456493d-81d5-4a4d-82c9-bb08300cc9c1
22:34:43.695 [main] INFO com.eju.ess.MyTest - >> f2078bd6-dfcb-4ab1-bc69-fdfbfb17437b
22:34:44.696 [main] INFO com.eju.ess.MyTest - >> 88c305fe-a888-4979-a3ca-a90c51290905
22:34:45.698 [main] INFO com.eju.ess.MyTest - >> d7d59be2-01a8-4023-9e13-14bb855b761a
22:34:46.699 [main] INFO com.eju.ess.MyTest - >> 13010e9a-6cbb-44dc-8c20-7e220104b9b9
22:34:47.700 [main] INFO com.eju.ess.MyTest - >> null
22:34:48.702 [main] INFO com.eju.ess.MyTest - >> null
22:34:49.703 [main] INFO com.eju.ess.MyTest - >> null
22:34:50.704 [main] INFO com.eju.ess.MyTest - >> null
22:34:51.706 [main] INFO com.eju.ess.MyTest - >> null
22:34:52.707 [main] INFO com.eju.ess.MyTest - >> 0b2c9cee-e8f6-4cab-b571-6628ebce10a5
22:34:53.709 [main] INFO com.eju.ess.MyTest - >> f93da7e3-f48f-4321-b573-d3f586775728
22:34:54.710 [main] INFO com.eju.ess.MyTest - >> 0e5f9b4a-a8b0-482e-89fb-5efb7b6fd458
22:34:55.711 [main] INFO com.eju.ess.MyTest - >> be271748-44bd-4850-9f2a-06b18986f536
22:34:56.713 [main] INFO com.eju.ess.MyTest - >> a645c321-35a2-4738-93f4-dbbf5224f000
22:34:57.714 [main] INFO com.eju.ess.MyTest - >> null
22:34:58.715 [main] INFO com.eju.ess.MyTest - >> null
22:34:59.717 [main] INFO com.eju.ess.MyTest - >> null
22:35:00.718 [main] INFO com.eju.ess.MyTest - >> null

从上面日志看到,只有b7a6da80-6e8c-489a-8892-d841974c36d6``b456493d-81d5-4a4d-82c9-bb08300cc9c1``0b2c9cee-e8f6-4cab-b571-6628ebce10a5这三条记录遵守了过期合约,其余的没有遵守,也就是5条之中有一条到期了,整个都会到期。

总结

上面的代码可以对分布式限流实现了部分,但是不够完美。

网站文章

  • Spring Boot系列八 spring boot集成jsp、restful接口、springmvc基本功能

    本文介绍spring boot里的spring mvc部分的用法,主要包括如下:- 1 实现 spring boot 集成jsp: @Controller+ @RequestMapping- 2 模拟登录功能: ModelAndView- 3 spring boot 实现restful接口:@RestController

    2024-04-01 00:11:04
  • databinding的简单使用步骤

    1.开启databindingandroid { //在 app 的 build.grandle 中添加开启配置 dataBinding { enabled = true }}2、创建实体public class User { public String name; public User(String name) { this...

    2024-04-01 00:10:57
  • ESP32 Arduino (十) HTTPClient库

    ESP32 Arduino (十) HTTPClient库

    很多时候我们需要ESP32去访问外网的服务器获取一些网络资源,HTTP服务器是最常见的服务器,在这个时候就需要ESP32作为HTTPClient使用,HttpClient是Apache中的一个开源的项...

    2024-04-01 00:10:33
  • 打印机文件服务器主机,打印机服务器主机名称是什么原因

    打印机文件服务器主机,打印机服务器主机名称是什么原因

    打印机服务器主机名称是什么原因 内容精选换一换成为进阶Linux大佬的第一步一、操作系统1、操作系统为接口的示意图2、不同领域的主流操作系统桌面操作系统服务器操作系统嵌入式操作系统移动设备操作系统 3...

    2024-04-01 00:10:20
  • Vue组件化学习之scoped

    Vue组件化学习之scoped

    简介主要介绍scoped的作用。先弄一个案例:main.js://引入vue依赖import Vue from 'vue'//引入组件Appimport App from './App.vue'// ...

    2024-04-01 00:10:14
  • JS判断是否为base64字符串&如何转换为图片src格式

    JS判断是否为base64字符串&如何转换为图片src格式

    JS判断是否为base64字符串&如何转换为图片src格式需求背景 :如何判断后端给返回的 字符串 是否为 base-64 位 呢 ?以及如果判断为是的话,如何给它进行转换为 img 标签可使用的那种 src 格式 呢 ?

    2024-04-01 00:09:44
  • 给我一个java 策略模式和工厂模式一起实用的例子

    策略模式可以用来实现一组可互换的算法,例如在不同环境下使用不同的算法。工厂模式可以用来创建一组相关的对象,例如在一个网站中创建不同类型的用户。一个实用的例子就是使用策略模式和工厂模式来创建一个网站,该网站根据不同的环境使用不同的算法来创建不同类型的用户。 ...

    2024-04-01 00:09:39
  • rmit计算机科学选课,RMIT最强最全的IT Master选课指南,“鼠”于你的高分秘籍!...

    rmit计算机科学选课,RMIT最强最全的IT Master选课指南,“鼠”于你的高分秘籍!...

    RMIT的IT课程设计十分地人性化,每个学期的课程根据自身知识的储备的不同会有多个方向的选择。但是,如果对自己的了解不够深入,或者对课程的英语描述不清楚的同学很容易踩雷。所以RMIT的学霸学长根据自己...

    2024-04-01 00:09:31
  • sql 字段中的百分号

    SQL模糊查询的时候,如果字段中有%,我们又希望查找出所有有%的字段,使用select * from table where name like &#39;%%%&#39;类似的语句是不行的。这时候我们可以使用select * from table where name like &#39;%[%]%。

    2024-04-01 00:09:06
  • Failed to execute goal org.apache.maven.pluginsmaven-install-plugin2.4install (default-install)

    Failed to execute goal org.apache.maven.pluginsmaven-install-plugin2.4install (default-install)

    项目打包的时候,出现下面的报错信息: Failed to execute goal org.apache.maven.plugins:maven-install-plugin:2.4:install ...

    2024-04-01 00:08:53