Redis + Lua 实现系统限流

文章参考翻译自搜云库的一篇文章:原文详细地址

 

高并发系统时有三把利器可以保护系统稳定:限流、降级、缓存。今天聊聊限流方案以及实现

 

▎了解什么是限流、以及限流的意义

为什么需要限流呢?相信大家都经历过春运高铁的安检,场景如下

为什么要摆这样的长龙阵进站呢?答案就是为了限流,如果一下涌进去太多人会对安检造成过大的负担,存在安全隐患

联系到互联网场景中,某些高并发系统的流量巨大,尤其像网站的促销秒杀活动,为了保证系统不被巨大的流量压垮,上线前会做流量峰值的评估,其中TPS/QPS是衡量系统处理能力两个重要指标

TPS(Transactions Per Second) 系统每秒事务数

QPS(Queries Per Second) 系统每秒查询率

限流就是当系统流量到达一定阀值的时候,拒绝掉一部分流量,假设系统每秒处理请求的阀值是100,理论上这一秒内100以后的请求都将被拒绝。

 

限流解决方案

1:漏铜算法

漏桶算法思路:

       我们把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。

 

2:令牌桶算法

令牌桶算法思路:

        我们可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。系统会维护一个令牌(token)桶,以一个恒定的速度往桶里放入令牌(token),这时如果有请求进来想要被处理,则需要先从桶里获取一个令牌(token),当桶里没有令牌(token)可取时,则该请求将被拒绝服务。令牌桶算法通过控制桶的容量、发放令牌的速率,来达到对请求的限制。

 

3:Redis + Lua

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能,Redis支持Lua脚本,所以通过Lua实现限流的算法。

Lua脚本实现算法对比操作Redis实现算法的优点:

  • 减少网络开销:使用Lua脚本,无需向Redis 发送多次请求,执行一次即可,减少网络传输

  • 原子操作:Redis 将整个Lua脚本作为一个命令执行,原子,无需担心并发

  • 复用:Lua脚本一旦执行,会永久保存 Redis 中,,其他客户端可复用

 

Redis + Lua 实现

Lua环境安装

Linux安装Lua步骤:

curl -R -O http://www.lua.org/ftp/lua-5.3.0.tar.gz
tar zxf lua-5.3.0.tar.gz
cd lua-5.3.0
make linux test # 检查依赖,缺什么就安装什么,通过后再执行下一步
make install

Windows安装Lua步骤

安装包下载地址:https://github.com/rjpcomputing/luaforwindows/releases

下载完成后、双击安装即可在该环境下编写 Lua 程序并运行

 

使用 lua -i 或 lua 命令检查Lua环境是否安装成功

$ lua -i 
$ Lua 5.3.0  Copyright (C) 1994-2015 Lua.org, PUC-Rio

Redis环境安装

Linux安装Redis

$ wget http://download.redis.io/releases/redis-5.0.8.tar.gz
$ tar xzf redis-5.0.8.tar.gz
$ cd redis-5.0.8
$ make

Windows安装Redis

安装包下载地址:https://redis.io/download

下载完成后,双击安装

 

搭建SpringBoot项目,引入依赖

<!-- web -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- redis -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- aop -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

项目整合Redis

application.properties配置

spring.redis.host=127.0.0.1
spring.redis.port=6379
# 如果没配置redis认证,password不需要配
spring.redis.password=Mote12345

配置RedisTemplate 

@Configuration
public class RedisConfig {
	
	@Bean
	public RedisTemplate<String, Serializable> limitRedisTemplate(
			LettuceConnectionFactory redisConnectionFactory) {
		RedisTemplate<String, Serializable> template = new RedisTemplate<>();
		template.setKeySerializer(new StringRedisSerializer());
		template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
		template.setConnectionFactory(redisConnectionFactory);
		return template;
	}
}

限流类型枚举类 

public enum LimitType {
	// 自定义key
	CUSTOMER,
	
	// 请求IP
	IP;
}

自定义@Limit注解

period表示请求限制时间段,count表示在period这个时间段内允许放行请求的次数。limitType代表限流的类型,可以根据请求的IP自定义key,如果不传limitType属性则默认用方法名作为默认key。

//表明注解可用于的地方  METHOD:方法上  TYPE:用于描述类、接口(包括注解类型) 或enum声明
@Target({ElementType.METHOD, ElementType.TYPE}) 
//存活阶段   runtime:运行期
@Retention(RetentionPolicy.RUNTIME)
//可继承
@Inherited
//作用域 javaDoc
@Documented
public @interface Limit {

	// key
	String key() default "";

	// 给定的时间范围
	int period();

