第11章 缓存设计

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出现数据不一致的情况
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容