SqSentinel基于Spring AOP使用注解@SqSentinelResource的方式对请求进行限流和降级处理, 使用方式可参见 SqSentinel接入指南, 在使用过程中需慎入如下陷阱。
陷阱1: Spring AOP仅对Spring管理的bean进行增强操作。NewOrderArrivalHallRunnable是内部类,非ApplicationContext管理的类,因此即使使用@SqSentinelResource注解,限流降级功能也不会生效。eclipse 警告fallbackExecute(降级方法)未使用,也从侧面说明这一问题。
@Service
public class DriverPushService {
private static final Logger LOG = LoggerFactory.getLogger(DriverPushService.class);
@Autowired
private IDriverInfoService driverInfoService;
@Autowired
private ISystemPushService systemPushService;
private ThreadPoolExecutor executor = new StandardThreadExecutor(4,10,1000 * 30,TimeUnit.MILLISECONDS,1000);
public void pushOrderArrival(int type, String orderNo, String driverIds) {
if(driverIds == null || driverIds.trim().length() == 0) {
return;
}
List<Integer> list = null;
if(type == 2) {
Map<String,List<Integer>> pdMap = JSON.parseObject(driverIds,new TypeReference<Map<String,List<Integer>>>(){});
list = pdMap.entrySet().stream().flatMap(entry -> entry.getValue().stream()).collect(Collectors.toList());
list.removeIf(driverId->driverId == -1);
} else {
list = Splitter.on(",").trimResults().splitToList(driverIds).stream().map(str -> Integer.parseInt(str)).collect(Collectors.toList());
}
executor.submit(new NewOrderArrivalHallRunnable(type, orderNo, list));
}
private class NewOrderArrivalHallRunnable extends MDCRunnable{
private int type;
private String orderNo;
private List<Integer> list;
public NewOrderArrivalHallRunnable(int type, String orderNo, List<Integer> list){
this.type = type;
this.orderNo = orderNo;
this.list = list;
}
@Override
@SqSentinelResource(configName = "sq-sentinel",
value = "newOrderArrivalPush",
blockHandler = "fallbackExecute",
fallback = "fallbackExecute")
public void execute() {
list.stream().forEach(driverId -> {
DriverDetailInfo info = driverInfoService.getDriverDetailInfo(driverId);
Map<String,String> bizMap = new HashMap<String,String>();
bizMap.put("type", type+"");
bizMap.put("orderNo", orderNo);
ObjectMapper mapper = new ObjectMapper();
String body = null;
try {
body = mapper.writeValueAsString(bizMap);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
Map<String,Object> paraMap = new HashMap<String,Object>();
paraMap.put("nick", null);
paraMap.put("msgId", UUID.randomUUID().toString().replace("-", ""));
paraMap.put("title", "选单大厅-新单到达");
paraMap.put("body", body);
if(info == null || info.getChatUserId() == null) {
LOG.warn("获取司机信息失败.driverId:[{}]", driverId);
} else {
systemPushService.sendSocket(info.getChatUserId(), paraMap);
}
});
LOG.info("给司机推送新单(选单大厅)到达通知。orderNo:[{}] total:[{}]", orderNo, list.size());
}
@SuppressWarnings("unused")
public void fallbackExecute() {
LOG.info("选单大厅新单到达通知降级处理,停止push.");
return;
}
}
}
陷阱2:下例的riskDriverValidateOnce使用@SqSentinelResource进行了降级增强。在riskDriverValidate和内部类RiskFilterDriverThread中调用了riskDriverValidateOnce方法,实际上aop增强(限流降级)都不会生效。 RiskFilterDriverThread#execute调用了外部类的riskDriverValidateOnce,本质上是调用RiskFilterDriverApiServiceImpl.this.riskDriverValidateOnce,并没有调用代理类的riskDriverValidateOnce方法。riskDriverValidate方法调用riskDriverValidateOnce限流降级也不会生效,解释详见陷阱3.
@Service("riskFilterDriverApiService")
public class RiskFilterDriverApiServiceImpl implements IRiskFilterDriverApiService {
private Logger LOGGER = LoggerFactory.getLogger(getClass());
private static final String RISK_CHECK_URL = "/risk/driver/beforedistributevalidate";
private static final Integer MAX_DRIVER = 99;
private static final Integer MAX_WAIT_TIME = 3;
@Override
public List<RiskFilterResponseBO> riskDriverValidate(RiskFilterParamDTO riskDTO) {
List<RiskFilterResponseBO> resList = null;
List<Integer> driverIds = riskDTO.getDriverIdList();
if(driverIds.size()>MAX_DRIVER){
//如果司机数量过多分批查询
List<RiskFilterResponseBO> resListAll = new ArrayList<>(driverIds.size());
List<List<Integer>> driverIdList = Lists.partition(driverIds, MAX_DRIVER);
List<Future<List<RiskFilterResponseBO>>> resFutureList = new ArrayList<>(driverIdList.size());
for (List<Integer> driverList0 : driverIdList){
RiskFilterParamDTO paramDTO = new RiskFilterParamDTO(
driverList0,
riskDTO.getPassengerPhone(),
riskDTO.getChannelsNum(),
riskDTO.getBookingDriverIds()
);
Future<List<RiskFilterResponseBO>> res = ThreadUtils.getStandardExecutorInstance().submit(new RiskFilterDriverThread(paramDTO,
TraceItemDTO
.createByCurrentMDC()));
resFutureList.add(res);
}
for (Future<List<RiskFilterResponseBO>> futrue : resFutureList) {
try {
List<RiskFilterResponseBO> res = futrue.get(MAX_WAIT_TIME,TimeUnit.SECONDS);
if(CollectionUtils.isNotEmpty(res)){
resListAll.addAll(res);
}
} catch (Exception e) {
LOGGER.error("调用风控接口出错",e);
}
}
if(resListAll.size()>0){
resList = resListAll;
}
}else {
resList = riskDriverValidateOnce(new RiskFilterParamDTO(
driverIds,
riskDTO.getPassengerPhone(),
riskDTO.getChannelsNum(),
riskDTO.getBookingDriverIds()
));
}
return resList;
}
@SqSentinelResource(configName = "sq-sentinel",
value = "/risk/driver/beforedistributevalidate",
blockHandler = "riskDriverValidateOnceFallback",blockHandlerClass = {RiskFilterDriverApiServiceFallBack.class},
fallback = "riskDriverValidateOnceFallback",fallbackClass = {RiskFilterDriverApiServiceFallBack.class})
private List<RiskFilterResponseBO> riskDriverValidateOnce(RiskFilterParamDTO riskDTO) {
String host = UrlDicts.get(DictKeys.RISK_URL);
String riskUrl = host + RISK_CHECK_URL;
HashMap<String, Object> params = Maps.newHashMap();
params.put("driverIds", riskDTO.getDriverIds());
params.put("passengerPhone", riskDTO.getPassengerPhone());
params.put("channelsNum", riskDTO.getChannelsNum());
if(StringUtils.isNotBlank(riskDTO.getBookingDriverIds())) {
params.put("bookingDriverIds", riskDTO.getBookingDriverIds());
}
long start = System.currentTimeMillis();
try {
ThreadLocalContext.setLocal(new ApiMonitor(CommonConstant.DISPATCHER, RemoteSystem.RISK, start,RISK_CHECK_URL));
String result = HttpUtil.getIntance().post(riskUrl, params);
BaseResponseBO<JSON> baseResponseBO = JSON.parseObject(result, BaseResponseBO.class);
if (baseResponseBO == null || baseResponseBO.getCode() != ResponseResult.SUCCESS_CODE || baseResponseBO.getData() == null) {
LOGGER.warn("调用风控验证司机接口失败 url:[{}], 参数:[{}], resp=[{}], 耗时:[{}]ms", RISK_CHECK_URL,
params,result, System.currentTimeMillis() - start);
return null;
}
LOGGER.info("调用风控验证司机接口成功 url:[{}], 耗时:[{}]ms", RISK_CHECK_URL, System.currentTimeMillis() - start);
LOGGER.info("调用风控验证司机接口成功 参数:[{}], resp=[{}] ",params,result);
JSON data = baseResponseBO.getData();
return JSON.parseArray(data.toJSONString(),RiskFilterResponseBO.class);
} catch (Exception e) {
LOGGER.error("请求获取风控抢单查询接口异常!!!, msg={}", e.getMessage(), e);
}
return null;
}
class RiskFilterDriverThread extends AbstractCallableWorker<List<RiskFilterResponseBO>> {
private RiskFilterParamDTO riskDTO;
private TraceItemDTO traceItemDTO;
public RiskFilterDriverThread(RiskFilterParamDTO riskDTO,TraceItemDTO traceItemDTO){
this.riskDTO = riskDTO;
this.traceItemDTO = traceItemDTO;
}
@Override
protected TraceItemDTO getTraceItem() {
return traceItemDTO;
}
@Override
protected List<RiskFilterResponseBO> execute() {
return riskDriverValidateOnce(riskDTO);
}
}
}
陷阱3:在本例RiskFilterDriverProcessor中Autowired了riskFilterDriverApiService对象,但这里的riskFilterDriverApiService并非RiskFilterDriverApiServiceImpl 的实例对象,而是经由spring aop(@SqSentinelResource使用的切面)生成的代理对象,以采用jdk动态代理实现aop为例, riskFilterDriverApiService实际上是IRiskFilterDriverApiService的子类,内部也提供了riskDriverValidate和riskDriverValidateOnce的实现,不同的是riskDriverValidateOnce通过SqSentinelResourceAspect#invokeResourceWithSentinel进行了增强,而riskDriverValidate只是简单的重定向调用了真实RiskFilterDriverApiServiceImpl实例对象的riskDriverValidate方法。真实的RiskFilterDriverApiServiceImpl实例对象的riskDriverValidate方法又调用自身的未被加强的riskDriverValidateOnce的方法。在这一过程中,代理对象的增强版的riskDriverValidateOnce方法并未被调用。
@Service
public class RiskFilterDriverProcessor extends AbstractDriverProcessor {
private static final String ALL_PARTNER = "all_partner";
@Autowired
private IRiskFilterDriverApiService riskFilterDriverApiService;
@Override
public void doProcessor(Accessory accessory, DriverResult result, DriverProcessorChain chain) {
//--------------------省略----------------------
List<RiskFilterResponseBO> resList = riskFilterDriverApiService.riskDriverValidate(new RiskFilterParamDTO(
driverIds,
carBizOrderDTO.getRiderPhone(),
carBizOrderDTO.getChannelsNum(),
//carBizOrderDTO.getBookingDriverIds()
StringUtils.join(carBizOrderDTO.getBookingDriverIdList(), ",")
));
//---------------------省略----------------------
}
}
结论: 因为 spring AOP的局限性,即使配置了@SqSentinelResource,也可能并未真正生效。正确的使用姿势是@SqSentinelResource修饰的方法必须是受spring applicationContext管理的bean的顶级方法,这里的顶级是指仅仅被外部的bean引用调用。如何确定你配置的sentinel生效了?Sentinel 除了提供了核心库,还提供了可视化的控制台,通过可视化控制台可以看到实时的qps,拒绝的qps及响应时长等信息。