应对并发之接口限流

       Java开发中,高并发一直都是我们比较关注的,网上针对高并发有各种各样的应对方案,有加缓存、服务降级、接口限流等等等,今天我们主要讲接口限流方面的东西。

       接口限流其实主要就是分布式限流,分布式限流最关键的是讲限流服务做成原子化,而解决方案可以使用redis+lua或者nginx+lua。

今天我主要讲的是redis+lua进行限流:

       首先在项目中自定义了一个Limit的注解,当接口加上这个@Limit注解时,代表该接口需要限流,然后使用了AOP切加了该注解的接口,在AOP方法中使用redis+lua做接口限流

Limit注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Limit {

    // 资源名称,用于描述接口功能
    String name() default "";

    // 资源 key
    String key() default "";

    // key prefix
    String prefix() default "";

    // 时间的,单位秒
    int period();

    // 限制访问次数
    int count();

    // 限制类型
    LimitType limitType() default LimitType.CUSTOMER;
}

切面类以及Lua脚本,都写在同一个方法里了 

@Aspect
@Component
public class LimitAspect {

    private static final Logger logger = LoggerFactory.getLogger(LimitAspect.class);

    private final RedisTemplate<String, Serializable> limitRedisTemplate;

    @Autowired
    public LimitAspect(RedisTemplate<String, Serializable> limitRedisTemplate) {
        this.limitRedisTemplate = limitRedisTemplate;
    }

    @Pointcut("@annotation(cc.mrbird.common.annotation.Limit)")
    public void pointcut() {
        // do nothing
    }

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();

        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Limit limitAnnotation = method.getAnnotation(Limit.class);
        LimitType limitType = limitAnnotation.limitType();
        String name = limitAnnotation.name();
        String key;
        int limitPeriod = limitAnnotation.period();
        int limitCount = limitAnnotation.count();
        switch (limitType) {
            case IP:
                key = IPUtils.getIpAddr(request);
                break;
            case CUSTOMER:
                key = limitAnnotation.key();
                break;
            default:
                key = StringUtils.upperCase(method.getName());
        }
        ImmutableList<String> keys = ImmutableList.of(StringUtils.join(limitAnnotation.prefix() + "_", key + "_" + request.getRequestedSessionId()));
        String luaScript = buildLuaScript();
        RedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
        Number count = limitRedisTemplate.execute(redisScript, keys, limitCount, limitPeriod);
        logger.info("第{}次访问key为 {},描述为 [{}] 的接口", count, keys, name);
        if (count != null && count.intValue() <= limitCount) {
            return point.proceed();
        } else {
            throw new LimitAccessException("接口访问超出频率限制");
        }

    }

    /**
     * 限流脚本
     * 调用的时候不超过阈值,则直接返回并执行计算器自加。
     *
     * @return lua脚本
     */
    private String buildLuaScript() {
        return "local c" +
                "\nc = redis.call('get',KEYS[1])" +
                "\nif c and tonumber(c) > tonumber(ARGV[1]) then" +
                "\nreturn c;" +
                "\nend" +
                "\nc = redis.call('incr',KEYS[1])" +
                "\nif tonumber(c) == 1 then" +
                "\nredis.call('expire',KEYS[1],ARGV[2])" +
                "\nend" +
                "\nreturn c;";
    }

}

测试接口:

@RestController
public class TestController {

    private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger();

    /**
     * 测试限流注解,下面配置说明该接口 60秒内最多只能访问 10次,保存到redis的键名为 limit_test,
     * 即 prefix + "_" + key,也可以根据 IP 来限流,需指定limitType = LimitType.IP
     */
    @Limit(key = "test", period = 60, count = 10, name = "resource", prefix = "limit")
    @GetMapping("/test")
    public int testLimiter() {
        return ATOMIC_INTEGER.incrementAndGet();
    }
}

测试结果:

当第十一次请求过来以后,提示接口访问超出频率限制。这个只是限制,其实可以将超出的请求都放入到队列中去,然后等已经来的接口处理完请求以后再让队列中的请求再次请求接口

### 黑马点评 API 限流解决方案 API 限流是一种常见的技术手段,用于保护服务免受高并发流量的影响以及防止恶意攻击。对于黑马点评这样的应用场景,可以采用以下几种主流的限流算法和技术实现。 #### 1. 计数器法 计数器法是最简单的限流方法之一,通过统计单位时间内某个资源的访问次数来判断是否超过设定阈值。如果超过了,则拒绝后续请求。 这种方法的优点是简单易懂、易于实现,缺点是对突发流量控制较差[^1]。 ```python import time class CounterLimiter: def __init__(self, limit, interval): self.limit = limit self.interval = interval self.requests = [] def allow_request(self): current_time = time.time() # 移除超出时间窗口的记录 self.requests = [t for t in self.requests if t > current_time - self.interval] if len(self.requests) < self.limit: self.requests.append(current_time) return True return False ``` #### 2. 漏桶算法 (Leaky Bucket Algorithm) 漏桶算法的核心思想是一个固定容量的桶以恒定速率流出水滴(处理请求),当流入速度大于流出速度时,多余的水流会被丢弃。该算法能够平滑流量并有效应对瞬时高峰压力。 ```java public class LeakyBucket { private int capacity; // 桶的最大容量 private double rate; // 流出速率 private long lastTime; // 上次漏水的时间戳 private int water; // 当前水量 public LeakyBucket(int cap, double r){ this.capacity = cap; this.rate = r; this.lastTime = System.currentTimeMillis(); this.water =0 ; } synchronized boolean grant(){ doLeak(); // 执行漏水操作 if(water<capacity){ // 如果还有剩余空间则允许进入新的请求 water++; return true; }else{ return false; // 否则不允许继续增加新请求 } } void doLeak(){ // 实际执行漏水过程的方法 long now=System.currentTimeMillis(); long deltaT=(now-lastTime); lastTime=now; float deltaWater=deltaT*rate/1000f; if(deltaWater>0){ water -= deltaWater; if(water<0){ water=0; } } } } ``` #### 3. 令牌桶算法 (Token Bucket Algorithm) 令牌桶算法类似于银行取号机制,在一定频率下向桶里放入令牌,客户端每发起一次请求消耗一枚令牌。如果没有足够的令牌可用,则延迟或者拒绝此次调用。 ```go type TokenBucket struct { ratePerSecond float64 // per second token generation speed. capacity uint64 // max number of tokens that can be stored at any given moment. tokens uint64 // current available tokens count. lastRefillAt time.Time } func NewTokenBucket(ratePerSec float64, cap uint64) *TokenBucket { return &TokenBucket{ratePerSecond: ratePerSec, capacity: cap, tokens: cap, lastRefillAt: time.Now()} } // Refills the bucket with new tokens based on elapsed time since last refill attempt and returns whether a request should proceed or not. func (tb *TokenBucket) TryConsume() bool { tb.refillTokens() if tb.tokens >= 1 { // Consume one token only when there are enough tokens present inside our bucket currently. tb.tokens-- return true } else { return false } } func (tb *TokenBucket) refillTokens() { now := time.Now().UnixNano() / int64(time.Millisecond) timeElapsedMs := float64(now-time.Duration(tb.lastRefillAt).Milliseconds()) newTokensToAdd := math.Min(float64(tb.capacity-tb.tokens), tb.ratePerSecond*(timeElapsedMs/1e3)) tb.tokens += uint64(newTokensToAdd) tb.lastRefillAt = time.Unix(0, now*int64(time.Millisecond)) } ``` 以上三种方案各有优劣,实际应用中需综合考虑性能需求、开发成本等因素选择最适合的方式实施限流策略。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值