redis配置与优化(2)
文章目录
- 八、性能管理与优化
- 1. 内存指标详解
- 2. 内存碎片(Memory Fragmentation)
- 产生原因
- 示例
- 3. 内存碎片率分析
- 4. 内存碎片率监控命令
- 5. 内存使用率优化
- (1)合理规划实例内存大小
- (2)优先使用高效数据结构
- (3)强制设置key过期时间(TTL)
- (4)配置内存上限与淘汰策略
- (5)关闭或限制swap
- 6. Redis 内存淘汰策略
- 6种淘汰策略及适用场景
- 实际应用建议
- 九、扩展 Redis缓存常见问题
- 1. 缓存穿透
- 定义
- 常见原因
- 解决方案(按优先级排序)
- (1)缓存空结果(快速拦截)
- (2)布隆过滤器(根源拦截)
- 2. 缓存击穿
- 定义
- 常见原因
- 解决方案(按有效性排序)
- (1)逻辑过期(无锁异步更新)
- (2)分布式锁(互斥更新)
- (3)热点key永不过期+主动更新
- 3. 缓存雪崩
- 定义
- 常见原因
- 解决方案(分层防护)
- (1)缓存层高可用(避免集群宕机)
- (2)避免key集中过期(分散过期时间)
- (3)数据库层防护(最后屏障)
- (4)双缓存机制(多层拦截)
八、性能管理与优化
1. 内存指标详解
Redis 内存统计核心围绕“实际数据占用”与“系统物理占用”两大维度,关键指标及含义如下:
指标 | 含义说明 |
---|---|
used_memory | Redis 实际占用的内存总量,包含两部分: 1. 存储业务数据的内存(如键值对); 2. 内部开销(键名、数据结构元信息、哈希表扩容预留等,即 used_memory_overhead )。 |
used_memory_rss | Redis 进程占用的操作系统物理内存(Resident Set Size),包含: 1. used_memory 对应的内存;2. 内存碎片(操作系统分配的未被完全利用的内存块); 3. 操作系统为进程分配的额外缓存(如页缓存)。 |
used_memory_peak | Redis 运行过程中的内存峰值,用于判断是否曾出现内存溢出风险。 |
used_memory_overhead | Redis 维护数据结构的内部开销(如键名存储、过期时间标记、哈希表元数据),不包含业务数据本身。 |
核心公式:
内存碎片率 = used_memory_rss / used_memory
(反映物理内存利用效率的核心指标)
2. 内存碎片(Memory Fragmentation)
内存碎片是 Redis 申请内存与操作系统实际分配内存不匹配 导致的物理内存浪费,本质是“可用内存总量足够,但无法被连续利用”。
产生原因
- Redis 申请内存时,需按业务数据量申请“连续内存块”(如存储一个大哈希表需连续空间);
- 操作系统分配内存时,会按“页大小”(通常4KB)或预分配策略分配内存块,若没有刚好匹配的连续块,会用多个小碎片块拼接满足需求;
- 长期读写操作(如频繁删除大键、哈希表扩容/缩容)会导致物理内存块分散,形成大量无法复用的小碎片。
示例
Redis 需存储1GB业务数据,申请1GB连续物理内存:
- 若操作系统只有多个200MB、300MB的分散碎片块(总大小1GB),需拼接这些碎片块才能满足需求;
- 拼接过程中产生的“块间管理开销”(如记录碎片块地址)和“未填满的碎片空间”(如最后一个碎片块剩50MB未用),即为内存碎片。
3. 内存碎片率分析
不同碎片率对应不同运行状态,需针对性处理:
内存碎片率范围 | 状态含义 | 建议操作 |
---|---|---|
≈ 1(0.9~1.1) | 碎片率合理,物理内存利用率高,Redis 运行正常。 | 无需操作,持续监控即可。 |
> 1.5 | 碎片率过高,物理内存浪费超过50%(如1GB业务数据占用1.5GB物理内存),可能导致内存溢出。 | 1. 安全重启:低峰期执行 SHUTDOWN SAVE (先持久化数据,避免丢失),重启后Redis会重新申请连续物理内存,碎片率显著降低;2. 优化配置:调整 hash-max-ziplist-entries (如设512)、list-max-ziplist-size (如设-2),减少小数据结构的内存碎片化。 |
< 0.9 | 碎片率过低,说明 used_memory_rss < used_memory ,大概率是 Redis 内存被交换到swap分区(操作系统因物理内存不足,将部分Redis内存写入磁盘)。 | 1. 关闭swap:执行 swapoff -a (临时关闭),并在 /etc/fstab 注释swap挂载(永久关闭),避免内存交换导致性能暴跌(swap读写速度比内存慢10倍以上);2. 扩容物理内存:若业务数据量持续增长,需增加服务器物理内存; 3. 减少内存占用:清理无效key(如用 scan 遍历删除过期key)、优化数据结构(如用Hash替代多个独立key)。 |
4. 内存碎片率监控命令
通过 redis-cli info memory
实时查看内存指标,关键输出示例及注释:
$ redis-cli info memory
# Memory
used_memory:1073741824 # 业务数据+内部开销共1GB(约1024^3字节)
used_memory_rss:1610612736 # 操作系统分配1.5GB物理内存(约1.5*1024^3字节)
used_memory_peak:1205862400 # 内存峰值约1.12GB
used_memory_overhead:10485760 # 内部开销约10MB(键名、元数据等)
mem_fragmentation_ratio:1.5 # 碎片率1.5(需关注,建议低峰期重启)
5. 内存使用率优化
(1)合理规划实例内存大小
- 单Redis实例内存建议不超过物理内存的 **70%80%**,预留20%30%给操作系统、页缓存及其他进程(如监控、日志);
- 若业务数据量超单实例承载(如100GB),建议拆分多个实例或使用Redis Cluster(数据分片存储)。
(2)优先使用高效数据结构
- 用Hash存储结构化数据:如用户属性(name/age/phone),小Hash(字段数≤512、值≤64字节)会被Redis用
ziplist
压缩存储,相比多个独立key(如user:1:name
、user:1:age
)减少50%以上的元数据开销;# 推荐:1个Hash存储所有属性 hmset user:1 name "Tom" age 20 phone "13800138000" # 不推荐:多个独立key set user:1:name "Tom" set user:1:age 20
- 用Set/List替代String存储集合数据:如用户标签,Set支持去重,List支持有序遍历,且内存占用比多个String更紧凑。
(3)强制设置key过期时间(TTL)
- 缓存类数据(如商品详情、用户会话)必须加过期时间,避免无效数据长期占用内存;
- 避免集中过期:给基础过期时间加随机偏移(如
EX 3600 + rand(500, 1000)
),防止大量key同时过期引发缓存雪崩;# 示例:1小时基础过期时间,加500~1000秒随机偏移 set session:123 "user:1001" EX $((3600 + RANDOM() % 501))
(4)配置内存上限与淘汰策略
- 在
/etc/redis/6379.conf
中设置maxmemory
,限制Redis最大内存占用:maxmemory 2gb # 单实例最大内存2GB
- 结合业务场景选择
maxmemory-policy
(详见第6节),避免内存满后写操作报错。
(5)关闭或限制swap
- 生产环境建议 永久关闭swap:编辑
/etc/fstab
,注释swap挂载行(如# /dev/mapper/cl-swap swap swap defaults 0 0
),重启后生效; - 若无法关闭,将
swappiness
设为0~10(减少内存交换倾向):echo 5 > /proc/sys/vm/swappiness # 临时生效 echo "vm.swappiness = 5" >> /etc/sysctl.conf # 永久生效
6. Redis 内存淘汰策略
当Redis内存占用达到 maxmemory
时,会触发淘汰策略回收空间,核心是“删除哪些key”的规则选择。配置路径:/etc/redis/6379.conf
,关键配置项:
maxmemory-policy <policy> # 默认值:noeviction
6种淘汰策略及适用场景
策略类型 | 核心逻辑 | 适用场景 |
---|---|---|
volatile-lru | 从已设置过期时间(TTL)的key中,淘汰“最近最少使用”(Least Recently Used)的key。 | 需保留核心数据(无过期时间,如用户余额),同时允许缓存数据(有过期时间,如商品列表)淘汰的场景。 |
volatile-ttl | 从已设置过期时间的key中,淘汰“剩余生存时间最短”(Time To Live)的key。 | 业务需优先淘汰即将过期的缓存数据(如限时活动商品缓存,过期前自动清理)。 |
volatile-random | 从已设置过期时间的key中,随机淘汰key。 | 对缓存数据优先级无要求,且需避免单一key被频繁淘汰的场景(如低频访问的非核心缓存)。 |
allkeys-lru | 从所有key中,淘汰“最近最少使用”的key。 | 典型缓存场景(如商品详情、用户会话),无需区分核心/非核心数据,保留高频访问key即可。 |
allkeys-random | 从所有key中,随机淘汰key。 | 缓存数据访问频率均匀(无明显热点),且允许随机淘汰的场景(如统计类临时数据)。 |
noeviction | 禁止淘汰任何key,内存满后新写操作直接报错(读操作正常)。 | 数据不可丢失的场景(如Redis用作分布式计数器、消息队列,key无过期时间),需提前规划 maxmemory 避免满内存。 |
实际应用建议
业务场景 | 推荐策略 | 注意事项 |
---|---|---|
电商缓存(商品、用户会话) | allkeys-lru | 提前设置 maxmemory 为物理内存的70%,避免内存溢出;结合 expire 给非热点key加过期时间。 |
核心业务数据(余额、订单) | volatile-lru | 核心数据不设过期时间,缓存数据设TTL;监控 used_memory ,避免核心数据被误淘汰。 |
数据不可丢失(计数器、队列) | noeviction | 定期清理无效临时key(如用 scan 遍历删除);实时监控内存使用,及时扩容。 |
九、扩展 Redis缓存常见问题
Redis 作为缓存时,需解决“缓存与数据库一致性”“高并发下穿透/击穿/雪崩”等问题,以下是面试高频问题的原理与解决方案优化。
1. 缓存穿透
定义
请求查询“不存在的数据”(如恶意构造不存在的用户ID、商品ID),导致缓存未命中(缓存中无该key),所有请求穿透至数据库,引发数据库压力骤增甚至宕机。
常见原因
- 恶意攻击:攻击者批量请求不存在的key(如
user:-1
、goods:999999
),利用缓存穿透压垮数据库; - 业务低频场景:如查询已下架商品、注销用户,这类key本身不存在,缓存无法命中。
解决方案(按优先级排序)
(1)缓存空结果(快速拦截)
- 原理:若数据库查询结果为空(数据不存在),仍将该key写入缓存,value设为“空标记”(如
NULL_FLAG
),并设置较短的过期时间(30秒~5分钟); - 操作示例:
String value = redis.get("user:-1"); if (value == null) {// 查数据库,结果为空User user = db.query("select * from user where id = -1");if (user == null) {// 缓存空结果,过期时间30秒redis.set("user:-1", "NULL_FLAG", 30, TimeUnit.SECONDS);return null;}// 数据存在,正常缓存redis.set("user:-1", JSON.toJSONString(user), 3600, TimeUnit.SECONDS); } else if ("NULL_FLAG".equals(value)) {// 命中空标记,直接返回,不查数据库return null; }
- 注意:避免设置过长过期时间,防止缓存大量空对象占用内存;空标记需与正常数据区分(如用特殊字符串),避免业务误判。
(2)布隆过滤器(根源拦截)
- 原理:基于“多个哈希函数+二进制数组”实现快速存在性判断——将所有合法key(如数据库中已有的用户ID、商品ID)提前导入布隆过滤器,请求先经过过滤器:
- 若过滤器判断“key不存在”:直接返回空结果,无需查缓存和数据库;
- 若过滤器判断“key存在”:再查缓存,缓存未命中则查数据库(允许极小误判率)。
- 适用场景:数据量极大(如亿级用户ID),缓存空结果无法覆盖所有不存在的key;
- 注意:
- 误判率:可通过调整“二进制数组大小”和“哈希函数数量”降低(如误判率0.1%,需约14倍数据量的数组空间);
- 不支持删除:若合法key被删除(如商品下架),过滤器无法实时更新,需定期重建(如每天凌晨重建一次)。
2. 缓存击穿
定义
热点key(高频访问的key)过期瞬间,大量并发请求同时穿透缓存至数据库,导致数据库短时间内承受极高压力(如秒杀商品缓存过期,每秒1万请求打向数据库)。
常见原因
- 热点key未设置过期时间:若业务强制要求过期(如商品库存实时更新),过期瞬间会引发击穿;
- 热点key集中过期:多个热点key设置相同过期时间(如凌晨3点批量更新缓存),过期时同时引发击穿。
解决方案(按有效性排序)
(1)逻辑过期(无锁异步更新)
- 原理:缓存key不设置实际过期时间(避免Redis自动删除),而是在value中嵌入“逻辑过期时间”(如
{"data": "...", "expireAt": 1699999999}
); - 流程:
- 请求查缓存,若命中,判断逻辑过期时间:
- 未过期:直接返回数据;
- 已过期:返回旧数据,同时异步线程更新缓存(查数据库→更新缓存,新逻辑过期时间+随机偏移);
- 缓存未命中:走数据库查询→更新缓存→返回数据;
- 请求查缓存,若命中,判断逻辑过期时间:
- 优势:无锁,不阻塞用户请求,避免并发等待;
- 适用场景:允许短期返回旧数据的业务(如商品详情、新闻列表)。
(2)分布式锁(互斥更新)
- 原理:热点key过期后,只有第一个请求能获取“分布式锁”,并执行“查数据库→更新缓存”操作;其他请求获取锁失败时,重试查询缓存(直到缓存更新完成);
- 操作示例(用Redis实现分布式锁):
String key = "goods:1001"; String value = redis.get(key); if (value == null) {// 尝试获取分布式锁(锁key=lock:goods:1001,过期时间5秒,避免死锁)Boolean lock = redis.setIfAbsent("lock:goods:1001", "1", 5, TimeUnit.SECONDS);if (lock != null && lock) {try {// 获取锁成功,查数据库并更新缓存Goods goods = db.query("select * from goods where id = 1001");redis.set(key, JSON.toJSONString(goods), 3600, TimeUnit.SECONDS);return goods;} finally {// 释放锁(避免锁残留)redis.delete("lock:goods:1001");}} else {// 获取锁失败,重试(间隔100ms,避免频繁重试)Thread.sleep(100);return getGoods(1001); // 递归或循环重试} } return JSON.parseObject(value, Goods.class);
- 注意:锁过期时间需大于“查数据库+更新缓存”的耗时(如5秒),避免锁提前释放导致并发更新;推荐用Redisson实现分布式锁(支持自动续期,避免死锁)。
(3)热点key永不过期+主动更新
- 原理:对核心热点key(如秒杀商品、首页Banner)不设置过期时间,通过“业务主动更新”保证数据一致性(如商品库存变更时,同步更新缓存);
- 优势:彻底避免过期引发的击穿;
- 注意:需结合业务监控热点key(如通过Redis
INFO stats
查看keyspace_hits
高频key),避免非热点key长期占用内存。
3. 缓存雪崩
定义
大量缓存不可用(如缓存集群宕机、大量key集中过期),导致所有请求穿透至数据库,数据库无法承受并发压力而宕机,引发“缓存-数据库”连锁故障。
常见原因
- 缓存集群宕机:Redis主从/Cluster集群因网络故障、节点崩溃整体不可用;
- 大量key集中过期:批量缓存key设置相同过期时间(如每天凌晨2点更新缓存,所有key过期时间设为24小时);
- 缓存穿透/击穿放大:未处理的穿透/击穿请求持续压垮数据库,间接导致缓存更新失败。
解决方案(分层防护)
(1)缓存层高可用(避免集群宕机)
- 部署Redis Cluster(3主3从)+ 哨兵:单个主节点故障时,从节点自动切换为新主,保证集群可用性;
- 异地多活:核心业务部署跨机房缓存集群(如北京、上海各一套Cluster),单机房故障时切换至异地集群;
- 限流熔断:用网关(如Nginx)或熔断组件(如Sentinel)对缓存请求限流,当缓存集群响应超时率超过阈值(如50%),触发熔断,直接返回默认值(如“服务繁忙,请稍后重试”)。
(2)避免key集中过期(分散过期时间)
- 过期时间加随机偏移:基础过期时间+随机值(如
3600 + rand(500, 1000)
秒),确保key过期时间分散在10~17分钟内,避免集中失效; - 分批次更新缓存:对批量key按业务分区(如商品按ID尾号1~10分区),每批缓存设置不同的过期时间(如尾号1的key过期时间23小时,尾号2的24小时),避免同时更新。
(3)数据库层防护(最后屏障)
- 加锁限流:对数据库访问加分布式锁(如用Redis
INCR
实现计数器,每秒允许100个请求访问数据库),避免同时大量请求打向数据库; - 读写分离:数据库部署主从架构,缓存失效时请求优先打向从库(读库),减轻主库压力;
- 预热缓存:业务低峰期(如凌晨3点)主动更新缓存(查数据库→写入缓存),避免高峰期缓存未命中。
(4)双缓存机制(多层拦截)
- 主缓存:Redis(高可用,有过期时间),承载大部分请求;
- 备缓存:本地缓存(如Java Caffeine、Python lru_cache,无过期时间或过期时间比主缓存长5~10分钟);
- 流程:请求先查备缓存→备缓存未命中查主缓存→主缓存未命中查数据库→更新主缓存和备缓存;
- 优势:主缓存失效时,备缓存可临时拦截请求,给主缓存更新留时间。