缓存的使用和设计

缓存的使用和设计

缓存的收益与成本

收益

  1. 加速读写

    • 通过缓存加速读写:CPU L1/L2/L3 Cache,浏览器缓存,Ehcache缓存数据库结果
  2. 降低后端负载

    • 后端服务器通过前端缓存降低负载:业务端使用Redis降低后端MySQL负载

成本

  1. 数据不一致

    • 缓存层和数据层有时间窗口不一致,和更新策略有关
  2. 代码维护成本:多了一层缓存逻辑

  3. 运维成本:Redis Cluster

使用场景

  1. 降低后端负载

    • 用于高消耗的SQL:join结果集/分组统计结果
  2. 加速请求响应

    • 利用Redis/Memcache优化IO时间
  3. 大量写合并为批量写

    • 计数器线Redis累加再批量更新到后端数据库

缓存更新策略

  1. LRU/LFU/FIFO算法剔除:例如maxmemory-policy

  2. 超时剔除:例如expire

  3. 主动更新:开发控制生命周期

  • 推荐结合剔除,超时,主动更新三种方案完成

三种策略比较

| 策略 | 一致性 | 维护成本 |

| —————- | —— | ——– |

| LRU/LIRS算法剔除 | 最差 | 底 |

| 超时剔除 | 较差 | 低 |

| 主动更新 | 强 | 高 |

TIPS

  1. 低一致性:最大内存和淘汰策略

  2. 高一致性:超时剔除和主动更新结合,最大内存和淘汰策略兜底

缓存粒度控制

什么是缓存粒度

  1. 从MySQL获取用户信息

    • select * from usr where id={id}
  2. 设置用户信息缓存

    • ```set usr:{id} `select * from usr where id={id}````
  3. 缓存粒度

    • 部分重要属性

```set usr:{id} `select * from usr where id={id}````

- 全部属性

```set usr:{id} `select * from usr where id={id}````

缓存粒度控制

  1. 通用性:全量属性更好

  2. 占用空间:部分属性更好

  3. 代码维护:综合考虑,是否使用这么多属性

缓存穿透优化

缓存穿透:大量请求不命中

大量没有结果的请求通过cache访问到后端,后端也没有命中

原因

  1. 业务代码,没有正确从后端拿到数据

  2. 恶意攻击,爬虫{大量请求携带未知数据去访问缓存以及数据库}

及时发现

  1. 业务的相应时间

  2. 业务的本身问题

  3. 监控几个指标

    • 总调用数

    • 缓存层命中数

    • 存储层命中数

解决方法

  1. 缓存空对象

    • 如果从后端数据库中的请求结果是一个空值,我们也保存,不过设置一个过期时间(有可能后端数据库故障或者接口故障),这样减小后端数据库的压力

    • 问题:

      1). 需要更多的键(设置过期时间解决)

      2). 缓存层和存储层数据“短期”不一致(订阅故障消息解决)

    • 伪代码


public String getPassThrough(String key){

    String cacheValue = cache.get(key);

    if(StringUtils.isBlank(cacheValue)){

        String storgeValue = storage.get(key)//如果cache中为空,就从storage中拿数据

        cache.set(key,storageValue);

        if(StringUtils.isBlank(storageValue)){

            cache.expire(key,60*5);//如果从后端接口获取值为空,设置一个过期时间

        }

        return storageValue;

    }else{

        return cacheValue;

    }

}
  1. 布隆过滤器

    • 数据很大不能做到实时

    • 利用算法,可以使用很小的内存判断一个值是否在一个大数据集中

    • 在请求cache之前先通过bloom filter过滤一次,判断请求是否有效

缓存无底洞:节点增加,性能下降

原因

  1. 更多的机器!=更高的性能

  2. 批量接口需求(mget,mset)等(节点增加,io时间增加)

  3. 数据增长与水平扩展需求

优化

  1. 命令优化:例如慢查询keys,hgetall

  2. 减少网络通信次数

  3. 降低接入成本:例如客户端长连接/连接池.NIO

优化方案比较

| 方案 | 优点 | 缺点 | 网络IO |

| ——– | ———————————— | ——————————————– | —————– |

| 串行mget | 编程简单少量keys满足需求 | 大量keys请求延迟严重 | O(keys) |

| 串行IO | 编程简单少量节点满足需求 | 大量node延迟严重 | O(nodes) |

| 并行IO | 利用并行特性延迟取决于最慢的节点 | 编程复杂超时定位问题难 | O(max_slow(node)) |

| hash_tag | 性能最高 | 读写增加tag维护成本tag分布易出现数据倾斜 | O1 |

缓存雪崩

缓存集中过期或者缓存服务器宕机

缓存集中过期

在某一时间段,缓存集中过期失效,访问压力会给到后端数据库

  • 为不同的分类设置不同的过期时间

  • 同一分类的不同商品在设置过期时间时加一个随机因子

  • 根据请求数量和密度设置过期时间

服务器宕机

  • 缓存层实现高可用

  • 客户端降级

  • 提前演练

热点key重建优化

原因

热点key在多次访问时,线程一直在做查询数据源,重建缓存的操作

例如微博热搜

优化目标

  1. 减少重缓存的次数

  2. 数据尽可能一致

  3. 减少潜在危险

优化思路

互斥锁

在查询数据源和重建缓存这个过程中加锁,如果有线程在执行这个操作,其他线程只能等待缓存重建完毕

  • 伪代码:

String get(String key){

    String value = redis.get(key);

    if(value == null){

        String mutexKey = "mutex🔑" + key;

        if(redis.set(mutexKey,"1","ex 180","nx")){

            value = db.get(key);

            redis.set(key,value);

            redis.delete(mutexKey);

        }else{

            //其他线程休息50ms

            Thread.sleep(50);

            get(key);

        }

    }

    return value;

}

永不过期

  1. 缓存:没有加expire

  2. 功能层面:为每个value添加逻辑过期时间,如果发现超过逻辑过期时间,使用单独的线程去构建缓存

我们的key永不过期,线程获取缓存不需要等待,如果中间发现value的过期时间到了,就新开一个线程去更新key。在更新完成前所有的请求获取得到的都是更新前的旧值,知道更新完成后,才会得到新值

  • 伪代码

String get(final String key){

    V v= redis.get(key);

    String value = v.getValue();

    long logicTimeout = v.getLogicTimeout();

    if(logicTimeout >= System.currentTimeMills()){

        String mutexKey = "mutex🔑" + 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(muteKey);

                }

        	});

        }

    }

    return value;

}

两种方案对比

| 方案 | y优点 | 缺点 |

| ———- | ————————- | ————————————————– |

| 永远不过期 | 基本杜绝热点key重建按问题 | 不保证一致性逻辑过期时间增加维护成本和内存成本 |

| 互斥锁 | 思路简单保证一致性 | 代码复杂度增加存在死锁的风险 |


CC BY-NC 4.0

软件架构1-Tier
无人驾驶概述

Comments