百度架构师分享:通过分布式系统解决限流问题

在分布式领域,我们难免会遇到并发量突增,对后端服务造成高压力,严重甚至会导致系统宕机 。为避免这种问题,我们通常会为接口添加限流、降级、熔断等能力,从而使接口更为健壮 。JAVA领域常见的开源组件有Netflix的hystrix,阿里系开源的sentinel等,都是蛮不错的限流熔断框架 。
今天我们就基于redis组件的特性,实现一个分布式限流组件,名字就定为shield-ratelimiter 。

百度架构师分享:通过分布式系统解决限流问题

文章插图
 
原理
首先解释下为何采用Redis作为限流组件的核心 。
通俗地讲,假设一个用户(用IP判断)每秒访问某服务接口的次数不能超过10次,那么我们可以在Redis中创建一个键,并设置键的过期时间为60秒 。
当一个用户对此服务接口发起一次访问就把键值加1,在单位时间(此处为1s)内当键值增加到10的时候,就禁止访问服务接口 。PS:在某种场景中添加访问时间间隔还是很有必要的 。我们本次不考虑间隔时间,只关注单位时间内的访问次数 。
需求
原理已经讲过了,说下需求 。
【百度架构师分享:通过分布式系统解决限流问题】1、基于Redis的incr及过期机制开发 2、调用方便,声明式 3、Spring支持
基于上述需求,我们决定基于注解方式进行核心功能开发,基于Spring-boot-starter作为基础环境,从而能够很好的适配Spring环境 。
另外,在本次开发中,我们不通过简单的调用Redis的java类库API实现对Redis的incr操作 。
原因在于,我们要保证整个限流的操作是原子性的,如果用Java代码去做操作及判断,会有并发问题 。这里我决定采用Lua脚本进行核心逻辑的定义 。
 
为何使用Lua
在正式开发前,我简单介绍下对Redis的操作中,为何推荐使用Lua脚本 。
1、减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求, 而脚本只需一次即可, 减少网络传输; 2、原子操作: Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务; 3、复用: 脚本会永久保存 Redis 中, 其他客户端可继续使用.
Redis添加了对Lua的支持,能够很好的满足原子性、事务性的支持,让我们免去了很多的异常逻辑处理 。对于Lua的语法不是本文的主要内容,
正式开发
到这里,我们正式开始手写限流组件的进程 。
1. 工程定义
项目基于maven构建,主要依赖Spring-boot-starter,我们主要在springboot上进行开发,因此自定义的开发包可以直接依赖下面这个坐标,方便进行包管理 。版本号自行选择稳定版 。
2. Redis整合
由于我们是基于Redis进行的限流操作,因此需要整合Redis的类库,上面已经讲到,我们是基于Springboot进行的开发,因此这里可以直接整合RedisTemplate 。
2.1 坐标引入
这里我们引入spring-boot-starter-redis的依赖 。
2.2 注入CacheManager及RedisTemplate
新建一个Redis的配置类,命名为RedisCacheConfig,使用javaconfig形式注入CacheManager及RedisTemplate 。为了操作方便,我们采用了Jackson进行序列化 。代码如下
注意要使用@Configuration 标注此类为一个配置类,当然你可以使用@Component ,但是不推荐,原因在于@Component 注解虽然也可以当作配置类,但是并不会为其生成CGLIB代理Class,而使用@Configuration ,CGLIB会为其生成代理类,进行性能的提升 。
2.3 调用方Application.propertie需要增加Redis配置
我们的包开发完毕之后,调用方的application.properties需要进行相关配置如下:
如果有密码的话,配置password即可 。
这里为单机配置,如果需要支持哨兵集群,则配置如下,Java代码不需要改动,只需要变动配置即可 。注意 两种配置不能共存!
3. 定义注解
为了调用方便,我们定义一个名为RateLimiter 的注解,内容如下
该注解明确只用于方法,主要有三个属性 。
1、key–表示限流模块名,指定该值用于区分不同应用,不同场景,推荐格式为:应用名:模块名:ip:接口名:方法名 2、limit–表示单位时间允许通过的请求数 3、expire–incr的值的过期时间,业务中表示限流的单位时间 。
 
4. 解析注解
定义好注解后,需要开发注解使用的切面,这里我们直接使用aspectj进行切面的开发 。先看代码
这里是注入了RedisTemplate,使用其API进行Lua脚本的调用 。
init() 方法在应用启动时会初始化DefaultRedisScript,并加载Lua脚本,方便进行调用 。


推荐阅读