	// 一定时间内最多访问次数
	int count();

	// 限流的类型  (自定义key或者请求ip)
	LimitType limitType() default LimitType.CUSTOMER;

}

定义切面类

@Aspect
@Configuration
public class LimitInterceptor {

	@Autowired
	private RedisTemplate<String, Serializable> redisTemplate;

	/**
	 * 拦截有@Limit注解的public方法
	 * 
	 * @param pjp
	 * @return
	 */
	@Around("execution(public * *(..)) && @annotation(com.mote.lua.Limit)")
	public Object interceptor(ProceedingJoinPoint ppt) {

		// 获取方法对象
		MethodSignature signature = (MethodSignature) ppt.getSignature();
		Method method = signature.getMethod();

		// 获取@Limit注解对象
		Limit limitAnnotation = method.getAnnotation(Limit.class);

		// 获取key类型
		LimitType limitType = limitAnnotation.limitType();

		// 获取请求限制时间段、请求限制次数
		int limitPeriod = limitAnnotation.period();
		int limitCount = limitAnnotation.count();

		// 根据限流类型获取不同的key ,如果不传以方法名作为key
		String key;
		switch (limitType) {
		case IP:
			key = getIpAddress();
			break;
		case CUSTOMER:
			key = limitAnnotation.key();
			break;
		default:
			key = method.getName();
		}

		// 定义key参数
		List<String> keys = new ArrayList<String>();
		keys.add(key);

		try {
			// 获取Lua脚本内容
			String luaScript = buildLuaScript();

			// Reids整合Lua
			RedisScript<Number> redisScript = new DefaultRedisScript<>(
					luaScript, Number.class);
			// 执行Lua,并返回key值
			Number count = redisTemplate.execute(redisScript, keys, limitCount,
					limitPeriod);

			// 判断是否阻止请求
			if (count != null && count.intValue() <= limitCount) {
				return ppt.proceed();
			} else {
				throw new RuntimeException("please try again later");
			}
		} catch (Throwable e) {
			if (e instanceof RuntimeException) {
				throw new RuntimeException(e.getLocalizedMessage());
			}
			throw new RuntimeException("server error");
		}

	}

	/**
	 * 编写 redis Lua 限流脚本
	 */
	public String buildLuaScript() {
		StringBuilder lua = new StringBuilder();
		lua.append("local c");
		lua.append("\nc = redis.call('get',KEYS[1])");
		// 调用不超过最大值,则直接返回
		lua.append("\nif c and tonumber(c) > tonumber(ARGV[1]) then");
		lua.append("\nreturn c;");
		lua.append("\nend");
		// 执行计算器自加
		lua.append("\nc = redis.call('incr',KEYS[1])");
		lua.append("\nif tonumber(c) == 1 then");
		// 从第一次调用开始限流,设置对应键值的过期
		lua.append("\nredis.call('expire',KEYS[1],ARGV[2])");
		lua.append("\nend");
		lua.append("\nreturn c;");
		return lua.toString();
	}

	/**
	 * 获取请求ip
	 */
	public String getIpAddress() {
		HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
				.getRequestAttributes()).getRequest();
		String ip = request.getHeader("x-forwarded-for");
		if (ip == null || ip.length() == 0) {
			ip = request.getHeader("Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0) {
			ip = request.getHeader("WL-Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0) {
			ip = request.getRemoteAddr();
		}
		return ip;
	}
}

下面写个Controller测试一下限流

@RestController
public class LimiterController {

	private static int count1 = 0;
	private static int count2 = 0;
	private static int count3 = 0;

	/**
	 * 20秒内允许请求3次,key为方法名称
	 * 
	 * @return
	 */
	@Limit(key = "limitTest", period = 20, count = 3)
	@GetMapping("/limit1")
	public String testLimiter1() {
		return "success--" + ++count1;
	}

	/**
	 * 20秒内允许请求3次,自定义key
	 * 
	 * @return
	 */
	@Limit(key = "customer_limit_test", period = 20, count = 3, limitType = LimitType.CUSTOMER)
	@GetMapping("/limit2")
	public String testLimiter2() {
		return "success--" + ++count2;
	}

	/**
	 * 20秒内允许请求3次,key为请求ip
	 * 
	 * @return
	 */
	@Limit(period = 20, count = 3, limitType = LimitType.IP)
	@GetMapping("/limit3")
	public String testLimiter3() {
		return "success--" + ++count3;
	}

}

 

测试:连续请求3次均可以成功,第4次请求被拒绝

----------------------------分割线----------------------------------- 

----------------------------分割线-----------------------------------  

----------------------------分割线-----------------------------------  

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值