分布式专题——6 Redis缓存设计与性能优化

1 多级缓存架构

在这里插入图片描述

2 缓存设计

2.1 缓存穿透

2.1.1 简介

  • 缓存穿透是什么?当查询一个根本不存在的数据时,缓存层和存储层都不会命中。正常逻辑下,存储层查不到数据就不会写入缓存层。这会导致:每次请求这个不存在的数据,都要去存储层查询,缓存就失去了保护后端存储的意义,存储层可能被大量无效请求压垮;

  • 出现缓存穿透的原因一般有以下两个:

    • 自身业务代码或数据有问题,导致查询了本不该存在的数据;

    • 遭遇恶意攻击、爬虫等,它们会发起大量查询不存在数据的请求。

2.1.2 缓存空对象

String get(String key) {// 从缓存中获取数据String cacheValue = cache.get(key);// 如果缓存为空(包括空字符串或null值)if (StringUtils.isBlank(cacheValue)) {// 缓存未命中,从持久存储层(如数据库)中获取数据String storageValue = storage.get(key);// 将从存储层获取的值写入缓存,以便后续请求可以直接从缓存中获取cache.set(key, storageValue);// 如果存储层中也不存在该键对应的值(即值为null)if (storageValue == null) {// 将"空值"(null)也存入缓存,并设置一个较短的过期时间(5分钟)// 这可以防止缓存穿透(大量请求查询不存在的key)cache.expire(key, 60 * 5);}// 返回从存储层获取的值(可能为null)return storageValue;} else {// 缓存命中,直接返回缓存中的值return cacheValue;}
}

