场景
线上一个接口qps较高需要优化,在检查内部逻辑没什么优化空间后(几个crud然后组装数据),决定加上缓存。避免了频繁的数据库访问
public AssistantGroupGetListResponse getList(AssistantGroupGetListRequest request) {
final Optional<AssistantGroupGetListResponse> optional = cacheClient
.getAssistedGroupCache(request.getOrgId(), null, 0L);
if (optional.isPresent()) {
return optional.get();
}
List<AssistantGroupRecord> assistantGroupRecords = assistantGroupDao
.getList(request.getOrgId());
List<AssistantGroupExtInfo> assistantGroupExtInfos = getListByRecords(assistantGroupRecords,
request.getOrgId(), null, null);
List<AssistantGroup> assistantGroups = assistantGroupExtInfos.stream()
.map(AssistantGroupExtInfo::getAssistantGroup)
.collect(Collectors.toList());
AssistantGroupGetListResponse response = AssistantGroupGetListResponse.newBuilder()
.setCode(DoorErrorCode.SUCCESS.getCode())
.setMsg(DoorErrorCode.SUCCESS.getMsg())
.addAllData(assistantGroups)
.build();
cacheClient.saveAssistedGroupCache(response, request.getOrgId(), null, 0L);
return response;
}
缓存方法
@Override
public Optional<StoreInfo> getStoreInfo(Long storeId) {
if (Objects.isNull(storeId) || AppConfig.getStoreInfoExpire() <= 0) {
return Optional.empty();
}
final Optional<String> optional = redisClient
.handle(jedis -> jedis.get(CacheKeyEnum.STORE_INFO_KEY.format(storeId)));
if (!optional.isPresent() || StringUtils.isBlank(optional.get())) {
return Optional.empty();
}
final StoreInfo storeInfo = GsonHolder.GSON.fromJson(optional.get(), StoreInfo.class);
return Optional.ofNullable(storeInfo);
}
@Override
public void saveStoreInfoCache(Long storeId, StoreInfo storeInfo) {
if (AppConfig.getStoreInfoExpire() <= 0 || Objects.isNull(storeInfo)) {
return;
}
final int expireTime = RandomUtils
.nextInt(AppConfig.getStoreInfoExpire(), AppConfig.getStoreInfoExpire() * 2);
redisClient
.handle(jedis -> jedis.set(CacheKeyEnum.STORE_INFO_KEY.format(storeId), GsonHolder.GSON
.toJson(storeInfo), SetParams.setParams().ex(expireTime)));
}
上面代码表面上看着没什么问题,一个很简单的将请求参数做key,response做value序列化到redis,当同样参数的请求来时,从redis反序列化出response然后返回。在测试环境也没任何问题,但是一上生产就出问题了,容器很快就被打爆。
首先是cpu被打满,限流率达到100%(下图与实际场景无关,只是找不到当时的图了)
对于cpu突然飙升,可以通过火焰图查看,通过火焰图我们可以看到Gson.fromjson对cpu的使用有26%之多。而且根据火焰图也能看到,就是我们跟我们之前上的代码有关。
代码中使用 GSON.fromJson 进行反序列化的类 AssistantGroupGetListResponse 是一个 protobuf message class 而不是一个 POJO class。使用 gson 对这个类进行序列化和反序列化存在比较明显的性能问题:
- protoc 生成的
AssistantGroupGetListResponse中包含了很多我们无法直接控制,也不是被业务逻辑直接需要的 private field,而 gson 会无差别地对这些 field 都执行序列化和反序列化。 AssistantGroupGetListResponse既然是一个 protobuf message,当然使用 pb 进行序列化/反序列化,而且在正确性和性能上更有保障。
修改后的代码如下
public Optional<AssistantGroupGetListResponse> getAssistedGroupCache(Integer orgId, List<Long> storeId) {
if (Objects.isNull(orgId)) {
return Optional.empty();
}
// 如果设置的缓存时间小于0 则认为不使用缓存
if (AppConfig.getAssistantGroupExpire() <= 0) {
return Optional.empty();
}
final String key = getAssistedGroupKey(orgId, storeId);
final Optional<byte[]> optional = redisClient
.handle(jedis -> jedis.get(key.getBytes(StandardCharsets.UTF_8)));
if (optional.isPresent()) {
try {
final AssistantGroupGetListResponse assistantGroupGetListResponse = AssistantGroupGetListResponse
.parseFrom(optional.get());
return Optional.ofNullable(assistantGroupGetListResponse);
} catch (Exception e) {
LOGGER.error("getAssistedGroupCache err:{}", e);
}
}
return Optional.empty();
}
上线后再次查看火焰图,可以看到Gson使用的cpu已经下降到13%