Redis+Lua脚本基于计数器算法的限流

保护高并发服务稳定主要有三大法宝:缓存、降级和限流。

  • 缓存:缓存是一种提高数据读取性能的技术,通过在内存中存储经常访问的数据,可以减少对数据库或其他存储系统的访问,从而提升系统的响应速度。缓存可以应用在多个层面,例如浏览器缓存、CDN 缓存、反向代理缓存和应用缓存等。
  • 降级:在系统压力过大或部分服务不可用时,降级可以暂时关闭一些非核心服务,以保证核心服务的正常运行。降级可以在多个层面进行,例如页面降级、功能降级和服务降级等。
  • 限流:限流是一种控制系统处理请求速率的技术,以防止系统过载。限流可以通过多种算法实现,例如令牌桶算法和漏桶算法等。

定义限流配置

 @Bean
    public DefaultRedisScript<Long> limitScript() {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        redisScript.setScriptText(limitScriptText());
        redisScript.setResultType(Long.class);
        return redisScript;
    }

    /**
     * 限流脚本
     */
    private String limitScriptText() {
        return "local key = KEYS[1]\n" +
                "local count = tonumber(ARGV[1])\n" +
                "local time = tonumber(ARGV[2])\n" +
                "local current = redis.call('get', key);\n" +
                "if current and tonumber(current) > count then\n" +
                "    return tonumber(current);\n" +
                "end\n" +
                "current = redis.call('incr', key)\n" +
                "if tonumber(current) == 1 then\n" +
                "    redis.call('expire', key, time)\n" +
                "end\n" +
                "return tonumber(current);";
    }
  • 如果当前计数值超过了给定的最大计数值 **count**,脚本直接返回当前计数值而不进行后续操作。
  • 否则,计数值会增加 1,并在计数器第一次增加时设置过期时间。

限流注解

  • 自定义注解 RateLimiter,方便在需要用到限流的方法中直接加入。

/**
 * 限流注解
 *
 * @author canghe
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
    /**
     * 限流key
     */
    public String key() default CacheConstants.RATE_LIMIT_KEY;

    /**
     * 限流时间,单位秒
     */
    public int time() default 60;

    /**
     * 限流次数
     */
    public int count() default 100;

    /**
     * 限流类型
     */
    public LimitType limitType() default LimitType.DEFAULT;
}

AOP切面类逻辑

  • 自定义 AOP 切面控制类,进行限流逻辑处理以及降级提醒。
/**
 * 限流处理
 *
 * @author canghe
 */
@Aspect
@Component
public class RateLimiterAspect {
    private static final Logger log = LoggerFactory.getLogger(RateLimiterAspect.class);

    private RedisTemplate<Object, Object> redisTemplate;

    private RedisScript<Long> limitScript;

    @Autowired
    public void setRedisTemplate1(RedisTemplate<Object, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    @Autowired
    public void setLimitScript(RedisScript<Long> limitScript) {
        this.limitScript = limitScript;
    }

    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
        int time = rateLimiter.time();
        int count = rateLimiter.count();

        String combineKey = getCombineKey(rateLimiter, point);
        List<Object> keys = Collections.singletonList(combineKey);
        try {
            Long number = redisTemplate.execute(limitScript, keys, count, time);
            if (StringUtils.isNull(number) || number.intValue() > count) {
                throw new ServiceException("访问过于频繁,请稍候再试");
            }
            log.info("限制请求'{}',当前请求'{}',缓存key'{}'", count, number.intValue(), combineKey);
        } catch (ServiceException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("服务器限流异常,请稍候再试");
        }
    }

    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
        StringBuffer stringBuffer = new StringBuffer(rateLimiter.key());
        if (rateLimiter.limitType() == LimitType.IP) {
            stringBuffer.append(IpUtils.getIpAddr(ServletUtils.getRequest())).append("-");
        }
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        Class<?> targetClass = method.getDeclaringClass();
        stringBuffer.append(targetClass.getName()).append("-").append(method.getName());
        return stringBuffer.toString();
    }
}

交给spring管理

image.png

限流具体使用场景

/**
 * 登录接口,因为登录接口无token,所以不走网关鉴权,且安全级别极高
 * 需要自定义Redis限流逻辑
 * 这里配置了 30 秒内仅允许访问 10 次
 * @param form
 * @return
 */
@RateLimiter(key = "rate_limit:login", time = 30, count = 10)
@PostMapping("login")
public AjaxResult login(@RequestBody LoginBody form) {
    AjaxResult ajax = success();
    // 用户登录
    LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
    // 获取登录token
    String token = tokenService.createToken(userInfo);
    ajax.put(Constants.TOKEN, token);
    return ajax;
}

JMeter测试限流接口

下载安装

Apache JMeter - Download Apache JMeter
image.png

启动JMeter

image.png

添加线程组

image.png

设置一秒10个请求

image.png

添加HTTP请求

image.png

配置HTTP请求参数

image.png

添加JMeter测试报告

image.png

保存再启动测试

在这里插入图片描述

测试结果
  • 可以看到10个请求只通过了2个, 其他的全失败了

image.png

查看测试报告

image.png

真实登录接口测试

添加配置元素

image.png

配置信息头

image.png

配置HTTP请求信息

在这里插入图片描述

正常情况, 1秒9个请求

image.png

测试结果 - 均为成功!

image.png

异常情况 1秒11个请求- 结果第11个为请求失败

image.png
image.png

  • 9
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值