Redis 缓存的使用与优化

缓存的使用与设计

  • 缓存的收益与成本
  • 缓存更新策略
  • 缓存粒度控制
  • 缓存穿透优化
  • 缓存无底洞问题
  • 缓存雪崩优化
  • 热点key重建优化
  • 缓存降级

缓存的收益与成本

收益

  1. 加速读写
  2. 降低后端负载,例如使用 redis 降低 mysql 负载等。

成本

  1. 数据不一致:缓存层和数据层有时间窗口不一致的情况,和更新策略有关。
  2. 代码维护成本:多了一层缓存逻辑。
  3. 运维成本:例如 Redis Cluster

使用场景

  1. 降低后端负载,对高消耗的SQL比如JOIN结果集、分组统计结果缓存。
  2. 加速请求响应,利用Redis/Memcache优化IO响应时间。
  3. 大量写合并为批量写,如计数器现在Redis累加再批量写DB。

缓存更新策略

三种策略

  1. LRU/LFU/FIFO算法剔除
  2. 超时剔除,例如expire。
  3. 主动更新,代码层面控制生命周期

对比

策略 一致性 维护成本
LRU/LFU/FIFO剔除算法 最差
超时剔除 较差
主动更新

两条建议

  • 对于一致性要求低的缓存:最大内存和淘汰策略,即直接往内存里扔,达到最大内存时直接淘汰
  • 对于一致性要求高的缓存:超时剔除和主动更新结合,最大内存和淘汰策略兜底。

缓存粒度控制

粒度问题

  1. 比如要缓存一个mysql中的用户信息:select * from user where id = {id}
  2. 设置用户缓存:set user:{id} '(上面sql语句的结果)'
  3. 缓存粒度-全部属性:即缓存 select *的结果
  4. 缓存粒度-部分重要属性:比如缓存 select user_name, phone部分字段结果

三个角度

  1. 通用性:比如加了个新的字段,全量属性最好。
  2. 占用空间:部分属性更好。
  3. 代码维护:表面上全量属性会更好。

缓存穿透问题

大量请求不命中

  • 即查询结果一直返回 null ,对于不存在的值,缓存层也没有进行缓存
    缓存穿透

原因

  • 业务代码自身问题
  • 恶意攻击或者爬虫等等,比如根据一堆不存在的 id 进行查询,每次都是穿透缓存直接查询数据库。

如何发现

  • 根据业务的响应时间
  • 审查业务本身代码问题
  • 相关指数:总调用数、缓存命中数、存储层命中数

解决方法一:缓存空对象

  • 可能存在的问题
    • 需要更多的键,比如说恶意攻击,需要存一堆无用的 id 键,值为 null。需要使用过期时间,降低风险。
    • 缓存层和存储层数据“短期”不一致。比如查询mysql,这时候mysql出现问题了,导致返回的是 null,然后又把这个空数据缓存到了redis,假如有五分钟过期时间,那这五分钟内都是错误数据,这个时候就是数据不一致了。这种情况的处理可以比如订阅一些mysql服务的消息,监听到mysql服务器恢复正常,可以把缓存重新刷新一遍。

解决方法二:布隆过滤器拦截(后面文章解析)

布隆过滤器拦截

缓存雪崩优化

问题描述

  • 由于cache服务承载了大量请求,当cache服务异常/脱机,流量直接压向后端组件(例如DB),造成级联故障。

优化方案

  1. 保证缓存高可用性,个别节点、个别机器或者个别机房出现问题时能保证高可用。
  2. 依赖隔离组件为后端限流,例如线程池、信号量隔离组件。
  3. 提前演练,例如压力测试。

无底洞优化

问题描述

  • 2010年,Facebook 有了3000个Memcache个节点。
  • 发现,加机器性能并没有提升,反而下降
  • 问题关键点:
    • 更多的机器不代表更高的性能
    • 一般对于的是批量接口的需求,比如mget、mset等
    • 数据增长和水平扩展需求矛盾,就是说数据增长也必须扩展节点以存储更多数据。

优化IO的几种方法

  1. 命令本身优化:例如慢查询keys、hgetall bigkey
  2. 减少网络通信的次数
  3. 降低接入成本:例如客户端长连接/连接池、NIO等

四种批量优化的方法

  1. 串行mget
  2. 串行IO
  3. 并行IO
  4. hash_tag

热点key重建优化

问题描述

  • 热点key + 较长的重建时间

三个目标

  • 减少重建缓存的次数
  • 数据尽可能一致
  • 减少潜在的风险

两个解决思路

  • 互斥锁,当检测到缓存需要重建的时候加上一把锁,缓存成功了就把锁释放,在这个期间如果有其他线程访问缓存时会一直等待。
    • 图示:
      互斥锁
    • 代码示例
    	String get(String key) throws InterruptedException {
            String value = redis.get(key);
            if (value == null) {
                String mutexKey = "mutex:key:" + key;
                // 加锁:如果key不存在,则set,且设置180秒的过期时间,如果能设置成功返回true
                if (redis.set(mutexKey, "1", "ex 180", "nx")) {
                    value = db.get(key);
                    redis.set(key, value);
                    redis.delete(mutexKey);
                } else {
                    // 其他线程休息50毫秒后重试
                    Thread.sleep(50);
                    get(key);
                }
            }
            return value;
        }
    
  • 永远不过期
    • 缓存层面:不设置过期时间
    • 功能层面:为每个value添加逻辑过期时间,但发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
    • 图示:
      永远不过期
    • 代码示例:
    String get(final String key) throws InterruptedException {
        V v = redis.get(key);
        String value = v.getValue();
        Long logicTimeout = v.getLogicTimeout();
        if (logicTimeout >= System.currentTimeMillis()) {
            String mutexKey = "mutex:key:" + key;
            // 加锁:如果key不存在,则set,且设置180秒的过期时间,如果能设置成功返回true
            if (redis.set(mutexKey, "1", "ex 180", "nx")) {
                // 异步更新后台异常执行
                threadPool.execute(new Runnable(){
                    @Override
                    public void run() {
                        String dbValue = db.get(key);
                        redis.set(key, (dbValue, newLogicTimeout));
                        redis.delete(mutexKey);
                    }
                });
            } else {
                // 其他线程休息50毫秒后重试
                Thread.sleep(50);
                get(key);
            }
        }
        return value;
    }
    

两种方案对比

方案 优点 缺点
互斥锁 思路简单、保证一致性 代码复杂度增加、存在死锁风险
永远不过期 基本杜绝热点key重建问题 不保证一致性、逻辑过期时间增加维护成本和内存成本

缓存降级

问题描述

  • 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证核心服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。

目的

  • 降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。

如何处理

  • 在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
  1. 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级。
  2. 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警。
  3. 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级。
  4. 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
发布了112 篇原创文章 · 获赞 303 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_36221788/article/details/104805982