Redis学习--缓存设计

2019-06-30  本文已影响0人  何何与呵呵呵
缓存的收益和成本
缓存更新策略

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


三种常见更新策略的对比

4.最佳实践

缓存粒度控制

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

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

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


使用布隆过滤器应对穿透问题
缓存空对象和布隆过滤器方案对比
无底洞优化
分布式存储批量操作多次网络时间
当一个节点存储批量操作只需一次网络时间
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问题,开发人员在使用时要了解它们各自的使用成本。

上一篇 下一篇

猜你喜欢

热点阅读