缓存也许是程序员心中最熟悉的性能优化手段之一, 在旧文中 微服务缓存漫谈之Guava Cache 和 Redis 集群的构建和监控 中分别介绍了最常用的本地内存的 Guava Cache 和远程的 Redis Cache. 这里我们重点聊聊缓存的度量。
对于缓存,我们关心这几个问题:
Cache hit ratio 缓存的命中率Cache key size 缓存的键值数量Cache resource usage 缓存的资源使用率Cache loading performance 缓存的加载性能Cache capacity 缓存的容量Cache lifetime 缓存的生命周期Cache 不可能无限增长, 不可能永远有效, 所以对于 Cache 的清除策略和失效策略要细细考量. 对于放在 Cache 中的数据也最好是读写比较高的, 即读得多, 写得少, 不会频繁地更新.
缓存不是万能药,缓存使用不当会生成缓存穿透,击穿和雪崩,先简单解释一下这几个概念
穿透 某条记录压根不存在,所以在缓存中找不到,每次都需要到数据库中读取,但是结果还是找不到。常用的应对方法是布隆过滤器(它的特点是)或者反向缓存(在缓存中保存这条记录,标识它是不存在的)
击穿 某条记录过期被移除了,恰好大量相关的查询请求这条记录,导致瞬时间大量请求绕过缓存访问数据库常用的应对方法是将从数据库加载数据的操作加锁,这样就不会有很多访问请求绕过缓存。 或者干脆不设置过期时间,而是用一个后台job 定时刷新缓存,外部的请求总能从缓存中读到数据
3.雪崩 多条记录在多个服务器上的缓存同时过期失效,导致瞬时间大量请求绕过缓存访问数据库,这个比击穿更严重。
常用的应对方法是多个服务器上的多条记录设置不同的失效时间,可以用个随机值作为零头,将大量的并发请求从某个时间点分布到一个时间段中
缓存的命中率,加载性能等等都是我们关心的重点,例如:
性能 Performance: Cache 加载的延迟 latency吞吐量Throughput: 每秒请求次数 CPS(Call Per Second)命中率:sucess_ratio = hitCount / (hitCount + missCount)资源使用量: 使用了多少内存资源饱和度 saturation: 由于容量限制被移出cache 的记录数,缓存满了无法增加的记录数注: 饱和度是资源负载超出其处理能力的地方。
以 Guava Cache 为例,它的缓存统计信息根据以下规则递增:
当缓存查找遇到现有缓存条目时,hitCount会增加。当缓存查找第一次遇到丢失的缓存条目时,将加载一个新条目。成功加载条目后,missCount和loadSuccessCount会增加,并将总加载时间(以纳秒为单位)添加到totalLoadTime中。在加载条目时引发异常时,missCount和loadExceptionCount会增加,并且总加载时间(以纳秒为单位)将添加到totalLoadTime中。遇到仍在加载的缺少高速缓存条目的高速缓存查找将等待加载完成(无论是否成功),然后递增missCount。从缓存中逐出条目时,evictionCount会增加。当缓存条目无效或手动删除时,不会修改任何统计信息。在缓存的asMap视图上调用的操作不会修改任何统计信息。我们在写代码时可以调用它的 recordStats 来记录这些度量数据
@Bean public LoadingCache<String, CityWeather> cityWeatherCache() { LoadingCache<String, CityWeather> cache = CacheBuilder.newBuilder() .recordStats() .maximumSize(1000) .expireAfterWrite(60, TimeUnit.MINUTES) .build(weatherCacheLoader()); recordCacheMetrics("cityWeatherCache", cache); return cache; } public void recordCacheMetrics(String cacheName, Cache cache) { MetricRegistry metricRegistry = metricRegistry(); metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "hitCount"), () -> () -> cache.stats().hitCount()); metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "hitRate"), () -> () -> cache.stats().hitRate()); metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "missCount"), () -> () -> cache.stats().missCount()); metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "missRate"), () -> () -> cache.stats().missRate()); metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "requestCount"), () -> () -> cache.stats().requestCount()); metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "loadCount"), () -> () -> cache.stats().loadCount()); metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "loadSuccessCount"), () -> () -> cache.stats().loadSuccessCount()); metricRegistry.gauge(generateMetricsKeyForCache(cacheName, "loadExceptionCount"), () -> () -> cache.stats().loadExceptionCount()); } public String generateMetricsKeyForCache(String cacheName, String keyName) { String metricKey = MetricRegistry.name("cache", cacheName, keyName); log.info("metric key generated for cache: {}", metricKey); return metricKey; }未完待续...