高并发系统有三大特征:限流、缓存和熔断,所以限流已经成为当下系统开发中必备的功能了。那么,什么是限流?如何实现限流?使用 Redis 能不能实现限流?接下来我们一起来看。
限流是指在各种应用场景中,通过技术和策略手段对数据流量、请求频率或资源消耗进行有计划的限制,以避免系统负载过高、性能下降甚至崩溃的情况发生。限流的目标在于维护系统的稳定性和可用性,并确保服务质量。
使用限流有以下几个好处:
限流的常见实现算法有以下几个:
使用 Redis 也可以实现简单的限流,它的常见限流方法有以下几种实现:
了解了以上概念后,接下来我们来看具体的实现。
此方法的实现思路是:使用一个计数器存储当前请求量(每次使用 incr 方法相加),并设置一个过期时间,计数器在一定时间内自动清零,从而实现限流。
它的具体操作步骤如下:
具体实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
import redis.clients.jedis.Jedis; public class RedisRateLimiter { private static final String REDIS_KEY = "request_counter"; private static final int REQUEST_LIMIT = 100; // 限流阈值 private static final int EXPIRE_TIME = 60; // 过期时间(秒) public boolean allowRequest() { Jedis jedis = new Jedis("localhost"); try { Long counter = jedis.incr(REDIS_KEY); if (counter == 1) { // 第一次设置过期时间 jedis.expire(REDIS_KEY, EXPIRE_TIME); } if (counter <= REQUEST_LIMIT) { return true; // 允许请求通过 } else { return false; // 请求达到限流阈值,拒绝请求 } } finally { jedis.close(); } } public static void main(String[] args) { RedisRateLimiter rateLimiter = new RedisRateLimiter(); for (int i = 0; i < 110; i++) { if (rateLimiter.allowRequest()) { System.out.println("Request Allowed"); } else { System.out.println("Request Denied (Rate Limited)"); } } } } |
在上述代码中,每次请求会通过 allowRequest() 方法判断是否达到限流阈值,如果未达到则允许通过,并递增计数器的值,否则拒绝请求。同时,第一次设置计数器的过期时间,使得计数器在指定的时间内自动清零。
PS:以上是一个简单的示例,实际应用中需要根据具体场景实现更复杂的限流逻辑,并考虑并发情况下的线程安全性等问题。
因为计算器算法有突刺问题,因此我们需要使用升级版的滑动窗口算法或其他限流算法来解决此问题。
此方法的实现思路是:将请求都存入到 ZSet 集合中,在分数(score)中存储当前请求时间。然后再使用 ZSet 提供的 range 方法轻易的获取到 2 个时间戳内的所有请求,通过获取的请求数和限流数进行比较并判断,从而实现限流。
它的具体操作步骤如下:
具体实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
import redis.clients.jedis.Jedis; import redis.clients.jedis.Tuple; import java.util.Set; public class RedisSlidingWindowRateLimiter { private static final String ZSET_KEY = "request_timestamps"; private static final int WINDOW_SIZE = 60; // 时间窗口大小(单位:秒) private static final int REQUEST_LIMIT = 100; // 限流阈值 public boolean allowRequest() { Jedis jedis = new Jedis("localhost"); long currentTimestamp = System.currentTimeMillis() / 1000; // 添加当前请求的时间戳到有序集合 jedis.zadd(ZSET_KEY, currentTimestamp, String.valueOf(currentTimestamp)); // 移除过期的请求时间戳,保持时间窗口内的请求 long start = currentTimestamp - WINDOW_SIZE; long end = currentTimestamp; jedis.zremrangeByScore(ZSET_KEY, 0, start); // 查询当前时间窗口内的请求数量 Set<Tuple> requestTimestamps = jedis.zrangeByScoreWithScores(ZSET_KEY, start, end); long requestCount = requestTimestamps.size(); jedis.close(); // 判断请求数量是否超过限流阈值 return requestCount <= REQUEST_LIMIT; } public static void main(String[] args) { RedisSlidingWindowRateLimiter rateLimiter = new RedisSlidingWindowRateLimiter(); for (int i = 0; i < 110; i++) { if (rateLimiter.allowRequest()) { System.out.println("Request Allowed"); } else { System.out.println("Request Denied (Rate Limited)"); } } } } |
在上述代码中,每次收到请求时,将当前请求的时间戳加入到有序集合中,并移除过期的请求时间戳,然后查询当前时间窗口内的请求数量,判断是否达到限流阈值。这样基于 Redis 的滑动窗口限流算法可以有效控制单位时间内的请求流量,避免系统被过多请求压垮。
此方法的实现思路是:在程序中使用定时任务给 Redis 中的 List 添加令牌,程序通过 List 提供的 leftPop 来获取令牌,得到令牌继续执行,否则就是限流不能继续运行。
在 Spring Boot 项目中,通过定时任务给 Redis 中的 List 每秒中添加一个令牌(当然也可以通过修改定时任务的执行时间来控制令牌的发放速度),具体实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 |
@Configuration // 1.注入到 IoC 中,启动程序时加载 @EnableScheduling // 2.开启定时任务 public class SaticScheduleTask { @Autowired private RedisTemplate redisTemplate; // 3.添加定时任务 @Scheduled(fixedRate = 1000) private void configureTasks() { redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString()); } } |
令牌的获取代码如下:
1 2 3 4 5 6 7 |
public boolean allowRequest(){ Object result = redisTemplate.opsForList().leftPop("limit_list"); if(result == null){ return false; } return true; } |
在上述代码中,我们每次访问 allowRequest() 方法时,会尝试从 Redis 中获取一个令牌,如果拿到令牌了,那就说明没超出限制,可以继续执行,反之则不能执行。
使用 Redis 实现限流有什么优缺点?为什么微服务中不会使用 Redis 实现限流?