Redis并发数限流

16 篇文章 0 订阅
10 篇文章 0 订阅

一、背景

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。缓存的目的是提升系统访问速度和增大系统能处理的容量;降级是当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉,待高峰或者问题解决后再打开;而有些场景并不能用缓存和降级来解决,比如稀缺资源(秒杀)、写服务(下单),因此需有一种手段来限制这些场景的并发/请求量,即限流。

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

高并发系统常见的限流主要有:限制并发数、限制每秒请求数等。

限流的实现方案有很多种,日常工作主要会遇到如下方式限流:

容器限流:比如 Tomcat、Nginx 等限流手段,Tomcat 可以设置最大线程数, Nginx 提供了两种限流手段:一是控制速率,二是控制并发链接数;
应用服务层限流:我们在应用程序通过限流算法实现限流。

 

如上图,一个完整的系统应该是有Rate_Limit、Rate_Contr两部分的,前者来自我保护,后者依据上游系统限制来控制请求速率,控制请求流量。

此篇文章我们从最简单的一块开始,设计实现对下游系统并发数的限制,即B应用的Rate_Limit部分,此部分通过对下游系统(A)并发数、请求数等限制达到自我保护的目的。

二、分析设计

1.分析
并发数即链接数,很多人搞不清并发数限流和请求数限流,这东西和你工作年限没直接关系,搞不清就搞不清吧。并发是一个茅房有十个坑,最多只能允许十个人同时蹲,不管蹲坑的蹲多久,只有一个提裤子走人,才能来下一个蹲,指的是某一时刻的状态。请求数是不管蹲坑的有没有提裤子走人,间隔指定时间后后面的人就进去蹲,怎么蹲的下就不是你所关心的事了。

如果也关注过限流这块东西,那就肯定知道滑动窗口、令牌桶算法、漏桶算法等... 老生常谈的东西。

不过我们这篇要讲的部分不涉及到这些算法,后面要设计的客户端/服务端请求数限流才会涉及到这部分。分布式环境下开源的一些组件已经不在适用,如Guava的 RateLimiter、Java 并发库的Semaphore,这些只是解决单节点流量限制。心情不好,废话少说,进入下部分.....

2.设计
此处,可以类比令牌桶算法,不同的是,当一块令牌被取走,系统不会自己再生产令牌,而是等着取走令牌的线程办完事归还回来,即固定数的令牌重复使用,依靠固定数的令牌来限制同一时刻在处理的最多任务数,说白了就是一个计数器,和单节点唯一不同的是,就是这个计数的地方不能放在JVM层了,而是放在各节点都可以访问到的地方,即放到Redis里,这么说就没啥难的了,难点就是在使用Redis什么数据结构了,以及怎么保证多操作指令的原子性问题上了。

比如根据交易类型+商户号为维度对下游系统进行并发数限流,我们编写如下Lua脚本:
 

 /**
     * 并发限流初始化Lua脚本
     */
    private final static String CONC_INIT_SCRIPT = "" +
            "local tab = KEYS[1]\n" +
            "local key = ARGV[1]\n" +
            "local init_size = tonumber(ARGV[2])\n" +
            "\n" +
            "local exists = tonumber(redis.call('HEXISTS', tab, key .. '_conc_size'))\n" +
            "if (exists == 0) then\n" +
            "    redis.call('HSET', tab, key .. '_conc_size', init_size)\n" +
            "    redis.call('HSET', tab, key .. '_conc_cur', 0)\n" +
            "else\n" +
            "    redis.call('HSET', tab, key .. '_conc_size', init_size)\n" +
            "end\n";
 
    /**
     * 并发限流Lua脚本
     */
    private final static String CONC_GET_SCRIPT = "" +
            "local tab = KEYS[1]\n" +
            "local key = ARGV[1]\n" +
            "local lock_t = tonumber(ARGV[2])\n" +
            "\n" +
            "local exists = tonumber(redis.call('HEXISTS', tab, key .. '_conc_size'))\n" +
            "if (exists == 0) then\n" +
            "   return -1\n" +
            "end\n"+
            "local max_size = tonumber(redis.call('HGET', tab, key .. '_conc_size'))\n" +
            "\n" +
            "if (lock_t == 1) then\n" +
            "    local lv = tonumber(redis.call('HGET', tab, key .. '_conc_cur'))\n" +
            "    if (lv >= max_size) then\n" +
            "        return 0\n" +
            "    else\n" +
            "        redis.call('HINCRBY', tab, key .. '_conc_cur', 1)\n" +
            "        return 1\n" +
            "    end\n" +
            "else\n" +
            "    local lv = tonumber(redis.call('HGET', tab, key .. '_conc_cur'))\n" +
            "    if (lv > 0) then\n" +
            "        redis.call('HINCRBY', tab, key .. '_conc_cur', -1)\n" +
            "        return 1\n" +
            "    else\n" +
            "        return 0\n" +
            "    end\n" +
            "end\n";

