image.png
11.1 缓存地收益和成本--有啥用
-
架构图
image.png
- 收益
- 加速读写
- 降低后端负载
- 成本
- 数据更新延迟导致的不一致
- 缓存相关处理代码维护成本
- 运维成本
- 场景
- 开销大的复杂计算
- 加速请求响应
11.2 缓存更新策略 -- 怎么用
- 三种缓存更新策略
策略 | 一致性 | 维护成本 |
---|---|---|
LRU/LFU/FIFO算法剔除 | 最差 | 低 |
超时剔除 | 较差 | 较低 |
主动更新 | 强 | 高 |
- 最佳实践
- 低一致性业务建议配置最大内存和LRU/LFU/FIFO淘汰策略低方式使用
- 高一致性业务可以结合使用超时剔除和主动更新
11.3 缓存粒度控制 -- 存什么东西在里面
数据类型 | 通用性 | 空间占用(内存空间+网络带宽) | 代码维护 |
---|---|---|---|
全部数据 | 高 | 大 | 简单 |
部分数据 | 低 | 小 | 较为复杂 |
11.4 穿透优化 -- 缓存和存储里都没有的值被访问了
-
穿透现象示意图
image.png -
造成穿透的原因
- 自身业务代码或者数据出现问题
- 一些恶意攻击,爬虫等造成大量空命中
-
解决办法--缓存空对象
image.png -
解决办法--布隆过滤器拦截
image.png
- 两种解决办法的对比
解决缓存穿透 | 适用场景 | 维护成本 |
---|---|---|
缓存空对象 | 数据命中不高, 数据频繁变化实时性高 | 代码维护简单, 需要过多低缓存空间, 数据不一致 |
布隆过滤器 | 数据命中不高, 数据相对固定实时性低 | 代码维护复杂, 缓存空间占用少 |
11.5 无底洞优化 -- 批量操作在节点增加时响应反而变慢
- 定义
为了满足业务要求,添加大量新的缓存节点,但性能不但没有好转反而下降了
-
原因
- 客户端一次批量操作会涉及多次网络操作,随着缓存节点的增多,网络耗时会逐渐增大
- 网络连接数变多,也影响了网络性能
-
优化方法
- 命令本身的优化,例如优化SQL语句
- 减少网络通信次数
- 降低接入成本,例如客户端使用长连接或连接池,NIO等
-
减少网络连接的方法
image.png
- 串行命令
List<String> serialMGet(List<String> keys) {
List<String> values = new ArrayList<String>();
for (String key : keys) {
String value = jedisCluster.get(key);
values.add(value);
}
return values;
}
- 串行IO
Map<String, String> serialIOMget(List<String> keys) {
Map<String, String> keyValueMap = new HashMap<String, String>();
Map<JedisPool, List<String>> nodeKeyListMap = new HashMap<JedisPool, List<String>>();
for (String key : keys) {
int slot = JedisClusterCRC16.getSlot(key);
JedisPool jedisPool = jedisCluster.getConnectionHandler().getJedisPoolFromSlot(slot);
if (nodeKeyListMap.containsKey(jedisPool)) {
nodeKeyListMap.get(jedisPool).add(key);
} else {
List<String> list = new ArrayList<String>();
list.add(key);
nodeKeyListMap.put(jedisPool, list);
}
}
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()]);
List<String> nodeValueList = jedisPool.getResource().mget(nodeKeyArray);
for (int i = 0; i < nodeKeyList.size(); i++) {
keyValueMap.put(nodeKeyList.get(i), nodeValueList.get(i));
}
}
}
- 并行IO
Map<String, String> parallelIOMget(List<String> keys) {
Map<String, String> keyValueMap = new HashMap<String, String>();
Map<JedisPool, List<String>> nodeKeyListMap = new HashMap<JedisPool, List<String>>();
for (Entry<JedisPool, List<String>> entry : nodeKeyListMap.entrySet()) {
// 多线程获取该node下的subkeys数据
}
return keyValueMap;
}
- hash_tag实现
利用Redis Cluster的hash_tag特性将多个key强制分配到一个节点上, 这样只需要1次网络时间+n次命令时间即可获得批量数据。
List<String> hashTagMget(String[] hashTagKeys) {
return jedisCluster.mget(hashTagKeys);
}
- 方案对比
方案 | 优点 | 缺点 | 网络IO |
---|---|---|---|
串行命令 | 编程简单,少量keys时性能尚可 | 大量keys请求延迟严重 | O(keys) |
串行IO | 编程简单, 少量节点时性能满足要求 | 大量node时延迟严重 | O(nodes) |
并行命令 | 利用并行命令,延迟取决于最慢的节点 | 编程复杂,多线程下定位问题较难 | O(max_slow(nodes)) |
hash_tag | 性能最高 | 业务维护成本高,容易出现数据倾斜 | O(1) |
11.6 雪崩优化 -- 缓存不能用了
- 预防办法
- 保证缓存层高可用
- 依赖隔离组件为后端限流并降级
- 提前演练缓存down掉的情况
11.7 热点key重建优化
目标
- 减少重建缓存的次数
- 数据尽可能一致
- 较少的潜在危险
办法
- 互斥锁
String get(String key) {
String value = redis.get(key);
if (value == null) {
String mutexKey = "mutex:key:" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
value = db.get(key);
redis.setex(key, timeout, value);
redis.delete(mutexKey);
} else {
Thread.sleep(50);
get(key);
}
}
return value;
}
- 永远不过期
- 老数据物理上一直存在
- 当发现老数据逻辑上过期时,使用单独的线程构建缓存
- 允许在异步构建缓存期间,热点key出现数据不一致的情况