java八股-流量封控系统

文章目录

请求后台管理的频率-流量限制

根据登录用户做出控制,比如 x 秒请求后管系统的频率最多 x 次。

实现原理也比较简单,通过 Redis increment 命令对一个数据进行递增,如果超过 x 次就会返回失败。这里有个细节就是我们的这个周期是 x 秒,需要对 Redis 的 Key 设置 x 秒有效期。

但是 Redis 中对于 increment 命令是没有提供过期命令的,这就需要两步操作,进而出现原子性问题。

lua脚本步骤:

  1. 递增key对应的访问次数
  2. 給该key设置过期时间TTL,TTL是限制时间内的秒数,
  3. 最后返回TTL内的访问次数

使用lua脚本保证原子性,这里主要的作用是记录在timeWindow秒限制内的访问次数AccessCnt。

其中timeWindow是多少多少秒,lua脚本返回值(是在timeWindow秒内)被访问了AccessCnt次,

lua 复制代码
-- 设置用户访问频率限制的参数
local username = KEYS[1]
local timeWindow = tonumber(ARGV[1]) -- 时间窗口,单位:秒

-- 构造 Redis 中存储用户访问次数的键名
local accessKey = "short-link:user-flow-risk-control:" .. username

-- 原子递增访问次数,并获取递增后的值
local currentAccessCount = redis.call("INCR", accessKey)

-- 设置键的过期时间
redis.call("EXPIRE", accessKey, timeWindow)

-- 返回当前访问次数
return currentAccessCount

流量限制的业务代码

UserFlowRiskControlFilter

java 复制代码
package com.nageoffer.shortlink.admin.common.biz.user;

import com.alibaba.fastjson2.JSON;
import com.google.common.collect.Lists;
import com.nageoffer.shortlink.admin.common.convention.exception.ClientException;
import com.nageoffer.shortlink.admin.common.convention.result.Results;
import com.nageoffer.shortlink.admin.config.UserFlowRiskControlConfiguration;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Optional;

import static com.nageoffer.shortlink.admin.common.convention.errorcode.BaseErrorCode.FLOW_LIMIT_ERROR;


@Slf4j
@RequiredArgsConstructor
//用户流量封控过滤器
public class UserFlowRiskControlFilter implements Filter {

    private final StringRedisTemplate stringRedisTemplate;
    //用户流量封控配置器
    //这里这个UserFlowRiskControlConfiguration 在下面会有介绍的,反正这里的MaxAccessCount和time-window都是从Application.yaml里面读取到的
    private final UserFlowRiskControlConfiguration userFlowRiskControlConfiguration;

    private static final String USER_FLOW_RISK_CONTROL_LUA_SCRIPT_PATH = "lua/user_flow_risk_control.lua";

    @SneakyThrows
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        //设置加载到lua脚本对象
        redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource(USER_FLOW_RISK_CONTROL_LUA_SCRIPT_PATH)));
        //设置lua脚本返回值类型是Long
        redisScript.setResultType(Long.class);
        String username = Optional.ofNullable(UserContext.getUsername()).orElse("other");
        Long result;
        try {
            result = stringRedisTemplate.execute(redisScript, Lists.newArrayList(username), userFlowRiskControlConfiguration.getTimeWindow());
        } catch (Throwable ex) {//设置为Throwable,防止捕获不到
            log.error("执行用户请求流量限制LUA脚本出错", ex);
            returnJson((HttpServletResponse) response, JSON.toJSONString(Results.failure(new ClientException(FLOW_LIMIT_ERROR))));
            return;
        }
        //如果访问次数Result>最大限制访问次数getMaxAccessCount,则返回流量限制异常
        if (result == null || result > userFlowRiskControlConfiguration.getMaxAccessCount()) {
            returnJson((HttpServletResponse) response, JSON.toJSONString(Results.failure(new ClientException(FLOW_LIMIT_ERROR))));
            return;
        }
        filterChain.doFilter(request, response);
    }

    private void returnJson(HttpServletResponse response, String json) throws Exception {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=utf-8");
        try (PrintWriter writer = response.getWriter()) {
            writer.print(json);
        }
    }
}

