缓存监控--来源于网络
前缀key设计,按照不同的业务区分了不同的业务场景的前缀Key
public class RedisKeyConstants {
public static final String REDIS_GAMEGROUP_NEW_KEY = "newgamegroup";
public static final String REDIS_GAMEGROUP_DETAIL_KEY = "gamegroup:detail";
public static final String REDIS_KEY_IUNIT_STRATEGY_COUNT = "activity:ihandler:strategy:count";
public static final String CONTENT_DISTRIBUTE_CURRENT = "content:distribute:current";
public static final String RECOMMEND_NOTE = "recommend:note";
}
public class RedisUtils {
public static final String COMMON_REDIS_KEY_SPLIT = ":";
public static String buildRedisKey(String key, Object... params) {
if (params == null || params.length == 0) {
return key;
}
for (Object param : params) {
key += COMMON_REDIS_KEY_SPLIT + param;
}
return key;
}
}
监控实现,通过 Aspect 的切面功能对 Redis 的指定操作进行拦截,如上图中的 Set 操作等,可以按需扩展到其他操作,针对前缀 key 的提取支持两个维度,默认场景和自定义场景,其中处理优先级为 自定义场景 > 默认场景,考虑自定义场景的灵活性,相关的自定义前缀通过配置中心实时生效
@Slf4j
@Aspect
@Order(0)
@Component
public class RedisMonitorAspect {
private static final String PREFIX_CONFIG = "redis.monitor.prefix";
private static final Set<String> PREFIX_SET = new HashSet<>();
@Resource
private MonitorComponent monitorComponent;
static {
// 更新前缀匹配的名单
String prefixValue = VivoConfigManager.getString(PREFIX_CONFIG, "");
refreshConf(prefixValue);
// 增加配置变更的回调
VivoConfigManager.addListener(new VivoConfigListener() {
@Override
public void eventReceived(PropertyItem propertyItem, ChangeEventType changeEventType) {
if (StringUtils.equalsIgnoreCase(propertyItem.getName(), PREFIX_CONFIG)) {
refreshConf(propertyItem.getValue());
}
}
});
}
/**
* 更新前缀匹配的名单
* @param prefixValue
*/
private static void refreshConf(String prefixValue) {
if (StringUtils.isNotEmpty(prefixValue)) {
String[] prefixArr = StringUtils.split(prefixValue, ",");
Arrays.stream(prefixArr).forEach(item -> PREFIX_SET.add(item));
}
}
@Pointcut("execution(* com.vivo.joint.dal.common.redis.dao.RedisDao.set*(..))")
public void point() {
}
@Around("point()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
//业务逻辑异常情况直接抛到业务层处理
Object result = pjp.proceed();
try {
if (VivoConfigManager.getBoolean("joint.center.redis.monitor.switch", true)) {
Object[] args = pjp.getArgs();
if (null != args && args.length > 0) {
String redisKey = String.valueOf(args[0]);
if (VivoConfigManager.getBoolean("joint.center.redis.monitor.send.log.switch", true)) {
LOGGER.info("更新redis的缓存 {}", redisKey);
}
String monitorKey = null;
// 先指定前缀匹配
if (!PREFIX_SET.isEmpty()) {
for (String prefix : PREFIX_SET) {
if (StringUtils.startsWithIgnoreCase(redisKey, prefix)) {
monitorKey = prefix;
break;
}
}
}
if (StringUtils.isEmpty(monitorKey) && StringUtils.contains(redisKey, ":")) {
// 需要考虑前缀的格式,保证数据写入不能膨胀
monitorKey = StringUtils.substringBeforeLast(redisKey, ":");
}
monitorComponent.sendRedisMonitorData(monitorKey);
}
}
} catch (Exception e) {
}
return result;
}
}
案例
public static final String REDISKEY_USER_POPUP_PLAN = "popup:user:plan";
public PopupWindowPlan findPlan(FindPlanParam param) {
String openId = param.getOpenId();
String imei = param.getImei();
String gamePackage = param.getGamePackage();
Integer planType = param.getPlanType();
String appId = param.getAppId();
// 1、获取缓存的数据
PopupWindowPlan cachedPlan = getPlanFromCache(openId, imei, gamePackage, planType);
if (cachedPlan != null) {
monitorPopWinPlan(cachedPlan);
return cachedPlan;
}
// 2、未命中换成后从持久化部分获取对应的 PopupWindowPlan 对象
// 3、保存到Redis换成
setPlanToCache(openId, imei, gamePackage, plan);
return cachedPlan;
}
// 从缓存中获取数据的逻辑
private PopupWindowPlan getPlanFromCache(String openId, String imei, String gamePackage, Integer planType) {
String key = RedisUtils.buildRedisKey(RedisKeyConstants.REDISKEY_USER_POPUP_PLAN, openId, imei, gamePackage, planType);
String cacheValue = redisDao.get(key);
if (StringUtils.isEmpty(cacheValue)) {
return null;
}
try {
PopupWindowPlan plan = objectMapper.readValue(cacheValue, PopupWindowPlan.class);
return plan;
} catch (Exception e) {
}
return null;
}
// 保存数据到缓存当中
private void setPlanToCache(String openId, String imei, String gamePackage, PopupWindowPlan plan, Integer planType) {
String key = RedisUtils.buildRedisKey(RedisKeyConstants.REDISKEY_USER_POPUP_PLAN, openId, imei, gamePackage, planType);
try {
String serializedStr = objectMapper.writeValueAsString(plan);
redisDao.set(key, serializedStr, VivoConfigManager.getInteger(ConfigConstants.POPUP_PLAN_CACHE_EXPIRE_TIME, 300));
} catch (Exception e) {
}
}
**如监控实现部分所述,通过 Redis Key 的前缀聚合监控,能够发现某一类业务场景的 Redis 的写请求数,进而发现 Redis 的无效使用场景。
上述案例是典型的Redis的缓存使用场景:1.访问 Redis 缓存;2.若命中则直接返回结果;3、如未命中则查询持久化存储获取数据并写入 Redis 缓存。
从业务监控的大盘发现前缀 popup:user:plan 存在大量的 set 操作命令,按照缓存读多写少的原则,该场景标明该缓存的设计是无效的。
通过业务分析后,发现在游戏的业务场景中 用户维度+游戏维度 不存在5分钟重复访问缓存的场景,确认缓存的无效**
本地缓存caffeine
public final class Caffeine<K, V> {
/**
* caffeine的实例名称
*/
String instanceName;
/**
* caffeine的实例维护的Map信息
*/
static Map<String, Cache> cacheInstanceMap = new ConcurrentHashMap<>();
@NonNull
public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
requireWeightWithWeigher();
requireNonLoadingCache();
@SuppressWarnings("unchecked")
Caffeine<K1, V1> self = (Caffeine<K1, V1>) this;
Cache localCache = isBounded() ? new BoundedLocalCache.BoundedLocalManualCache<>(self) : new UnboundedLocalCache.UnboundedLocalManualCache<>(self);
if (null != localCache && StringUtils.isNotEmpty(localCache.getInstanceName())) {
cacheInstanceMap.put(localCache.getInstanceName(), localCache);
}
return localCache;
}
}
static Cache<String, List<String>> accountWhiteCache = Caffeine.newBuilder().applyName("accountWhiteCache")
.expireAfterWrite(VivoConfigManager.getInteger("trade.account.white.list.cache.ttl", 10), TimeUnit.MINUTES)
.recordStats().maximumSize(VivoConfigManager.getInteger("trade.account.white.list.cache.size", 100)).build();
public static StatsData getCacheStats(String instanceName) {
Cache cache = Caffeine.getCacheByInstanceName(instanceName);
CacheStats cacheStats = cache.stats();
StatsData statsData = new StatsData();
statsData.setInstanceName(instanceName);
statsData.setTimeStamp(System.currentTimeMillis()/1000);
statsData.setMemoryUsed(String.valueOf(cache.getMemoryUsed()));
statsData.setEstimatedSize(String.valueOf(cache.estimatedSize()));
statsData.setRequestCount(String.valueOf(cacheStats.requestCount()));
statsData.setHitCount(String.valueOf(cacheStats.hitCount()));
statsData.setHitRate(String.valueOf(cacheStats.hitRate()));
statsData.setMissCount(String.valueOf(cacheStats.missCount()));
statsData.setMissRate(String.valueOf(cacheStats.missRate()));
statsData.setLoadCount(String.valueOf(cacheStats.loadCount()));
statsData.setLoadSuccessCount(String.valueOf(cacheStats.loadSuccessCount()));
statsData.setLoadFailureCount(String.valueOf(cacheStats.loadFailureCount()));
statsData.setLoadFailureRate(String.valueOf(cacheStats.loadFailureRate()));
Optional<Eviction> optionalEviction = cache.policy().eviction();
optionalEviction.ifPresent(eviction -> statsData.setMaximumSize(String.valueOf(eviction.getMaximum())));
Optional<Expiration> optionalExpiration = cache.policy().expireAfterWrite();
optionalExpiration.ifPresent(expiration -> statsData.setExpireAfterWrite(String.valueOf(expiration.getExpiresAfter(TimeUnit.SECONDS))));
optionalExpiration = cache.policy().expireAfterAccess();
optionalExpiration.ifPresent(expiration -> statsData.setExpireAfterAccess(String.valueOf(expiration.getExpiresAfter(TimeUnit.SECONDS))));
optionalExpiration = cache.policy().refreshAfterWrite();
optionalExpiration.ifPresent(expiration -> statsData.setRefreshAfterWrite(String.valueOf(expiration.getExpiresAfter(TimeUnit.SECONDS))));
return statsData;
}
public static void sendReportData() {
try {
if (!VivoConfigManager.getBoolean("memory.caffeine.data.report.switch", true)) {
return;
}
// 1、获取所有的cache实例对象
Method listCacheInstanceMethod = HANDLER_MANAGER_CLASS.getMethod("listCacheInstance", null);
List<String> instanceNames = (List)listCacheInstanceMethod.invoke(null, null);
if (CollectionUtils.isEmpty(instanceNames)) {
return;
}
String appName = System.getProperty("app.name");
String localIp = getLocalIp();
String localPort = String.valueOf(NetPortUtils.getWorkPort());
ReportData reportData = new ReportData();
InstanceData instanceData = new InstanceData();
instanceData.setAppName(appName);
instanceData.setIp(localIp);
instanceData.setPort(localPort);
// 2、遍历cache实例对象获取缓存监控数据
Method getCacheStatsMethod = HANDLER_MANAGER_CLASS.getMethod("getCacheStats", String.class);
Map<String, StatsData> statsDataMap = new HashMap<>();
instanceNames.stream().forEach(instanceName -> {
try {
StatsData statsData = (StatsData)getCacheStatsMethod.invoke(null, instanceName);
statsDataMap.put(instanceName, statsData);
} catch (Exception e) {
}
});
// 3、构建上报对象
reportData.setInstanceData(instanceData);
reportData.setStatsDataMap(statsDataMap);
// 4、发送Http的POST请求
HttpPost httpPost = new HttpPost(getReportDataUrl());
httpPost.setConfig(requestConfig);
StringEntity stringEntity = new StringEntity(JSON.toJSONString(reportData));
stringEntity.setContentType("application/json");
httpPost.setEntity(stringEntity);
HttpResponse response = httpClient.execute(httpPost);
String result = EntityUtils.toString(response.getEntity(),"UTF-8");
EntityUtils.consume(response.getEntity());
logger.info("Caffeine 数据上报成功 URL {} 参数 {} 结果 {}", getReportDataUrl(), JSON.toJSONString(reportData), result);
} catch (Throwable throwable) {
logger.error("Caffeine 数据上报失败 URL {} ", getReportDataUrl(), throwable);
}
}

浙公网安备 33010602011771号