好久没写博客了…
最近公司组织了一次技术大赛,内容大概是基于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调用一下就好了!!!