短链接中台的流量限制

使用sentinel来做中台的流量限制,首先引入依赖

xml 复制代码
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-annotation-aspectj</artifactId>
</dependency>
CustomBlockHandler
java 复制代码
package com.nageoffer.shortlink.project.handler;

import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.nageoffer.shortlink.project.common.convention.result.Result;
import com.nageoffer.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkCreateRespDTO;


public class CustomBlockHandler {

    public static Result<ShortLinkCreateRespDTO> createShortLinkBlockHandlerMethod(ShortLinkCreateReqDTO requestParam, BlockException exception) {
        return new Result<ShortLinkCreateRespDTO>().setCode("B100000").setMessage("当前访问网站人数过多,请稍后再试...");
    }
}

sentinel技术对接口限流,超过指定的QPS之后会接口限流,Block住

下面的@SentinelResource里面的value是对保护资源的名称的指定,Blockhandler是保护资源用到的方法,BlockHandlerClass是保护资源用到的类Class

对指定接口限流

这里对接口限流,指定他的资源名称为create-short-link,以后带着这个名称,在SentinelRuleConfig(文章下面会介绍到位,也可以在目录里面快速定位到相关内容)里面会加入对这个资源的保护

UserFlowRiskControlConfiguration

Application.yaml和UserFlowRiskControlConfiguration

java 复制代码
@Data
@Component
@ConfigurationProperties(prefix = "short-link.flow-limit") //从Application.yaml配置文件里面读取相应的数据信息
public class UserFlowRiskControlConfiguration {

    /**
     * 是否开启用户流量风控验证
     */
    private Boolean enable;

    /**
     * 流量风控时间窗口,单位:秒
     */
    private String timeWindow;

    /**
     * 流量风控时间窗口内可访问次数
     */
    private Long maxAccessCount;
}

SentinelRuleConfig

定义接口规则

java 复制代码
package com.nageoffer.shortlink.project.config;

import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

//解释一下,实现InitializingBean 接口并重写afterPropertiesSet是为了在Bean初始化完成之后,
//执行FlowRuleManager.loadRules(rules)把这个流量限制规则加入到位!!!!
@Component
public class SentinelRuleConfig implements InitializingBean { //

    @Override
    public void afterPropertiesSet() throws Exception {
        List<FlowRule> rules = new ArrayList<>();
        FlowRule createOrderRule = new FlowRule();
        //通过对指定资源名称进行保护限流
        createOrderRule.setResource("create_short-link");
        //通过QPS限制
        createOrderRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        //QPS超过1就限流
        createOrderRule.setCount(1);
        rules.add(createOrderRule);
        FlowRuleManager.loadRules(rules);
    }
}

InitializingBean 接口,实现该接口的类需要提供一个 afterPropertiesSet 方法,该方法会在所有依赖注入完成后被调用

java 复制代码
package org.springframework.beans.factory;

public interface InitializingBean {
    void afterPropertiesSet() throws Exception;
}
相关推荐
努力学习java的哈吉米大王25 分钟前
初识JAVA-面向对象的三大特征之多态
java·开发语言
KpLn_HJL1 小时前
leetcode - 3223. Minimum Length of String After Operations
java·算法·leetcode
okmacong1 小时前
04.计算机体系三层结构与优化(操作系统、计算机网络、)
java·服务器·计算机网络
kikyo哎哟喂1 小时前
Spring&SpringBoot常用注解总结
java·spring boot·spring
S-X-S1 小时前
自定义异常模块
java·开发语言·spring boot
重生之Java开发工程师1 小时前
Java中Map常用遍历方式以及性能对比
java·开发语言
成为编程高手O_o1 小时前
Redis 实战篇 ——《黑马点评》(中)
java·数据库·redis·学习·缓存
一二小选手2 小时前
【SpringSecurity】SpringSecurity安全框架授权
java·springsecurity·安全框架
上海拔俗网络2 小时前
“AI 自动化效能评估系统:开启企业高效发展新征程
java·团队开发
多多*2 小时前
JUC Java并发编程 高级 学习大纲 动员
java·开发语言·学习·面试·架构·bash·intellij-idea