2.1.3 布隆过滤器

  • 针对恶意攻击等导致的“大量请求不存在数据”的缓存穿透场景,可以用布隆过滤器先做一次过滤,其判定逻辑是:

    • 如果布隆过滤器说“某个值不存在”,那这个值肯定不存在
    • 如果它说“某个值存在”,这个值可能不存在(有误判概率);
  • 原理

    在这里插入图片描述

    • 它由大型位数组多个无偏 hash 函数(“无偏”指能把元素的 hash 值分布得比较均匀)组成;

    • 添加元素(如 key):用多个 hash 函数对 key 做 hash,得到整数索引值,其再对位数组长度取模,得到具体位置(每个 hash 函数都会算得一个不同的位置),最后把这些位置都置为 1,就完成了添加;

    • 查询元素是否存在:同样用多个 hash 函数算出 key 对应的位置,看位数组中这些位置是否都为 1。只要有一个位为 0,说明 key 不存在;如果都为 1,只能说“很可能存在”(因为其他 key 也可能把这些位置置为 1,导致误判);

    • 位数组越稀疏(空闲位多),误判概率越大;越拥挤(1 多),误判概率越低;

  • 适用场景:数据命中不高(很多请求查的是不存在数据)、数据相对固定(新增/变更不频繁)、实时性低(能接受一定延迟或误判)且数据集较大的场景。

  • 它的优势是缓存空间占用极少,但代码维护相对复杂;

  • 注意:布隆过滤器不能删除数据,如果要删除元素,得重新初始化所有数据(因为位数组的位一旦置为 1,无法精准回退,删除会破坏现有判断逻辑);

  • 可以用 Redisson 实现布隆过滤器,引入依赖:

    <dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.6.5</version>
    </dependency>
    
  • 示例伪代码:初始化与使用

    package com.redisson;import org.redisson.Redisson;
    import org.redisson.api.RBloomFilter;
    import org.redisson.api.RedissonClient;
    import org.redisson.config.Config;public class RedissonBloomFilter {public static void main(String[] args) {// 创建Redisson配置对象Config config = new Config();// 配置单机Redis服务器地址config.useSingleServer().setAddress("redis://localhost:6379");// 根据配置创建Redisson客户端实例RedissonClient redisson = Redisson.create(config);// 从Redisson客户端获取或创建一个名为"nameList"的布隆过滤器RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");// 初始化布隆过滤器参数:// - 预计要插入的元素数量:100,000,000个// - 期望的误判率:3%(即允许有3%的概率将不存在的元素误判为存在)// 底层会根据这两个参数自动计算所需的bit数组大小和哈希函数数量bloomFilter.tryInit(100000000L, 0.03);// 向布隆过滤器中插入元素"shisan"bloomFilter.add("shisan");// 检查元素是否可能存在于布隆过滤器中System.out.println(bloomFilter.contains("guojia")); // falseSystem.out.println(bloomFilter.contains("jiating")); // falseSystem.out.println(bloomFilter.contains("shisan")); // true// 注意:布隆过滤器的特性:// 1. 如果contains()返回false,则该元素一定不存在// 2. 如果contains()返回true,则该元素可能存在(可能有误判)// 3. 布隆过滤器不支持元素删除操作}
    }
    
  • 使用布隆过滤器需要把所有数据提前放入布隆过滤器,并且在后续新增数据时也要记得往布隆过滤器里放,布隆过滤器缓存过滤伪代码:

    RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
    bloomFilter.tryInit(100000000L,0.03);// 初始化方法:将所有数据预先加载到布隆过滤器中
    // 这通常在系统启动时执行一次,用于构建完整的布隆过滤器
    void init(){// 遍历所有键,将其添加到布隆过滤器中for (String key: keys) {// 将键放入布隆过滤器,建立快速查找的索引bloomFilter.put(key);}
    }// 根据键获取值的业务方法,集成了布隆过滤器优化查询
    String get(String key) {// 首先通过布隆过滤器快速判断key是否可能存在Boolean exist = bloomFilter.contains(key);// 如果布隆过滤器确认key不存在,直接返回空字符串,避免后续不必要的缓存和存储查询if(!exist){return "";}// 布隆过滤器判断key可能存在,继续从Redis缓存中获取数据String cacheValue = cache.get(key);if (StringUtils.isBlank(cacheValue)) {String storageValue = storage.get(key);cache.set(key, storageValue);if (storageValue == null) {cache.expire(key, 60 * 5);}return storageValue;} else {return cacheValue;}
    }
    

2.2 缓存击穿(失效)

  • 缓存击穿(失效)是什么?大批量缓存数据在同一时间过期失效时,会导致大量请求同时无法从缓存中获取数据,只能直接穿透到数据库查询。这可能瞬间给数据库带来巨大压力,甚至导致数据库崩溃;

  • 这种情况通常发生在:

    • 系统初始化时批量加载了一批数据到缓存,且设置了相同的过期时间;

    • 某类热点数据集中过期(比如电商促销活动结束时间统一);

  • 解决方案:错开缓存过期时间,即在批量设置缓存时,将过期时间分散在一个时间区间内,而不是使用固定值。这样可以避免大量缓存同时失效,将数据库压力分散到不同时间点;

    String get(String key) {String cacheValue = cache.get(key);if (StringUtils.isBlank(cacheValue)) {String storageValue = storage.get(key);cache.set(key, storageValue);// 生成一个300到600秒之间的随机过期时间(5到10分钟)// 这种随机化策略可以有效防止缓存雪崩(大量缓存同时失效导致请求直接打到数据库)int expireTime = new Random().nextInt(300) + 300;if (storageValue == null) {// 为不存在的键设置随机过期时间// 这样即使大量不存在的键同时被缓存,它们也会在不同的时间点过期cache.expire(key, expireTime);}return storageValue;} else {return cacheValue;}
    }
    

2.3 缓存雪崩

  • **缓存雪崩是什么?**当缓存层因为各种原因(像遭遇超大并发请求但是系统扛不住、缓存设计不佳,比如大量请求访问“大 key”导致缓存能支撑的并发量急剧下降等),无法正常提供服务甚至宕机时,原本由缓存层承接的大量请求,会像失控的野牛一样,全部涌向后端存储层。由于存储层原本依赖缓存层分担压力,突然面临这么多请求,调用量暴增,很可能也会被压垮,进而引发级联宕机的严重情况;

  • 如何预防和解决缓存雪崩?

    • 保证缓存层服务高可用性:可以采用像 Redis Sentinel(哨兵模式)或者 Redis Cluster(集群模式)这样的方案。Redis Sentinel 能对 Redis 进行监控、提醒和自动故障转移,确保缓存服务的可用性;Redis Cluster 则通过将数据分布在多个节点上,实现数据的高可用和高并发访问;

    • 依赖隔离组件进行后端限流、熔断与降级:可以使用 Sentinel 或者 Hystrix 这类限流降级组件

      • 服务降级方面,能针对不同数据采取不同处理方式;
      • 比如对于非核心数据(像电商商品属性、用户信息等),当缓存层出问题时,暂时停止从缓存查询这些数据,直接返回预先定义好的默认降级信息、空值或者错误提示;
      • 而对于核心数据(像电商商品库存),仍然允许查询缓存,若缓存里没有,还能从数据库读取;
      • 这样既保障核心业务不受太大影响,又减轻了存储层压力;
    • 提前演练:在项目上线之前,模拟缓存层宕机的情况,演练应用以及后端的负载情况和可能出现的问题,然后基于演练结果制定相应的预案,以便在实际出现缓存雪崩时能快速应对。

2.4 热点缓存 key 重建优化

  • 当同时满足以下两个条件时,可能对应用造成致命危害:

    • 热点key:某个缓存key访问量极大(比如热门新闻、爆款商品)

    • 缓存重建耗时:从数据源(如数据库)重新加载并计算该 key 对应的缓存数据需要很长时间(可能涉及复杂SQL、多次IO操作或多个依赖服务调用)

    • 此时,当这个热点 key 的缓存过期失效瞬间,会有大量并发线程同时发现缓存缺失,然后同时去重建缓存,这会导致后端数据源压力骤增,甚至可能让应用崩溃;

  • 解决方案:互斥锁机制,即只允许一个线程负责重建缓存,其他线程等待重建完成后再从缓存获取数据,避免大量线程同时冲击数据源

    String get(String key) {// 从Redis缓存中获取指定key的数据String value = redis.get(key);// 如果缓存中不存在该key的值(缓存未命中)if (value == null) {// 创建互斥锁的key,格式为"mutext:key:原key",用于防止缓存击穿String mutexKey = "mutext:key:" + key;// 尝试获取分布式锁:设置一个值为"1"的锁,过期时间为180秒,只有在key不存在时才能设置成功(NX选项)if (redis.set(mutexKey, "1", "ex 180", "nx")) {try {// 成功获取到锁的线程,从数据库(或其他数据源)获取真实数据value = db.get(key);// 将获取到的数据写入Redis缓存,并设置正常的过期时间redis.setex(key, timeout, value);} finally {// 无论是否成功获取数据,都释放分布式锁redis.delete(mutexKey);}} else {// 未获取到锁的线程(其他线程正在重构缓存),等待50毫秒,让持有锁的线程完成缓存重构Thread.sleep(50);// 递归调用自身,重新尝试从缓存获取数据(此时可能已经重构完成)return get(key);}}// 返回获取到的值(可能来自缓存,也可能是刚重构的数据)return value;
    }
    

2.5 缓存与数据库双写不一致

  • 在大并发下,同时操作缓存与数据库会存在数据不一致性的问题;

    • 双写不一致:多线程并发写数据库和更新缓存时,因执行顺序差异,可能导致缓存与数据库数据不匹配

      在这里插入图片描述

      • 比如线程1先写数据库后更新缓存,线程2写数据库后更新缓存,若线程1的缓存更新动作滞后,就会使缓存数据不符合最终数据库状态;
    • 读写并发不一致:读写操作并发时,也易引发数据不一致

      在这里插入图片描述

      • 像线程1写数据库后删除缓存,线程2写数据库后删除缓存,线程3查缓存(为空)后查数据库(取到线程1写入的旧数据)并更新缓存,最终缓存会留存旧数据,与数据库最新数据(线程2写入的)不符;
  • 解决方案

    • 对于并发概率小的数据(如个人订单、用户数据),因本身并发冲突少,很少出现缓存不一致,可给缓存设过期时间,通过定时读操作主动更新缓存;

    • 若业务能容忍短时缓存不一致(如商品名称、分类菜单),给缓存加过期时间,也能满足大部分业务对缓存的需求,过期后缓存会重新从数据库加载最新数据;

    • 若有强一致性要求

      • 加分布式读写锁,保证并发读写或写写操作有序进行,读操作间无锁,既保障数据一致性,又尽可能减少对读性能的影响;

      • 用阿里开源的 Canal,监听数据库 binlog 日志,实时修改缓存。当数据库数据变更,binlog 记录变更,Canal 监听到后同步更新缓存,能实时保证缓存与数据库一致,但引入了新中间件,增加了系统复杂度;

        在这里插入图片描述

  • 总结:

    • 缓存主要用于读多写少场景以提升性能,若写多读多且不能容忍缓存不一致,直接操作数据库更合适;若数据库压力大,也可将缓存作为主存储,异步同步数据到数据库,数据库作为备份;

    • 放入缓存的数据应是对实时性、一致性要求不高的,不要为追求缓存绝对一致做过度设计,否则会徒增系统复杂度。

3 开发规范与性能优化

3.1 键与值的设计

3.1.1 key 的设计

  • 可读性和可管理性

    • 避免不同业务 / 模块的 key 冲突,同时让 key 更容易理解和维护;

    • 业务名(或数据库名)作为前缀,用**冒号(:)**分隔不同层级的信息;

    • 例:trade:order:1,能清晰看出这是“交易(trade)”业务下,“订单(order)”模块中 ID 为 1 的订单数据对应的 key;

  • 简洁性

    • 减少 key 的长度,降低内存占用(当 key 数量极多时,长 key 会累积占用较多内存);

    • 保证语义清晰的前提下,对 key 进行简化缩写;

    • 例:把 user:{uid}:friends:messages:{mid} 简化为 u:{uid}:fr:m:{mid},通过缩写(userufriendsfrmessagesm)缩短 key 长度,同时仍能体现“用户 - 好友 - 消息”的层级关系;

  • 不要包含特殊字符

    • 避免特殊字符导致 key 解析、存储或访问时出现异常(如语法错误、转义问题等);

    • key 中不能包含空格、换行、单双引号以及其他转义字符(如 \ 等);

    • 原因:这些特殊字符可能会干扰缓存系统对 key 的识别,甚至引发程序报错,影响缓存的正常使用。

3.1.2 value 的设计

  • 在 Redis 中,一个字符串最大 512 MB,一个二级数据结构(例如 Hash、List、Set、Zset)可以存储大约 40 亿(2^32-1)个元素,但实际上如果出现下面两种情况,就认为它是 bigkey

    • 字符串类型:它的“big”体现在单个 value 值很大,一般认为超过 10KB 就是 bigkey;
    • 非字符串类型:哈希、列表、集合、有序集合,它们的 big 体现在元素个数太多(建议不超过5000个);
  • bigkey 的危害

    • Redis阻塞:操作 bigkey 需要消耗更多 CPU 和内存资源,可能导致 Redis 服务阻塞;

    • 网络拥塞:bigkey 会产生大量网络流量,例如 1MB 的 bigkey 每秒被访问 1000 次,会产生 1000MB/s 的流量,远超普通千兆网卡的承载能力;

    • 过期删除问题:bigkey 过期时,如果没有启用 Redis 4.0 的异步删除功能,删除操作可能阻塞 Redis;

  • bigkey 的产生原因:程序设计不当、对数据规模预估不足。例:

    • 社交类:粉丝列表,对于某些明星或者大v的粉丝列表,如果不精心设计一下,必是 bigkey;
    • 统计类:例如按天存储某项功能或者网站的用户集合,除非没几个人用,否则必是 bigkey;
    • 缓存类:将数据从数据库 load 出来序列化放到 Redis 时,把所有字段或关联数据都缓存,造成 bigkey;
  • bigkey 的优化方案

    • 拆分

      • 对于 List:将一个大 List 拆分为多个小 List;
      • 对于Hash:将大 hash 按分段存储(如将100万用户数据拆分为200个key,每个存储5000个用户);
    • 如果有 bigkey,对其的操作:避免一次性获取或删除所有元素(会阻塞),使用 hmget 而非 hgetall,采用 hscan、sscan、zscan 等渐进式删除方法;

    • 选择适合的数据类型

      • 反例:将一个实体的不同属性用多个 string 存储(如set user:1:nameset user:1:age等);
      • 正例:使用 Hash 存储实体数据(如hmset user:1 name tom age 19 favor football),更节省内存且操作更高效;
    • 控制 key 的生命周期

      • Redis 通常不是用于持久存储,应给 key 设置合理的过期时间;
      • 条件允许时,应打散过期时间,避免大量 key 集中过期导致缓存击穿问题。

3.2 命令使用

  • 关注O(N)命令的N值大小

    • hgetalllrangesmemberszrangesinter等命令的时间复杂度是O(N),执行效率与数据量N直接相关;

      hgetall:获取哈希表中所有字段和值,适用于小型哈希但可能阻塞Redis服务
      lrange:获取列表指定范围内的元素,支持分页查询列表数据
      smembers:返回集合中的所有成员,当集合很大时会导致Redis阻塞
      zrange:返回有序集合中指定排名范围的成员(可带分数),支持按排名范围查询
      sinter:计算多个集合的交集,返回所有给定集合中都存在的成员

    • 并非不能使用这些命令,但必须清楚N的具体数量,避免在大数据量上(比如 bigkey)执行;

    • 有遍历需求时,推荐使用hscansscanzscan等渐进式遍历命令,它们可以分批获取数据,避免一次性处理大量数据导致 Redis 阻塞;

      hscan:增量迭代哈希表中的键值对,避免一次性获取大哈希造成的阻塞
      sscan:增量迭代集合中的元素,安全遍历大集合的解决方案
      zscan:增量迭代有序集合中的元素和分数,用于处理大型有序集合

  • 禁用危险命令

    • 禁止在线上环境使用keysflushallflushdb等命令:

      • keys命令会遍历整个数据库,在数据量大时会严重阻塞Redis
      • flushallflushdb会清空数据库,风险极高
    • 可以通过Redis的rename机制禁用这些命令,或用scan命令替代keys进行渐进式处理

  • 合理使用select命令(多数据库)

    • Redis 的多数据库功能较弱,其通过数字(0-15)区分不同数据库

    • 很多客户端对多数据库支持不好,且多业务共用同一 Redis 实例的不同数据库时,仍然是单线程处理,会相互干扰

    • 建议谨慎使用多数据库,更好的做法是按业务拆分不同的 Redis 实例

  • 使用批量操作提高效率

    • 原生命令:如mgetmset,可以一次性操作多个key,减少网络往返

    • pipeline:非原生命令的批量处理方式,能打包多个命令一次性发送

    • 注意事项:

      • 控制批量操作的元素个数(建议500以内,具体与元素大小有关),避免单次操作过大
      • 原生命令是原子操作,pipeline 是非原子操作
      • pipeline 可以打包不同命令,原生命令只能处理同类型命令
      • pipeline 需要客户端和服务端同时支持
  • 谨慎使用事务功能

    • Redis 的事务功能相对较弱,不建议过多依赖

    • 可以使用 Lua 脚本替代事务,Lua 脚本在 Redis 中是原子执行的,能保证复杂操作的原子性

3.3 客户端使用

3.3.1 连接池

  • 客户端实例使用建议:避免多个应用共用一个 Redis 实例,建议不同业务拆分 Redis 实例,可防止公共数据服务劣化,让各业务数据访问更独立、稳定;

  • 推荐使用连接池:能有效控制连接数,提升效率;

    // 通过JedisPoolConfig配置连接池参数
    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
    jedisPoolConfig.setMaxTotal(5); // 最大连接数
    jedisPoolConfig.setMaxIdle(2); // 最大空闲连接数
    jedisPoolConfig.setTestOnBorrow(true); // 向资源池借用连接时是否做连接有效性检测(ping)// 基于配置创建JedisPool获取连接
    JedisPool jedisPool = new JedisPool(jedisPoolConfig, "192.168.0.60", 6379, 3000, null);Jedis jedis = null;
    try {jedis = jedisPool.getResource();//具体的命令jedis.executeCommand()
    } catch (Exception e) {logger.error("op key {} error: " + e.getMessage(), key, e);
    } finally {//注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池if (jedis != null) jedis.close();
    }
    

3.3.2 连接池参数含义与优化建议(连接池预热)

  • maxTotal:最大连接数(早期版本叫maxActive),需结合业务 Redis 并发量、客户端执行命令耗时、Redis 资源等因素确定,通常比理论计算值略大,同时要注意不能超过 Redis 最大连接数(maxclients)限制;

    以一个例子说明,假设:

    • 单连接处理能力:每个Redis连接完成一次命令操作(获取连接+执行命令+归还连接)平均耗时1ms
    • 单连接QPS:1000(因为1秒/1ms = 1000)
    • 目标业务QPS:50000

    理论最小连接数 = 目标QPS / 单连接QPS = 50000 / 1000 = 50个连接

    实际配置考虑:

    • 需要预留缓冲资源:实际mxTotal应该略大于理论值(如55-60),以应对流量波动和突发请求
    • 连接数不是越多越好:过多连接会消耗客户端和服务端资源,增加管理开销
    • 性能瓶颈分析:Redis是单线程模型,如果遇到大命令阻塞(如keys*、大型集合操作),即使增加再多客户端连接也无法提高吞吐量,因为服务端处理能力已成为瓶颈

    所以:连接池大小设置需要平衡理论计算、资源消耗和实际业务特性,同时要认识到连接数不能解决 Redis 单线程架构下的命令阻塞问题

  • maxIdle:最大空闲连接数,建议设为业务所需最大连接数,maxTotal为其留出余量,且maxIdle不超过maxTotal,避免资源浪费;

  • minIdle:最小空闲连接数,即至少需要保持的空闲连接数,用于维持连接池基本空闲连接,若连接数超过minIdle,多余连接会在完成业务后被移出连接池释放;

    如果系统启动完马上就会有很多的请求过来,那么可以给 Redis 连接池做预热

    比如快速地创建一些 Redis 连接,执行一些简单命令(比如ping()),快速地将连接池里的空闲连接提升到minIdle的数量;

    连接池预热示例代码:

    // 创建一个ArrayList用于存储预热的Jedis连接,初始容量设置为连接池的最小空闲连接数
    List<Jedis> minIdleJedisList = new ArrayList<Jedis>(jedisPoolConfig.getMinIdle());// 第一阶段:预热连接池,创建最小空闲连接数指定的连接数量
    for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {Jedis jedis = null;try {// 从连接池获取一个Jedis连接实例jedis = pool.getResource();// 将获取的连接添加到预热列表中暂存minIdleJedisList.add(jedis);// 执行ping命令测试连接有效性,确保连接是可用的jedis.ping();} catch (Exception e) {// 记录连接预热过程中的异常信息logger.error(e.getMessage(), e);} finally {// 此处不能立即归还连接,否则连接池会重复使用同一个连接,目的是保持多个不同的连接实例在预热列表中// jedis.close();}
    }// 第二阶段:将所有预热的连接统一归还到连接池
    for (int i = 0; i < jedisPoolConfig.getMinIdle(); i++) {Jedis jedis = null;try {// 从预热列表中获取预先创建的连接jedis = minIdleJedisList.get(i);// 将连接归还回连接池,此时连接池中就会有minIdle个已初始化的可用连接jedis.close();} catch (Exception e) {// 记录连接归还过程中的异常信息logger.error(e.getMessage(), e);} finally {// 可选的清理操作}
    }
    
  • blockWhenExhausted:当连接池用尽时,是否让调用者等待,建议用默认值true

  • maxWaitMillis:调用者等待连接的最大时长,不建议用默认的“不超时”,避免长时间阻塞;

  • testOnBorrowtestOnReturn:分别在借连接和还连接时测试连接可用性(如ping),业务量大时建议开启,确保连接有效;

3.3.3 高并发与安全建议

  • 高并发下,建议客户端添加熔断功能(如结合 Sentinel、Hystrix),增强系统稳定性;
  • 设置合理密码,必要时用 SSL 加密访问,保障 Redis 访问安全。

3.3.4 Redis 的过期键清理策略

  • Redis 对于过期键有三种清除策略:

    • 被动删除:当读写一个已过期的键时,会触发惰性删除策略删除该键;

    • 主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以 Redis 会定期(默认100ms)主动淘汰一部分已过期键;

      • 注意:这里淘汰的是一部分,所以可能会出现部分 key 已经过期但还没有被清理掉的情况;
    • 内存超限触发:当内存超过maxmemory限制时,触发主动清理策略;

  • 主动清理策略在 Redis 4.0 之前一共实现了 6 种内存淘汰策略,在 4.0 之后,又增加了 2 种策略,总共8种:

    • 针对设置了过期时间的 key 做处理:

      • volatile-ttl:按过期时间先后删除

      • volatile-random:随机删除

      • volatile-lru:用 LRU 算法删除

        LRU 算法(Least Recently Used,最近最少使用):淘汰很久没被访问过的数据,以最近一次访问时间作为参考;

        存在热点数据时 LRU 效率好;

      • volatile-lfu:用 LFU 算法删除

        LFU 算法(Least Frequently Used,最不经常使用):淘汰最近一段时间被访问次数最少的数据,以次数作为参考;

        但若有偶发、周期性批量操作,LRU 命中率会下降,缓存污染严重,此时 LFU 更优;

    • 针对所有的key做处理:

      • allkeys-random:随机删除
      • allkeys-lru:用 LRU 算法删除
      • allkeys-lfu:用 LFU 算法删除
    • noeviction:不删除任何数据,拒绝所有写入操作并返回客户端错误信息(error) OOM command not allowed when used memory,此时 Redis 只响应读操作;

  • 推荐配置maxmemory-policy(默认是noeviction)为volatile-lru,且要设置最大内存,否则 Redis 内存超出物理内存限制时会频繁换页(Swap),导致性能急剧下降。

  • 当 Redis 运行在主从模式时,只有主节点才会执行过期删除策略,再将删除操作del key同步到从节点删除数据。

4 系统内核参数优化

4.1 虚拟内存交换(vm.swappiness

  • 作用

    • 当物理内存不足时,系统会把部分内存页(page)交换到磁盘的 swap 分区,暂时缓解内存压力。但 swap 依赖磁盘 IO,高并发场景下磁盘 IO 会成为系统瓶颈;
    • 在 Linux 中,并不是要等到所有物理内存都使用完才会使用到 swap,系统参数 swppiness 会决定操作系统使用 swap 的倾向程度;
  • swappiness 取值范围是 0 - 100,值越大,系统越倾向用 swap;值越小,越倾向用物理内存;

  • Redis 优化建议

    • 若 Linux 内核版本 < 3.5,设 swappiness = 0,减少 swap 使用,避免触发 OOM killer(系统内存不足时强制杀用户进程);

    • 若内核版本 >= 3.5,设 swappiness = 1,同样减少 swap 依赖,降低 OOM killer 风险;

    • 操作示例:

      cat /proc/version  # 查看Linux内核版本
      echo 1 > /proc/sys/vm/swappiness # 临时写入
      echo vm.swapiness=1 >> /etc/sysctl.conf # 写入配置文件,永久生效
      

4.2 内存超额提交(vm.overcommit_memory

  • 参数含义:控制内核是否允许“超额”分配物理内存

    • 0:检查可用物理内存,足够才允许内存申请,否则申请失败;
    • 1:内核允许分配所有物理内存,不管当前内存状态;
  • Redis 优化建议:设为 1,确保 fork 操作(Redis 持久化等场景会用到)能在内存不足时也成功执行,避免因内存申请失败导致 fork 等操作异常;

  • 操作示例

    cat /proc/sys/vm/overcommit_memory
    echo "vm.overcommit_memory=1" >> /etc/sysctl.conf
    sysctl vm.overcommit_memory=1 # 使其立即生效
    

4.3 文件句柄数

  • 问题场景:系统进程打开文件(或者称作句柄)的数量达到上限时,会报 Too many open files 错误,影响 Redis 等服务的文件操作(如网络连接、日志文件等);
  • 优化操作
    • ulimit -a 查看系统文件句柄数限制;
    • ulimit -n 65535 临时提高文件句柄数(此处设为 65535),也可通过系统配置永久调整,让 Redis 能支持更多并发连接等文件操作。

4.4 慢查询日志(slowlog

  • 作用:记录 Redis 中执行耗时超阈值的命令,用于排查性能问题(如慢查询导致的 Redis 卡顿);
  • 关键配置与命令
    • config get slowlog-*:查看慢查询日志相关配置;
    • config set slowlog-log-slower-than 1000:设置慢查询阈值(单位微秒,示例中设为 1000 微秒即 1 毫秒,超过该耗时的命令会被记录)。若要更高并发场景下的细粒度监控,可设为 500 微秒;
    • config set slowlog-max-len 1024:设置慢查询日志最大保存条数,满了会删除最早的记录,保留最新的;
    • config rewrite:将当前生效的配置持久化到 redis.conf
    • slowlog len:查看慢查询日志当前长度;
    • slowlog get 5:获取最新 5 条慢查询日志,每条包含标识 ID、发生时间、命令耗时、命令及参数;
    • slowlog reset:重置慢查询日志。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/921885.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/921885.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一文了解大模型压缩与部署

一文了解大模型压缩与部署&#xff1a;从 INT4 量化到 MoE&#xff0c;让大模型跑在手机、边缘设备和云端&#x1f3af; 为什么需要模型压缩与部署&#xff1f;你训练了一个强大的大模型&#xff08;如 Qwen-72B、LLaMA-3-70B&#xff09;&#xff0c;但在部署时发现&#xff1…

新手向:中文语言识别的进化之路

自然语言处理&#xff08;NLP&#xff09;技术正在以前所未有的速度改变我们与机器的交互方式。根据Gartner最新报告显示&#xff0c;全球NLP市场规模预计在2025年将达到430亿美元&#xff0c;年复合增长率高达21%。而中文作为世界上使用人数最多的语言&#xff08;全球约15亿使…

LeetCode100-206反转链表

本文基于各个大佬的文章上点关注下点赞&#xff0c;明天一定更灿烂&#xff01;前言Python基础好像会了又好像没会&#xff0c;所有我直接开始刷leetcode一边抄样例代码一边学习吧。本系列文章用来记录学习中的思考&#xff0c;写给自己看的&#xff0c;也欢迎大家在评论区指导…

uniapp开源多商户小程序商城平台源码 支持二次开发+永久免费升级

在电商行业竞争日益激烈的今天&#xff0c;拥有一个功能强大、灵活可拓展的多商户小程序商城至关重要。今天给大家分享一款 uniapp 开源多商户小程序商城平台源码&#xff0c;它不仅具备丰富的基础功能&#xff0c;还支持二次开发&#xff0c;更能享受永久免费升级服务&#xf…

使用脚本一键更新NTP服务器地址为自定义地址

【使用场景】 在银河麒麟桌面操作系统V10SP1-2303版本中使用脚本一键修改NTP服务器地址为自定义地址。 【操作步骤】 步骤1. 编写shell脚本 ```bash desktop2303@desktop2303-pc:~$ vim setntptimeserver.sh #!/bin/bashfunction modifykylinconf() { # 检查是否已存在目标配置…

linux内核 - 内核架构概览

当 Linux 系统启动时,内核会在启动过程的早期阶段接管控制——紧跟在固件(BIOS 或 UEFI)和引导加载程序完成任务之后。此时,压缩的 Linux 内核镜像会被加载到内存中,通常会附带一个称为 initramfs 的最小临时根文件系统,它用于在切换到真实根文件系统并继续系统初始化之前…

[react] react-router-dom是啥?

页面路由&#xff0c;注意页面路由不是路由器&#xff0c;因为我之前总是把路由和路由器搞混。而且我总是把前端页面的路由和路由器的路由搞混。那么这里一定要明白&#xff0c;这里我所说的页面路由就是指在浏览器里面的导航路由。 npm create vitelatest my-react-app – --t…

HTTP简易客户端实现

&#x1f310; HTTP简易客户端实现 流程图&#xff1a; 引用&#xff1a; chnroutes2.cpp#L474 chnroutes2_getiplist() chnroutes2.cpp#L443 http_easy_get(…) &#x1f552; 1. 超时管理机制 (http_easy_timeout) &#x1f539; 核心功能&#xff1a;创建定时器自动关…

建筑面LAS点云高度计算工具

效果 例如中位数,计算后,在shp建筑面中添加一个字段meidian_hei 准备数据 1、建筑矢量面.shp 2、点云.las 界面 脚本 import laspy import shapefile # pyshp库,处理POLYGONZ坐标格式异常 import pandas as pd import numpy as np import os import traceback # 打印…

java day18

继续学习&#xff0c;学习sringboot案例&#xff1b;熟悉的三件套&#xff1b;比如做一个表&#xff0c;前端搭建好框架&#xff0c;然后返回给后端一个请求&#xff0c;说要这个表的数据吧&#xff1b;然后通过请求和规定的格式返回给后端之后&#xff0c;我们后端进行接收处理…

并发编程原理与实战(二十八)深入无锁并发演进,AtomicInteger核心API详解与典型场景举例

无锁并发演进背景 随着系统高并发的压力越来越大&#xff0c;传统同步机制在高并发场景下的性能瓶颈和缺点可能会逐渐显露&#xff1a; &#xff08;1&#xff09;性能损耗&#xff1a;synchronized等锁机制会导致线程阻塞和上下文切换&#xff0c;在高并发场景下性能损耗显著。…

整体设计 之 绪 思维导图引擎 之 引 认知系统 之 引 认知系统 之 序 认知元架构 之5 : Class 的uml profile(豆包助手 之7)

摘要&#xff08;AI生成&#xff09;三层中间件架构的约束逻辑体系1. 架构定位与功能分工三个中间层&#xff08;隔离层/隐藏层/防腐层&#xff09;构成数据处理管道&#xff0c;分别承担&#xff1a;隔离层&#xff1a;跨系统数据转换处理对象&#xff1a;异构数据&#xff08…

iframe引入界面有el-date-picker日期框,点击出现闪退问题处理

前言&#xff1a;iframe引入界面有el-date-picker日期框&#xff0c;点击出现闪退问题处理。问题情况&#xff1a;点击开始日期的输入部分&#xff0c;会出现闪退情况&#xff0c;该组件是iframe调用的内容问题分析&#xff1a;事件冒泡&#xff0c;点击与聚焦的时候&#xff0…

docker 拉取本地镜像

要在Docker中拉取本地镜像&#xff0c;通常有以下几种实现方法&#xff1a; 使用docker pull命令&#xff1a;可以使用docker pull命令从本地镜像仓库拉取镜像。例如&#xff0c;如果本地镜像的名称是my-image&#xff0c;则可以运行以下命令拉取镜像&#xff1a; docker pull …

嘉立创EDA从原理图框选住器件进行PCB布局

1、先选中需要布局的模块的相关器件2、设计-》布局传递3、在PCB会选中模块相关的元器件&#xff0c;拖动进行布局4、依次将每个模块都分类出来5、板框设计&#xff1a;如果有要求大小&#xff0c;可以先将单位设置为mm&#xff0c;然后画出来板框的尺寸

http接口幂等性

实现 HTTP 接口的幂等性是确保多次相同请求产生相同结果的重要设计原则&#xff0c;尤其在网络不稳定或分布式系统中非常关键。以下是几种常见的实现方式&#xff1a;1. 基于幂等性令牌&#xff08;Token&#xff09;的实现适合支付、订单创建等场景&#xff0c;步骤如下&#…

【华为OD】贪吃的猴子

文章目录【华为OD】贪吃的猴子题目描述输入描述输出描述示例示例一示例二解题思路解法一&#xff1a;前缀和枚举法Java实现Python实现C实现解法二&#xff1a;滑动窗口法Java实现Python实现C实现解法三&#xff1a;优化的动态规划法Java实现Python实现C实现算法复杂度分析解法一…

Flie ,IO流(一)

一.File&#xff0c;IO流概述二.File文件1.File文件对象的创建&#xff08;路径&#xff1a;&#xff09;2.常用方法1:判断文件类型、获取文件信息&#xff08;注意&#xff1a;&#xff09;3.常用方法2:创建文件、删除文件&#xff08;creatNewFile&#xff08;&#xff09;会…

第2讲 机器学习 - 导论

我们正处在一个"数据时代"&#xff0c;更强的计算能力和更丰富的存储资源使数据总量与日俱增。然而真正的挑战在于如何从海量数据中提取价值。企业与组织正通过数据科学、数据挖掘和机器学习的技术体系构建智能系统应对这一挑战。其中&#xff0c;机器学习已成为计算…

如何解决pip安装报错ModuleNotFoundError: No module named ‘python-dateutil’问题

【Python系列Bug修复PyCharm控制台pip install报错】如何解决pip安装报错ModuleNotFoundError: No module named ‘python-dateutil’问题 摘要 在日常 Python 开发过程中&#xff0c;我们经常会遇到各种 pip install 的报错&#xff0c;尤其是在 PyCharm 2025 控制台环境下&…