秒杀系统限流,听上去有点反人类!既然是秒杀系统为什么要限流呢?秒杀系统限流操作是为了解决高并发场景中由于秒杀活动中,用户请求量大导致的服务器过载、崩溃,甚至有是有因为业务逻辑处理不及时导致的数据不一致的问题。为了避免这些问题,限流就成了保证秒杀系统正常使用必不可少的环节,下面就来看看,在秒杀系统中进行限流的常见的解决方案。
令牌桶算法
根据系统的处理能力预先生成一定数量的令牌,用户每次请求需要获取一个令牌,如果令牌池中没有足够的令牌,则拒绝请求。令牌桶算法可以通过限制令牌的生成速率来控制系统的请求速率,适用于平滑限流。
常见的令牌桶算法可以选择Guava RateLimiter、Redisson RateLimiter 等。具体实现如下所示。
// 使用 Guava RateLimiter 实现令牌桶算法
RateLimiter limiter = RateLimiter.create(100); // 每秒产生 100 个令牌
public void seckill() {
if (limiter.tryAcquire()) {
// 处理秒杀请求
} else {
// 返回秒杀失败信息
}
}
漏桶算法
类似于一个漏斗,固定速率的请求会被放入漏斗中,超出的请求会被丢弃或等待下一次处理。漏桶算法适用于平稳流量和突发流量的限流场景。
常见的漏桶算法可以使用自定义实现、或者是使用Guava RateLimiter。
// 使用自定义漏桶算法实现
private final BlockingQueue<Object> bucket = new ArrayBlockingQueue<>(100); // 漏桶容量为 100
public void seckill() {
if (bucket.offer(new Object())) {
// 处理秒杀请求
} else {
// 返回秒杀失败信息
}
}
基于队列的流控
基于队列实现流控是在秒杀系统、或者是在大促场景下经常使用的一种方案。通过队列对请求进行排队和调度,限制队列的长度或排队等待时间来控制系统的请求量。队列的长度超过一定阈值时,拒绝新的请求加入队列。来实现对于进入系统流量的控制。
常见的队列可以使用LinkedBlockingQueue、DelayQueue 等,当然还可以使用一些高级的消息中间件来实现。
// 使用 LinkedBlockingQueue 实现队列流控
private final BlockingQueue<Request> queue = new LinkedBlockingQueue<>(100); // 队列容量为 100
public void seckill(Request request) {
if (queue.offer(request)) {
// 处理秒杀请求
} else {
// 返回秒杀失败信息
}
}
限制IP访问频率
根据用户的IP地址来限制其访问频率,设置一定的时间窗口内允许的最大访问次数,超过限制则拒绝请求。避免单个IP多次刷新,刷爆秒杀接口。
可以使用Servlet Filter、Spring Interceptor、Guava Cache 等来实现限制,当然还可以已使用Redis来完成IP限流操作。下面是使用 Guava Cache 实现 IP 访问频率限制实现。
// 使用 Guava Cache 实现 IP 访问频率限制
private static final Cache<String, AtomicInteger> cache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.SECONDS) // 设置过期时间为 1 秒
.build();
public void seckill(HttpServletRequest request) {
String ip = request.getRemoteAddr();
AtomicInteger counter = cache.get(ip, () -> new AtomicInteger(0));
if (counter.incrementAndGet() <= 100) { // 每秒最多处理 100 次请求
// 处理秒杀请求
} else {
// 返回秒杀失败信息
}
}
使用Redis实现IP限流操作?
基于Redis实现IP访问率限制的流控操作可以通过Redis的计数器和过期时间来实现。代码如下所示。
public class IPAccessRateLimiter {
private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
private static final int MAX_REQUESTS_PER_SECOND = 100; // 每秒最大请求次数
private Jedis jedis;
public IPAccessRateLimiter() {
jedis = new Jedis(REDIS_HOST, REDIS_PORT);
}
// 判断 IP 是否允许访问
public boolean allowAccess(String ip) {
String key = "rate_limit:" + ip;
long count = jedis.incr(key); // 计数器自增
jedis.expire(key, 1); // 设置过期时间为 1 秒
return count <= MAX_REQUESTS_PER_SECOND; // 判断访问次数是否超过限制
}
public static void main(String[] args) {
IPAccessRateLimiter limiter = new IPAccessRateLimiter();
for (int i = 0; i < 150; i++) {
String ip = "192.168.1.1"; // 模拟 IP 地址
boolean allowed = limiter.allowAccess(ip);
System.out.println("IP " + ip + " access allowed: " + allowed);
}
}
}
在上面的示例中,我们使用Redis中的计数器来记录每个IP地址的访问次数,然后设置了过期时间为1秒,即每秒钟重置一次访问次数。通过比较当前的访问次数与最大请求次数,判断是否允许访问。如果访问次数超过了限制,则返回false,否则返回true。
将上述的逻辑判断加入到Spring Boot拦截器或者是过滤器中,就可以实现接口的限流操作。
在实际使用场景中,需要根据系统的访问流量,操作业务等选择合适的流控方案。
本文暂时没有评论,来添加一个吧(●'◡'●)