Cache Best Practices via Hugh Yang
General Guideline
- 避免重复造轮子。实现一个sophisticated的缓存并不是容易的事情,尽量使用well-tested, open source的缓存产品
- Key-value的Map不是真正的缓存,除了用于存放数量有限的immutable数据,并不能代替缓存
- 避免在open source的缓存外面再加一层包装。如果有包装的需要,必须经过核心开发人员review
- 避免使用二级缓存。如果有使用二级缓存的需要,必须经过核心开发人员review
- 数据一致性问题。在使用缓存时,必须考虑到在后端数据源变化的情况下,缓存数据的一致性处理
脏数据处理策略
经常会遇到读取到缓存的脏数据导致线上故障。很多情况下都会产生脏数据,例如数据库数据变更,下游service数据结构变化等。这时在开发时需要考虑全面,如果不够确定可以请核心开发人员帮忙代码review。下面是预防此类问题的常用策略:
- 改变cache key。这样即使缓存中有脏数据,也不会影响到新代码
- 版本控制,版本号做为cache key的一部分,避免新旧数据相互影响
- 上线前刷新缓存,从缓存中清除脏数据
- 在读取缓存数据时,代码加入适当验证机制,确保读到脏数据时能够graceful degradation
缓存击穿处理策略
后台线程
启动专门的后台线程,负责更新缓存,其它线程只做读取操作。这样无论并发量多大,一个application instance最多只有一个线程会回源,避免了缓存突然失效时瞬间出现大量回源请求。对于有多个application instances的应用,例如mapi目前有400个application instances,最大仍有可能有400个回源请求,这种情况可以加入随机offset,避免所有application instacnes在同一时间回源。示例代码如下:
private class ApiKeyUpdater implements Runnable {
private final Logger logger = LoggerFactory.getLogger(ApiKeyUpdater.class);
private Random rand = new Random();
@Override
public void run() {
while (true) {
try {
if (stopped) {
logger.info("Termination signal received, exit...");
break;
}
long sleepTime = UPDATE_INTERVAL + rand.nextInt(UPDATE_OFFSET);
Thread.sleep(sleepTime);
refreshApiKeyMap();
}
catch (InterruptedException e) {
logger.info("Interruption signal received, exit...");
break;
}
catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
}
}
当然后台线程带来的一个问题是,如果很多地方都有类似的配置需求,不可能所有地方都启动一个后台线程。对于这种情况,最好引入统一的配置中心实现。
Memcached vs Redis
Memcached
Pros
- 简单,就是一个key-value storage
- 一般来说,单台instance所能支持的并发量更大
Cons
- 缺乏数据持久化支持
- 缺乏replication支持
Redis
Pros
- 多种数据类型支持
- 多种集合操作,在需要对集合做intersection,union,complement的时候很方便
- 数据持久化支持
Cons
- Redis基于单线程,一般来说单台instance所能支持的并发量不如memcached
EhCache vs Guava Cache
EhCache
Pros
- 成熟,功能很完善
- easy to scale,支持JVM缓存,off-heap缓存和分布式缓存。虽然off-heap和分布式缓存需要商业license
- 很多open source framework,例如Spring,Hibernate都提供了集成支持
Cons
- 相对来说,有点重
Guava Cache
Pro
- 简单,缓存功能强于ConcurrentMap
Cons
- Scalability较差,只是一个local memory缓存