redis实现流量控制策略

好久没写博客了…

最近公司组织了一次技术大赛,内容大概是基于dubbo-filter的机制完成消费者调用服务提供者的前置控制。

其中有一项是流量限制,内容如下
流量控制,实现服务级别的限流策略
 优化已有限流代码:目前总控已有对领单接口进行限流的功能,优化这部分
代码到流量控制功能的统一架构下
 限流策略 1:支持对服务进行限流策略 1 设置,可按天或者按小时限制服务
请求的次数,到达限制次数后返回拒绝服务的报文
 限流策略 2:支持对服务进行限流策略 2 设置,可按请求间隔时间限制服务
请求,间隔时间短于限制时间返回拒绝服务的报文

其实实现方式还是比较多的,例如:openresty、zuul、还有阿里开源的Sentinel号称为 Dubbo 服务保驾护航。

后来我们决定使用redis做流量控制,当然主要是因为没啥时间了,开始做的时候已经快比赛结束了…

回到正题!

/**
 * @ClassName FlowConfig
 * @Description 配置类,从数据库中读取配置,放到redis中
 * @Author wugz
 * @Date 2019/10/11 19:26
 * @Version 1.0
 */
@Component
public class LimitConfig {
    
    /** dubbo服务端id */
    @Value("${dubbo.application.id}")
    private String dubboId;

    @Autowired
    private CpParamMapper cpParamMapper;

    @Autowired
    private GoodGameRedisUtils goodGameRedisUtils;
    
    /**
     * @Description: 初始化限流的配置加载到redis中
     * @param 
     * @Date: 2019/10/11 19:49
     * @Author: wuguizhen
     * @Return void
     * @Throws
     */
    @PostConstruct
    public void init(){
        //处理按天/小时限流的配置
        String sql = "select * from cp_param where product_code in ('"
                +LimitConstant.LIMIT_CONFIG_BY_DATE + "','" + LimitConstant.LIMIT_CONFIG_BY_TIME + "');";
        List flowConfig = cpParamMapper.getListBySql(sql);

        /** 按时间限流 例:每天或者每小时最多访问多少次*/
        Map dateRule = new HashMap(16);
        /** 按间隔时间限流 例:每10s最多访问多少次*/
        Map timeRule = new HashMap(16);
        if(flowConfig != null && !flowConfig.isEmpty()){
            for(CpParam cpParam:flowConfig){
                String enName = cpParam.getParamEnname();
                String[] range = enName.split(LimitConstant.INTERVAL);
                /** 应用名称 */
                String applicationId = range[0];
                /** 只处理针对该应用的配置 */
                if(LimitConstant.START.equals(applicationId) || dubboId.replaceAll(LimitConstant.INTERVAL,"").equals(applicationId)){
                    /** 配置类型 */
                    String productCode = cpParam.getProductCode();
                    /** 服务名称 */
                    String serviceName = range[1];
                    /** 服务规则 */
                    String rule = cpParam.getParamValue();
                    /** 规则key */
                    String ruleKey = serviceName + "|" +cpParam.getSeqNo();
                    /** 根据不同的规则类型添加到map中 */
                    switch (productCode){
                        case LimitConstant.LIMIT_CONFIG_BY_DATE:
                            dateRule.put(ruleKey,rule);
                            break;
                        case LimitConstant.LIMIT_CONFIG_BY_TIME:
                            timeRule.put(ruleKey,rule);
                            break;
                        default:
                            break;
                    }
                }
            }

        }
        /** 将涉及该应用的限流规则到redis */
        if(!dateRule.isEmpty()){
            goodGameRedisUtils.hmPutAll(LimitConstant.LIMIT_CONFIG_BY_DATE_REDIS_KEY + dubboId + ":",dateRule);
        }

        if(!timeRule.isEmpty()){
            goodGameRedisUtils.hmPutAll(LimitConstant.LIMIT_CONFIG_BY_TIME_REDIS_KEY + dubboId + ":",timeRule);
        }
    }

    /**
     * @Description: 获取缓存配置
     * @param mainKey
     * @Date: 2019/10/12 19:24
     * @Author: wuguizhen
     * @Return java.util.Map
     * @Throws
     */
    public Map getMap(String mainKey){
        Map map = goodGameRedisUtils.hmGetAll(mainKey);
        if(map!=null){
            return map;
        }
        init();
        return goodGameRedisUtils.hmGetAll(mainKey);
    }

}

控制实现类

/**
 * @ClassName FlowControl
 * @Description 流量控制,从redis读取流量控制参数
 * @Author wugz
 * @Date 2019/10/11 18:17
 * @Version 1.0
 */
@Component("LimitControl")
public class LimitControl implements CommonControl {

    /** 限流脚本 */
    private final String REDIS_SCRIPT = buildLuaScript();

    /** dubbo服务端id */
    @Value("${dubbo.application.id}")
    private String dubboId;

    @Autowired
    private GoodGameRedisUtils goodGameRedisUtils;

    @Autowired
    private LimitConfig limitConfig;
    
