缓存设计
11.1穿透优化
缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层
- 缓存层不命中
- 存储层不命中,不将空结果写回缓存。
- 返回空结果。
缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。
缓存穿透问题可能会使后端存储负载加大,由于很多后端存储不具备高并发性,甚至可能造成后端存储宕掉。通常可以在程序中分别统计总调用数、缓存层命中数、存储层命中数,如果发现大量存储层空命中,可能就是
出现了缓存穿透问题。
如何解决缓存穿透问题:
- 缓存空对象
- 布隆过滤器拦截
- 两种方案对比
11.2 无底洞优化
如何解决问题:
1.串行命令
由于n个key是比较均匀地分布在Redis Cluster的各个节点上,因此无法使用mget命令一次性获取,所以通常来讲要获取n个key的值,最简单的方法就是逐次执行n个get命令,这种操作时间复杂度较高,它的操作时间=n次网络时间+n次命令时间,网络次数是n。很显然这种方案不是最优的,但是实现起来比较简单
List<String> serialMGet(List<String> keys) {
// 结果集
List<String> values = new ArrayList<String>();
// n次串行get
for (String key : keys) {
String value = jedisCluster.get(key);
values.add(value);
}
return values;
}
2.串行IO
Map<String, String> serialIOMget(List<String> keys) {
// 结果集
Map<String, String> keyValueMap = new HashMap<String, String>();
// 属于各个节点的key列表,JedisPool要提供基于ip和port的hashcode方法
Map<JedisPool, List<String>> nodeKeyListMap = new HashMap<JedisPool, List<String>>();
// 遍历所有的keyfor (String key : keys) {
// 使用CRC16本地计算每个key的slot
int slot = JedisClusterCRC16.getSlot(key);
// 通过jedisCluster本地slot->node映射获取slot对应的node
JedisPool jedisPool = jedisCluster.getConnectionHandler().getJedisPoolFrom
Slot(slot);
// 归档
if (nodeKeyListMap.containsKey(jedisPool)) {
nodeKeyListMap.get(jedisPool).add(key);
} else {
List<String> list = new ArrayList<String>();
list.add(key);
nodeKeyListMap.put(jedisPool, list);
}
}
// 从每个节点上批量获取,这里使用mget也可以使用pipeline
for (Entry<JedisPool, List<String>> entry : nodeKeyListMap.entrySet()) {
JedisPool jedisPool = entry.getKey();
List<String> nodeKeyList = entry.getValue();
// 列表变为数组
String[] nodeKeyArray = nodeKeyList.toArray(new String[nodeKeyList.size()]);
// 批量获取,可以使用mget或者Pipeline
List<String> nodeValueList = jedisPool.getResource().mget(nodeKeyArray);
// 归档
for (int i = 0; i < nodeKeyList.size(); i++) {
keyValueMap.put(nodeKeyList.get(i), nodeValueList.get(i));
}
}
return keyValueMap;
}
3.并行IO
11.3 雪崩优化
预防和解决缓存雪崩问题,可以从以下三个方面进行着手。
- 保证缓存层服务高可用性
- 依赖隔离组件为后端限流并降级
- 提前演练。
11.4 热点key重建优化
-
互斥锁
此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可 -
永远不过期
本章重点回顾
- 缓存的使用带来的收益是能够加速读写,降低后端存储负载。
- 缓存的使用带来的成本是缓存和存储数据不一致性,代码维护成本增大,架构复杂度增大。
- 比较推荐的缓存更新策略是结合剔除、超时、主动更新三种方案共同完成。
- 穿透问题:使用缓存空对象和布隆过滤器来解决,注意它们各自的使用场景和局限性。
- 无底洞问题:分布式缓存中,有更多的机器不保证有更高的性能。有四种批量操作方式:串行命令、串行IO、并行IO、hash_tag。
- 雪崩问题:缓存层高可用、客户端降级、提前演练是解决雪崩问题的重要方法。
- 热点key问题:互斥锁、“永远不过期”能够在一定程度上解决热点,key问题,开发人员在使用时要了解它们各自的使用成本。