定义如上Lua脚本后,我们可以在Redis中创建了如下数据结构:

Key:trans:limit:concurtable 

Field:transType#merchantNo+_conc_size       最大链接数(总茅坑数)

Field:transType#merchantNo+_conc_cur         已使用链接数(已被蹲数)

127.0.0.1:6379> hgetall trans:limit:concurtable
1) "deduct#20021122_conc_size"
2) "10"
3) "deduct#20021122_conc_cur"
4) "0"

当客户端发起调用,请求到达服务端后,先尝试获取令牌,获取到令牌后才进行下面处理流程,否则直接退出,这步骤实现方式有很多种,过滤器、拦截器、切面等等方式,目的都是在请求到达Controller层之前进行流量控制,案例使用的是切面方式,自定义注解@AccessLimit,标注在指定接口方法,然后切面环绕通知,也就是+1、-1 问题,即完成功能。

源码地址:https://gitee.com/kyn1479/rate_limit.git

下载后只需修改Redis配置,启动后调用限流配置接口:

localhost:8080/testRateLimit/addCurrentLimit?transType=deduct&merchantNo=20021122&maxConc=10

然后调用测试接口:

http://localhost:8080/testRateLimit/currentLimit
{
  "transType": "deduct",
  "merchantNo": "20021122",
  "time": "20211128"
}

运行结果:

2021-12-05 18:41:26.520  INFO 11064 --- [nio-8080-exec-1] c.k.c.aspect.AccessLimitAspect           : AccessLimitAspect.around 进入并发数限流,获取令牌....
2021-12-05 18:41:26.520  INFO 11064 --- [nio-8080-exec-1] c.k.c.service.impl.ConcurentLimitImpl    : 并发控制限流获取令牌key:deduct#20021122
2021-12-05 18:41:26.522  INFO 11064 --- [nio-8080-exec-1] c.k.c.service.impl.ConcurentLimitImpl    : 并发控制限流获取令牌key:deduct#20021122,结果:1
2021-12-05 18:41:26.522  INFO 11064 --- [nio-8080-exec-1] c.k.c.aspect.AccessLimitAspect           : AccessLimitAspect.around before....
2021-12-05 18:41:26.577  INFO 11064 --- [nio-8080-exec-1] c.k.c.controller.RateLimitController     : RateLimitController.test入参:{"merchantNo":"20021122","transType":"deduct"}
2021-12-05 18:41:26.577  INFO 11064 --- [nio-8080-exec-1] c.k.c.aspect.AccessLimitAspect           : AccessLimitAspect.around after....
2021-12-05 18:41:26.577  INFO 11064 --- [nio-8080-exec-1] c.k.c.aspect.AccessLimitAspect           : AccessLimitAspect.around 进入并发数限流,归还令牌....
2021-12-05 18:41:26.577  INFO 11064 --- [nio-8080-exec-1] c.k.c.service.impl.ConcurentLimitImpl    : 并发控制限流归还令牌key:deduct#20021122
2021-12-05 18:41:26.577  INFO 11064 --- [nio-8080-exec-1] c.k.c.service.impl.ConcurentLimitImpl    : 并发控制限流归还令牌key:deduct#20021122,结果:1

三、总结

不管多忙都要有产出吧....
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值