    /**
     * @Description: 流量控制
     * @param param
     * @Date: 2019/10/11 19:19
     * @Author: wuguizhen
     * @Return boolean
     * @Throws
     */
    @Override
    public Result handle(FilterParam param){
        /** 按时间限流规则 */
        Map dateRule = limitConfig.getMap(LimitConstant.LIMIT_CONFIG_BY_DATE_REDIS_KEY + dubboId + ":");
        if(dateRule != null && !dateRule.isEmpty()){
            for(Map.Entry entry:dateRule.entrySet()){
                String ruleKey = entry.getKey();
                /** 单位DD/dd-限制时间-限制次数 */
                String rule = (String) entry.getValue();
                /** 获取规则配置中的服务名称 */
                String ruleServiceName =ruleKey.split("|")[0];
                /** 当前服务有配置的限流规则,进行流量限制 */
                if(hitRule(param.getServiceName(),ruleServiceName)){
                    String[] ruleDetail = rule.split(LimitConstant.INTERVAL);
                    /** 时间间隔 如果是DD为单位的则有天转换成秒,否则由小时转换成秒 */
                    int limitPeriod = "DD".equals(ruleDetail[0])?Integer.valueOf(ruleDetail[1])*60*60*12:Integer.valueOf(ruleDetail[1])*60*60;
                    int limitCount = Integer.valueOf(ruleDetail[2]);
                    /** 被限制住了返回false */
                    if(!handleRule( ImmutableList.of(ruleKey),limitCount,limitPeriod)){
                        return RefuseMessage.result("服务暂不可使用");
                    }
                }
            }
        }

        /** 按间隔时间限流 */
        Map timeRule = limitConfig.getMap(LimitConstant.LIMIT_CONFIG_BY_TIME_REDIS_KEY + dubboId + ":");
        if(timeRule != null && !timeRule.isEmpty()){
            for(Map.Entry entry:timeRule.entrySet()){
                String ruleKey = entry.getKey();
                /** 限制时间-限制次数 */
                String rule = (String) entry.getValue();
                /** 获取规则配置中的服务名称 */
                String ruleServiceName =ruleKey.split("|")[0];
                /** 当前服务有配置的限流规则,进行流量限制 */
                if(hitRule(param.getServiceName(),ruleServiceName)){
                    String[] ruleDetail = rule.split(LimitConstant.INTERVAL);
                    int limitPeriod = Integer.valueOf(ruleDetail[0]);
                    int limitCount = Integer.valueOf(ruleDetail[1]);
                    /** 被限制住了返回false */
                    if(!handleRule( ImmutableList.of(ruleKey),limitCount,limitPeriod)){
                        return RefuseMessage.result("服务暂不可使用");
                    }
                }
            }
        }
        return null;
    }

    /**
     * @Description: 判断当前服务名称是否命中配置的规则
     * @param serviceName 当前调用的服务名称 包含调用的方法名
     * @param ruleServiceName 规则配置的服务名称
     * @Date: 2019/10/11 21:22
     * @Author: wuguizhen
     * @Return boolean
     * @Throws
     */
    private boolean hitRule(String serviceName, String ruleServiceName) {
        /** 若配置通配符直接返回* */
        if(LimitConstant.START.equals(ruleServiceName)){
            return Boolean.TRUE;
        }
        
        /** 直接相等 */
        if(serviceName.equals(ruleServiceName)){
            return Boolean.TRUE;
        }

        if(ruleServiceName.contains(LimitConstant.START)){
            /** 或包含*则取出前置报名 例 com.wugz.app.* 判断服务是否在当前包名下 */
            String preName = ruleServiceName.split(LimitConstant.START)[0];
            if(serviceName.startsWith(preName)){
                return Boolean.TRUE;
            }
        }
        return Boolean.FALSE;
    }

    /**
     * @Description:调用lua脚本
     * @param keys
     * @param limitCount 最大方法次数
     * @param limitPeriod 时间段 单位为秒
     * @Date: 2019/10/11 21:10
     * @Author: wuguizhen
     * @Return boolean
     * @Throws
     */
    public boolean handleRule(ImmutableList keys, int limitCount,int limitPeriod){
        Number count = goodGameRedisUtils.execute(REDIS_SCRIPT, keys, limitCount, limitPeriod);
        if(count != null && count.intValue() <= limitCount) {
            return true;
        } else {
            return false;
        }
    }


    /**
     * 限流 脚本
     *
     * @return lua脚本
     */
    private String buildLuaScript() {
        StringBuilder lua = new StringBuilder();
        lua.append("local c")
                .append("\nc = redis.call('get', KEYS[1])")
                // 调用不超过最大值,则直接返回
                .append("\nif c and tonumber(c) > tonumber(ARGV[1]) then")
                .append("\nreturn c;")
                .append("\nend")
                // 执行计算器自加
                .append("\nc = redis.call('incr', KEYS[1])")
                .append("\nif tonumber(c) == 1 then")
                // 从第一次调用开始限流,设置对应键值的过期
                .append("\nredis.call('expire', KEYS[1], ARGV[2])")
                .append("\nend")
                .append("\nreturn c;");
        return lua.toString();
    }

}

配置表数据

在这里插入图片描述

到这里就实现啦,具体用法在filter调用一下就好了!!!

你可能感兴趣的:(Java)