美文网首页
Redis学习--缓存设计

Redis学习--缓存设计

作者: 何何与呵呵呵 | 来源:发表于2019-06-30 15:20 被阅读0次
缓存的收益和成本
  • 加速读写
  • 降低后端负载


    缓存层+存储层基本流程
  • 数据不一致性
  • 代码维护成本
  • 运维成本
缓存更新策略

1.LRU/LFU/FIFO算法剔除
剔除算法通常用于缓存使用量超过了预设的最大值时候,如何对现有的数据进行剔除。
2.超时剔除
超时剔除通过给缓存数据设置过期时间,让其在过期时间后自动删除。
3.主动更新
可以利用消息系统或者其他方式通知缓存更新。


三种常见更新策略的对比

4.最佳实践

  • 低一致性业务建议配置最大内存和淘汰策略的方式使用。
  • 高一致性业务可以结合使用超时剔除和主动更新,这样即使主动更新出了问题,也能保证数据过期时间后删除脏数据。
缓存粒度控制

通用性。缓存全部数据比部分数据更加通用,但从实际经验看,很长时间内应用只需要几个重要的属性。
空间占用。缓存全部数据要比部分数据占用更多的空间。

缓存全部数据和部分数据对比
穿透优化

缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中。
1.缓存空对象
第一,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。
2.布隆过滤器拦截
在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截。


使用布隆过滤器应对穿透问题
缓存空对象和布隆过滤器方案对比
无底洞优化
分布式存储批量操作多次网络时间
当一个节点存储批量操作只需一次网络时间
  • 命令本身的优化,例如优化SQL语句等。
  • 减少网络通信次数。
  • 降低接入成本,例如客户端使用长连/连接池、NIO等。
    1.串行命令
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>>();
    // 遍历所有的key
    for (String key : keys) {
        // 使用CRC16本地计算每个key的slot
        int slot = JedisClusterCRC16.getSlot(key);
        // 通过jedisCluster本地slot->node映射获取slot对应的node
        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);
        }
    }
    // 从每个节点上批量获取,这里使用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

Map<String, String> parallelIOMget(List<String> keys) {
    // 结果集
    Map<String, String> keyValueMap = new HashMap<String, String>();
    // 属于各个节点的key列表
    Map<JedisPool, List<String>> nodeKeyListMap = new HashMap<JedisPool, List<String>>();
    ...和前面一样
    // 多线程mget,最终汇总结果
    for (Entry<JedisPool, List<String>> entry : nodeKeyListMap.entrySet()) {
        // 多线程实现
    }
    return keyValueMap;
}
客户端并行node次网络IO

4.hash_tag实现
将多个key强制分配到一个节点上,它的操作时间=1次网络时间+n次命令时间


四种批量操作解决方案对比
雪崩优化

指的是缓存层宕掉后,流量会像奔逃的野牛一样,打向后端存储。


缓存层不可用引起的雪崩

1)保证缓存层服务高可用性。
2)依赖隔离组件为后端限流并降级。
3)提前演练。

热点key重建优化

使用“缓存+过期时间”的策略可能带来的弊端
1.当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。
2.重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次IO、多个依赖等。
在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。


热点key失效后大量线程重建缓存

解决方案:
1.互斥锁(mutex key)

String get(String key) {
    // 从Redis中获取数据
    String value = redis.get(key);
    // 如果value为空,则开始重构缓存
    if (value == null) {
        // 只允许一个线程重构缓存,使用nx,并设置过期时间ex
        String mutexKey = "mutext:key:" + key;
        if (redis.set(mutexKey, "1", "ex 180", "nx")) {
            // 从数据源获取数据
            value = db.get(key);
            // 回写Redis,并设置过期时间
            redis.setex(key, timeout, value);
            // 删除key_mutex
            redis.delete(mutexKey);
        }
        // 其他线程休息50毫秒后重试
        else {
            Thread.sleep(50);
            get(key);
        }
    }
    return value;
}
使用互斥锁重建缓存

2.永远不过期


“永远不过期”策略
String get(final String key) {
    V v = redis.get(key);
    String value = v.getValue();
    // 逻辑过期时间
    long logicTimeout = v.getLogicTimeout();
    // 如果逻辑过期时间小于当前时间,开始后台构建
    if (v.logicTimeout <= System.currentTimeMillis()) {
        String mutexKey = "mutex:key:" + key;
        if (redis.set(mutexKey, "1", "ex 180", "nx")) {
            // 重构缓存
            threadPool.execute(new Runnable() {
                public void run() {
                    String dbValue = db.get(key);
                    redis.set(key, (dbvalue,newLogicTimeout));
                    redis.delete(mutexKey);
                }
          });
    }
}
return value;
}

方案比较:

互斥锁(mutex key):这种方案思路比较简单,但是存在一定的隐患,如果构建缓存过程出现问题或者时间较长,可能会存在死锁和线程池阻塞的风险,但是这种方法能够较好地降低后端存储负载,并在一致性上做得比较好。
“永远不过期”:这种方案由于没有设置真正的过期时间,实际上已经不存在热点key产生的一系列危害,但是会存在数据不一致的情况,同时代码复杂度会增大。


两种热点key的解决方法
总结

1)缓存的使用带来的收益是能够加速读写,降低后端存储负载。
2)缓存的使用带来的成本是缓存和存储数据不一致性,代码维护成本增大,架构复杂度增大。
3)比较推荐的缓存更新策略是结合剔除、超时、主动更新三种方案共同完成。
4)穿透问题:使用缓存空对象和布隆过滤器来解决,注意它们各自的使用场景和局限性。
5)无底洞问题:分布式缓存中,有更多的机器不保证有更高的性能。有四种批量操作方式:串行命令、串行IO、并行IO、hash_tag。
6)雪崩问题:缓存层高可用、客户端降级、提前演练是解决雪崩问题的重要方法。
7)热点key问题:互斥锁、“永远不过期”能够在一定程度上解决热点key问题,开发人员在使用时要了解它们各自的使用成本。

相关文章

  • Redis学习--缓存设计

    缓存的收益和成本 加速读写 降低后端负载缓存层+存储层基本流程 数据不一致性 代码维护成本 运维成本 缓存更新策略...

  • 高并发扩容之缓存

    缓存 缓存 Guava Cache 学习redis网站 redis.cn RedisConfig RedisCli...

  • 【MyBatis】学习纪要八:缓存(二)

    缓存工具 MyBatis缓存 Ehcache Redis 缓存设计 缓存接口 我们来讨论一下,说到MyBatis,...

  • MI 2021-07-09

    一面 问的设计题偏多 Redis 缓存热点数据 热点数据指的是什么? Redis缓存数据量关注过吗? 如果全部缓存...

  • redis缓存设计

    缓存的利于弊及应用场景 这里我们主要讨论以Redis为代表的基于内存的缓存方案。 缓存的优点 提升访问速度,减少后...

  • redis学习笔记(八) 缓存设计

    1. 缓存优缺点 缓存常用的结构如下: 1.1. 优点 加速读写:由于数据库读写速度慢,而基于内存的读写速度快,所...

  • Redis 缓存使用(使用代理设计模式)

    Redis 缓存接口 Redis 缓存接口实现 Redis 工具类 Redis 配置 Redis 枚举

  • redis 分片

    为什么要分片 问题:公司用户量3千万,用户基本信息缓存到redis中,需要内存10G,如何设计Redis的缓存架构...

  • SpringBoot 使用 Redis 做缓存

    springboot 使用 redis 做缓存 maven 添加 redis 缓存支持