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次请求被拒绝

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

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

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
实现分布式限流可以使用 RedisLua 脚本来完成。以下是可能的实现方案: 1. 使用 Redis 的 SETNX 命令来实现基于令牌桶算法的限流 令牌桶算法是一种常见的限流算法,它可以通过令牌的放置和消耗来控制流量。在 Redis 中,我们可以使用 SETNX 命令来实现令牌桶算法。 具体实现步骤如下: - 在 Redis 中创建一个有序集合,用于存储令牌桶的令牌数量和时间戳。 - 每当一个请求到达时,我们首先获取当前令牌桶中的令牌数量和时间戳。 - 如果当前时间戳与最后一次请求的时间戳之差大于等于令牌桶中每个令牌的发放时间间隔,则将当前时间戳更新为最后一次请求的时间戳,并且将令牌桶中的令牌数量增加相应的数量,同时不超过最大容量。 - 如果当前令牌桶中的令牌数量大于等于请求需要的令牌数量,则返回 true 表示通过限流,将令牌桶中的令牌数量减去请求需要的令牌数量。 - 如果令牌桶中的令牌数量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现令牌桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 令牌桶的容量 local capacity = tonumber(ARGV[1]) -- 令牌的发放速率 local rate = tonumber(ARGV[2]) -- 请求需要的令牌数量 local tokens = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取当前令牌桶中的令牌数量和时间戳 local bucket = redis.call('ZREVRANGEBYSCORE', key, now, 0, 'WITHSCORES', 'LIMIT', 0, 1) -- 如果令牌桶为空,则初始化令牌桶 if not bucket[1] then redis.call('ZADD', key, now, capacity - tokens) return 1 end -- 计算当前令牌桶中的令牌数量和时间戳 local last = tonumber(bucket[2]) local tokensInBucket = tonumber(bucket[1]) -- 计算时间间隔和新的令牌数量 local timePassed = now - last local newTokens = math.floor(timePassed * rate) -- 更新令牌桶 if newTokens > 0 then tokensInBucket = math.min(tokensInBucket + newTokens, capacity) redis.call('ZADD', key, now, tokensInBucket) end -- 检查令牌数量是否足够 if tokensInBucket >= tokens then redis.call('ZREM', key, bucket[1]) return 1 else return 0 end ``` 2. 使用 RedisLua 脚本来实现基于漏桶算法的限流 漏桶算法是另一种常见的限流算法,它可以通过漏桶的容量和漏水速度来控制流量。在 Redis 中,我们可以使用 Lua 脚本来实现漏桶算法。 具体实现步骤如下: - 在 Redis 中创建一个键值对,用于存储漏桶的容量和最后一次请求的时间戳。 - 每当一个请求到达时,我们首先获取当前漏桶的容量和最后一次请求的时间戳。 - 计算漏水速度和漏水的数量,将漏桶中的容量减去漏水的数量。 - 如果漏桶中的容量大于等于请求需要的容量,则返回 true 表示通过限流,将漏桶中的容量减去请求需要的容量。 - 如果漏桶中的容量不足,则返回 false 表示未通过限流。 下面是使用 RedisLua 脚本实现漏桶算法的示例代码: ```lua -- 限流的 key local key = KEYS[1] -- 漏桶的容量 local capacity = tonumber(ARGV[1]) -- 漏水速度 local rate = tonumber(ARGV[2]) -- 请求需要的容量 local size = tonumber(ARGV[3]) -- 当前时间戳 local now = redis.call('TIME')[1] -- 获取漏桶中的容量和最后一次请求的时间戳 local bucket = redis.call('HMGET', key, 'capacity', 'last') -- 如果漏桶为空,则初始化漏桶 if not bucket[1] then redis.call('HMSET', key, 'capacity', capacity, 'last', now) return 1 end -- 计算漏水的数量和漏桶中的容量 local last = tonumber(bucket[2]) local capacityInBucket = tonumber(bucket[1]) local leak = math.floor((now - last) * rate) -- 更新漏桶 capacityInBucket = math.min(capacity, capacityInBucket + leak) redis.call('HSET', key, 'capacity', capacityInBucket) redis.call('HSET', key, 'last', now) -- 检查容量是否足够 if capacityInBucket >= size then return 1 else return 0 end ``` 以上是使用 RedisLua 脚本实现分布式限流的两种方案,可以根据实际需求选择适合的方案。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值