热点参数限流
Overview
我们看看官方对于热点Key限流的描述
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
热点参数规则
热点参数规则(ParamFlowRule
)类似于流量控制规则(FlowRule
):
属性 | 说明 | 默认值 |
---|---|---|
resource | 资源名,必填 | |
count | 限流阈值,必填 | |
grade | 限流模式 | QPS 模式 |
durationInSec | 统计窗口时间长度(单位为秒),1.6.0 版本开始支持 | 1s |
controlBehavior | 流控效果(支持快速失败和匀速排队模式),1.6.0 版本开始支持 | 快速失败 |
maxQueueingTimeMs | 最大排队等待时长(仅在匀速排队模式生效),1.6.0 版本开始支持 | 0ms |
paramIdx | 热点参数的索引,必填,对应 SphU.entry(xxx, args) 中的参数索引位置 | |
paramFlowItemList | 参数例外项,可以针对指定的参数值单独设置限流阈值,不受前面 count 阈值的限制。仅支持基本类型和字符串类型 | |
clusterMode | 是否是集群参数流控规则 | false |
clusterConfig | 集群流控相关配置 |
关键点
我们可以看到在官方的ParamFlowRule中我们只能设置我们需要限流的参数索引
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey")
public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false) String p2) {
return "success";
}
也就是以上代码中的p1或者p2参数索引,从0开始
**但是!!!我们在实际开发中通常传参是传的一个复杂类型的对象。**例如下面的接口
/**
* 同步发送
*
* @param dto
* @return
*/
@PostMapping("/sync/send")
@SentinelResource(value = "sendSyncMsg", blockHandlerClass = SentinelBlockHandlerImpl.class, blockHandler = "limitFlow")
public R sendSyncMsg(@RequestBody UnifySendReqDto dto) {
log.info("应用调用消息中心接口传参:{}", JSON.toJSONString(dto));
dto.setSyncIdentify(true);
if (dto.getDeliveryTime() != null && dto.getDeliveryTime() >= 1) {
throw new BusinessException("同步发送不支持定时发送功能,请调用异步发送接口");
}
return msgPushAppService.sendMsg(dto);
}
这是在实际开发中的调用信息发送接口。可以看到UnifySendReqDto对象,是一个复杂对象,而我们设置热点Key限流只能设置其索引为0
@Data
public class UnifySendReqDto {
/**
* 应用唯一标识
*/
private String appCode;
/**
* 密钥
*/
private String secret;
/**
* 消息内容
*/
private String content;
/**
* 接收人集合
*/
private List<RecipientInfoDto> recipientInfoDtos;
// 省略其他的成员变量......
}
针对RecipientInfoDto中的userId进行限流
@Data
public class RecipientInfoDto {
/**
* 接收人飞书id(飞书需要、必传),接收aunid
*/
private String userId;
// 省略其他的成员变量......
}
如何解决
解决方案很简单,我们可以思考,他是热点key限流。如果是我们,我们要怎么做?
简单结构:
- 使用者设置QPS为n
- 拿到该接口请求参数
- 在1秒内比较参数是否相同
- 若相同则出现次数+1,若次数超过了n,则限流
重点是比较
sentinel首先是通过hashCode()去判断这个key是否相同,若相同则继续判断其equals(Object o)是否为true
- 若hashCode()相同,equals()为true,则相同
- 若hashCode()相同,equals()为false,则不相同
- 若hashCode()不同,则不相同
因此针对上述问题,我们只需要在UnifySendReqDto对象中重写equals()和hashCode()方法了
@Override
public boolean equals(Object o) {
AtomicBoolean flag = new AtomicBoolean(false);
if (o instanceof UnifySendReqDto) {
Map<String, RecipientInfoDto> collect = this.recipientInfoDtos.stream().collect(Collectors.toMap(RecipientInfoDto::getUserId, Function.identity(), (o1, o2) -> o1));
((UnifySendReqDto) o).getRecipientInfoDtos().forEach(e -> {
flag.set(!Objects.isNull(collect.get(e.getUserId())));
});
}
return flag.get();
}
@Override
public int hashCode() {
return 1;
}
因为我们是对集合中的RecipientInfoDto对象中的userId限流,因此hashCode不可能相同,则我们先满足hashCode都相同,根据equals(Object o)来判断对象是否相同。
这样我们就可以对其复杂类型的热点Key限流了。
总结
对于请求参数是一个复杂对象中的某个成员变量限流,我们就可以重新这个类的hashCode()和equals(Object o)方法来判断对象中的热点key是否相同,让sentinel对其限流。