CodeAshen's blog CodeAshen's blog
首页
  • Spring Framework

    • 《剖析Spring5核心原理》
    • 《Spring源码轻松学》
  • Spring Boot

    • Spring Boot 2.0深度实践
  • Spring Cloud

    • Spring Cloud
    • Spring Cloud Alibaba
  • RabbitMQ
  • RocketMQ
  • Kafka
  • MySQL8.0详解
  • Redis从入门到高可用
  • Elastic Stack
  • 操作系统
  • 计算机网络
  • 数据结构与算法
  • 云原生
  • Devops
  • 前端
  • 实用工具
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
  • Reference
GitHub (opens new window)

CodeAshen

后端界的小学生
首页
  • Spring Framework

    • 《剖析Spring5核心原理》
    • 《Spring源码轻松学》
  • Spring Boot

    • Spring Boot 2.0深度实践
  • Spring Cloud

    • Spring Cloud
    • Spring Cloud Alibaba
  • RabbitMQ
  • RocketMQ
  • Kafka
  • MySQL8.0详解
  • Redis从入门到高可用
  • Elastic Stack
  • 操作系统
  • 计算机网络
  • 数据结构与算法
  • 云原生
  • Devops
  • 前端
  • 实用工具
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
  • Reference
GitHub (opens new window)
  • MySQL8.0详解与实战

  • MySQL面试指南

  • Redis从入门到高可用

    • 第01章-初识Redis
    • 第02章-API理解和使用
    • 第03章-Redis客户端
    • 第04章-Redis其他功能
    • 第05章-Redis持久化
    • 第06章-Redis主从复制
    • 第07章-Redis Sentinel
    • 第08章-Redis Cluster
    • 第09章-缓存设计与优化
      • 1.1 收益
      • 1.2 成本
      • 1.3 使用场景
      • 2.1 缓存更新方式
      • 2.2 Redis 淘汰策略
    • 第10章-Cache Cloud
  • Elastic-Stack

  • 数据库
  • Redis从入门到高可用
CodeAshen
2023-02-10
目录

第09章-缓存设计与优化

# 一、缓存的收益与成本

# 1.1 收益

  1. 加速读写

    通过缓存加速读写速度:CPUL1/L2/L3 Cache、Linux page Cache加速硬盘读写、浏览器缓存、Ehcache缓存数据库结果。

  2. 降低后端负载

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

# 1.2 成本

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

# 1.3 使用场景

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

# 二、缓存更新策略

# 2.1 缓存更新方式

  1. LRU/LFU/FIFO 算法剔除:例如 maxmemory-policy。
  2. 超时剔除:例如 expire。
  3. 主动更新:开发控制生命周期
策略 一致性 维护成本
LRU/LIRS 算法剔除 最差 低
超时剔除 较差 低
主动更新 强 高

两条建议:

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

# 2.2 Redis 淘汰策略

Redis 通过 maxmemory-policy 配置数据的淘汰策略,一共六种淘汰策略,分成四大类

  • lru:最近最少使用的淘汰
    • allkeys:所有
    • volatile:设置过期时间的
  • ttl:从已设置过期时间中挑选将要过期的淘汰
  • random:数据中随机淘汰
    • allkeys:所有
    • volatile:设置过期时间的
  • no-enviction:禁止驱逐,直接报错

# 三、缓存粒度控制

缓存数据时,是否需要缓存全量信息。

image-20210609193929928

  1. 通用性:全量属性更好
  2. 占用空间:部分属性更好
  3. 代码维护:表面上全量属性更好

# 四、缓存穿透

问题描述:缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

image-20210609194200024

原因:

  1. 业务代码自身问题
  2. 恶意攻击、爬虫等等

如何发现:

  1. 业务的相应时间
  2. 业务本身问题
  3. 相关指标:总调用数、缓存层命中数、存储层命中数

解决方案:

  1. 接口参数校验,不合法参数直接返回(如 id<0)

  2. 缓存空对象

    image-20210610100558759

    本方案有两个问题:

    • 需要更多的键。
    • 缓存层和存储层数据“短期”不一致。
  3. 布隆过滤器拦截

    image-20210610100705450

# 五、缓存击穿

问题描述:某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。

解决方案:

  1. 加互斥锁。在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。
  2. 热点数据不过期。直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。

# 六、缓存雪崩

问题描述:缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

解决方案:

  1. 过期时间打散。既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间时加上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效。
  2. 热点数据不过期。该方式和缓存击穿一样,也是要着重考虑刷新的时间间隔和数据异常如何处理的情况。
  3. 加互斥锁。该方式和缓存击穿一样,按 key 维度加锁,对于同一个 key,只允许一个线程去计算,其他线程原地阻塞等待第一个线程的计算结果,然后直接走缓存即可。

# 七、无底洞问题优化

问题描述:

  • 2010年,Facebook有了3000个Memcache节点。
  • 发现问题:“加”机器性能没能提升,反而下降。

以mget为例,mget需要客户端根据节点不同数据分批查询。一次mget操作随着节点的个数越来越多,网络次数也会越来越多,对客户端一次命令的执行效率会有很大的影响。节点数从一个变成node个,一次mget操作的IO从 O(1) 变成 O(node),并行情况下要等最慢的节点完成,串行的情况下需要依次等各个节点完成。

image-20210610105002288

问题关键点:

  • 更多的机器 != 更高的性能
  • 批量接口需求(mget,mset等)
  • 数据增长与水平扩展需求

优化IO的几种方案:

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

优化批量操作的几种方法(Redis Cluster章节有详细解释):

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

# 八、热点key重建优化

缓存重建:从数据库查询到数据,缓存到redis的过程。

问题描述:热点key + 较长的重建时间

一个key对应的数据是热点数据,同时它的重建过程比较长,在第一次查询时开始重建缓存,但是重建过程中时间较长, 在重建期间有很多请求进来,由于缓存还没重建成功,所以这些请求都会去查询数据库并重建缓存。会造成数据库压力比较大,以及响应时间较慢的问题。过程如下图所示:

image-20210610110217537

要解决这个问题,有以下三个目标:

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

两个解决方案:

  • 互斥锁(mutex key)

    image-20210610110732233

    不需要大量重建过程,但是需要等待,可能大量线程等待。

    // 伪代码
    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.set(key,value);
                redis.delete(mutexKey); 
            } else { 
                //其他线程休息50毫秒后重试 
                Thread.sleep(50); 
                get(key); 
            }
        }
        return value;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  • 永远不过期

    1. 缓存层面:没有设置过期时间(没有用expire)。
    2. 功能层面:为每个value添加逻辑过期时间,但发现超过逻辑过期时间后,会使用单独的线程去构建缓存。

    image-20210610111051419

    // 伪代码
    String get(final String key) { 
        V v = redis.get(key); 
        String value = v.getValue(); 
        long logicTimeout = v.getLogicTimeout(); 
        if (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(keyMutex); 
                    }
                }); 
            }
        }
        return value;
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

两种方案对比:

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

# 九、总结

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

参考文章:

  • 缓存穿透、缓存击穿、缓存雪崩解决方案 (opens new window)
编辑 (opens new window)
上次更新: 2023/06/04, 12:34:19
第08章-Redis Cluster
第10章-Cache Cloud

← 第08章-Redis Cluster 第10章-Cache Cloud→

最近更新
01
第01章-RabbitMQ导学
02-10
02
第02章-入门RabbitMQ核心概念
02-10
03
第03章-RabbitMQ高级特性
02-10
更多文章>
Theme by Vdoing | Copyright © 2020-2023 CodeAshen | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式