Redis 缓存数据库
本文合并自:01-Redis入门与部署.md、02-Redis命令详解.md、03-Redis配置参数说明.md、02-Redis集群.md
一、Redis 概述
1.1 NoSQL 与缓存的定位
Redis(Remote Dictionary Server,远程字典服务)是一款开源的非关系型(NoSQL)数据库,基于内存运行,采用 key-value 数据模型。
详见 11-08-NoSQL与MongoDB.md
最典型的键值对形态就是"键 → 值"的一一映射:
a:1
b:2
关系型数据库(MySQL / MariaDB)以磁盘为主存储、以 SQL 与表结构为核心,强调事务与一致性;NoSQL 则牺牲部分一致性与结构化能力,换取极高的读写吞吐与横向扩展能力。在实际架构中,Redis 通常不是 MySQL 的替代品,而是站在 MySQL 前面的缓存层:
客户端 → 应用程序 → Redis(内存,命中直接返回)
↓ 未命中
MySQL(磁盘,回填 Redis)
Redis 相对于其他 NoSQL 产品(如 Memcached)的两个突出区别:
- 所有操作具备原子性;
- 支持数据持久化,可在宕机后恢复数据。
1.2 Redis 的特点
| 特性 | 说明 |
|---|---|
| 速度快 | 读速度约 110000 次/秒,写速度约 81000 次/秒 |
| 数据类型丰富 | 支持 string、hash、list、set、zset(有序集合)等 |
| 原子性 | 所有操作均具有原子性,单条命令天然线程安全 |
| 特性丰富 | 支持 publish/subscribe(发布订阅)、通知、key 过期等 |
| 数据持久化 | 可将内存中的数据永久写入磁盘(RDB / AOF),实现数据持久化 |
| 主从复制 | 原生支持 master-slave 架构,天然满足读写分离 |
| 支持集群 | 提供 Sentinel 高可用与 Redis Cluster 分布式分片 |
内存存储
Redis 把全部数据集放在内存中,磁盘只用于持久化和重启恢复。这是它快的根本原因:内存随机访问是纳秒级,机械磁盘是毫秒级,相差 5~6 个数量级。代价是容量受物理内存限制,成本远高于磁盘。
单线程模型与为什么快
Redis 的命令执行是单线程的(Redis 6.0 之后网络 I/O 可以多线程,但命令仍由单线程串行执行)。单线程反而快,原因有四点:
- 纯内存操作,几乎没有 I/O 等待,CPU 不是瓶颈;
- 避免了锁竞争与线程上下文切换开销,也不需要考虑并发安全;
- I/O 多路复用(epoll):单线程用事件驱动同时处理成千上万个连接;
- 自研的高效数据结构与协议:SDS 动态字符串、跳表、ziplist 等,配合简洁的 RESP 文本协议。
单线程带来的副作用:一条慢命令会阻塞所有客户端。因此生产环境必须禁用 KEYS *、FLUSHALL、大 key 的 HGETALL 等命令(详见第五章)。
丰富的数据类型
不只是简单的 key-value,还支持字符串、哈希、列表、集合、有序集合五种基本类型,以及 Bitmap、HyperLogLog、GEO、Stream 等扩展类型。这让 Redis 可以直接支撑排行榜、去重统计、地理位置检索等业务语义,而不是只当一个"内存里的字典"。
持久化
提供 RDB(快照)与 AOF(追加日志)两种机制,可单用也可混用,兼顾恢复速度与数据安全(详见第四章)。
支持集群
- 主从复制:数据冗余 + 读写分离;
- Sentinel 哨兵:主库故障时自动选举、自动切换;
- Redis Cluster:去中心化分片,16384 个哈希槽横向扩容。
1.3 Redis 与 Memcached 的对比
| 对比项 | Redis | Memcached |
|---|---|---|
| 数据类型 | string / hash / list / set / zset / bitmap / HLL / GEO / stream | 仅字符串(key-value) |
| 持久化 | 支持 RDB + AOF | 不支持,重启数据全丢 |
| 主从复制 | 原生支持 | 不支持(需第三方方案) |
| 高可用 | Sentinel、Cluster | 无原生方案 |
| 线程模型 | 命令执行单线程(6.0 起 I/O 多线程) | 多线程 |
| 内存管理 | 自身 + jemalloc,支持 8 种淘汰策略 | Slab 分配,LRU 淘汰 |
| 单值最大容量 | String 512 MB | 默认 1 MB |
| 过期删除 | 惰性删除 + 定期删除 | 惰性删除 |
| 事务 | 支持 MULTI/EXEC/WATCH | 不支持(仅 CAS) |
| Lua 脚本 | 支持 | 不支持 |
| 分布式 | Cluster 原生分片 | 客户端一致性哈希 |
| 适用场景 | 需要数据结构、持久化、高可用的缓存与轻量存储 | 纯粹的、大并发、简单 KV 缓存 |
结论:绝大多数新项目直接选 Redis。只有在"纯字符串缓存 + 需要多核吃满单机吞吐"的极端场景下,Memcached 仍有一点优势。
1.4 典型应用场景
| 场景 | 实现要点 | 常用数据类型 |
|---|---|---|
| 缓存 | 热点数据前置到内存,配合 EX 过期与淘汰策略,降低数据库压力 |
String / Hash |
| 会话共享(Session) | 多台应用服务器共用一份 Redis 中的 Session,解决集群下登录态不同步 | Hash + 过期时间 |
| 计数器 | INCR / INCRBY 原子自增,做浏览量、点赞数、库存扣减 |
String |
| 排行榜 | ZADD 写分数,ZREVRANGE 取 Top N,天然有序 |
ZSet |
| 消息队列 | LPUSH + BRPOP 做简单队列;RPOPLPUSH 做可靠队列;Stream 做消费组 |
List / Stream |
| 分布式锁 | SET key value NX EX seconds 加锁,Lua 脚本校验后删除解锁 |
String |
| 限流 | 固定窗口用 INCR + EXPIRE;滑动窗口用 ZSet 存时间戳按分数清理 |
String / ZSet |
| 去重统计 | 精确去重用 Set;海量 UV 近似去重用 HyperLogLog(误差约 0.81%) | Set / HLL |
| 签到打卡 | 一天一位,一年 365 bit,SETBIT / BITCOUNT 极省内存 |
Bitmap |
| 附近的人 | GEOADD 存经纬度,GEORADIUS 按半径检索 |
GEO |
| 发布订阅 | 配置变更推送、简单实时通知 | Pub/Sub |
二、安装部署
2.1 源码编译安装(完整步骤)
步骤 1:安装依赖
# 编译必备
yum install -y gcc gcc-c++ make
# tcl 用于执行 make test 单元测试(可选但建议)
yum install -y tcl
# CentOS 7 下若需要更高版本 gcc(Redis 6+ 需 gcc 5 以上)
yum install -y centos-release-scl
yum install -y devtoolset-9-gcc devtoolset-9-gcc-c++
scl enable devtoolset-9 bash
步骤 2:下载源码
# 官网
wget http://download.redis.io/releases/redis-4.0.11.tar.gz
# 笔记站点
wget 192.168.2.254/redis/pack/redis-4.0.11.tar.gz
步骤 3:解压并编译
# 解压到 /usr/local/
tar xf redis-4.0.11.tar.gz -C /usr/local/
cd /usr/local/redis-4.0.11
make
编译报错处理
若出现
致命错误:jemalloc/jemalloc.h:没有那个文件或目录,使用以下命令重新编译:make MALLOC=libc原因:Redis 默认使用 jemalloc 作为内存分配器,源码树中的 deps 未正确编译时会找不到头文件。改用 libc 分配器即可绕过,也可以先
make distclean清理后重新make。
可选的单元测试(依赖 tcl):
make test
步骤 4:安装到系统路径
# 默认安装到 /usr/local/bin
make install
# 也可以指定前缀
make PREFIX=/usr/local/redis install
安装后生成的可执行文件:
| 命令 | 作用 |
|---|---|
redis-server |
Redis 服务端 |
redis-cli |
命令行客户端 |
redis-sentinel |
哨兵进程(软链到 redis-server) |
redis-benchmark |
官方压力测试工具 |
redis-check-aof |
AOF 文件检查修复工具 |
redis-check-rdb |
RDB 文件检查工具 |
步骤 5:统一目录名
将目录重命名为统一名称,便于脚本引用:
mv /usr/local/redis-4.0.11/ /usr/local/redis
至此安装完成。
步骤 6:install_server.sh 交互式配置
Redis 源码包的 utils/ 目录提供了交互式部署脚本,可一次性生成配置文件、数据目录、日志文件和 init 启动脚本:
cd /usr/local/redis/utils
./install_server.sh
脚本会依次询问以下五项,直接回车即接受方括号中的默认值:
Please select the redis port for this instance: [6379]
Please select the redis config file name [/etc/redis/6379.conf]
Please select the redis log file name [/var/log/redis_6379.log]
Please select the data directory for this instance [/var/lib/redis/6379]
Please select the redis executable path [/usr/local/bin/redis-server]
确认后脚本自动完成:
- 生成配置文件
/etc/redis/6379.conf(已设置daemonize yes); - 创建数据目录
/var/lib/redis/6379; - 生成启动脚本
/etc/init.d/redis_6379; - 通过
chkconfig --add(CentOS 6)或chkconfig兼容层(CentOS 7)注册为服务并启动。
管理方式:
/etc/init.d/redis_6379 start
/etc/init.d/redis_6379 stop
/etc/init.d/redis_6379 status
若在 CentOS 7 上脚本因
systemd检测报错,可注释掉脚本中的 systemd 判断行,或改用后文的 systemd unit 方式。
步骤 7:目录规划建议
生产环境建议把配置、数据、日志分离,便于备份与多实例部署:
mkdir -p /usr/local/redis/{conf,data,log,run}
# 目录职责
# /usr/local/redis/conf 配置文件 redis.conf / sentinel.conf
# /usr/local/redis/data RDB 与 AOF 数据文件(对应 dir 参数)
# /usr/local/redis/log 日志文件(对应 logfile 参数)
# /usr/local/redis/run pid 文件(对应 pidfile 参数)
# 创建专用运行用户(不要用 root 运行 Redis)
useradd -M -s /sbin/nologin redis
chown -R redis:redis /usr/local/redis
多实例(如集群)目录规划示例:
/opt/truplatform/redis/ # 6379 实例
etc/redis.conf
log/redis_6379.log
redisdb/ # 数据目录
/opt/truplatform/redis-6380/ # 6380 实例
etc/redis.conf
log/redis_6380.log
redisdb/
2.2 配置环境变量
由于是源码安装,redis-server、redis-cli 等命令默认可能不在系统 PATH 中,需要手动配置(若已执行 make install 到 /usr/local/bin 则可跳过)。
临时生效:
export PATH=$PATH:/usr/local/redis/src
永久生效:
vim /etc/profile
# 在文件中加入:
export PATH=$PATH:/usr/local/redis/src
source /etc/profile
也可以使用 /etc/profile.d/ 下的独立脚本(更规范):
cat > /etc/profile.d/redis.sh <<'EOF'
export PATH=$PATH:/usr/local/redis/src
EOF
source /etc/profile.d/redis.sh
2.3 yum 与 EPEL 安装方式
源码编译灵活但繁琐,测试环境可直接用包管理器。
CentOS 7(需 EPEL 源):
# 安装 EPEL 源
yum install -y epel-release
# 安装 Redis(EPEL 中通常为 3.2.x)
yum install -y redis
# 配置文件、数据目录与日志的默认位置
# 配置:/etc/redis.conf
# 数据:/var/lib/redis
# 日志:/var/log/redis/redis.log
# 服务:redis.service
systemctl start redis
systemctl enable redis
systemctl status redis
CentOS 6(需 EPEL 源):
yum install -y epel-release
yum install -y redis
# CentOS 6 使用 SysV init 管理
service redis start
service redis stop
service redis restart
chkconfig redis on
chkconfig --list redis
使用 Remi 源安装较新版本(CentOS 7):
yum install -y https://rpms.remirepo.net/enterprise/remi-release-7.rpm
yum --enablerepo=remi install -y redis
yum 安装的版本通常较旧(EPEL 7 为 3.2),若需要 Redis 5/6/7 的新特性(Stream、ACL、多线程 I/O),请使用源码编译或 Remi 源。
2.4 启动与关闭
直接启动(指定配置文件)
未配置环境变量时(前台 / 后台):
cd /usr/local/redis
./redis-server # 前台启动,使用内置默认配置
./redis-server & # 后台启动(作业方式,不推荐)
配置环境变量后:
redis-server # 前台,默认配置
redis-server & # 后台
生产环境必须显式指定配置文件(否则所有 redis.conf 里的设置都不生效):
redis-server /usr/local/redis/conf/redis.conf
还可以在命令行覆盖单个配置项(优先级高于配置文件):
redis-server /usr/local/redis/conf/redis.conf --port 6380 --daemonize yes
配合 daemonize yes 时,进程会自动转入后台:
vim /usr/local/redis/redis.conf
daemonize yes
nohup 后台启动
若不想用 daemonize,也可以用 nohup 让进程脱离终端:
nohup redis-server /usr/local/redis/conf/redis.conf > /usr/local/redis/log/nohup.out 2>&1 &
# 查看进程
ps -ef | grep redis-server
netstat -tunlp | grep 6379
注意:
daemonize yes与nohup二者取其一即可,同时使用没有意义。
关闭 Redis
推荐方式:redis-cli shutdown,它会先完成持久化再退出,数据安全:
redis-cli shutdown # 本地默认端口
redis-cli -p 6380 shutdown # 指定端口
redis-cli -a 'test123' shutdown # 有密码时
redis-cli shutdown save # 强制先做 RDB 快照再退出
redis-cli shutdown nosave # 不做快照直接退出(慎用)
不推荐方式:
kill -9 <pid> # 强杀,未落盘的数据会丢失
pkill -9 redis # 更危险,会杀掉本机所有 redis 进程(含哨兵)
正确的信号是 SIGTERM,Redis 收到后会做优雅退出:
kill -15 <pid>
SysV init 脚本管理(CentOS 6 / CentOS 7 均可用)
拷贝官方提供的启动脚本:
cp /usr/local/redis/utils/redis_init_script /etc/init.d/redis
chmod +x /etc/init.d/redis
编辑脚本,指定安装路径:
vim /etc/init.d/redis
REDIS_DIR=/usr/local/redis # Redis 安装目录
EXEC=$REDIS_DIR/src/redis-server # redis-server 路径
CLIEXEC=$REDIS_DIR/src/redis-cli # redis-cli 路径
CONF="$REDIS_DIR/redis.conf" # 配置文件路径
修改配置文件,使其以守护进程方式运行:
daemonize yes
使用脚本启停:
/etc/init.d/redis start
/etc/init.d/redis stop
/etc/init.d/redis restart
注册开机自启(CentOS 6 使用 chkconfig;CentOS 7 亦兼容 SysV 脚本):
chkconfig --add redis
chkconfig redis on
chkconfig --list redis
多实例时复制脚本并修改路径即可:
cp /etc/init.d/redis /etc/init.d/redis-6380
vim /etc/init.d/redis-6380 # 修改端口、配置文件、pid 路径
chkconfig --add redis-6380
service redis-6380 start
设置密码后停止报错的处理
若设置了
requirepass,用 init 脚本停止时可能一直卡在:Waiting for Redis to shutdown ... Waiting for Redis to shutdown ... Waiting for Redis to shutdown ...原因是脚本里的
redis-cli shutdown没有带认证信息,命令被拒绝。解决方法:编辑/etc/init.d/redis,在 shutdown 命令中加上-a密码参数:$CLIEXEC -a "password" -p $REDISPORT shutdown
systemd unit 文件(CentOS 7 及以上,推荐)
创建 unit 文件:
vim /usr/lib/systemd/system/redis.service
完整内容:
[Unit]
Description=Redis In-Memory Data Store
Documentation=https://redis.io/documentation
After=network.target
[Service]
Type=forking
ExecStart=/usr/local/redis/src/redis-server /usr/local/redis/conf/redis.conf
ExecStop=/usr/local/redis/src/redis-cli -p 6379 shutdown
ExecReload=/bin/kill -s HUP $MAINPID
PIDFile=/usr/local/redis/run/redis_6379.pid
User=redis
Group=redis
Restart=always
RestartSec=3
LimitNOFILE=65535
TimeoutStopSec=30
[Install]
WantedBy=multi-user.target
Type 的选择:
- 配置文件中
daemonize yes→Type=forking,并配置PIDFile;- 配置文件中
daemonize no→Type=simple(更推荐,交给 systemd 托管,也可配Type=notify+supervised systemd)。
重载并管理服务:
systemctl daemon-reload
systemctl start redis # 启动
systemctl stop redis # 停止
systemctl restart redis # 重启
systemctl status redis # 查看状态
systemctl enable redis # 开机自启
systemctl disable redis # 取消开机自启
systemctl is-enabled redis # 查询自启状态
journalctl -u redis -f # 查看服务日志
多实例可使用模板 unit(redis@.service):
[Unit]
Description=Redis Instance %i
After=network.target
[Service]
Type=forking
ExecStart=/usr/local/redis/src/redis-server /usr/local/redis/conf/redis_%i.conf
ExecStop=/usr/local/redis/src/redis-cli -p %i shutdown
User=redis
Group=redis
[Install]
WantedBy=multi-user.target
systemctl start redis@6379
systemctl start redis@6380
systemctl enable redis@6379
CentOS 6 与 CentOS 7 服务管理命令对照:
| 操作 | CentOS 6(SysV) | CentOS 7(systemd) |
|---|---|---|
| 启动 | service redis start |
systemctl start redis |
| 停止 | service redis stop |
systemctl stop redis |
| 重启 | service redis restart |
systemctl restart redis |
| 状态 | service redis status |
systemctl status redis |
| 开机自启 | chkconfig redis on |
systemctl enable redis |
| 取消自启 | chkconfig redis off |
systemctl disable redis |
| 查看自启 | chkconfig --list redis |
systemctl is-enabled redis |
| 防火墙放行 | iptables -I INPUT -p tcp --dport 6379 -j ACCEPT |
firewall-cmd --permanent --add-port=6379/tcp; firewall-cmd --reload |
| 关闭防火墙 | service iptables stop |
systemctl stop firewalld |
2.5 redis-cli 客户端
连接本地
未配置环境变量:
cd /usr/local/redis/src
./redis-cli
配置环境变量后:
redis-cli
登录成功提示符:
127.0.0.1:6379>
有密码时的两种认证方式:
redis-cli # 先连接
127.0.0.1:6379> AUTH password # 连接后输入密码
OK
redis-cli -a password # 登录时直接携带密码
Redis 4.0 之后使用
-a会打印Warning: Using a password with '-a' ... may be insecure,因为密码会出现在进程列表和 history 中。Redis 5+ 可加--no-auth-warning抑制警告,或改用REDISCLI_AUTH环境变量:export REDISCLI_AUTH='test123' redis-cli
连接远程
redis-cli -h host -p port -a passwd
# 示例
redis-cli -h 3.3.3.8 -p 6379 -a 'test123'
常用连接参数:
| 参数 | 作用 |
|---|---|
-h <host> |
服务器地址,默认 127.0.0.1 |
-p <port> |
服务器端口,默认 6379 |
-a <password> |
认证密码 |
-n <db> |
连接后自动切换到指定数据库编号(0~15) |
-c |
以集群模式连接,自动跟随 MOVED / ASK 重定向 |
-s <socket> |
通过 Unix socket 连接 |
--raw |
原始输出,解决中文显示为 \xe4\xb8\xad 的问题 |
--no-raw |
强制转义输出 |
-r <count> |
重复执行命令 count 次 |
-i <delay> |
配合 -r,每次执行间隔 delay 秒 |
--scan |
使用 SCAN 遍历所有 key(安全替代 KEYS) |
--bigkeys |
扫描并报告每种类型最大的 key |
--hotkeys |
扫描热 key(需 maxmemory-policy 为 LFU) |
--memkeys |
按内存占用扫描 key(Redis 6+) |
--latency |
持续采样并输出延迟统计 |
--latency-history |
分时段输出延迟历史 |
--stat |
实时输出 key 数量、内存、连接数、QPS 等统计 |
--pipe |
以 pipeline 模式批量导入命令 |
--rdb <file> |
从服务器拉取一份 RDB 到本地(在线备份) |
--cluster <subcmd> |
集群管理子命令(Redis 5+) |
指定数据库编号连接:
redis-cli -h 3.3.3.8 -p 6379 -a 'test123' -n 3
执行单条命令(非交互模式)
在 shell 中直接把命令作为参数传给 redis-cli,执行完即退出,非常适合写脚本:
redis-cli PING
redis-cli SET name zhangsan
redis-cli GET name
redis-cli -h 3.3.3.8 -a 'test123' INFO replication
redis-cli DBSIZE
redis-cli CONFIG GET maxmemory
结合 shell 使用:
# 取值赋给变量
NAME=$(redis-cli --raw GET name)
echo $NAME
# 判断服务是否存活
if [ "$(redis-cli PING 2>/dev/null)" = "PONG" ]; then
echo "Redis is running"
else
echo "Redis is down"
fi
--raw 解决中文显示
默认情况下(非交互模式或旧版本),中文会以转义字节显示:
127.0.0.1:6379> SET city 北京
OK
127.0.0.1:6379> GET city
"\xe5\x8c\x97\xe4\xba\xac"
加上 --raw 即可正常显示:
redis-cli --raw
127.0.0.1:6379> GET city
北京
或直接单条执行:
redis-cli --raw GET city
批量执行
方式 1:从文件读取命令逐条执行
cat > /tmp/cmd.txt <<'EOF'
SET k1 v1
SET k2 v2
SET k3 v3
EOF
redis-cli < /tmp/cmd.txt
方式 2:使用 --pipe 高速导入(推荐用于大批量)
--pipe 使用 RESP 协议批量发送,比逐条执行快几十倍:
# 生成 10 万条 SET 命令
for i in $(seq 1 100000); do
echo "SET key:$i value:$i"
done > /tmp/mass.txt
cat /tmp/mass.txt | redis-cli --pipe
输出示例:
All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 100000
方式 3:管道传入单条命令流
echo -e "SET a 1\nSET b 2\nGET a" | redis-cli
方式 4:安全地批量删除匹配的 key(用 SCAN,不要用 KEYS)
redis-cli --scan --pattern 'session:*' | xargs -L 100 redis-cli UNLINK
PING 验证
redis-cli PING
# PONG 表示服务正常
redis-cli -h 3.3.3.8 -p 6379 -a 'test123' PING
交互模式下:
127.0.0.1:6379> PING
PONG
127.0.0.1:6379> PING hello
"hello"
若返回以下内容,说明配置了密码但未认证:
(error) NOAUTH Authentication required.
查看所有配置项,确认服务加载的是预期配置:
127.0.0.1:6379> CONFIG GET *
2.6 redis-benchmark 压力测试
redis-benchmark 是官方自带的压测工具,用于评估当前硬件与配置下 Redis 的吞吐能力。
常用参数:
| 参数 | 说明 |
|---|---|
-h <host> |
目标地址,默认 127.0.0.1 |
-p <port> |
目标端口,默认 6379 |
-a <password> |
认证密码 |
-s <socket> |
使用 Unix socket |
-c <clients> |
并发连接数,默认 50 |
-n <requests> |
总请求数,默认 100000 |
-d <size> |
SET/GET 的数据体大小(字节),默认 3 |
-t <tests> |
只测试指定命令,逗号分隔,如 -t set,get,incr |
-P <numreq> |
使用 pipeline,每次发送 numreq 条命令,默认 1 |
-r <keyspacelen> |
使用随机 key,key 空间大小为 keyspacelen |
-k <0/1> |
1 保持连接(keep alive,默认),0 每次重连 |
-q |
精简输出,只打印每秒请求数 |
--csv |
以 CSV 格式输出,便于导入表格 |
-l |
循环持续压测(loop) |
-e |
显示服务器返回的错误 |
--dbnum <db> |
指定测试使用的数据库编号 |
典型用法:
# 默认压测(50 并发,10 万请求,测试全部命令)
redis-benchmark
# 只测 set 与 get,100 并发,10 万请求,精简输出
redis-benchmark -t set,get -n 100000 -c 100 -q
# 远程压测并带密码
redis-benchmark -h 3.3.3.8 -p 6379 -a 'test123' -n 100000 -q
# 数据体 1KB,模拟真实业务负载
redis-benchmark -t set -n 100000 -d 1024 -q
# 使用 pipeline 批量 16 条,观察吞吐提升
redis-benchmark -t set,get -n 100000 -P 16 -q
# 使用随机 key,避免所有请求命中同一个 key
redis-benchmark -t set -n 100000 -r 100000 -q
# CSV 输出,方便做性能基线对比
redis-benchmark -t set,get,incr,lpush -n 100000 --csv
输出解读(-q 精简模式):
SET: 85763.29 requests per second
GET: 89285.71 requests per second
INCR: 87950.13 requests per second
LPUSH: 86730.09 requests per second
完整模式的输出解读:
====== SET ======
100000 requests completed in 1.17 seconds # 总请求数与总耗时
50 parallel clients # 并发客户端数
3 bytes payload # 数据体大小
keep alive: 1 # 复用连接
99.62% <= 1 milliseconds # 延迟分布:99.62% 请求在 1ms 内完成
99.95% <= 2 milliseconds
100.00% <= 2 milliseconds
85470.09 requests per second # QPS,核心指标
关注要点:
- requests per second(QPS):核心吞吐指标。本机 loopback 上单实例通常可达 8~12 万;跨网络会显著下降(受 RTT 影响)。
- 延迟分布:正常情况 99% 以上应在 1ms 内。若出现大量
<= 10 milliseconds甚至更高,说明存在阻塞(大 key、AOFalways刷盘、fork 卡顿、swap 等)。 - pipeline 的影响:
-P 16通常能把 QPS 提升到 50 万以上,因为节省了大量 RTT。这说明网络往返而非 Redis 本身常常是瓶颈。 - 压测注意:
redis-benchmark会往库里写入大量key:xxx测试数据,不要在生产库直接压测。测试完可用FLUSHDB清理(确认是测试库)。
搭配延迟诊断工具:
# 采样最大延迟(内建延迟监控)
redis-cli --latency
# min: 0, max: 1, avg: 0.11 (1329 samples)
# 分时段历史延迟
redis-cli --latency-history -i 5
# 系统级延迟(检测内核/硬件抖动)
redis-cli --intrinsic-latency 10
# 实时统计
redis-cli --stat
三、配置文件详解(redis.conf)
以下为入门阶段需重点关注的最小配置集,随后按功能分类逐项详解全部常用参数。
# 是否以守护进程启动(生产环境建议改为 yes)
daemonize no
# 守护进程模式下 pid 文件路径
pidfile /var/run/redis.pid
# 监听端口
port 6379
# 绑定的主机地址
bind 127.0.0.1
# 数据目录路径(RDB/AOF 文件均存放于此)
dir ./
# 客户端空闲多久后断开,0 表示不断开
timeout 300
# 日志输出方式,默认标准输出,可改为文件路径
logfile stdout
# RDB 快照触发条件:seconds 秒内至少 changes 次写入则落盘
save 900 1
save 300 10
save 60 10000
# slave 连接 master 时使用的密码
masterauth <master-password>
# 客户端连接 Redis 所需密码
requirepass foobared
# 是否开启 AOF 追加持久化(默认异步)
appendonly no
配置文件语法要点:一行一个指令,
指令名 参数;#起始为注释;含空格的值需用双引号包裹;单位不区分大小写,1k= 1000 字节,1kb= 1024 字节,1m= 1000000,1mb= 1024*1024。
3.1 网络配置(NETWORK)
| 参数名 | 默认值 | 说明 |
|---|---|---|
bind |
127.0.0.1 |
绑定的主机地址,可写多个 IP 以空格分隔(如 bind 127.0.0.1 192.168.1.220)。注释掉或写 0.0.0.0 则监听所有网卡——仅在有防火墙与密码保护时才可这样做 |
protected-mode |
yes |
保护模式(Redis 3.2+)。开启时,若既没有 bind 也没有 requirepass,则只允许本机 loopback 访问,远程连接会被拒绝。这是防止"未授权访问"漏洞的默认防线 |
port |
6379 |
Redis 监听端口。设为 0 表示不监听 TCP(仅用 Unix socket)。作者在博文中解释,6379 取自手机按键上 "MERZ" 对应的号码,而 MERZ 取自意大利歌女 Alessia Merz 的名字 |
tcp-backlog |
511 |
TCP 三次握手完成队列(accept queue)长度。高并发短连接场景需增大,同时必须同步调大内核参数 net.core.somaxconn(否则被内核截断)与 net.ipv4.tcp_max_syn_backlog |
timeout |
0(讲义示例为 300) |
客户端空闲多少秒后关闭连接,设为 0 表示关闭该功能(永不主动断开)。使用连接池的应用建议设 0,否则连接池里的空闲连接会被服务端悄悄断掉 |
tcp-keepalive |
300 |
每隔多少秒向客户端发送一次 TCP ACK 探测(SO_KEEPALIVE),用于及时发现半开连接并回收资源。设为 0 关闭。建议保持 300 |
unixsocket |
无 | 监听 Unix domain socket 的路径,如 /tmp/redis.sock。同机通信比 TCP 快约 30% |
unixsocketperm |
700 |
Unix socket 文件权限 |
bind 127.0.0.1 192.168.1.220
protected-mode yes
port 6379
tcp-backlog 511
timeout 300
tcp-keepalive 300
# unixsocket /tmp/redis.sock
# unixsocketperm 700
3.2 通用配置(GENERAL)
| 参数名 | 默认值 | 说明 |
|---|---|---|
daemonize |
no |
是否以守护进程方式运行。Redis 默认不是守护进程,生产环境建议设为 yes。若交给 systemd 用 Type=simple 托管,则应保持 no |
supervised |
no |
与进程管理器的交互方式:no 不交互;upstart 通过 SIGSTOP 通知 upstart;systemd 向 $NOTIFY_SOCKET 写 READY=1;auto 根据环境变量自动检测。配合 systemd Type=notify 使用时设为 systemd |
pidfile |
/var/run/redis_6379.pid(讲义示例 /var/run/redis.pid) |
当以守护进程方式运行时,pid 文件的写入路径。多实例必须各自区分 |
loglevel |
notice(讲义示例 verbose) |
日志记录级别,共支持四个级别:debug(信息最多,仅开发用)、verbose(较多,含不太有用的信息)、notice(适度详细,生产推荐)、warning(只记录非常重要的信息) |
logfile |
""(讲义示例 stdout) |
日志记录方式,默认为标准输出(空字符串即 stdout)。若以守护进程方式运行且此处仍为 stdout(或空),日志将被发送到 /dev/null,即彻底丢失。生产必须指定绝对路径 |
syslog-enabled |
no |
是否同时把日志写入系统 syslog |
syslog-ident |
redis |
写入 syslog 时的标识名 |
syslog-facility |
local0 |
syslog 设施等级 |
databases |
16 |
设置数据库数量,编号 0~15,默认使用数据库 0;可使用 SELECT <dbid> 在同一连接上切换数据库。注意:Redis Cluster 模式下只支持 db 0 |
always-show-logo |
yes |
启动时是否总是显示 ASCII 艺术字 Redis logo。守护进程模式或日志写文件时设为 no 可让日志更干净 |
daemonize yes
supervised no
pidfile /usr/local/redis/run/redis_6379.pid
loglevel notice
logfile "/usr/local/redis/log/redis_6379.log"
databases 16
always-show-logo no
3.3 快照持久化配置(SNAPSHOTTING / RDB)
| 参数名 | 默认值 | 说明 |
|---|---|---|
save <seconds> <changes> |
save 900 1save 300 10save 60 10000 |
在指定秒数内发生指定次数更新时,将数据同步到数据文件(触发 RDB 快照)。可配置多个条件,满足任意一条即触发:900 秒(15 分钟)内 1 次更改、300 秒(5 分钟)内 10 次更改、60 秒内 10000 次更改。写 save ""(空字符串)可完全关闭 RDB 自动快照 |
stop-writes-on-bgsave-error |
yes |
当后台 BGSAVE 失败时,是否停止接收写请求。设为 yes 是为了让运维尽快发现磁盘问题(此时客户端写入会报 MISCONF Redis is configured to save RDB snapshots...)。若已有完善监控且不希望业务因此中断,可设为 no |
rdbcompression |
yes |
存储至本地数据库时是否压缩数据。Redis 采用 LZF 压缩;关闭可节省 CPU,但会使数据库文件变大 |
rdbchecksum |
yes |
RDB 文件末尾是否写入 CRC64 校验和,用于检测文件损坏。开启后读写约有 10% 性能损失,设为 no 时校验和字段为 0,加载时跳过校验 |
dbfilename |
dump.rdb |
本地数据库(RDB)文件名 |
dir |
./ |
本地数据库存放目录(RDB 与 AOF 文件均存放于此)。必须是目录而非文件名;建议写绝对路径,避免因启动时的工作目录不同而找不到数据 |
rdb-del-sync-files |
no |
无持久化时,是否删除主从复制用的临时 RDB 文件(Redis 6+) |
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /usr/local/redis/data
3.4 主从复制配置(REPLICATION)
| 参数名 | 默认值 | 说明 |
|---|---|---|
replicaof <masterip> <masterport> |
无 | 当本机作为从库时,设置主库的 IP 与端口。Redis 启动时会自动从 master 进行数据同步。Redis 5.0 起的新名称 |
slaveof <masterip> <masterport> |
无 | 同上,Redis 5.0 之前的旧名称,5.0+ 仍兼容但已废弃。教学中两者都会见到,注意版本 |
masterauth <master-password> |
无 | 当 master 设置了密码保护(requirepass)时,slave 连接 master 使用的密码。主从两端密码通常配成一致,以便故障转移后角色互换仍可正常同步 |
masteruser <username> |
无 | 使用 ACL 时连接主库的用户名(Redis 6+) |
replica-serve-stale-data(旧名 slave-serve-stale-data) |
yes |
当从库与主库失联、或正在进行全量同步时,从库如何响应读请求:yes 仍返回(可能是过期数据,可用性优先);no 除少数命令外全部返回 SYNC with master in progress 错误(一致性优先) |
replica-read-only(旧名 slave-read-only) |
yes |
从库是否只读。强烈建议保持 yes,否则往从库写入的数据在下次全量同步时会被清除,且会造成主从数据不一致 |
repl-diskless-sync |
no |
全量同步时是否使用无盘复制:no 先把 RDB 落到磁盘再发送;yes 由子进程直接把 RDB 流写到从库 socket。磁盘慢、网络快时开启 yes 更优 |
repl-diskless-sync-delay |
5 |
无盘复制前等待的秒数,用于攒够多个从库一起同步(一次 fork 服务多个从库) |
repl-diskless-load |
disabled |
从库侧无盘加载(Redis 6+):disabled / on-empty-db / swapdb |
repl-ping-replica-period(旧名 repl-ping-slave-period) |
10 |
主库每隔多少秒向从库发送 PING,用于探活与传递复制偏移 |
repl-timeout |
60 |
复制超时时间(秒)。适用于:从库视角的批量数据传输超时、主从双向的 PING 超时。必须大于 repl-ping-replica-period,否则在低流量时会被误判为超时 |
repl-disable-tcp-nodelay |
no |
是否在复制 socket 上关闭 TCP_NODELAY。yes 会合并小包,省带宽但增加最多 40ms 延迟;跨机房可考虑 yes |
repl-backlog-size |
1mb |
复制积压缓冲区大小。这是一个环形缓冲区,保存主库最近的写命令。从库短暂断线重连后,若断线期间的数据仍在 backlog 中,就只需做部分重同步(psync)而无需全量同步。写入量大或网络不稳时应调大到 64mb~256mb |
repl-backlog-ttl |
3600 |
当没有任何从库连接时,主库保留 backlog 的秒数,超时后释放内存。设为 0 表示永不释放 |
replica-priority(旧名 slave-priority) |
100 |
从库优先级。哨兵故障转移时,数值越小越优先被选为新主库;设为 0 表示该从库永不参与选主(适合专用于备份或统计的从库) |
min-replicas-to-write |
0 |
主库至少需要有 N 个健康从库才接受写请求,0 为关闭 |
min-replicas-max-lag |
10 |
判定从库"健康"的最大延迟秒数,配合上一项使用 |
replica-announce-ip / replica-announce-port |
无 | 在 NAT / 端口映射 / 容器环境中,从库向主库和哨兵上报的对外 IP 与端口 |
# 从库配置示例
replicaof 192.168.1.220 6379
masterauth 123
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-ping-replica-period 10
repl-timeout 60
repl-disable-tcp-nodelay no
repl-backlog-size 64mb
repl-backlog-ttl 3600
replica-priority 100
旧版本(Redis 4.x 及之前)写法:
slaveof 3.3.3.8 6379
masterauth 123
slave-serve-stale-data no
slave-read-only yes
slave-priority 100
3.5 安全配置(SECURITY)
| 参数名 | 默认值 | 说明 |
|---|---|---|
requirepass |
无(默认关闭) | 设置 Redis 连接密码。配置后,客户端连接时需要通过 AUTH <password> 提供密码。因 Redis 性能极高,暴力破解每秒可尝试十几万次,密码必须足够长且随机(建议 32 位以上随机串) |
rename-command |
无 | 重命名或屏蔽危险命令。重命名为空字符串 "" 即彻底禁用该命令 |
aclfile |
无 | ACL 规则文件路径(Redis 6+ 的细粒度权限体系) |
user <name> ... |
无 | 直接在配置文件中定义 ACL 用户(Redis 6+) |
maxmemory-clients |
0 |
客户端输出缓冲区总内存上限(Redis 7+) |
requirepass "Xk9#mQ2vLp7$nRt4wZs8yBc1eHj5uAd0"
# 屏蔽/重命名危险命令
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command KEYS ""
rename-command CONFIG "CONFIG_a8f3d91c"
rename-command SHUTDOWN "SHUTDOWN_a8f3d91c"
rename-command DEBUG ""
rename-command EVAL ""
注意:
rename-command只能写在主配置文件中,不能写在被include的文件里,也不能通过CONFIG SET动态修改。- 主从环境中,主库与所有从库的
rename-command必须完全一致,否则复制会失败。- 重命名
CONFIG后,很多运维工具与哨兵可能无法正常工作,需评估影响。
Redis 6+ 的 ACL 用户示例:
# 默认用户仅允许本机、只读
user default on >StrongPass123 ~* +@read
# 应用账号:可读写 app: 前缀的 key,禁止管理类命令
user appuser on >AppPass456 ~app:* +@all -@admin -@dangerous
3.6 客户端与内存配置(CLIENTS / MEMORY MANAGEMENT)
| 参数名 | 默认值 | 说明 |
|---|---|---|
maxclients |
10000(讲义示例 128) |
同一时间最大客户端连接数。Redis 可打开的连接数受限于进程最大文件描述符数(ulimit -n),实际生效值 = min(maxclients, ulimit -n - 32)。设为 0 表示不作限制;达到上限后新连接将被立即关闭,并返回 max number of clients reached 错误 |
maxmemory <bytes> |
无(0,不限制) |
Redis 最大内存限制。Redis 启动时会将数据加载到内存中,达到上限后会按 maxmemory-policy 先尝试清除已到期或即将到期的 key;若策略为 noeviction 或仍无法释放足够空间,写命令将返回 OOM command not allowed,但仍可读取。(讲义补充:早期 Redis 的 VM 机制会将 key 存放在内存、value 存放在 swap 区,该机制已废弃) |
maxmemory-policy |
noeviction |
达到 maxmemory 后的淘汰策略,共八种,详见下表 |
maxmemory-samples |
5 |
LRU / LFU / TTL 淘汰采用近似算法,每次随机采样 N 个 key 再从中挑最合适的淘汰。5 已足够接近精确 LRU;10 更精确但更耗 CPU;3 更快但精度差 |
maxmemory-eviction-tenacity |
10 |
淘汰工作的"努力程度"(Redis 6.2+),值越大越激进 |
replica-ignore-maxmemory |
yes |
从库是否忽略 maxmemory(淘汰由主库驱动并通过复制流下发) |
八种淘汰策略(maxmemory-policy)逐项说明:
| 策略 | 含义 | 适用建议 |
|---|---|---|
noeviction |
默认值。不淘汰任何数据,内存满后所有写命令返回 OOM 错误,读命令正常 |
把 Redis 当数据库而非缓存用时选它,保证数据绝不丢失,但必须配好内存监控与告警 |
allkeys-lru |
在所有 key 中,淘汰最近最少使用(Least Recently Used)的 | 最常用的纯缓存策略。业务访问有明显冷热分布时首选 |
allkeys-lfu |
在所有 key 中,淘汰访问频率最低(Least Frequently Used)的(Redis 4.0+) | 存在"偶发被扫一次就被认为是热点"的场景时优于 LRU;如新闻缓存、商品详情缓存 |
allkeys-random |
在所有 key 中随机淘汰 | 访问分布非常均匀、没有冷热之分时使用;开销最低 |
volatile-lru |
只在设置了过期时间的 key 中,淘汰最近最少使用的 | 同一实例里既有"可丢的缓存"又有"不可丢的持久数据"时使用:给缓存设 TTL,持久数据不设 TTL 即永不被淘汰 |
volatile-lfu |
只在设置了过期时间的 key 中,淘汰访问频率最低的(Redis 4.0+) | 同上,且更看重访问频率 |
volatile-random |
只在设置了过期时间的 key 中随机淘汰 | 同上,开销最低 |
volatile-ttl |
只在设置了过期时间的 key 中,淘汰剩余存活时间最短(TTL 最小)的 | 希望"快要过期的先走",适合缓存数据本身带明确时效性的场景 |
选择建议:
- 纯缓存用途 →
allkeys-lru(保守稳妥)或allkeys-lfu(抗扫描污染更好)。- 缓存与持久数据混布 →
volatile-lru,且务必给缓存 key 都设置 TTL;若所有 key 都没有 TTL,volatile-* 策略等价于 noeviction,内存满后直接报 OOM,这是最常见的踩坑点。- 当数据库使用、不允许丢数据 →
noeviction+ 严格监控。maxmemory建议设为物理内存的 50%~70%:因为 RDB/AOF 重写时 fork 子进程可能额外占用大量内存(copy-on-write),留出余量可避免 OOM Killer 杀进程。
maxclients 10000
maxmemory 4gb
maxmemory-policy allkeys-lru
maxmemory-samples 5
3.7 AOF 追加模式配置(APPEND ONLY MODE)
| 参数名 | 默认值 | 说明 |
|---|---|---|
appendonly |
no |
是否在每次更新操作后记录日志(开启 AOF 持久化)。Redis 默认异步将数据写入磁盘(按 save 条件同步 RDB),若不开启 AOF,断电时可能丢失一段时间内的数据——期间部分数据仅存在于内存中 |
appendfilename |
"appendonly.aof" |
AOF 更新日志文件名(存放在 dir 指定的目录下) |
appenddirname |
"appendonlydir" |
AOF 多文件目录名(Redis 7+,7.0 起 AOF 拆分为 base + incr 多个文件) |
appendfsync |
everysec |
AOF 刷盘策略,三种取值详见下表 |
no-appendfsync-on-rewrite |
no |
在执行 BGSAVE 或 AOF 重写(子进程正在大量写磁盘)期间,是否暂停主进程的 fsync。设 yes 可避免磁盘 I/O 争抢导致的主进程阻塞(延迟毛刺),代价是这段时间内策略退化为 appendfsync no,极端情况可能丢失最多 30 秒数据。对延迟敏感的业务可设 yes |
auto-aof-rewrite-percentage |
100 |
当前 AOF 文件大小相比上次重写后的大小增长达到该百分比时,自动触发重写。100 表示体积翻倍时重写。设为 0 表示关闭自动重写 |
auto-aof-rewrite-min-size |
64mb |
触发自动重写的最小文件体积。避免 AOF 很小时(如 1MB → 2MB)频繁重写。两个条件必须同时满足才触发 |
aof-load-truncated |
yes |
启动加载 AOF 时,若发现文件末尾被截断(通常因断电导致最后一条命令写了一半):yes 加载已有的完整部分并打印日志继续启动(推荐);no 拒绝启动并报错,需人工用 redis-check-aof --fix 修复 |
aof-use-rdb-preamble |
yes |
混合持久化(Redis 4.0+)。开启后 AOF 重写生成的文件采用 "RDB 格式的全量前缀 + 后续增量 AOF 命令" 的复合结构,兼具 RDB 的加载速度与 AOF 的数据安全性。强烈建议开启 |
aof-rewrite-incremental-fsync |
yes |
重写时每积累 4MB 执行一次增量 fsync,避免一次性大量写盘造成延迟尖峰 |
aof-timestamp-enabled |
no |
在 AOF 中写入时间戳注解,支持按时间点恢复(Redis 7+) |
appendfsync 三种取值与取舍:
| 策略 | 行为 | 数据安全性 | 性能 |
|---|---|---|---|
no |
Redis 不主动调用 fsync() 刷盘,完全交由操作系统决定何时把缓冲区写入磁盘。多数 Linux 系统默认每 30 秒将缓冲区数据写入磁盘 |
最低,宕机最多丢 30 秒数据 | 最快 |
everysec |
Redis 默认级别。每秒由后台线程执行一次 fsync;若某次写入超过 1 秒,下一次写入会被延后。该机制在多数数据库中称为 "group commit"(批量提交) |
折中,宕机最多丢 1~2 秒数据 | 接近 no |
always |
每次写操作都立即调用 fsync() 同步到磁盘。数据安全性最高,但因写入次数频繁,对性能影响较大 |
最高,理论上不丢数据 | 最慢,QPS 可能只有几千 |
官方建议直接使用默认级别
everysec即可。它在安全性与性能之间取得了最佳平衡。只有金融级、绝对不能丢数据的场景才考虑always(并配合 SSD)。
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
aof-rewrite-incremental-fsync yes
手动触发 AOF 重写:
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started
3.8 慢日志配置(SLOW LOG)
| 参数名 | 默认值 | 说明 |
|---|---|---|
slowlog-log-slower-than |
10000 |
慢查询阈值,单位微秒。10000 微秒 = 10 毫秒,超过该执行时间的命令会被记入慢日志。注意统计的只是命令执行耗时,不含网络传输与排队等待时间。设为负数关闭慢日志;设为 0 记录所有命令(仅调试用)。生产建议调到 1000(1ms),以便更早发现问题 |
slowlog-max-len |
128 |
慢日志最多保留的条目数。这是一个先进先出队列,超出后丢弃最旧的记录。慢日志占用内存,但每条很小,可调到 1000 |
slowlog-log-slower-than 10000
slowlog-max-len 128
配套命令:
127.0.0.1:6379> SLOWLOG GET 10 # 获取最近 10 条慢查询
127.0.0.1:6379> SLOWLOG LEN # 当前慢日志条目数
127.0.0.1:6379> SLOWLOG RESET # 清空慢日志
3.9 延迟监控(LATENCY MONITOR)
| 参数名 | 默认值 | 说明 |
|---|---|---|
latency-monitor-threshold |
0 |
延迟监控阈值(毫秒),超过该值的事件被记录。0 表示关闭。排查抖动时设为 100 |
latency-monitor-threshold 100
127.0.0.1:6379> LATENCY LATEST # 最近的延迟事件
127.0.0.1:6379> LATENCY HISTORY command
127.0.0.1:6379> LATENCY RESET
127.0.0.1:6379> LATENCY DOCTOR # 自动诊断建议
3.10 键空间通知(KEYSPACE NOTIFICATIONS)
| 参数名 | 默认值 | 说明 |
|---|---|---|
notify-keyspace-events |
""(关闭) |
是否开启键空间事件通知。开启后,key 的变化会以 Pub/Sub 消息的形式发布到 __keyspace@<db>__:<key> 与 __keyevent@<db>__:<event> 频道。开启有性能开销,只订阅需要的事件类型 |
事件类型字符表:
| 字符 | 含义 |
|---|---|
K |
Keyspace 通知,频道前缀 __keyspace@<db>__ |
E |
Keyevent 通知,频道前缀 __keyevent@<db>__ |
g |
通用命令(DEL、EXPIRE、RENAME 等) |
$ |
String 命令 |
l |
List 命令 |
s |
Set 命令 |
h |
Hash 命令 |
z |
ZSet 命令 |
x |
过期事件(key 过期时发出) |
e |
驱逐事件(key 因 maxmemory 被淘汰时发出) |
t |
Stream 命令 |
m |
key miss 事件(Redis 6+) |
n |
新 key 事件(Redis 7+) |
A |
g$lshzxet 的别名,即除 m、n 外的全部事件 |
# 关闭(默认)
notify-keyspace-events ""
# 只关心过期事件(常用于"延迟任务"实现)
notify-keyspace-events "Ex"
# 全部事件
notify-keyspace-events "KEA"
订阅示例:
# 订阅 db 0 的所有过期事件
redis-cli PSUBSCRIBE '__keyevent@0__:expired'
重要提醒:过期事件通知不保证可靠投递(Pub/Sub 是 fire-and-forget,订阅者离线期间的消息会丢失),且事件在 key 被实际删除时才发出(惰性删除可能显著延后)。不要用它实现对可靠性要求高的延迟队列。
3.11 高级配置(ADVANCED CONFIG)
编码优化参数(内存压缩)
Redis 为小集合使用紧凑编码(ziplist / listpack / intset / embstr),在元素数量与元素长度都不超过阈值时用连续内存存储,极大节省内存;一旦超过任一阈值,自动转为标准编码(hashtable / skiplist / quicklist),且不会再转回去。
| 参数名 | 默认值 | 说明 |
|---|---|---|
hash-max-ziplist-entries |
128 |
Hash 字段数不超过该值时使用 ziplist 编码(Redis 7 改名 hash-max-listpack-entries) |
hash-max-ziplist-value |
64 |
Hash 中每个字段值长度不超过该字节数时使用 ziplist(Redis 7 改名 hash-max-listpack-value) |
list-max-ziplist-size |
128(新版为 -2) |
List 的 quicklist 中每个节点的大小。正数表示元素个数上限;负数表示按字节:-1=4KB、-2=8KB(默认,推荐)、-3=16KB、-4=32KB、-5=64KB |
list-compress-depth |
0 |
quicklist 两端不压缩的节点数。0 不压缩(默认);1 表示除首尾各 1 个节点外其余压缩 |
set-max-intset-entries |
512 |
Set 中全部为整数且元素数不超过该值时使用 intset 编码(有序整数数组,二分查找) |
set-max-listpack-entries |
128 |
Set 含非整数元素且元素数不超过该值时使用 listpack(Redis 7.2+) |
set-max-listpack-value |
64 |
同上,单个元素长度上限 |
zset-max-ziplist-entries |
128 |
ZSet 成员数不超过该值时使用 ziplist(Redis 7 改名 zset-max-listpack-entries) |
zset-max-ziplist-value |
64 |
ZSet 中每个成员长度不超过该字节数时使用 ziplist |
hll-sparse-max-bytes |
3000 |
HyperLogLog 稀疏编码的最大字节数,超过转为稠密编码。稀疏更省内存但 CPU 开销大 |
stream-node-max-entries |
100 |
Stream 每个 radix tree 节点最多容纳的条目数 |
stream-node-max-bytes |
4096 |
Stream 每个节点的最大字节数 |
activerehashing |
yes |
是否每 100ms 用 1ms CPU 主动做渐进式 rehash 以回收内存。对延迟极敏感的场景可设 no(代价是内存回收变慢) |
hash-max-ziplist-entries 128
hash-max-ziplist-value 64
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
activerehashing yes
调优思路:适度调大 entries 阈值(如 Hash 调到 512)能显著降低内存占用,但紧凑编码的查找是 O(n) 线性扫描,调得过大会让 CPU 升高。建议不超过 1000。可用
OBJECT ENCODING key查看当前编码。
客户端输出缓冲区
| 参数名 | 默认值 | 说明 |
|---|---|---|
client-output-buffer-limit normal |
0 0 0 |
普通客户端的输出缓冲区限制:<hard limit> <soft limit> <soft seconds>。0 表示不限制(普通客户端一般是同步读取,不需要限制) |
client-output-buffer-limit replica |
256mb 64mb 60 |
从库连接的缓冲区限制:硬限 256MB 立即断开;软限 64MB 持续 60 秒则断开。主从复制反复中断且日志出现 "client scheduled to be closed ASAP" 时应调大此项 |
client-output-buffer-limit pubsub |
32mb 8mb 60 |
发布订阅客户端的缓冲区限制。慢消费者会被断开 |
client-query-buffer-limit |
1gb |
单个客户端查询缓冲区上限 |
proto-max-bulk-len |
512mb |
单个 bulk 请求的最大长度(即 String 单值上限 512MB 的来源) |
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 512mb 128mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
lazyfree(惰性删除,Redis 4.0+)
删除一个包含百万元素的大 key 时,同步释放内存会阻塞主线程数百毫秒。lazyfree 把内存释放交给后台线程(BIO)异步完成。
| 参数名 | 默认值 | 说明 |
|---|---|---|
lazyfree-lazy-eviction |
no |
因 maxmemory 淘汰 key 时是否异步释放 |
lazyfree-lazy-expire |
no |
key 过期删除时是否异步释放 |
lazyfree-lazy-server-del |
no |
服务端隐式删除(如 SET 覆盖一个大 key、RENAME 覆盖目标)时是否异步释放 |
replica-lazy-flush |
no |
从库全量同步前清空旧数据集时是否异步释放(旧名 slave-lazy-flush) |
lazyfree-lazy-user-del |
no |
是否让 DEL 命令的行为等同于 UNLINK(Redis 6+) |
lazyfree-lazy-user-flush |
no |
FLUSHALL / FLUSHDB 默认是否异步(Redis 6+) |
# 生产环境建议全部开启,可显著降低延迟毛刺
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
replica-lazy-flush yes
lazyfree-lazy-user-del yes
lazyfree-lazy-user-flush yes
多线程 I/O(io-threads,Redis 6.0+)
Redis 6.0 引入多线程网络 I/O(读取请求 / 写回响应),但命令的执行仍然是单线程,因此不会破坏原子性语义。
| 参数名 | 默认值 | 说明 |
|---|---|---|
io-threads |
1 |
I/O 线程数(含主线程)。1 表示关闭多线程。建议设为 CPU 核数的一半左右,不要超过 8,4 核机器设 2~3,8 核设 6 |
io-threads-do-reads |
no |
是否也用多线程处理读(解析请求)。默认只多线程写回响应 |
io-threads 4
io-threads-do-reads yes
注意:
io-threads不能通过CONFIG SET动态修改,必须写入配置文件并重启。- 只有在网络带宽或 socket 读写成为瓶颈时才有明显收益(通常 QPS 已达 10 万级)。小流量场景开启反而略有损耗。
- 官方建议开启多线程 I/O 时把 Redis 进程绑核(
taskset)以避免 CPU 迁移开销。
其他高级参数
| 参数名 | 默认值 | 说明 |
|---|---|---|
hz |
10 |
后台任务(过期删除、客户端超时检测等)每秒执行的频率。范围 1~500。调高可让过期 key 更快被回收,但更耗 CPU |
dynamic-hz |
yes |
根据客户端数量自动调整 hz(Redis 5+) |
jemalloc-bg-thread |
yes |
是否启用 jemalloc 后台线程回收内存碎片 |
activedefrag |
no |
主动内存碎片整理(Redis 4.0+)。当 mem_fragmentation_ratio 长期大于 1.5 时可开启 |
active-defrag-ignore-bytes |
100mb |
碎片字节数超过该值才开始整理 |
active-defrag-threshold-lower |
10 |
碎片率超过该百分比才开始整理 |
active-defrag-threshold-upper |
100 |
碎片率达到该百分比时使用最大整理力度 |
active-defrag-cycle-min |
1 |
整理占用 CPU 的最小百分比 |
active-defrag-cycle-max |
25 |
整理占用 CPU 的最大百分比 |
lua-time-limit |
5000 |
Lua 脚本执行的最大毫秒数,超时后其他客户端会收到 BUSY 错误(但脚本不会被自动终止,需 SCRIPT KILL) |
busy-reply-threshold |
5000 |
lua-time-limit 在 Redis 7 中的新名称 |
hz 10
dynamic-hz yes
activedefrag no
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10
active-defrag-threshold-upper 100
active-defrag-cycle-min 1
active-defrag-cycle-max 25
lua-time-limit 5000
3.12 集群配置(CLUSTER)
| 参数名 | 默认值 | 说明 |
|---|---|---|
cluster-enabled |
no |
是否以集群模式启动该实例 |
cluster-config-file |
nodes.conf |
集群状态自动持久化文件,由 Redis 自行维护,不要手工编辑。多实例必须区分文件名 |
cluster-node-timeout |
15000 |
节点失联超过该毫秒数即被判定不可用,触发故障转移 |
cluster-replica-validity-factor(旧名 cluster-slave-validity-factor) |
10 |
从节点有效性因子。从节点与主节点断联时间超过 cluster-node-timeout * factor 时,不参与选主。设为 0 表示从节点无论断联多久都会尝试故障转移(可用性优先,可能丢更多数据) |
cluster-migration-barrier |
1 |
一个主节点至少要保留几个从节点,才允许多余的从节点被迁移给其他"裸奔"的主节点 |
cluster-require-full-coverage |
yes |
是否要求 16384 个槽全部被覆盖才提供服务。yes 时若有槽无人负责,整个集群拒绝服务(返回 CLUSTERDOWN);no 则只让未覆盖的槽失败,其他槽仍可用 |
cluster-allow-replica-migration |
yes |
是否允许从节点自动迁移(Redis 6+) |
cluster-replica-no-failover |
no |
设为 yes 时该从节点不会自动故障转移(用于跨机房场景) |
cluster-announce-ip / cluster-announce-port / cluster-announce-bus-port |
无 | Docker / NAT 环境下节点对外宣告的地址与端口 |
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 15000
cluster-replica-validity-factor 10
cluster-migration-barrier 1
cluster-require-full-coverage yes
3.13 虚拟内存配置(VM,已废弃)
虚拟内存(VM)机制在较新版本的 Redis 中已被废弃(Redis 2.6 起移除),此处按讲义原文保留,仅作参数参考与历史了解。现代方案是用
maxmemory+ 淘汰策略,或使用 SSD 型的 Redis 变种(如 SSDB、Pika)。
| 参数名 | 默认值 | 说明 |
|---|---|---|
vm-enabled |
no |
是否启用虚拟内存机制。VM 将数据分页存放,Redis 把访问量较少的"冷数据" swap 到磁盘,访问频繁的页面由磁盘换入内存 |
vm-swap-file |
/tmp/redis.swap |
虚拟内存文件路径,不可被多个 Redis 实例共享 |
vm-max-memory |
0 |
将大于该值的数据存入虚拟内存。无论设置多小,所有索引数据(即 keys)都保存在内存中;当设为 0 时,所有 value 都存在磁盘 |
vm-max-threads |
4 |
访问 swap 文件的线程数,最好不要超过机器核数;设为 0 时所有 swap 文件操作串行,可能造成较长延迟 |
vm-page-size |
32 |
每页大小(字节)。存储很多小对象时应设小,存大对象时设大 |
vm-pages |
134217728 |
swap 文件的总页数。页表占用内存约为 vm-pages / 8 字节 |
vm-enabled no
vm-swap-file /tmp/redis.swap
vm-max-memory 0
vm-max-threads 4
3.14 配置包含(INCLUDES)
| 参数名 | 默认值 | 说明 |
|---|---|---|
include /path/to/local.conf |
无 | 指定包含其他配置文件。可在同一主机的多个 Redis 实例之间共用一份配置文件,同时各实例又可拥有自己的特定配置 |
include /path/to/local.conf
多实例共用公共配置的典型组织方式:
# /usr/local/redis/conf/redis-6380.conf
include /usr/local/redis/conf/common.conf
# 实例私有配置(写在 include 之后才能覆盖公共值)
port 6380
pidfile /usr/local/redis/run/redis_6380.pid
logfile /usr/local/redis/log/redis_6380.log
dir /usr/local/redis/data/6380
dbfilename dump_6380.rdb
重要:
include的位置决定优先级——后出现的同名指令覆盖先出现的。若希望公共配置生效,把include放在文件开头;若希望公共配置作为兜底、私有配置优先,也是把include放开头再写私有项。此外rename-command与requirepass等不能写在被 include 的文件中(部分版本限制)。
3.15 运行时修改配置
Redis 支持在不重启的情况下查询与修改绝大多数配置项。
CONFIG GET —— 查询配置
127.0.0.1:6379> CONFIG GET maxmemory
1) "maxmemory"
2) "0"
# 支持 glob 通配
127.0.0.1:6379> CONFIG GET maxmemory*
1) "maxmemory"
2) "4294967296"
3) "maxmemory-policy"
4) "allkeys-lru"
5) "maxmemory-samples"
6) "5"
7) "maxmemory-clients"
8) "0"
# 查询全部配置项
127.0.0.1:6379> CONFIG GET *
# 常用查询
127.0.0.1:6379> CONFIG GET appendonly
127.0.0.1:6379> CONFIG GET save
127.0.0.1:6379> CONFIG GET requirepass
127.0.0.1:6379> CONFIG GET dir
127.0.0.1:6379> CONFIG GET slowlog-log-slower-than
CONFIG SET —— 动态修改配置
# 设置内存上限与淘汰策略(立即生效)
127.0.0.1:6379> CONFIG SET maxmemory 4gb
OK
127.0.0.1:6379> CONFIG SET maxmemory-policy allkeys-lru
OK
# 动态开启 AOF(会立刻触发一次 AOF 重写生成基础文件)
127.0.0.1:6379> CONFIG SET appendonly yes
OK
# 动态修改 RDB 触发条件
127.0.0.1:6379> CONFIG SET save "900 1 300 10 60 10000"
OK
# 关闭 RDB 自动快照
127.0.0.1:6379> CONFIG SET save ""
OK
# 动态设置密码(立即对新连接生效)
127.0.0.1:6379> CONFIG SET requirepass "NewStrongPass123"
OK
# 调整慢查询阈值为 1ms
127.0.0.1:6379> CONFIG SET slowlog-log-slower-than 1000
OK
# 提高最大连接数
127.0.0.1:6379> CONFIG SET maxclients 20000
OK
# 开启 lazyfree
127.0.0.1:6379> CONFIG SET lazyfree-lazy-expire yes
OK
Redis 7 起支持一次设置多项:
127.0.0.1:6379> CONFIG SET maxmemory 4gb maxmemory-policy allkeys-lru
OK
CONFIG REWRITE —— 把运行时配置写回文件
CONFIG SET 只改内存中的运行时值,重启即丢失。执行 CONFIG REWRITE 可将当前生效的全部配置写回启动时使用的那个配置文件,实现持久化:
127.0.0.1:6379> CONFIG REWRITE
OK
注意:
- 启动时必须显式指定了配置文件,否则报错
The server is running without a config file。- 重写会保留原文件的注释结构,但可能重排顺序、追加新段落。建议先备份配置文件。
- 标准操作流程:
CONFIG SET验证效果 → 确认无误后CONFIG REWRITE落盘(或手工同步修改配置文件),两步都做才能保证重启后仍生效。
不能动态修改的参数
以下参数只能改配置文件后重启:
| 参数 | 原因 |
|---|---|
daemonize |
进程模型在启动时已确定 |
databases |
数据库数组在启动时分配 |
io-threads / io-threads-do-reads |
线程池在启动时创建 |
cluster-enabled |
集群模式需重新初始化 |
rename-command |
命令表在启动时构建 |
include |
配置加载阶段处理 |
unixsocket |
监听套接字在启动时创建 |
port(部分版本) |
6.2 起可用 CONFIG SET port 动态改 |
其他运行时管理命令:
127.0.0.1:6379> CONFIG RESETSTAT # 重置 INFO 中的统计计数器
127.0.0.1:6379> COMMAND COUNT # 服务端支持的命令总数
127.0.0.1:6379> COMMAND INFO get # 查看某命令的元信息
127.0.0.1:6379> DEBUG SLEEP 1 # 让服务阻塞 1 秒(测试用,慎用)
127.0.0.1:6379> DEBUG OBJECT mykey # 查看 key 的内部信息(编码、序列化长度)
四、数据类型与命令详解
说明:以下命令语法中,方括号
[]表示可选参数,竖线|表示互斥选项。Redis 命令不区分大小写,本文统一大写以便区分命令与参数。
4.1 通用键(Key)命令
适用于所有数据类型的 key 操作。
| 序号 | 命令 | 说明 |
|---|---|---|
| 1 | DEL key [key ...] |
删除一个或多个 key,返回实际删除的个数 |
| 2 | UNLINK key [key ...] |
异步删除(Redis 4.0+),主线程只解除引用,实际内存回收交给后台线程 |
| 3 | EXISTS key [key ...] |
检查给定 key 是否存在。多个 key 时返回存在的个数 |
| 4 | EXPIRE key seconds |
为给定 key 设置过期时间,以秒计 |
| 5 | EXPIREAT key timestamp |
作用与 EXPIRE 类似,区别在于接受 UNIX 时间戳(秒)作为过期时间 |
| 6 | PEXPIRE key milliseconds |
设置 key 的过期时间,以毫秒计 |
| 7 | PEXPIREAT key milliseconds-timestamp |
设置 key 的过期时间戳,以毫秒计 |
| 8 | KEYS pattern |
查找所有符合给定模式 pattern 的 key。生产环境禁用 |
| 9 | SCAN cursor [MATCH pattern] [COUNT count] [TYPE type] |
基于游标增量遍历 key,生产环境的安全替代 |
| 10 | MOVE key db |
将当前数据库的 key 移动到给定的数据库 db |
| 11 | PERSIST key |
移除 key 的过期时间,key 将持久保持 |
| 12 | PTTL key |
以毫秒为单位返回 key 的剩余过期时间 |
| 13 | TTL key |
以秒为单位返回 key 的剩余生存时间(TTL, time to live) |
| 14 | RANDOMKEY |
从当前数据库中随机返回一个 key |
| 15 | RENAME key newkey |
修改 key 的名称;若 newkey 已存在则覆盖 |
| 16 | RENAMENX key newkey |
仅当 newkey 不存在时,将 key 改名为 newkey;已存在则返回 0 不修改 |
| 17 | TYPE key |
返回 key 所存储值的类型 |
| 18 | DBSIZE |
返回当前数据库的 key 总数 |
| 19 | SELECT index |
切换到编号为 index 的数据库(0~15) |
| 20 | SWAPDB index1 index2 |
交换两个数据库的数据(Redis 4.0+) |
| 21 | FLUSHDB [ASYNC\|SYNC] |
清空当前数据库的所有 key |
| 22 | FLUSHALL [ASYNC\|SYNC] |
清空所有数据库的所有 key |
| 23 | OBJECT ENCODING key |
查看 key 值的底层编码 |
| 24 | OBJECT REFCOUNT key |
查看引用计数(共享整数对象) |
| 25 | OBJECT IDLETIME key |
查看 key 的空闲秒数(LRU 时钟) |
| 26 | OBJECT FREQ key |
查看访问频率(需 LFU 策略) |
| 27 | DUMP key |
序列化给定 key,返回被序列化的值 |
| 28 | RESTORE key ttl serialized-value [REPLACE] |
反序列化,用于 key 迁移 |
| 29 | MIGRATE host port key dest-db timeout [COPY] [REPLACE] [KEYS key ...] |
原子性地把 key 迁移到另一个实例 |
| 30 | TOUCH key [key ...] |
更新 key 的最后访问时间(不返回值,用于影响 LRU) |
| 31 | COPY source destination [DB db] [REPLACE] |
复制 key(Redis 6.2+) |
KEYS 与生产禁用原因
127.0.0.1:6379> KEYS * # 列出所有 key
127.0.0.1:6379> KEYS user:* # 前缀匹配
127.0.0.1:6379> KEYS h?llo # ? 匹配单个字符:hello、hallo
127.0.0.1:6379> KEYS h[ae]llo # [] 匹配集合内字符:hello、hallo
127.0.0.1:6379> KEYS *name* # 包含匹配
为什么生产环境必须禁用 KEYS:
- 时间复杂度 O(N):N 是数据库中 key 的总数。100 万 key 的实例执行一次
KEYS *通常耗时数百毫秒到数秒。 - 单线程阻塞:Redis 命令串行执行,
KEYS执行期间所有其他客户端全部被阻塞,表现为业务整体卡顿甚至超时雪崩。 - 返回结果巨大:一次性把百万个 key 名塞进输出缓冲区,可能触发
client-output-buffer-limit甚至打爆内存。
正确做法:用 SCAN 替代,或直接 rename-command KEYS "" 从服务端禁用。
SCAN 游标遍历
SCAN 采用游标(cursor) 方式分批返回,每次只做常数级工作,不会长时间阻塞。
# 从游标 0 开始,每次返回约 100 个
127.0.0.1:6379> SCAN 0 COUNT 100
1) "17" # 下一次调用要用的游标;返回 "0" 表示遍历结束
2) 1) "key:1"
2) "key:2"
...
# 用返回的游标继续
127.0.0.1:6379> SCAN 17 COUNT 100
# 配合模式匹配
127.0.0.1:6379> SCAN 0 MATCH "session:*" COUNT 100
# 只遍历指定类型(Redis 6+)
127.0.0.1:6379> SCAN 0 TYPE hash COUNT 100
命令行下用 redis-cli --scan 自动完成整轮遍历:
redis-cli --scan
redis-cli --scan --pattern 'session:*'
# 统计匹配数量
redis-cli --scan --pattern 'user:*' | wc -l
# 安全批量删除
redis-cli --scan --pattern 'tmp:*' | xargs -L 100 redis-cli UNLINK
SCAN 的保证与限制:
- 保证:遍历开始前就存在、且遍历结束时仍存在的 key,一定会被返回至少一次。
- 不保证:可能重复返回同一个 key(客户端需自行去重);遍历期间新增或删除的 key 是否返回不确定。
COUNT只是提示值,不是精确条数;默认 10,建议设 100~1000。- 同族命令:
HSCAN(遍历 Hash)、SSCAN(遍历 Set)、ZSCAN(遍历 ZSet),语法一致。
存在性、类型与删除
127.0.0.1:6379> SET name zhangsan
OK
127.0.0.1:6379> EXISTS name
(integer) 1
127.0.0.1:6379> EXISTS nokey
(integer) 0
127.0.0.1:6379> EXISTS name name nokey
(integer) 2 # 重复计数
127.0.0.1:6379> TYPE name
string
127.0.0.1:6379> LPUSH mylist a
(integer) 1
127.0.0.1:6379> TYPE mylist
list
127.0.0.1:6379> TYPE nokey
none
127.0.0.1:6379> DEL name
(integer) 1
127.0.0.1:6379> DEL k1 k2 k3
(integer) 3
DEL 与 UNLINK 的区别:
| 命令 | 行为 | 适用 |
|---|---|---|
DEL |
同步删除,主线程直接释放内存。删除包含百万元素的大 key 时会阻塞主线程几百毫秒 | 小 key |
UNLINK |
异步删除(Redis 4.0+),主线程只从键空间摘除引用(O(1)),内存回收交给 BIO 后台线程 | 大 key 必须用它 |
127.0.0.1:6379> UNLINK bigkey
(integer) 1
可通过
lazyfree-lazy-user-del yes让DEL自动具备UNLINK的行为。
过期时间管理
127.0.0.1:6379> SET code 123456
OK
127.0.0.1:6379> EXPIRE code 60 # 60 秒后过期
(integer) 1
127.0.0.1:6379> TTL code
(integer) 57
127.0.0.1:6379> PTTL code # 毫秒精度
(integer) 54321
127.0.0.1:6379> PEXPIRE code 30000 # 30000 毫秒 = 30 秒
(integer) 1
# 按绝对时间戳过期
127.0.0.1:6379> EXPIREAT code 1735660800 # UNIX 时间戳(秒)
(integer) 1
127.0.0.1:6379> PEXPIREAT code 1735660800000 # UNIX 时间戳(毫秒)
(integer) 1
127.0.0.1:6379> PERSIST code # 移除过期时间,变为永久
(integer) 1
127.0.0.1:6379> TTL code
(integer) -1 # -1 表示存在但无过期时间
127.0.0.1:6379> TTL nokey
(integer) -2 # -2 表示 key 不存在
TTL 返回值含义:
| 返回值 | 含义 |
|---|---|
| 正整数 | 剩余存活秒数 |
-1 |
key 存在,但未设置过期时间 |
-2 |
key 不存在(或已过期被删除) |
关键陷阱:使用
SET key value重新赋值会清除原有的过期时间(变为永久 key),而INCR、APPEND、HSET、LPUSH等修改操作不会清除 TTL。要保持过期时间,请用SET key value KEEPTTL(Redis 6+)或重新EXPIRE。
Redis 的过期删除机制(两者结合):
- 惰性删除(lazy expiration):客户端访问某个 key 时才检查是否过期,过期则删除并返回空。优点是不浪费 CPU,缺点是过期但从不被访问的 key 会一直占内存。
- 定期删除(active expiration):后台每秒执行
hz次(默认 10 次)循环,每次从设置了过期时间的 key 中随机抽样 20 个,删掉其中已过期的;若过期比例超过 25% 则立即重复本轮,否则结束。这是一种概率算法,不保证及时。
重命名
127.0.0.1:6379> SET oldkey hello
OK
127.0.0.1:6379> RENAME oldkey newkey # 若 newkey 已存在则被覆盖
OK
127.0.0.1:6379> RENAME nokey abc
(error) ERR no such key # 源 key 不存在则报错
127.0.0.1:6379> SET a 1
OK
127.0.0.1:6379> SET b 2
OK
127.0.0.1:6379> RENAMENX a b
(integer) 0 # b 已存在,不执行
127.0.0.1:6379> RENAMENX a c
(integer) 1 # c 不存在,改名成功
随机 key 与库信息
127.0.0.1:6379> RANDOMKEY
"user:1024"
127.0.0.1:6379> DBSIZE
(integer) 138472
RANDOMKEY 常用于抽样检查数据形态,比 KEYS * 安全得多。
数据库切换与移动
127.0.0.1:6379> SELECT 0 # 默认库
OK
127.0.0.1:6379> SET k1 v1
OK
127.0.0.1:6379> MOVE k1 1 # 把 k1 移到 db 1
(integer) 1
127.0.0.1:6379> EXISTS k1
(integer) 0
127.0.0.1:6379> SELECT 1
OK
127.0.0.1:6379[1]> GET k1
"v1"
127.0.0.1:6379[1]> SWAPDB 0 1 # 整体交换两个库(Redis 4.0+)
OK
注意:多数据库(db 0~15)是一个不推荐使用的特性:
- 所有库共享同一个实例的内存与单线程,无法起到隔离作用;
FLUSHALL会清空全部库;- Redis Cluster 模式下只支持 db 0,用了多库后无法迁移到集群;
- 很多客户端与运维工具默认只处理 db 0。
正确的隔离方式是用 key 前缀命名空间(如
order:1001、user:1001)或部署多个实例。
FLUSHDB 与 FLUSHALL 的风险
127.0.0.1:6379> FLUSHDB # 清空当前数据库
OK
127.0.0.1:6379> FLUSHALL # 清空所有数据库(全部 16 个)
OK
127.0.0.1:6379> FLUSHALL ASYNC # 异步清空(Redis 4.0+),不阻塞主线程
OK
127.0.0.1:6379> FLUSHDB SYNC # 显式同步清空
OK
风险与防护:
- 不可撤销:执行后内存中的数据立即消失,并且会通过复制流传播到所有从库,从库数据同时被清空。
- 持久化文件会被覆盖:清空后若触发 RDB 快照或 AOF 重写,磁盘上的备份也会变成空的,届时连恢复的机会都没有。
- 生产环境的标准防护:
rename-command FLUSHALL ""
rename-command FLUSHDB ""
- 误操作后的紧急处理:立刻执行
CONFIG SET save ""与CONFIG SET appendonly no阻止持久化文件被覆盖,然后停止写入、从最近的 RDB/AOF 备份恢复。
4.2 String(字符串)
Redis 最基本的类型,一个 key 对应一个 value,单个 value 最大可存储 512 MB。String 是二进制安全的,可以存文本、JSON、序列化对象,甚至图片。
127.0.0.1:6379> SET name 'zhangsan'
OK
127.0.0.1:6379> GET name
"zhangsan"
全部常用命令
| 序号 | 命令 | 说明 |
|---|---|---|
| 1 | SET key value [EX seconds] [PX ms] [NX\|XX] [KEEPTTL] [GET] |
设置指定 key 的值,可带过期与条件选项 |
| 2 | GET key |
获取指定 key 的值 |
| 3 | GETRANGE key start end |
返回 key 中字符串值的子字符(闭区间,支持负索引) |
| 4 | GETSET key value |
将给定 key 的值设为 value,并返回 key 的旧值(old value) |
| 5 | GETDEL key |
取值并删除 key(Redis 6.2+) |
| 6 | GETEX key [EX sec\|PX ms\|EXAT ts\|PXAT ts\|PERSIST] |
取值并同时设置过期(Redis 6.2+) |
| 7 | GETBIT key offset |
对 key 所存储的字符串值,获取指定偏移量上的位(bit) |
| 8 | SETBIT key offset value |
对 key 所存储的字符串值,设置或清除指定偏移量上的位(bit) |
| 9 | MGET key1 [key2 ...] |
获取所有(一个或多个)给定 key 的值 |
| 10 | MSET key value [key value ...] |
同时设置一个或多个 key-value 对 |
| 11 | MSETNX key value [key value ...] |
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在(原子性:一个存在则全部不设置) |
| 12 | SETEX key seconds value |
将值 value 关联到 key,并将 key 的过期时间设为 seconds(秒) |
| 13 | PSETEX key milliseconds value |
与 SETEX 相似,但以毫秒为单位设置 key 的生存时间 |
| 14 | SETNX key value |
仅当 key 不存在时设置 key 的值(SET if Not eXists) |
| 15 | SETRANGE key offset value |
用 value 参数覆写 key 所存储的字符串值,从偏移量 offset 开始 |
| 16 | STRLEN key |
返回 key 所存储字符串值的长度 |
| 17 | INCR key |
将 key 中存储的数字值增一 |
| 18 | INCRBY key increment |
将 key 所存储的值加上给定的增量值(increment) |
| 19 | INCRBYFLOAT key increment |
将 key 所存储的值加上给定的浮点增量值(increment) |
| 20 | DECR key |
将 key 中存储的数字值减一 |
| 21 | DECRBY key decrement |
key 所存储的值减去给定的减量值(decrement) |
| 22 | APPEND key value |
如果 key 已存在并且是一个字符串,将 value 追加到该 key 原来值的末尾 |
SET 的选项详解
# 基础赋值
127.0.0.1:6379> SET user:1:name "zhangsan"
OK
# NX:只在 key 不存在时设置(Not eXists)——分布式锁的基础
127.0.0.1:6379> SET lock:order "client1" NX
OK
127.0.0.1:6379> SET lock:order "client2" NX
(nil) # 已存在,设置失败
# XX:只在 key 已存在时设置(eXists)——用于"仅更新不新建"
127.0.0.1:6379> SET user:999:name "lisi" XX
(nil)
127.0.0.1:6379> SET user:1:name "lisi" XX
OK
# EX:设置过期时间(秒)
127.0.0.1:6379> SET session:abc "data" EX 3600
OK
127.0.0.1:6379> TTL session:abc
(integer) 3600
# PX:设置过期时间(毫秒)
127.0.0.1:6379> SET token:xyz "data" PX 30000
OK
# EXAT / PXAT:按绝对时间戳过期(Redis 6.2+)
127.0.0.1:6379> SET k v EXAT 1735660800
OK
# KEEPTTL:保留原有过期时间(Redis 6.0+)
127.0.0.1:6379> SET session:abc "newdata" KEEPTTL
OK
127.0.0.1:6379> TTL session:abc
(integer) 3598 # TTL 未被清零
# GET:返回旧值(Redis 6.2+),等价于原子的 GETSET
127.0.0.1:6379> SET counter 100 GET
"99"
# 组合使用:分布式锁的标准写法(原子的"加锁+超时")
127.0.0.1:6379> SET lock:order:1001 "uuid-abc-123" NX EX 30
OK
重点:
SET key value NX EX 30是一条原子命令,同时完成"不存在才设置"与"设置超时"。早期用SETNX+EXPIRE两条命令实现锁存在严重缺陷:如果SETNX成功后客户端崩溃、EXPIRE未执行,这把锁将永远不会释放(死锁)。
取值与批量操作
127.0.0.1:6379> GET user:1:name
"lisi"
127.0.0.1:6379> GET nokey
(nil)
# 批量设置(原子,减少 RTT)
127.0.0.1:6379> MSET k1 v1 k2 v2 k3 v3
OK
# 批量获取
127.0.0.1:6379> MGET k1 k2 k3 nokey
1) "v1"
2) "v2"
3) "v3"
4) (nil)
# 全不存在才批量设置
127.0.0.1:6379> MSETNX n1 v1 n2 v2
(integer) 1
127.0.0.1:6379> MSETNX n2 vx n3 v3
(integer) 0 # n2 已存在,n3 也不会被设置
# 只在 key 不存在时设置
127.0.0.1:6379> SETNX unique:id "1001"
(integer) 1
127.0.0.1:6379> SETNX unique:id "1002"
(integer) 0
# 设置新值并返回旧值(原子替换,常用于"取出并重置计数器")
127.0.0.1:6379> SET counter 100
OK
127.0.0.1:6379> GETSET counter 0
"100"
127.0.0.1:6379> GET counter
"0"
# 带过期的设置
127.0.0.1:6379> SETEX verify:code 300 "889231"
OK
127.0.0.1:6379> TTL verify:code
(integer) 300
127.0.0.1:6379> PSETEX quick:key 5000 "value"
OK
字符串操作
127.0.0.1:6379> SET greeting "Hello"
OK
# 追加
127.0.0.1:6379> APPEND greeting " World"
(integer) 11 # 返回追加后的总长度
127.0.0.1:6379> GET greeting
"Hello World"
127.0.0.1:6379> APPEND newkey "abc" # key 不存在时相当于 SET
(integer) 3
# 长度
127.0.0.1:6379> STRLEN greeting
(integer) 11
# 截取子串(闭区间 [start, end],支持负索引,-1 为最后一个字符)
127.0.0.1:6379> GETRANGE greeting 0 4
"Hello"
127.0.0.1:6379> GETRANGE greeting 6 -1
"World"
127.0.0.1:6379> GETRANGE greeting 0 -1
"Hello World"
127.0.0.1:6379> GETRANGE greeting -5 -1
"World"
# 从指定偏移覆写
127.0.0.1:6379> SETRANGE greeting 6 "Redis"
(integer) 11
127.0.0.1:6379> GET greeting
"Hello Redis"
# 偏移量超过原长度时,中间用 \x00 填充
127.0.0.1:6379> SET pad "ab"
OK
127.0.0.1:6379> SETRANGE pad 5 "z"
(integer) 6
127.0.0.1:6379> GET pad
"ab\x00\x00\x00z"
数值操作(原子计数器)
127.0.0.1:6379> SET views 100
OK
127.0.0.1:6379> INCR views
(integer) 101
127.0.0.1:6379> INCR views
(integer) 102
127.0.0.1:6379> DECR views
(integer) 101
127.0.0.1:6379> INCRBY views 50
(integer) 151
127.0.0.1:6379> DECRBY views 51
(integer) 100
# 浮点数增减(用负数实现减法,没有 DECRBYFLOAT)
127.0.0.1:6379> SET price 19.99
OK
127.0.0.1:6379> INCRBYFLOAT price 5.01
"25"
127.0.0.1:6379> INCRBYFLOAT price -0.5
"24.5"
# key 不存在时,视为 0 再操作
127.0.0.1:6379> INCR brand-new
(integer) 1
# 非数字值会报错
127.0.0.1:6379> SET name "abc"
OK
127.0.0.1:6379> INCR name
(error) ERR value is not an integer or out of range
# 超出 64 位有符号整数范围会报错
127.0.0.1:6379> SET big 9223372036854775807
OK
127.0.0.1:6379> INCR big
(error) ERR increment or decrement would overflow
INCR/DECR系列是原子操作,天然线程安全,不需要加锁。这是实现浏览量、点赞数、限流计数、库存扣减的标准做法。注意INCRBYFLOAT使用 long double 计算,精度约 17 位有效数字,不适合做金额精确计算(金额应用整数分存储,用INCRBY)。
底层编码与适用场景
| 编码 | 触发条件 | 特点 |
|---|---|---|
int |
值是可用 long 表示的整数(且长度 ≤ 20) | 直接存 long,8 字节;0~9999 使用共享对象,零内存开销 |
embstr |
字符串长度 ≤ 44 字节 | RedisObject 与 SDS 连续分配,一次 malloc,缓存友好 |
raw |
字符串长度 > 44 字节,或对 embstr 执行过 APPEND/SETRANGE |
RedisObject 与 SDS 分开分配,两次 malloc;SDS 预留空间以支持追加 |
127.0.0.1:6379> SET n 12345
OK
127.0.0.1:6379> OBJECT ENCODING n
"int"
127.0.0.1:6379> SET s "hello"
OK
127.0.0.1:6379> OBJECT ENCODING s
"embstr"
127.0.0.1:6379> SET l "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
OK
127.0.0.1:6379> OBJECT ENCODING l
"raw"
适用场景:缓存对象的 JSON 序列化、Session、验证码、Token、分布式锁、计数器、限流窗口、Bitmap 位图(底层就是 String)。
注意:
embstr一旦被APPEND或SETRANGE修改就会永久变成raw,即使长度仍小于 44。频繁追加的 key 建议直接用 List 或分片。
4.3 Hash(哈希)
字段-值(field-value)映射集合,最常用于存储对象,一个 Hash 最多可存储 2³²-1 个键值对。
127.0.0.1:6379> HMSET myhash name1 'zhangsan' name2 'lisi'
OK
127.0.0.1:6379> HGET myhash name1
"zhangsan"
127.0.0.1:6379> HGET myhash name2
"lisi"
全部常用命令
| 序号 | 命令 | 说明 |
|---|---|---|
| 1 | HSET key field value [field value ...] |
将哈希表 key 中字段 field 的值设为 value(Redis 4.0+ 支持多对) |
| 2 | HSETNX key field value |
仅当字段 field 不存在时,设置哈希表字段的值 |
| 3 | HGET key field |
获取哈希表中指定字段的值 |
| 4 | HMSET key field1 value1 [field2 value2 ...] |
同时将多个 field-value(域-值)对设置到哈希表 key 中(Redis 4.0 起被 HSET 取代,已标记废弃但仍可用) |
| 5 | HMGET key field1 [field2 ...] |
获取所有给定字段的值 |
| 6 | HGETALL key |
获取哈希表中指定 key 的所有字段和值 |
| 7 | HKEYS key |
获取哈希表中所有字段名 |
| 8 | HVALS key |
获取哈希表中所有值 |
| 9 | HLEN key |
获取哈希表中字段的数量 |
| 10 | HDEL key field1 [field2 ...] |
删除一个或多个哈希表字段 |
| 11 | HEXISTS key field |
查看哈希表 key 中指定字段是否存在 |
| 12 | HINCRBY key field increment |
为哈希表 key 中指定字段的整数值加上增量 increment |
| 13 | HINCRBYFLOAT key field increment |
为哈希表 key 中指定字段的浮点数值加上增量 increment |
| 14 | HSCAN key cursor [MATCH pattern] [COUNT count] |
迭代哈希表中的键值对 |
| 15 | HSTRLEN key field |
返回字段值的字符串长度(Redis 3.2+) |
| 16 | HRANDFIELD key [count [WITHVALUES]] |
随机返回若干字段(Redis 6.2+) |
完整示例
# 设置单个/多个字段
127.0.0.1:6379> HSET user:1001 name "zhangsan"
(integer) 1
127.0.0.1:6379> HSET user:1001 age 25 city "Beijing" email "z@example.com"
(integer) 3
# 旧写法(等效)
127.0.0.1:6379> HMSET user:1002 name "lisi" age 30 city "Shanghai"
OK
# 仅当字段不存在时设置
127.0.0.1:6379> HSETNX user:1001 name "wangwu"
(integer) 0 # name 已存在,未修改
127.0.0.1:6379> HSETNX user:1001 phone "13800138000"
(integer) 1
# 取单个字段
127.0.0.1:6379> HGET user:1001 name
"zhangsan"
127.0.0.1:6379> HGET user:1001 nofield
(nil)
# 取多个字段
127.0.0.1:6379> HMGET user:1001 name age city
1) "zhangsan"
2) "25"
3) "Beijing"
# 取全部字段与值(field 与 value 交替返回)
127.0.0.1:6379> HGETALL user:1001
1) "name"
2) "zhangsan"
3) "age"
4) "25"
5) "city"
6) "Beijing"
7) "email"
8) "z@example.com"
9) "phone"
10) "13800138000"
# 只取字段名 / 只取值
127.0.0.1:6379> HKEYS user:1001
1) "name"
2) "age"
3) "city"
4) "email"
5) "phone"
127.0.0.1:6379> HVALS user:1001
1) "zhangsan"
2) "25"
3) "Beijing"
4) "z@example.com"
5) "13800138000"
# 字段数量
127.0.0.1:6379> HLEN user:1001
(integer) 5
# 字段是否存在
127.0.0.1:6379> HEXISTS user:1001 email
(integer) 1
127.0.0.1:6379> HEXISTS user:1001 address
(integer) 0
# 删除字段(删完所有字段后,key 自动被删除)
127.0.0.1:6379> HDEL user:1001 email phone
(integer) 2
# 字段值自增(原子)——常用于商品库存、多维计数
127.0.0.1:6379> HINCRBY user:1001 age 1
(integer) 26
127.0.0.1:6379> HINCRBY user:1001 age -6
(integer) 20
127.0.0.1:6379> HSET product:1 price 19.99
(integer) 1
127.0.0.1:6379> HINCRBYFLOAT product:1 price 5.01
"25"
# 字段值长度
127.0.0.1:6379> HSTRLEN user:1001 name
(integer) 8
# 增量遍历大 Hash(替代 HGETALL)
127.0.0.1:6379> HSCAN user:1001 0 COUNT 10
1) "0"
2) 1) "name"
2) "zhangsan"
3) "age"
4) "20"
5) "city"
6) "Beijing"
127.0.0.1:6379> HSCAN user:1001 0 MATCH "na*"
1) "0"
2) 1) "name"
2) "zhangsan"
# 随机字段
127.0.0.1:6379> HRANDFIELD user:1001 2 WITHVALUES
1) "city"
2) "Beijing"
3) "name"
4) "zhangsan"
底层编码与适用场景
| 编码 | 触发条件 | 特点 |
|---|---|---|
ziplist / listpack |
字段数 ≤ hash-max-ziplist-entries(128)且每个字段值长度 ≤ hash-max-ziplist-value(64) |
连续内存顺序存储,极省内存;查找为 O(n) 线性扫描 |
hashtable |
超过上述任一阈值 | 标准哈希表,O(1) 读写;每个字段有额外的 dictEntry 开销(约 50+ 字节) |
127.0.0.1:6379> HSET small f1 v1
(integer) 1
127.0.0.1:6379> OBJECT ENCODING small
"ziplist" # Redis 7 显示 "listpack"
适用场景:
- 存储对象:
user:1001存 name/age/city,比"多个 String key"节省大量内存(少了 key 本身的开销与 RedisObject 头),且可以只更新单个字段而不必读写整个 JSON。 - 购物车:key =
cart:userId,field = 商品 ID,value = 数量,用HINCRBY加减数量。 - 多维计数器:key =
stat:20260822,field = 各指标名。 - 配置项集合:一个 Hash 存一组相关配置。
注意:
- Hash 的字段不能单独设置过期时间(只能对整个 key 设 TTL),Redis 7.4 才引入
HEXPIRE。HGETALL对大 Hash 是危险操作(O(n) + 大量网络传输),字段数上千时请用HSCAN或HMGET精确取值。- Hash 无法直接分片到 Cluster 的多个节点上,单个 Hash 过大会造成数据倾斜。
4.4 List(列表)
字符串列表,按插入顺序排列,最多可存储 2³²-1 个元素。可从两端进行推入/弹出操作,本质是一个双端队列。
127.0.0.1:6379> LPUSH key_name values1 values2 values3
(integer) 3
127.0.0.1:6379> LRANGE key_name 0 0
1) "values1"
127.0.0.1:6379> LRANGE key_name 0 1
1) "values2"
2) "values1"
上例来自讲义原文,注意
LPUSH多个值时是逐个从左侧压入,因此最终顺序与参数顺序相反。
全部常用命令
| 序号 | 命令 | 说明 |
|---|---|---|
| 1 | LPUSH key value1 [value2 ...] |
将一个或多个值插入到列表头部(左端) |
| 2 | RPUSH key value1 [value2 ...] |
在列表尾部(右端)添加一个或多个值 |
| 3 | LPUSHX key value |
将一个值插入到已存在列表的头部;key 不存在则不操作 |
| 4 | RPUSHX key value |
为已存在的列表尾部添加值 |
| 5 | LPOP key [count] |
移出并获取列表的第一个元素 |
| 6 | RPOP key [count] |
移除并获取列表最后一个元素 |
| 7 | LRANGE key start stop |
获取列表指定范围内的元素(闭区间,支持负索引) |
| 8 | LLEN key |
获取列表长度 |
| 9 | LINDEX key index |
通过索引获取列表中的元素 |
| 10 | LSET key index value |
通过索引设置列表元素的值 |
| 11 | LINSERT key BEFORE\|AFTER pivot value |
在列表的元素(pivot)前或后插入元素 |
| 12 | LREM key count value |
移除列表元素。count > 0 从头往尾删 count 个;count < 0 从尾往头删 |count| 个;count = 0 删除全部匹配项 |
| 13 | LTRIM key start stop |
对列表进行修剪(trim),只保留指定区间内的元素,区间外的元素都将被删除 |
| 14 | RPOPLPUSH source destination |
移除列表最后一个元素,并将其添加到另一个列表(头部)后返回 |
| 15 | BLPOP key1 [key2 ...] timeout |
移出并获取列表的第一个元素;若列表没有元素,则阻塞直到等待超时或发现可弹出元素 |
| 16 | BRPOP key1 [key2 ...] timeout |
移出并获取列表的最后一个元素;若列表没有元素,则阻塞直到等待超时或发现可弹出元素 |
| 17 | BRPOPLPUSH source destination timeout |
从列表中弹出一个值,将其插入到另一个列表并返回;若列表没有元素则阻塞直到超时或发现可弹出元素 |
| 18 | LMOVE source destination LEFT\|RIGHT LEFT\|RIGHT |
通用版的元素移动(Redis 6.2+),取代 RPOPLPUSH |
| 19 | BLMOVE source destination LEFT\|RIGHT LEFT\|RIGHT timeout |
阻塞版的 LMOVE(Redis 6.2+) |
| 20 | LPOS key element [RANK rank] [COUNT count] [MAXLEN len] |
查找元素的索引位置(Redis 6.0.6+) |
完整示例
# 左侧压入(栈语义)
127.0.0.1:6379> LPUSH mylist a b c
(integer) 3
127.0.0.1:6379> LRANGE mylist 0 -1
1) "c"
2) "b"
3) "a"
# 右侧压入(队列语义)
127.0.0.1:6379> RPUSH queue task1 task2 task3
(integer) 3
127.0.0.1:6379> LRANGE queue 0 -1
1) "task1"
2) "task2"
3) "task3"
# 仅当列表已存在时压入
127.0.0.1:6379> LPUSHX nolist x
(integer) 0 # 列表不存在,不创建
127.0.0.1:6379> LPUSHX mylist d
(integer) 4
# 弹出
127.0.0.1:6379> LPOP mylist
"d"
127.0.0.1:6379> RPOP mylist
"a"
127.0.0.1:6379> LPOP mylist 2 # 一次弹出 2 个(Redis 6.2+)
1) "c"
2) "b"
127.0.0.1:6379> LPOP mylist
(nil) # 空列表返回 nil,key 自动删除
# 范围读取
127.0.0.1:6379> RPUSH nums 1 2 3 4 5 6 7 8 9 10
(integer) 10
127.0.0.1:6379> LRANGE nums 0 2
1) "1"
2) "2"
3) "3"
127.0.0.1:6379> LRANGE nums 0 -1 # 全部
127.0.0.1:6379> LRANGE nums -3 -1 # 最后 3 个
1) "8"
2) "9"
3) "10"
127.0.0.1:6379> LRANGE nums 5 100 # 超出范围自动截断,不报错
1) "6"
2) "7"
3) "8"
4) "9"
5) "10"
# 长度
127.0.0.1:6379> LLEN nums
(integer) 10
# 按索引取值 / 赋值
127.0.0.1:6379> LINDEX nums 0
"1"
127.0.0.1:6379> LINDEX nums -1
"10"
127.0.0.1:6379> LINDEX nums 999
(nil)
127.0.0.1:6379> LSET nums 0 "one"
OK
127.0.0.1:6379> LINDEX nums 0
"one"
127.0.0.1:6379> LSET nums 999 x
(error) ERR index out of range
# 在指定元素前后插入
127.0.0.1:6379> RPUSH fruits apple banana cherry
(integer) 3
127.0.0.1:6379> LINSERT fruits BEFORE banana "orange"
(integer) 4
127.0.0.1:6379> LRANGE fruits 0 -1
1) "apple"
2) "orange"
3) "banana"
4) "cherry"
127.0.0.1:6379> LINSERT fruits AFTER cherry "grape"
(integer) 5
127.0.0.1:6379> LINSERT fruits BEFORE nothere "x"
(integer) -1 # pivot 不存在返回 -1
# 删除匹配元素
127.0.0.1:6379> RPUSH letters a b a c a d
(integer) 6
127.0.0.1:6379> LREM letters 2 a # 从头开始删 2 个 "a"
(integer) 2
127.0.0.1:6379> LRANGE letters 0 -1
1) "b"
2) "c"
3) "a"
4) "d"
127.0.0.1:6379> RPUSH letters2 a b a c a
(integer) 5
127.0.0.1:6379> LREM letters2 -1 a # 从尾开始删 1 个 "a"
(integer) 1
127.0.0.1:6379> LREM letters2 0 a # 删除全部 "a"
(integer) 2
# 修剪:只保留区间内元素——实现"定长列表"的关键
127.0.0.1:6379> RPUSH logs l1 l2 l3 l4 l5 l6 l7
(integer) 7
127.0.0.1:6379> LTRIM logs 0 2
OK
127.0.0.1:6379> LRANGE logs 0 -1
1) "l1"
2) "l2"
3) "l3"
# 只保留最新 100 条(配合 LPUSH 使用)
127.0.0.1:6379> LPUSH recent:user:1 "item999"
(integer) 1
127.0.0.1:6379> LTRIM recent:user:1 0 99
OK
# 查找元素位置
127.0.0.1:6379> RPUSH arr a b c b a
(integer) 5
127.0.0.1:6379> LPOS arr b
(integer) 1
127.0.0.1:6379> LPOS arr b RANK 2 # 第 2 个匹配
(integer) 3
127.0.0.1:6379> LPOS arr b COUNT 0 # 所有匹配位置
1) (integer) 1
2) (integer) 3
RPOPLPUSH 与可靠队列
RPOPLPUSH 是原子的"从一个列表尾部取出并压入另一个列表头部",是实现可靠消息队列的核心。
127.0.0.1:6379> RPUSH task:pending t1 t2 t3
(integer) 3
# 消费者取任务:从 pending 取出,同时放入 processing(备份)
127.0.0.1:6379> RPOPLPUSH task:pending task:processing
"t3"
127.0.0.1:6379> LRANGE task:processing 0 -1
1) "t3"
# 任务处理成功后,从 processing 中移除
127.0.0.1:6379> LREM task:processing 1 "t3"
(integer) 1
# 环形列表:source 与 destination 相同,实现轮询
127.0.0.1:6379> RPUSH ring a b c
(integer) 3
127.0.0.1:6379> RPOPLPUSH ring ring
"c"
127.0.0.1:6379> LRANGE ring 0 -1
1) "c"
2) "a"
3) "b"
为什么可靠:普通
RPOP取出任务后,如果消费者在处理过程中崩溃,任务就永久丢失了。RPOPLPUSH把任务同时放进processing列表作为"在途备份",消费者崩溃后,监控程序可以扫描processing把超时任务重新放回pending,保证至少一次投递。
Redis 6.2+ 推荐用 LMOVE(语义更清晰,可指定两端方向):
127.0.0.1:6379> LMOVE task:pending task:processing RIGHT LEFT # 等价于 RPOPLPUSH
"t2"
127.0.0.1:6379> LMOVE src dst LEFT LEFT
127.0.0.1:6379> LMOVE src dst RIGHT RIGHT
BLPOP 与 BRPOP 阻塞弹出
阻塞版命令在列表为空时挂起连接等待,而不是立即返回 nil。这样消费者不需要写"轮询 + sleep"的死循环,既降低延迟又减少无效请求。
# 客户端 A:阻塞等待,最多 30 秒
127.0.0.1:6379> BLPOP task:queue 30
# 客户端 B:压入任务
127.0.0.1:6379> LPUSH task:queue "job-001"
(integer) 1
# 客户端 A 立即返回(列表名 + 元素值)
1) "task:queue"
2) "job-001"
(3.42s) # 显示实际等待时长
超时返回:
127.0.0.1:6379> BLPOP empty:queue 3
(nil)
(3.05s)
无限阻塞(timeout = 0,永久等待):
127.0.0.1:6379> BLPOP task:queue 0
监听多个列表(按参数顺序优先,第一个非空的列表被消费):
127.0.0.1:6379> BLPOP queue:high queue:normal queue:low 10
1) "queue:high" # 优先消费高优先级队列
2) "urgent-job"
阻塞版的移动命令:
127.0.0.1:6379> BRPOPLPUSH task:pending task:processing 0
127.0.0.1:6379> BLMOVE task:pending task:processing RIGHT LEFT 0 # Redis 6.2+
注意事项:
- 不要在事务(MULTI/EXEC)或 Lua 脚本中使用阻塞命令。事务内
BLPOP不会阻塞,空列表直接返回 nil;Lua 中会报错。- 阻塞的是该客户端连接,不是整个 Redis 服务,其他客户端不受影响。
- 多个客户端同时阻塞在同一列表上时,Redis 按先阻塞先服务(FIFO)的顺序唤醒,且每个元素只交给一个客户端。
- 客户端设置的 socket 超时必须大于
timeout,否则连接会先被客户端断开。timeout支持小数(Redis 6+),如0.5表示 500 毫秒。- 阻塞期间连接不能执行其他命令,因此连接池中要为阻塞消费预留独立连接。
底层编码与适用场景
| 版本 | 编码 | 说明 |
|---|---|---|
| Redis 3.0 及之前 | ziplist / linkedlist |
元素少且短用 ziplist,超阈值转双向链表 |
| Redis 3.2 ~ 6.x | quicklist |
统一为 quicklist:由多个 ziplist 节点串成的双向链表,兼顾内存与两端操作效率 |
| Redis 7.0+ | listpack / quicklist |
小列表用单个 listpack,大列表用 quicklist |
127.0.0.1:6379> RPUSH lst a b c
(integer) 3
127.0.0.1:6379> OBJECT ENCODING lst
"quicklist" # Redis 7 可能显示 "listpack"
性能特征:两端操作(LPUSH/RPUSH/LPOP/RPOP)是 O(1);按索引访问(LINDEX/LSET)与中间插入是 O(n),因为需要遍历节点链。因此不要把 List 当数组随机访问。
适用场景:
- 消息队列:
LPUSH生产 +BRPOP消费;RPOPLPUSH实现可靠队列。 - 最新列表:朋友圈时间线、最近浏览、最新评论,
LPUSH+LTRIM 0 99保持定长。 - 栈:
LPUSH+LPOP(后进先出)。 - 队列:
LPUSH+RPOP(先进先出)。 - 分页:
LRANGE key (page-1)*size page*size-1(注意深分页时 O(n) 性能下降)。
4.5 Set(集合,无序)
字符串的无序集合,底层通过哈希表实现,集合内元素不可重复,最多可存储 2³²-1 个成员。通过 SADD 创建。
127.0.0.1:6379> SADD set_name values1
(integer) 1
127.0.0.1:6379> SADD set_name values2
(integer) 1
127.0.0.1:6379> SADD set_name values2
(integer) 0 # 重复元素返回 0,被直接忽略
127.0.0.1:6379> SADD set_name values3
(integer) 1
127.0.0.1:6379> SMEMBERS set_name
1) "values2"
2) "values1"
3) "values3"
注意返回顺序与插入顺序无关——Set 是无序的。
全部常用命令
| 序号 | 命令 | 说明 |
|---|---|---|
| 1 | SADD key member1 [member2 ...] |
向集合添加一个或多个成员,返回实际新增的个数 |
| 2 | SCARD key |
获取集合的成员数(cardinality,基数) |
| 3 | SMEMBERS key |
返回集合中的所有成员。O(n),大集合慎用 |
| 4 | SISMEMBER key member |
判断 member 是否为集合 key 的成员 |
| 5 | SMISMEMBER key member [member ...] |
批量判断成员是否存在(Redis 6.2+) |
| 6 | SREM key member1 [member2 ...] |
移除集合中一个或多个成员 |
| 7 | SPOP key [count] |
移除并返回集合中的一个(或 count 个)随机元素 |
| 8 | SRANDMEMBER key [count] |
返回集合中一个或多个随机元素(不移除) |
| 9 | SMOVE source destination member |
将 member 元素从 source 集合原子性地移动到 destination 集合 |
| 10 | SDIFF key1 [key2 ...] |
返回给定所有集合的差集(以第一个集合为基准) |
| 11 | SDIFFSTORE destination key1 [key2 ...] |
返回给定所有集合的差集并存储在 destination 中 |
| 12 | SINTER key1 [key2 ...] |
返回给定所有集合的交集 |
| 13 | SINTERSTORE destination key1 [key2 ...] |
返回给定所有集合的交集并存储在 destination 中 |
| 14 | SINTERCARD numkeys key [key ...] [LIMIT limit] |
只返回交集的基数,不返回元素(Redis 7.0+) |
| 15 | SUNION key1 [key2 ...] |
返回所有给定集合的并集 |
| 16 | SUNIONSTORE destination key1 [key2 ...] |
将所有给定集合的并集存储到 destination 集合中 |
| 17 | SSCAN key cursor [MATCH pattern] [COUNT count] |
迭代集合中的元素 |
完整示例
# 添加成员
127.0.0.1:6379> SADD tags redis mysql nginx
(integer) 3
127.0.0.1:6379> SADD tags redis docker
(integer) 1 # redis 已存在,只新增了 docker
# 成员数
127.0.0.1:6379> SCARD tags
(integer) 4
# 所有成员
127.0.0.1:6379> SMEMBERS tags
1) "nginx"
2) "mysql"
3) "docker"
4) "redis"
# 判断成员是否存在(O(1))
127.0.0.1:6379> SISMEMBER tags redis
(integer) 1
127.0.0.1:6379> SISMEMBER tags java
(integer) 0
127.0.0.1:6379> SMISMEMBER tags redis java nginx
1) (integer) 1
2) (integer) 0
3) (integer) 1
# 移除成员
127.0.0.1:6379> SREM tags nginx docker
(integer) 2
# 随机弹出(移除)——常用于抽奖不重复
127.0.0.1:6379> SADD lottery u1 u2 u3 u4 u5
(integer) 5
127.0.0.1:6379> SPOP lottery
"u3"
127.0.0.1:6379> SPOP lottery 2
1) "u1"
2) "u5"
# 随机取(不移除)——常用于随机推荐
127.0.0.1:6379> SADD pool a b c d e
(integer) 5
127.0.0.1:6379> SRANDMEMBER pool
"c"
127.0.0.1:6379> SRANDMEMBER pool 3 # 正数:不重复取 3 个
1) "a"
2) "d"
3) "b"
127.0.0.1:6379> SRANDMEMBER pool -5 # 负数:可重复取 5 个(可能有重复)
1) "b"
2) "b"
3) "e"
4) "a"
5) "b"
# 原子移动成员
127.0.0.1:6379> SADD s1 a b c
(integer) 3
127.0.0.1:6379> SADD s2 x
(integer) 1
127.0.0.1:6379> SMOVE s1 s2 a
(integer) 1
127.0.0.1:6379> SMEMBERS s2
1) "x"
2) "a"
# 增量遍历
127.0.0.1:6379> SSCAN pool 0 COUNT 10
1) "0"
2) 1) "a"
2) "b"
3) "c"
4) "d"
5) "e"
127.0.0.1:6379> SSCAN pool 0 MATCH "a*"
集合运算与 STORE 变体
127.0.0.1:6379> SADD user:1:tags redis mysql nginx docker
(integer) 4
127.0.0.1:6379> SADD user:2:tags redis mongodb nginx k8s
(integer) 4
# 交集:共同兴趣(共同好友、共同标签)
127.0.0.1:6379> SINTER user:1:tags user:2:tags
1) "nginx"
2) "redis"
# 并集:全部标签去重
127.0.0.1:6379> SUNION user:1:tags user:2:tags
1) "mysql"
2) "mongodb"
3) "nginx"
4) "k8s"
5) "docker"
6) "redis"
# 差集:user1 有但 user2 没有(以第一个集合为基准)
127.0.0.1:6379> SDIFF user:1:tags user:2:tags
1) "mysql"
2) "docker"
127.0.0.1:6379> SDIFF user:2:tags user:1:tags
1) "mongodb"
2) "k8s"
# 三个以上集合:交集为全部共有
127.0.0.1:6379> SADD user:3:tags redis nginx java
(integer) 3
127.0.0.1:6379> SINTER user:1:tags user:2:tags user:3:tags
1) "nginx"
2) "redis"
STORE 变体把结果直接存到新 key,避免把大结果集传回客户端(节省网络与客户端内存),也便于结果复用与设置 TTL:
# 交集存储
127.0.0.1:6379> SINTERSTORE common:tags user:1:tags user:2:tags
(integer) 2 # 返回结果集的成员数
127.0.0.1:6379> SMEMBERS common:tags
1) "nginx"
2) "redis"
# 并集存储
127.0.0.1:6379> SUNIONSTORE all:tags user:1:tags user:2:tags user:3:tags
(integer) 7
# 差集存储
127.0.0.1:6379> SDIFFSTORE only:user1 user:1:tags user:2:tags
(integer) 2
# 结果集可以设置过期时间,作为"计算结果缓存"
127.0.0.1:6379> EXPIRE common:tags 300
(integer) 1
# 只要基数不要元素(Redis 7.0+),性能更好
127.0.0.1:6379> SINTERCARD 2 user:1:tags user:2:tags
(integer) 2
127.0.0.1:6379> SINTERCARD 2 user:1:tags user:2:tags LIMIT 1 # 达到 1 就提前返回
(integer) 1
重要:
SINTER/SUNION/SDIFF的时间复杂度是 O(N*M) 级别。对超大集合(十万级以上)做集合运算会长时间阻塞主线程,务必:
- 优先用
*STORE版本,并在从库上执行(如果允许);- 交集时把最小的集合放在第一个参数(Redis 会以最小集合为基准迭代);
- 或者改用
SINTERCARD只求数量;- Cluster 模式下多 key 命令要求所有 key 在同一槽位,需用 hash tag。
底层编码与适用场景
| 编码 | 触发条件 | 特点 |
|---|---|---|
intset |
所有元素都是整数且元素数 ≤ set-max-intset-entries(512) |
有序整数数组,二分查找 O(log n),最省内存 |
listpack |
含非整数元素且元素数 ≤ set-max-listpack-entries(128)且元素长度 ≤ 64(Redis 7.2+) |
连续内存,O(n) 查找 |
hashtable |
超过上述阈值 | 哈希表,所有 value 为 NULL,查找 O(1) |
127.0.0.1:6379> SADD nums 1 2 3
(integer) 3
127.0.0.1:6379> OBJECT ENCODING nums
"intset"
127.0.0.1:6379> SADD nums "abc"
(integer) 1
127.0.0.1:6379> OBJECT ENCODING nums
"listpack" # 旧版本直接变为 hashtable
适用场景:
- 去重:文章标签、UV 精确统计(用户 ID 集合)。
- 共同好友 / 共同关注:
SINTER。 - 可能认识的人:
SDIFF(我关注的人关注的人 减去 我已关注的)。 - 抽奖:
SPOP(不重复抽取)、SRANDMEMBER(可重复)。 - 黑白名单:
SISMEMBERO(1) 判定。 - 点赞用户列表:
SADD/SREM/SCARD。
4.6 ZSet(Sorted Set,有序集合)
与 Set 类似,但每个成员会关联一个 score(分数),Redis 据此对成员从小到大排序。同样不允许成员重复(但分数可以重复),通过 ZADD 创建。分数相同时按成员的字典序排列。
127.0.0.1:6379> ZADD zset_name 0 values1
(integer) 1
127.0.0.1:6379> ZADD zset_name 0 values2
(integer) 1
127.0.0.1:6379> ZADD zset_name 0 values2
(integer) 0 # 与 set 一样,无法插入重复成员
127.0.0.1:6379> ZADD zset_name 0 values3
(integer) 1
127.0.0.1:6379> ZRANGE zset_name 0 2
1) "values1"
2) "values2"
3) "values3"
ZRANGE key start stop:start为起始索引(0 表示第一个成员),stop为结束索引,用于控制返回多少条。
全部常用命令
| 序号 | 命令 | 说明 |
|---|---|---|
| 1 | ZADD key [NX\|XX] [GT\|LT] [CH] [INCR] score1 member1 [score2 member2 ...] |
向有序集合添加一个或多个成员,或更新已存在成员的分数 |
| 2 | ZCARD key |
获取有序集合的成员数 |
| 3 | ZCOUNT key min max |
计算有序集合中指定分数区间内的成员数 |
| 4 | ZSCORE key member |
返回有序集合中成员的分数值 |
| 5 | ZMSCORE key member [member ...] |
批量返回多个成员的分数(Redis 6.2+) |
| 6 | ZINCRBY key increment member |
对有序集合中指定成员的分数加上增量 increment |
| 7 | ZRANK key member |
返回有序集合中指定成员的索引(分数从小到大,0 开始) |
| 8 | ZREVRANK key member |
返回有序集合中指定成员的排名(成员按分数值从大到小排序) |
| 9 | ZRANGE key start stop [WITHSCORES] |
通过索引区间返回有序集合指定区间内的成员(升序) |
| 10 | ZREVRANGE key start stop [WITHSCORES] |
返回有序集合中指定区间内的成员(按索引,分数从高到低) |
| 11 | ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] |
通过分数返回有序集合指定区间内的成员 |
| 12 | ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count] |
返回有序集合中指定分数区间内的成员,分数从高到低排序 |
| 13 | ZRANGEBYLEX key min max [LIMIT offset count] |
通过字典区间返回有序集合的成员(要求所有成员分数相同) |
| 14 | ZREVRANGEBYLEX key max min [LIMIT offset count] |
逆序的字典区间查询 |
| 15 | ZLEXCOUNT key min max |
计算有序集合中指定字典区间内的成员数量 |
| 16 | ZREM key member [member ...] |
移除有序集合中的一个或多个成员 |
| 17 | ZREMRANGEBYRANK key start stop |
移除有序集合中给定排名区间内的所有成员 |
| 18 | ZREMRANGEBYSCORE key min max |
移除有序集合中给定分数区间内的所有成员 |
| 19 | ZREMRANGEBYLEX key min max |
移除有序集合中给定字典区间内的所有成员 |
| 20 | ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS w ...] [AGGREGATE SUM\|MIN\|MAX] |
计算一个或多个有序集的并集,并存储到新的 key 中 |
| 21 | ZINTERSTORE destination numkeys key [key ...] [WEIGHTS w ...] [AGGREGATE SUM\|MIN\|MAX] |
计算一个或多个有序集的交集,并将结果集存储到新的有序集合 destination 中 |
| 22 | ZDIFFSTORE destination numkeys key [key ...] |
计算差集并存储(Redis 6.2+) |
| 23 | ZUNION numkeys key [key ...] [WITHSCORES] |
直接返回并集,不存储(Redis 6.2+) |
| 24 | ZINTER numkeys key [key ...] [WITHSCORES] |
直接返回交集,不存储(Redis 6.2+) |
| 25 | ZDIFF numkeys key [key ...] [WITHSCORES] |
直接返回差集,不存储(Redis 6.2+) |
| 26 | ZINTERCARD numkeys key [key ...] [LIMIT limit] |
只返回交集基数(Redis 7.0+) |
| 27 | ZPOPMIN key [count] |
弹出分数最小的成员(Redis 5.0+) |
| 28 | ZPOPMAX key [count] |
弹出分数最大的成员(Redis 5.0+) |
| 29 | BZPOPMIN key [key ...] timeout |
阻塞版 ZPOPMIN(Redis 5.0+) |
| 30 | BZPOPMAX key [key ...] timeout |
阻塞版 ZPOPMAX(Redis 5.0+) |
| 31 | ZRANDMEMBER key [count [WITHSCORES]] |
随机返回成员(Redis 6.2+) |
| 32 | ZSCAN key cursor [MATCH pattern] [COUNT count] |
迭代有序集合中的元素(包括成员与分数) |
完整示例
# 添加成员与分数
127.0.0.1:6379> ZADD rank 100 "zhangsan" 85 "lisi" 92 "wangwu" 78 "zhaoliu"
(integer) 4
# ZADD 选项
127.0.0.1:6379> ZADD rank NX 999 "zhangsan" # NX:只新增不更新
(integer) 0
127.0.0.1:6379> ZADD rank XX 105 "zhangsan" # XX:只更新不新增
(integer) 0 # 返回 0 因为只是更新
127.0.0.1:6379> ZADD rank XX 105 "newuser" # 不存在,不新增
(integer) 0
127.0.0.1:6379> ZADD rank CH 110 "zhangsan" # CH:返回"被改变"的成员数
(integer) 1
127.0.0.1:6379> ZADD rank GT 100 "zhangsan" # GT:仅当新分数更大时更新(6.2+)
(integer) 0
127.0.0.1:6379> ZADD rank INCR 5 "lisi" # INCR:把分数当增量,返回新分数
"90"
# 成员数
127.0.0.1:6379> ZCARD rank
(integer) 4
# 查询分数
127.0.0.1:6379> ZSCORE rank "lisi"
"90"
127.0.0.1:6379> ZSCORE rank "nobody"
(nil)
127.0.0.1:6379> ZMSCORE rank "lisi" "wangwu" "nobody"
1) "90"
2) "92"
3) (nil)
# 分数自增(排行榜加分的标准做法)
127.0.0.1:6379> ZINCRBY rank 10 "zhaoliu"
"88"
127.0.0.1:6379> ZINCRBY rank -5 "zhaoliu" # 负数即减分
"83"
127.0.0.1:6379> ZINCRBY rank 1 "newuser" # 成员不存在则以 0 为基础创建
"1"
按索引范围查询
# 升序(分数从小到大)
127.0.0.1:6379> ZRANGE rank 0 -1
1) "newuser"
2) "lisi"
3) "wangwu"
4) "zhangsan"
5) "zhaoliu"
# 带分数一起返回
127.0.0.1:6379> ZRANGE rank 0 -1 WITHSCORES
1) "newuser"
2) "1"
3) "zhaoliu"
4) "83"
5) "lisi"
6) "90"
7) "wangwu"
8) "92"
9) "zhangsan"
10) "110"
# 降序(分数从大到小)——排行榜 Top N 的标准写法
127.0.0.1:6379> ZREVRANGE rank 0 2 WITHSCORES
1) "zhangsan"
2) "110"
3) "wangwu"
4) "92"
5) "lisi"
6) "90"
# 取第 1 名
127.0.0.1:6379> ZREVRANGE rank 0 0 WITHSCORES
1) "zhangsan"
2) "110"
# 分页:第 2 页,每页 10 条
127.0.0.1:6379> ZREVRANGE rank 10 19 WITHSCORES
按分数范围查询
# 闭区间 [85, 100]
127.0.0.1:6379> ZRANGEBYSCORE rank 85 100 WITHSCORES
1) "lisi"
2) "90"
3) "wangwu"
4) "92"
# 开区间:分数前加 ( 表示不包含
127.0.0.1:6379> ZRANGEBYSCORE rank (85 (100
1) "lisi"
2) "wangwu"
# 无穷区间
127.0.0.1:6379> ZRANGEBYSCORE rank -inf +inf WITHSCORES # 全部
127.0.0.1:6379> ZRANGEBYSCORE rank 90 +inf # 90 分及以上
127.0.0.1:6379> ZRANGEBYSCORE rank -inf 90 # 90 分及以下
# LIMIT 分页(语义同 SQL 的 LIMIT offset, count)
127.0.0.1:6379> ZRANGEBYSCORE rank -inf +inf WITHSCORES LIMIT 0 3
1) "newuser"
2) "1"
3) "zhaoliu"
4) "83"
5) "lisi"
6) "90"
# 降序(注意 max 在前,min 在后)
127.0.0.1:6379> ZREVRANGEBYSCORE rank 100 85 WITHSCORES
1) "wangwu"
2) "92"
3) "lisi"
4) "90"
127.0.0.1:6379> ZREVRANGEBYSCORE rank +inf -inf LIMIT 0 10 # Top 10
# 统计区间内成员数
127.0.0.1:6379> ZCOUNT rank 85 100
(integer) 2
127.0.0.1:6379> ZCOUNT rank -inf +inf
(integer) 5
排名查询
# 升序排名(分数最小的是第 0 名)
127.0.0.1:6379> ZRANK rank "lisi"
(integer) 2
127.0.0.1:6379> ZRANK rank "nobody"
(nil)
# 降序排名(分数最大的是第 0 名)——查"我是第几名"
127.0.0.1:6379> ZREVRANK rank "zhangsan"
(integer) 0
127.0.0.1:6379> ZREVRANK rank "lisi"
(integer) 2
# 展示为"第 N 名"需要 +1
# 名次 = ZREVRANK + 1
删除成员
# 按成员删除
127.0.0.1:6379> ZREM rank "newuser"
(integer) 1
127.0.0.1:6379> ZREM rank "lisi" "wangwu"
(integer) 2
# 按排名区间删除——只保留 Top 100 的经典做法
127.0.0.1:6379> ZADD board 1 a 2 b 3 c 4 d 5 e
(integer) 5
127.0.0.1:6379> ZREMRANGEBYRANK board 0 1 # 删除分数最低的 2 个
(integer) 2
127.0.0.1:6379> ZRANGE board 0 -1
1) "c"
2) "d"
3) "e"
# 只保留分数最高的 100 个(删除排名 0 到 -101 的成员)
127.0.0.1:6379> ZREMRANGEBYRANK board 0 -101
(integer) 0
# 按分数区间删除——滑动窗口限流清理过期时间戳的核心
127.0.0.1:6379> ZADD window 1000 r1 2000 r2 3000 r3
(integer) 3
127.0.0.1:6379> ZREMRANGEBYSCORE window -inf 2000
(integer) 2
127.0.0.1:6379> ZRANGE window 0 -1
1) "r3"
# 按字典区间删除
127.0.0.1:6379> ZADD lex 0 a 0 b 0 c 0 d
(integer) 4
127.0.0.1:6379> ZREMRANGEBYLEX lex [a [b
(integer) 2
弹出与随机取
127.0.0.1:6379> ZADD delay 1000 job1 2000 job2 3000 job3
(integer) 3
# 弹出分数最小的(延迟队列取最早到期的任务)
127.0.0.1:6379> ZPOPMIN delay
1) "job1"
2) "1000"
127.0.0.1:6379> ZPOPMAX delay 2
1) "job3"
2) "3000"
3) "job2"
4) "2000"
# 阻塞弹出
127.0.0.1:6379> BZPOPMIN delay 10
# 随机成员
127.0.0.1:6379> ZRANDMEMBER rank 2 WITHSCORES
字典区间查询(ZRANGEBYLEX)
要求集合内所有成员的分数完全相同(通常都设为 0),此时 ZSet 退化为按字典序排列的有序集合,可做前缀检索、自动补全。
127.0.0.1:6379> ZADD words 0 apple 0 banana 0 cherry 0 grape 0 orange
(integer) 5
# [ 表示包含,( 表示不包含,- 表示最小,+ 表示最大
127.0.0.1:6379> ZRANGEBYLEX words - + # 全部
1) "apple"
2) "banana"
3) "cherry"
4) "grape"
5) "orange"
127.0.0.1:6379> ZRANGEBYLEX words [b [g # b 到 g 之间
1) "banana"
2) "cherry"
127.0.0.1:6379> ZRANGEBYLEX words (banana + # banana 之后(不含)
1) "cherry"
2) "grape"
3) "orange"
# 前缀匹配(自动补全):以 "ba" 开头的词
127.0.0.1:6379> ZRANGEBYLEX words [ba [ba\xff
1) "banana"
127.0.0.1:6379> ZLEXCOUNT words - +
(integer) 5
127.0.0.1:6379> ZLEXCOUNT words [b [g
(integer) 2
127.0.0.1:6379> ZREVRANGEBYLEX words + -
集合运算(ZUNIONSTORE / ZINTERSTORE)
127.0.0.1:6379> ZADD week1 100 "zhangsan" 80 "lisi" 90 "wangwu"
(integer) 3
127.0.0.1:6379> ZADD week2 50 "zhangsan" 120 "lisi" 60 "zhaoliu"
(integer) 3
# 并集:分数默认相加(AGGREGATE SUM)——合并周榜为月榜
127.0.0.1:6379> ZUNIONSTORE month 2 week1 week2
(integer) 4
127.0.0.1:6379> ZREVRANGE month 0 -1 WITHSCORES
1) "lisi"
2) "200" # 80 + 120
3) "zhangsan"
4) "150" # 100 + 50
5) "wangwu"
6) "90"
7) "zhaoliu"
8) "60"
# WEIGHTS:为每个集合的分数设置权重(加权求和)
# 本周权重 1,上周权重 0.5(衰减)
127.0.0.1:6379> ZUNIONSTORE weighted 2 week2 week1 WEIGHTS 1 0.5
(integer) 4
127.0.0.1:6379> ZREVRANGE weighted 0 -1 WITHSCORES
1) "lisi"
2) "160" # 120*1 + 80*0.5
3) "zhangsan"
4) "100" # 50*1 + 100*0.5
5) "wangwu"
6) "45" # 0 + 90*0.5
7) "zhaoliu"
8) "60"
# AGGREGATE:SUM(默认)/ MIN / MAX
127.0.0.1:6379> ZUNIONSTORE maxboard 2 week1 week2 AGGREGATE MAX
(integer) 4
127.0.0.1:6379> ZUNIONSTORE minboard 2 week1 week2 AGGREGATE MIN
(integer) 4
# 交集:两周都上榜的人
127.0.0.1:6379> ZINTERSTORE both 2 week1 week2
(integer) 2
127.0.0.1:6379> ZRANGE both 0 -1 WITHSCORES
1) "zhangsan"
2) "150"
3) "lisi"
4) "200"
# 差集(Redis 6.2+)
127.0.0.1:6379> ZDIFFSTORE only1 2 week1 week2
(integer) 1
# 不存储直接返回(Redis 6.2+)
127.0.0.1:6379> ZUNION 2 week1 week2 WITHSCORES
127.0.0.1:6379> ZINTER 2 week1 week2 WITHSCORES
127.0.0.1:6379> ZDIFF 2 week1 week2 WITHSCORES
# 只要交集基数(Redis 7.0+)
127.0.0.1:6379> ZINTERCARD 2 week1 week2
(integer) 2
技巧:
ZUNIONSTORE也可以用来给 Set 加权计分——把 Set 当作"所有成员分数为 1 的 ZSet"参与运算,实现"多标签匹配度排序"。
增量遍历
127.0.0.1:6379> ZSCAN rank 0 COUNT 10
1) "0"
2) 1) "zhaoliu"
2) "83"
3) "zhangsan"
4) "110"
127.0.0.1:6379> ZSCAN rank 0 MATCH "zh*"
底层编码与适用场景
| 编码 | 触发条件 | 特点 |
|---|---|---|
ziplist / listpack |
成员数 ≤ zset-max-ziplist-entries(128)且每个成员长度 ≤ zset-max-ziplist-value(64) |
连续内存,member 与 score 交替存放并按 score 排序;查找 O(n) |
skiplist |
超过上述任一阈值 | 跳表 + 哈希表双结构:跳表支持 O(log n) 的范围查询与排名,哈希表支持 O(1) 的 ZSCORE 单点查分 |
127.0.0.1:6379> ZADD z 1 a
(integer) 1
127.0.0.1:6379> OBJECT ENCODING z
"ziplist" # Redis 7 显示 "listpack"
为什么用跳表而不是平衡树:跳表实现简单得多(无需旋转维护),范围查询天然高效(底层是有序链表),且内存开销可通过概率参数调节。Redis 作者认为在这三点上跳表优于红黑树/AVL。
注意:ZSet 的 score 是 IEEE 754 双精度浮点数(double),能精确表示的整数范围是 ±2⁵³。用毫秒时间戳(约 1.7×10¹²)作 score 是安全的,但用纳秒时间戳或超过 2⁵³ 的整数会丢失精度。
适用场景:
- 排行榜:游戏积分、热搜榜、销量榜。
ZINCRBY加分 +ZREVRANGE 0 9 WITHSCORES取 Top 10 +ZREVRANK查自己名次。 - 延迟队列 / 定时任务:score = 执行时间戳,
ZRANGEBYSCORE key -inf <now> LIMIT 0 10取出到期任务,或用ZPOPMIN。 - 滑动窗口限流:score = 请求时间戳,member = 请求唯一 ID。先
ZREMRANGEBYSCORE清掉窗口外的记录,再ZCARD判断是否超限,最后ZADD记录本次请求。 - 带权重的优先级队列:score = 优先级。
- 范围检索:按价格区间筛商品、按时间区间查记录(
ZRANGEBYSCORE)。 - 自动补全 / 前缀检索:所有 score 设为 0,用
ZRANGEBYLEX。
4.7 五种基本类型对比总结
| 类型 | 底层编码 | 有序 | 允许重复 | 典型场景 |
|---|---|---|---|---|
| String | int / embstr / raw | — | — | 缓存、计数器、分布式锁、Session、Bitmap |
| Hash | ziplist(listpack) / hashtable | 无序 | field 唯一 | 对象存储、购物车、多维计数 |
| List | quicklist / listpack | 按插入顺序 | 允许 | 消息队列、时间线、栈/队列 |
| Set | intset / listpack / hashtable | 无序 | 不允许 | 去重、共同好友、抽奖、黑白名单 |
| ZSet | ziplist(listpack) / skiplist | 按 score | 成员不允许,score 允许 | 排行榜、延迟队列、限流、范围检索 |
4.8 扩展数据类型
Bitmap(位图)
Bitmap 不是独立类型,底层就是 String,把字符串当作位数组操作。1 亿个用户的签到状态只需 12.5 MB。
| 命令 | 说明 |
|---|---|
SETBIT key offset value |
设置指定偏移位的值(0 或 1),返回原值 |
GETBIT key offset |
获取指定偏移位的值 |
BITCOUNT key [start end [BYTE\|BIT]] |
统计值为 1 的位数 |
BITPOS key bit [start [end]] |
查找第一个为指定值的位的位置 |
BITOP AND\|OR\|XOR\|NOT destkey key [key ...] |
对多个 Bitmap 做位运算并存入 destkey |
BITFIELD key [GET/SET/INCRBY ...] |
把 Bitmap 当作多个定长整数字段操作 |
# 用户 1001 在 2026 年第 1、5、100 天签到
127.0.0.1:6379> SETBIT sign:1001:2026 0 1
(integer) 0
127.0.0.1:6379> SETBIT sign:1001:2026 4 1
(integer) 0
127.0.0.1:6379> SETBIT sign:1001:2026 99 1
(integer) 0
# 查询第 5 天是否签到
127.0.0.1:6379> GETBIT sign:1001:2026 4
(integer) 1
127.0.0.1:6379> GETBIT sign:1001:2026 3
(integer) 0
# 统计全年签到总天数
127.0.0.1:6379> BITCOUNT sign:1001:2026
(integer) 3
# 统计指定字节范围(第 0 到第 1 个字节,即第 1~16 天)
127.0.0.1:6379> BITCOUNT sign:1001:2026 0 1
(integer) 2
# 查找第一个未签到的日期
127.0.0.1:6379> BITPOS sign:1001:2026 0
(integer) 1
# 位运算:统计连续两天都活跃的用户
127.0.0.1:6379> SETBIT active:20260821 100 1
(integer) 0
127.0.0.1:6379> SETBIT active:20260821 200 1
(integer) 0
127.0.0.1:6379> SETBIT active:20260822 100 1
(integer) 0
127.0.0.1:6379> BITOP AND both:days active:20260821 active:20260822
(integer) 26
127.0.0.1:6379> BITCOUNT both:days
(integer) 1 # 只有用户 100 连续两天活跃
# 任意一天活跃(月活)
127.0.0.1:6379> BITOP OR monthly:active active:20260821 active:20260822
(integer) 26
适用场景:签到打卡、用户在线状态、活跃用户统计(日活/月活/留存)、布隆过滤器的底层、特性开关(feature flag)。
注意:
SETBIT的 offset 上限是 2³²-1(512MB)。如果直接用用户 ID 当 offset,而 ID 是稀疏的大数(如 10 亿),会立即分配 128MB 内存。应该用连续的自增索引或先做哈希映射。
HyperLogLog(基数统计)
用于近似统计一个集合中不重复元素的数量(基数),标准误差约 0.81%,无论元素多少,每个 HLL 只占用固定的 12 KB 内存。
| 命令 | 说明 |
|---|---|
PFADD key element [element ...] |
添加元素。返回 1 表示内部结构有变化(基数可能增加),0 表示无变化 |
PFCOUNT key [key ...] |
返回基数的近似值。多个 key 时返回它们并集的基数 |
PFMERGE destkey sourcekey [sourcekey ...] |
合并多个 HLL 到 destkey |
127.0.0.1:6379> PFADD uv:20260822 "user1" "user2" "user3"
(integer) 1
127.0.0.1:6379> PFADD uv:20260822 "user1"
(integer) 0 # 已存在,结构未变
127.0.0.1:6379> PFCOUNT uv:20260822
(integer) 3
# 合并多天数据,得到周 UV
127.0.0.1:6379> PFADD uv:20260821 "user2" "user4" "user5"
(integer) 1
127.0.0.1:6379> PFMERGE uv:week uv:20260821 uv:20260822
OK
127.0.0.1:6379> PFCOUNT uv:week
(integer) 5
# 也可以不合并直接统计并集基数
127.0.0.1:6379> PFCOUNT uv:20260821 uv:20260822
(integer) 5
# 内存占用验证:100 万元素仍然只有 12KB
127.0.0.1:6379> DEBUG OBJECT uv:week
适用场景:网站/页面 UV 统计、搜索关键词的独立搜索数、大规模去重计数。
限制:
- 只能算数量,无法判断某个元素是否存在(不能替代 Set);
- 结果是近似值,误差 0.81%,不适合精确计费、精确对账;
- 元素数量很小(几百以内)时,HLL 使用稀疏编码,内存占用远小于 12KB,且此时误差极小甚至精确。
Set vs HyperLogLog 选择:需要精确数字或需要判断成员存在 → Set;只要大致 UV 且数据量巨大(百万级以上)→ HLL(内存可省 99%+)。
GEO(地理位置)
底层用 ZSet 实现,把经纬度通过 GeoHash 编码成一个 52 位整数作为 score。
| 命令 | 说明 |
|---|---|
GEOADD key [NX\|XX] [CH] longitude latitude member [...] |
添加地理位置(经度、纬度、名称) |
GEOPOS key member [member ...] |
返回成员的经纬度 |
GEODIST key member1 member2 [m\|km\|mi\|ft] |
计算两个成员之间的距离 |
GEORADIUS key longitude latitude radius m\|km\|mi\|ft [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT n [ANY]] [ASC\|DESC] [STORE key] [STOREDIST key] |
以给定经纬度为中心,查找半径内的成员 |
GEORADIUSBYMEMBER key member radius unit [...] |
以某个成员为中心查找半径内的成员 |
GEOSEARCH key FROMMEMBER m\|FROMLONLAT lon lat BYRADIUS r unit\|BYBOX w h unit [ASC\|DESC] [COUNT n] |
统一的检索命令(Redis 6.2+),支持矩形范围 |
GEOHASH key member [member ...] |
返回成员的标准 GeoHash 字符串 |
# 添加城市坐标(经度 纬度 名称)
127.0.0.1:6379> GEOADD cities 116.405285 39.904989 "beijing"
(integer) 1
127.0.0.1:6379> GEOADD cities 121.472644 31.231706 "shanghai" 113.280637 23.125178 "guangzhou" 114.085947 22.547 "shenzhen"
(integer) 3
# 查询坐标
127.0.0.1:6379> GEOPOS cities "beijing"
1) 1) "116.40528291463851929"
2) "39.90498886299871603"
# 计算距离
127.0.0.1:6379> GEODIST cities "beijing" "shanghai"
"1067597.9668" # 默认单位:米
127.0.0.1:6379> GEODIST cities "beijing" "shanghai" km
"1067.5980"
127.0.0.1:6379> GEODIST cities "guangzhou" "shenzhen" km
"104.6667"
# 以坐标为中心,查找 1500km 内的城市,带距离和坐标,按距离升序
127.0.0.1:6379> GEORADIUS cities 116.405285 39.904989 1500 km WITHDIST ASC
1) 1) "beijing"
2) "0.0002"
2) 1) "shanghai"
2) "1067.5980"
# 以某个成员为中心("附近的人/店"的标准做法)
127.0.0.1:6379> GEORADIUSBYMEMBER cities "guangzhou" 200 km WITHDIST WITHCOORD ASC COUNT 10
1) 1) "guangzhou"
2) "0.0000"
3) 1) "113.28063815832138062"
2) "23.12517743334439543"
2) 1) "shenzhen"
2) "104.6667"
3) 1) "114.08594459295272827"
2) "22.54699993773966327"
# Redis 6.2+ 统一检索,支持矩形范围
127.0.0.1:6379> GEOSEARCH cities FROMLONLAT 116.405285 39.904989 BYRADIUS 1500 km ASC WITHDIST
127.0.0.1:6379> GEOSEARCH cities FROMMEMBER "beijing" BYBOX 400 400 km ASC
# GeoHash 字符串(可用于外部系统对接)
127.0.0.1:6379> GEOHASH cities "beijing" "shanghai"
1) "wx4g0b7xrt0"
2) "wtw3sj5zbi0"
# 因为底层是 ZSet,可以直接用 ZSet 命令操作
127.0.0.1:6379> ZCARD cities
(integer) 4
127.0.0.1:6379> ZREM cities "shenzhen"
(integer) 1
适用场景:附近的人、附近的商家/门店、打车派单范围、地理围栏、外卖配送范围判断。
Stream(消息流,Redis 5.0+)
Stream 是 Redis 5.0 引入的专业消息队列类型,借鉴 Kafka 设计,是 List 与 Pub/Sub 的替代方案。核心特性:
- 消息持久化:消息存在 Stream 里,不会因消费者离线而丢失;
- 消费者组(Consumer Group):多个消费者分摊同一 Stream 的消息,实现负载均衡;
- ACK 确认机制:消息被消费后需显式
XACK,未确认的消息进入 PEL(Pending Entries List),可重新投递,实现"至少一次"投递; - 消息 ID 自增有序:格式
<毫秒时间戳>-<序号>,可按 ID 范围回溯历史消息。
核心命令简述:
| 命令 | 说明 |
|---|---|
XADD key ID field value [field value ...] |
追加消息,ID 用 * 表示自动生成 |
XLEN key |
消息条数 |
XRANGE key start end [COUNT n] |
按 ID 范围读取(- 最小、+ 最大) |
XREVRANGE key end start [COUNT n] |
逆序读取 |
XREAD [COUNT n] [BLOCK ms] STREAMS key ID |
独立消费者读取,$ 表示只读新消息 |
XGROUP CREATE key group ID [MKSTREAM] |
创建消费者组 |
XREADGROUP GROUP group consumer [COUNT n] [BLOCK ms] [NOACK] STREAMS key > |
以消费者组身份读取,> 表示未投递过的新消息 |
XACK key group ID [ID ...] |
确认消息已处理 |
XPENDING key group [start end count [consumer]] |
查看待确认消息 |
XCLAIM key group consumer min-idle-time ID |
抢占超时未确认的消息(故障转移) |
XAUTOCLAIM key group consumer min-idle-time start |
自动抢占(Redis 6.2+) |
XDEL key ID [ID ...] |
删除消息 |
XTRIM key MAXLEN [~] count \| MINID [~] id |
修剪 Stream 长度 |
XINFO STREAM\|GROUPS\|CONSUMERS key |
查看元信息 |
# 生产消息(* 自动生成 ID)
127.0.0.1:6379> XADD orders * orderId 1001 amount 99.9
"1756000000000-0"
127.0.0.1:6379> XADD orders * orderId 1002 amount 199.0
"1756000000001-0"
127.0.0.1:6379> XLEN orders
(integer) 2
# 读取全部消息
127.0.0.1:6379> XRANGE orders - +
1) 1) "1756000000000-0"
2) 1) "orderId"
2) "1001"
3) "amount"
4) "99.9"
2) 1) "1756000000001-0"
2) 1) "orderId"
2) "1002"
3) "amount"
4) "199.0"
# 创建消费者组,从头开始消费(0),MKSTREAM 表示 Stream 不存在时自动创建
127.0.0.1:6379> XGROUP CREATE orders order-group 0 MKSTREAM
OK
# 消费者 c1 读取新消息(> 表示未被投递过的)
127.0.0.1:6379> XREADGROUP GROUP order-group c1 COUNT 10 STREAMS orders >
# 处理完成后确认
127.0.0.1:6379> XACK orders order-group 1756000000000-0
(integer) 1
# 查看未确认的消息
127.0.0.1:6379> XPENDING orders order-group
# 阻塞消费(生产标准写法)
127.0.0.1:6379> XREADGROUP GROUP order-group c1 COUNT 10 BLOCK 5000 STREAMS orders >
# 限制 Stream 长度,防止无限增长(~ 表示近似,性能更好)
127.0.0.1:6379> XTRIM orders MAXLEN ~ 10000
(integer) 0
# 也可在 XADD 时自动修剪
127.0.0.1:6379> XADD orders MAXLEN ~ 10000 * orderId 1003 amount 50
Stream 与其他方案对比:
| 方案 | 持久化 | 消费者组 | ACK | 消息回溯 | 适用 |
|---|---|---|---|---|---|
| List + BRPOP | 是 | 否(一条消息只给一个消费者,但无组概念) | 否 | 否 | 简单任务队列 |
| Pub/Sub | 否 | 否(广播) | 否 | 否 | 实时通知,可丢 |
| Stream | 是 | 是 | 是 | 是 | 需要可靠投递的业务消息 |
| Kafka / RocketMQ | 是 | 是 | 是 | 是 | 超大吞吐、跨系统解耦 |
选择建议:日增消息量在千万级以内、不想引入独立中间件时,Stream 完全够用;超大吞吐、需要长期存储与多主题治理时,用 Kafka。
4.9 事务
Redis 事务把多条命令打包,在 EXEC 时按顺序、不被其他客户端命令插入地一次性执行。
| 命令 | 说明 |
|---|---|
MULTI |
开启事务,后续命令进入队列而非立即执行 |
EXEC |
执行事务队列中的所有命令,返回各命令结果的数组 |
DISCARD |
取消事务,清空队列 |
WATCH key [key ...] |
监视一个或多个 key,若在 EXEC 前被其他客户端改动,事务将失败 |
UNWATCH |
取消所有监视 |
基本用法
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET account:A 100
QUEUED # 命令进入队列,未执行
127.0.0.1:6379> SET account:B 200
QUEUED
127.0.0.1:6379> INCRBY account:A -50
QUEUED
127.0.0.1:6379> INCRBY account:B 50
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
3) (integer) 50
4) (integer) 250
取消事务:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> GET k1
(nil) # 命令未被执行
两类错误与无回滚特性
错误 1:命令入队时就报错(语法错误、命令不存在) → 整个事务被拒绝执行:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 v1
QUEUED
127.0.0.1:6379> NOSUCHCOMMAND k2 v2
(error) ERR unknown command 'NOSUCHCOMMAND'
127.0.0.1:6379> SET k3 v3
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> GET k1
(nil) # 全部未执行
错误 2:执行时才报错(类型不匹配等运行时错误) → 出错的命令失败,其余命令照常执行,已执行的不回滚:
127.0.0.1:6379> SET str "hello"
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET k1 "v1"
QUEUED
127.0.0.1:6379> LPUSH str "x" # 对 String 执行 List 命令,运行时才发现类型错误
QUEUED
127.0.0.1:6379> SET k2 "v2"
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK # 第 3 条依然执行成功
127.0.0.1:6379> GET k1
"v1" # 没有回滚
127.0.0.1:6379> GET k2
"v2"
Redis 事务不支持回滚。官方理由:Redis 命令只会因"编程错误"(用错类型、参数错)而失败,这类错误应该在开发阶段发现,而不是在生产环境靠回滚兜底;不支持回滚让 Redis 的实现保持简单与高速。
WATCH 与乐观锁
WATCH 实现 CAS(Check-And-Set)乐观锁:监视的 key 在 EXEC 之前被任何客户端修改,事务就会失败(返回 nil),由客户端决定是否重试。
# 客户端 A
127.0.0.1:6379> SET stock 10
OK
127.0.0.1:6379> WATCH stock
OK
127.0.0.1:6379> GET stock
"10"
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY stock 1
QUEUED
# ==== 此时客户端 B 执行: SET stock 5 ====
127.0.0.1:6379> EXEC
(nil) # 事务失败,因为 stock 被改过
未被干扰时正常成功:
127.0.0.1:6379> WATCH stock
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY stock 1
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 4
取消监视:
127.0.0.1:6379> UNWATCH
OK
伪代码形式的重试循环(库存扣减):
# 应用层逻辑
# while true:
# WATCH stock:1001
# current = GET stock:1001
# if current <= 0:
# UNWATCH
# return "sold out"
# MULTI
# DECR stock:1001
# result = EXEC
# if result != nil:
# return "success"
# # 否则被别人改了,重新循环重试
WATCH 注意事项:
WATCH必须在MULTI之前执行,事务内不能WATCH。EXEC执行后(无论成败)以及DISCARD之后,所有WATCH自动失效。- 只要被监视 key 的值被修改过就算冲突(即使改成了相同的值也可能触发,Redis 是按"是否被 touch"判断)。
- 高并发下重试率会很高(活锁风险),这种场景更推荐用 Lua 脚本一次性原子完成判断与修改。
与关系型数据库事务的区别
| 特性 | Redis 事务 | MySQL 事务(InnoDB) |
|---|---|---|
| 原子性(A) | 部分满足:命令批量提交、不被打断,但运行时错误不回滚 | 完全满足,出错自动回滚 |
| 一致性(C) | 弱:无约束检查、无外键 | 强:约束、外键、触发器 |
| 隔离性(I) | 满足:单线程串行执行,天然最高隔离级别,无并发问题 | 通过 MVCC + 锁实现四种隔离级别 |
| 持久性(D) | 取决于持久化配置:appendfsync always 才接近满足 |
redo log 保证,默认满足 |
| 回滚 | 不支持 | 支持 ROLLBACK |
| 保存点 | 不支持 | 支持 SAVEPOINT |
| 事务中读取 | 不能基于事务内前一条命令的结果做判断(命令未执行,拿不到返回值) | 可以 |
| 锁机制 | 乐观锁(WATCH) | 悲观锁(行锁、间隙锁)+ 乐观锁 |
最关键的差异:Redis 事务内无法读取中间结果做条件判断(因为命令都在
EXEC时才执行,QUEUED阶段拿不到值)。任何需要"先读后判断再写"的逻辑,都必须用 Lua 脚本,而不是 MULTI/EXEC。
Cluster 模式下的事务限制
集群模式下,事务中所有 key 必须位于同一个哈希槽,否则报错 CROSSSLOT Keys in request don't hash to the same slot。需要用 hash tag(见第五章)把相关 key 强制分配到同一槽。
4.10 发布订阅(Pub/Sub)
一种消息广播机制:发布者向频道(channel)发消息,所有订阅该频道的客户端都会收到。
| 命令 | 说明 |
|---|---|
SUBSCRIBE channel [channel ...] |
订阅一个或多个频道 |
UNSUBSCRIBE [channel ...] |
退订频道(不带参数则退订全部) |
PSUBSCRIBE pattern [pattern ...] |
按模式订阅(支持 *、?、[]) |
PUNSUBSCRIBE [pattern ...] |
退订模式 |
PUBLISH channel message |
向频道发布消息,返回收到消息的订阅者数量 |
PUBSUB CHANNELS [pattern] |
列出当前活跃的频道 |
PUBSUB NUMSUB [channel ...] |
查看频道的订阅者数量 |
PUBSUB NUMPAT |
查看模式订阅的数量 |
SSUBSCRIBE / SPUBLISH / SUNSUBSCRIBE |
分片订阅,Cluster 模式专用(Redis 7.0+) |
基本用法
终端 1(订阅者):
127.0.0.1:6379> SUBSCRIBE news
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "news"
3) (integer) 1
终端 2(发布者):
127.0.0.1:6379> PUBLISH news "hello world"
(integer) 1 # 1 个订阅者收到了
127.0.0.1:6379> PUBLISH news "second message"
(integer) 1
127.0.0.1:6379> PUBLISH nobody-listening "msg"
(integer) 0 # 无人订阅,消息直接丢弃
终端 1 收到:
1) "message"
2) "news"
3) "hello world"
1) "message"
2) "news"
3) "second message"
模式订阅
# 订阅所有 news. 开头的频道
127.0.0.1:6379> PSUBSCRIBE news.*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "news.*"
3) (integer) 1
发布到匹配的频道:
127.0.0.1:6379> PUBLISH news.sports "match result"
(integer) 1
127.0.0.1:6379> PUBLISH news.tech "new release"
(integer) 1
订阅者收到(注意消息类型是 pmessage,会多返回一个匹配的模式):
1) "pmessage"
2) "news.*"
3) "news.sports"
4) "match result"
多模式与通配符示例:
127.0.0.1:6379> PSUBSCRIBE "user.*" "order.*"
127.0.0.1:6379> PSUBSCRIBE "log.[ew]*" # log.error*、log.warn*
查看订阅状态
127.0.0.1:6379> PUBSUB CHANNELS
1) "news"
2) "news.sports"
127.0.0.1:6379> PUBSUB CHANNELS "news*"
1) "news"
2) "news.sports"
127.0.0.1:6379> PUBSUB NUMSUB news
1) "news"
2) (integer) 1
127.0.0.1:6379> PUBSUB NUMPAT
(integer) 2
命令行下的订阅:
redis-cli SUBSCRIBE news
redis-cli PSUBSCRIBE 'news.*'
# 监听 key 过期事件(需先开启 notify-keyspace-events Ex)
redis-cli PSUBSCRIBE '__keyevent@0__:expired'
作为消息队列的局限
Pub/Sub 是 fire-and-forget(发后即忘) 模型,作为业务消息队列有严重缺陷:
| 局限 | 说明 |
|---|---|
| 不持久化 | 消息不存储在任何地方,发布即转发。订阅者离线期间的消息全部丢失,重连后也拿不回来 |
| 无 ACK 确认 | 服务端不知道消费者是否处理成功,无法重投递,只能"最多一次" |
| 无消费者组 | 所有订阅者都收到全量消息(广播),无法多消费者分摊负载 |
| 慢消费者被断开 | 消费太慢会撑爆客户端输出缓冲区,触发 client-output-buffer-limit pubsub 后连接被直接断开 |
| 无消息回溯 | 无法重新读取历史消息 |
| 无背压机制 | 生产快于消费时消息在服务端缓冲区堆积,只能靠断开连接来"泄压" |
| Cluster 下全节点广播 | 普通 PUBLISH 会在集群所有节点间广播,浪费带宽(Redis 7 的 SPUBLISH 分片订阅解决了这点) |
| 订阅态限制 | 订阅模式下的连接只能执行订阅/退订/PING/QUIT 等少数命令,不能执行普通命令 |
适用场景(可容忍丢消息的实时通知):
- 配置变更推送(应用收到后重新拉取配置,丢了下次也会拉);
- 缓存失效通知(多节点本地缓存同步);
- 简单的聊天室 / 弹幕;
- 监控告警的实时推送;
- 键空间通知(
notify-keyspace-events)。
需要可靠投递时的替代方案:
- Redis Stream(首选,Redis 5.0+):持久化 + 消费者组 + ACK。
- List + BRPOP(简单场景):消息持久化,但无消费者组与 ACK。
- 专业 MQ(Kafka / RabbitMQ / RocketMQ):超大吞吐、复杂路由、跨系统解耦。
4.11 Lua 脚本
Lua 脚本是 Redis 提供的服务端脚本能力,让一组操作在服务端原子地执行。它是 Redis 事务的强力替代品。
| 命令 | 说明 |
|---|---|
EVAL script numkeys key [key ...] arg [arg ...] |
执行 Lua 脚本 |
EVALSHA sha1 numkeys key [key ...] arg [arg ...] |
按脚本的 SHA1 摘要执行(节省带宽) |
SCRIPT LOAD script |
把脚本加载到服务端脚本缓存,返回 SHA1 |
SCRIPT EXISTS sha1 [sha1 ...] |
检查脚本是否已缓存 |
SCRIPT FLUSH [ASYNC\|SYNC] |
清空脚本缓存 |
SCRIPT KILL |
终止正在运行的(只读)脚本 |
EVAL_RO / EVALSHA_RO |
只读版本,可在从库执行(Redis 7.0+) |
FUNCTION LOAD 等 |
Redis Functions,Lua 脚本的持久化升级版(Redis 7.0+) |
EVAL 基础
脚本中通过 KEYS[i] 访问 key 参数、ARGV[i] 访问其他参数(下标从 1 开始),通过 redis.call() 调用 Redis 命令。
# 最简单的例子
127.0.0.1:6379> EVAL "return 'Hello Lua'" 0
"Hello Lua"
# 使用 KEYS 与 ARGV
127.0.0.1:6379> EVAL "return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}" 2 k1 k2 v1 v2
1) "k1"
2) "k2"
3) "v1"
4) "v2"
# 调用 Redis 命令
127.0.0.1:6379> EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey myvalue
OK
127.0.0.1:6379> EVAL "return redis.call('GET', KEYS[1])" 1 mykey
"myvalue"
# 条件逻辑:只有值匹配才删除(Compare-And-Delete)
127.0.0.1:6379> EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 mykey myvalue
(integer) 1
# 循环批量设置
127.0.0.1:6379> EVAL "for i=1,tonumber(ARGV[1]) do redis.call('SET', KEYS[1]..':'..i, i) end return ARGV[1]" 1 batch 5
"5"
# 带条件的库存扣减(原子)
127.0.0.1:6379> SET stock:1001 10
OK
127.0.0.1:6379> EVAL "local s = tonumber(redis.call('GET', KEYS[1])) if s == nil then return -1 end if s >= tonumber(ARGV[1]) then return redis.call('DECRBY', KEYS[1], ARGV[1]) else return -2 end" 1 stock:1001 3
(integer) 7
127.0.0.1:6379> EVAL "local s = tonumber(redis.call('GET', KEYS[1])) if s >= tonumber(ARGV[1]) then return redis.call('DECRBY', KEYS[1], ARGV[1]) else return -2 end" 1 stock:1001 100
(integer) -2 # 库存不足
redis.call 与 redis.pcall 的区别:
redis.call():命令出错时直接终止脚本并把错误返回给客户端;redis.pcall():命令出错时返回一个 Lua error 表,脚本可以捕获并继续处理。
其他常用 Lua API:
# 日志
127.0.0.1:6379> EVAL "redis.log(redis.LOG_WARNING, 'script executed') return 1" 0
# JSON 编解码
127.0.0.1:6379> EVAL "return cjson.encode({1, 2, 'three'})" 0
"[1,2,\"three\"]"
127.0.0.1:6379> EVAL "local t = cjson.decode(ARGV[1]) return t['name']" 0 '{"name":"redis"}'
"redis"
# SHA1 与状态回复
127.0.0.1:6379> EVAL "return redis.sha1hex('')" 0
"da39a3ee5e6b4b0d3255bfef95601890afd80709"
127.0.0.1:6379> EVAL "return redis.status_reply('DONE')" 0
DONE
127.0.0.1:6379> EVAL "return redis.error_reply('my error')" 0
(error) my error
SCRIPT LOAD 与 EVALSHA
每次 EVAL 都要传输完整脚本文本,浪费带宽。用 SCRIPT LOAD 预加载后用 EVALSHA 调用,只需传 40 字符的 SHA1。
# 加载脚本,返回 SHA1
127.0.0.1:6379> SCRIPT LOAD "return redis.call('GET', KEYS[1])"
"4e6d8fc8bb01276962cce5371fa795a7763657ae"
# 用 SHA1 执行
127.0.0.1:6379> EVALSHA 4e6d8fc8bb01276962cce5371fa795a7763657ae 1 mykey
"myvalue"
# 检查脚本是否已缓存
127.0.0.1:6379> SCRIPT EXISTS 4e6d8fc8bb01276962cce5371fa795a7763657ae aaaa
1) (integer) 1
2) (integer) 0
# 清空脚本缓存
127.0.0.1:6379> SCRIPT FLUSH ASYNC
OK
从文件加载脚本:
cat > /tmp/incr_limit.lua <<'EOF'
local current = redis.call('INCR', KEYS[1])
if tonumber(current) == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[1])
end
if tonumber(current) > tonumber(ARGV[2]) then
return 0
end
return 1
EOF
# 直接执行脚本文件(注意 key 与 arg 用 , 分隔)
redis-cli --eval /tmp/incr_limit.lua rate:user:1001 , 60 10
# 预加载并记录 SHA1
SHA=$(redis-cli SCRIPT LOAD "$(cat /tmp/incr_limit.lua)")
echo $SHA
redis-cli EVALSHA $SHA 1 rate:user:1001 60 10
客户端标准做法:先尝试
EVALSHA,如果返回NOSCRIPT错误(脚本缓存被清空或实例重启),再回退到EVAL全量发送并重新缓存。主流客户端(Jedis、Lettuce、redis-py)都内置了这个逻辑。
原子性
Lua 脚本在 Redis 中整体作为一个命令执行:
- 脚本执行期间,不会有任何其他客户端的命令被插入执行(单线程串行);
- 脚本内的多条 Redis 命令要么全部按顺序执行完,要么(脚本中途报错时)已执行的部分不会回滚;
- 因此脚本可以安全地做"读取 → 判断 → 写入"这类必须原子完成的复合操作,这是 MULTI/EXEC 做不到的。
Lua 脚本 vs MULTI/EXEC:
| 能力 | MULTI/EXEC | Lua 脚本 |
|---|---|---|
| 原子执行 | 是 | 是 |
| 读取中间结果做条件判断 | 不能 | 能 |
| 循环与分支逻辑 | 不能 | 能 |
| 减少网络往返 | 是 | 是(更彻底) |
| 复用(服务端缓存) | 否 | 是(EVALSHA) |
| 回滚 | 不支持 | 不支持 |
脚本编写纪律:
- 必须把所有访问的 key 通过
KEYS传入,不要在脚本里硬编码或用ARGV拼 key 名。否则 Cluster 模式下无法正确路由,也无法通过预检查。- 脚本必须是纯函数:相同输入产生相同结果。不要用
os.time()、math.random()(不带种子)等非确定性函数,否则主从复制与 AOF 重放会不一致。需要时间就从ARGV传入。Redis 5+ 采用"复制执行结果"(effect replication)而非"复制脚本",缓解了这个问题,但仍应遵守。- 脚本要短:脚本执行期间整个 Redis 被阻塞。避免大循环、避免遍历大集合。超过
lua-time-limit(默认 5000ms)后其他客户端会收到BUSY错误。- 无限循环的脚本无法被
SCRIPT KILL终止(如果脚本已经执行了写操作),只能SHUTDOWN NOSAVE。务必在测试环境验证。
# 脚本卡死时的处理
127.0.0.1:6379> SCRIPT KILL # 只能杀掉尚未执行写操作的脚本
OK
# 若已有写操作,只能:
127.0.0.1:6379> SHUTDOWN NOSAVE # 最后手段,会丢失未持久化的数据
分布式锁的正确实现
错误实现 1:SETNX + EXPIRE 两步
127.0.0.1:6379> SETNX lock:order "client1"
(integer) 1
# 如果客户端在这里崩溃,EXPIRE 没执行 → 锁永远不释放(死锁)
127.0.0.1:6379> EXPIRE lock:order 30
错误实现 2:value 不带唯一标识,直接 DEL 解锁
# 客户端 A 加锁,超时 30 秒
127.0.0.1:6379> SET lock:order "1" NX EX 30
OK
# A 执行业务耗时 35 秒,锁已自动过期
# 客户端 B 此时成功加锁
# A 执行完毕,执行 DEL → 删掉的是 B 的锁!
127.0.0.1:6379> DEL lock:order
正确实现:加锁用 SET NX EX + 唯一 value,解锁用 Lua 校验后删除
步骤 1:加锁(原子的"不存在才设置 + 设置超时")
# value 使用全局唯一标识(UUID + 线程 ID),用于证明"这把锁是我的"
127.0.0.1:6379> SET lock:order:1001 "uuid-a3f9c2-thread-7" NX EX 30
OK # 加锁成功
127.0.0.1:6379> SET lock:order:1001 "uuid-other" NX EX 30
(nil) # 锁被占用,加锁失败
步骤 2:解锁(Lua 脚本:校验持有者 + 删除,两步原子完成)
127.0.0.1:6379> EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock:order:1001 "uuid-a3f9c2-thread-7"
(integer) 1 # 是我的锁,删除成功
127.0.0.1:6379> EVAL "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end" 1 lock:order:1001 "uuid-wrong"
(integer) 0 # 不是我的锁,不删除
预加载解锁脚本:
cat > /tmp/unlock.lua <<'EOF'
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
EOF
UNLOCK_SHA=$(redis-cli SCRIPT LOAD "$(cat /tmp/unlock.lua)")
redis-cli EVALSHA $UNLOCK_SHA 1 lock:order:1001 "uuid-a3f9c2-thread-7"
步骤 3:可重入锁(用 Hash 记录持有者与重入次数)
cat > /tmp/lock_reentrant.lua <<'EOF'
-- KEYS[1]=锁名, ARGV[1]=持有者标识, ARGV[2]=超时秒数
if redis.call('EXISTS', KEYS[1]) == 0 then
redis.call('HSET', KEYS[1], ARGV[1], 1)
redis.call('EXPIRE', KEYS[1], ARGV[2])
return 1
end
if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 1 then
redis.call('HINCRBY', KEYS[1], ARGV[1], 1)
redis.call('EXPIRE', KEYS[1], ARGV[2])
return 1
end
return 0
EOF
cat > /tmp/unlock_reentrant.lua <<'EOF'
-- KEYS[1]=锁名, ARGV[1]=持有者标识
if redis.call('HEXISTS', KEYS[1], ARGV[1]) == 0 then
return 0
end
local counter = redis.call('HINCRBY', KEYS[1], ARGV[1], -1)
if counter <= 0 then
redis.call('DEL', KEYS[1])
end
return 1
EOF
redis-cli --eval /tmp/lock_reentrant.lua lock:order:1001 , "uuid-abc" 30
redis-cli --eval /tmp/unlock_reentrant.lua lock:order:1001 , "uuid-abc"
步骤 4:续期(看门狗 / watchdog)
业务执行时间可能超过锁的超时时间。正确做法是启动后台线程定期续期:
cat > /tmp/renew.lua <<'EOF'
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('EXPIRE', KEYS[1], ARGV[2])
else
return 0
end
EOF
# 每 10 秒续期到 30 秒(超时时间的 1/3 处续期)
redis-cli --eval /tmp/renew.lua lock:order:1001 , "uuid-abc" 30
分布式锁设计要点清单:
| 要点 | 做法 |
|---|---|
| 互斥 | SET key value NX |
| 防死锁 | EX seconds 设置超时,且与 NX 在同一条命令中 |
| 防误删 | value 使用唯一标识(UUID + 线程 ID),解锁用 Lua 校验 |
| 解锁原子 | Lua 脚本完成"比对 + 删除" |
| 可重入 | 用 Hash 记录持有者与计数 |
| 自动续期 | 后台看门狗线程定期 EXPIRE(超时时间 1/3 处续期) |
| 阻塞等待 | 客户端自旋 + 随机退避;或用 Pub/Sub 通知释放(Redisson 的做法) |
| 主从切换丢锁 | 主库宕机时未同步到从库的锁会丢失 → 用 RedLock 算法(多个独立 Redis 实例,多数派成功才算加锁),或改用 ZooKeeper / etcd |
| 生产实践 | Java 直接用 Redisson(已实现上述全部机制);Go 用 redsync;Python 用 redis-py 的 Lock |
重要提醒:单实例 Redis 锁在主从故障转移场景下无法保证互斥(异步复制导致锁数据丢失)。对正确性要求极高的场景(如金融扣款)应使用数据库唯一约束、ZooKeeper 或 etcd 这类基于共识算法的方案,而不是 Redis 锁。Redis 锁适合"绝大多数时候正确即可、偶发重复执行可容忍或可幂等"的场景。
五、持久化机制
Redis 提供三类持久化方式:RDB 快照、AOF 追加日志,以及两者结合的混合持久化。
5.1 RDB(快照)
RDB 把某一时刻内存中的全量数据以压缩的二进制格式写入磁盘文件(默认 dump.rdb),相当于一张"内存照片"。
默认情况下 Redis 会开启自动快照,快照文件为数据目录(dir)下的 dump.rdb(文件名由 dbfilename 配置)。
触发方式
1. SAVE(同步,前台)
127.0.0.1:6379> SAVE
OK
由主进程直接执行快照写盘,期间Redis 完全阻塞,所有客户端请求全部挂起。数据量大时可能阻塞数秒到数十秒。生产环境绝对禁止使用,只在维护窗口或数据量极小时使用。
2. BGSAVE(异步,后台,推荐)
127.0.0.1:6379> BGSAVE
Background saving started
# 查看上次成功保存的时间戳
127.0.0.1:6379> LASTSAVE
(integer) 1756000123
# 查看后台保存是否正在进行
127.0.0.1:6379> INFO persistence
# rdb_bgsave_in_progress:0
# rdb_last_bgsave_status:ok
主进程调用 fork() 创建子进程,由子进程负责写盘,主进程继续处理请求。只在 fork() 的瞬间会短暂阻塞(大内存实例可达数百毫秒)。子进程将快照文件完整写入磁盘后,会用新文件原子替换旧文件(rename)。
3. 自动触发(配置文件的 save 规则)
自动触发条件在配置文件中通过 save <seconds> <changes> 指定,满足任意一条即自动执行 BGSAVE:
save 900 1 # 900 秒(15 分钟)内至少 1 次写入
save 300 10 # 300 秒(5 分钟)内至少 10 次写入
save 60 10000 # 60 秒内至少 10000 次写入
4. 其他隐式触发场景
| 场景 | 说明 |
|---|---|
SHUTDOWN / SHUTDOWN SAVE |
正常关闭时自动执行一次快照(若配置了 save 规则) |
DEBUG RELOAD |
重新加载数据集 |
| 主从全量同步 | 主库执行 BGSAVE 生成 RDB 发给从库(除非启用无盘复制) |
FLUSHALL |
若配置了 save 规则,会触发一次(写出空 RDB) |
执行 BGREWRITEAOF 时 |
若同时有 BGSAVE 在跑,会排队等待 |
关闭自动 RDB:
save ""
127.0.0.1:6379> CONFIG SET save ""
OK
fork 与 copy-on-write 原理
BGSAVE 的核心是 Linux 的 fork() + 写时复制(Copy-On-Write,COW):
1. 主进程调用 fork()
↓
2. 内核创建子进程,但【不复制物理内存】,
父子进程的页表指向【同一批物理内存页】,并把这些页标记为【只读】
↓
3. 子进程遍历这份"逻辑快照",把数据序列化写入临时 RDB 文件
↓
4. 期间主进程若要【修改】某个内存页:
触发缺页中断 → 内核复制该页 → 主进程改副本,子进程仍看到旧页
↓
5. 子进程写完,rename 临时文件覆盖 dump.rdb,退出
关键结论:
- 子进程看到的是 fork 那一瞬间的数据,之后主进程的写入不会体现在这次快照中。这正是"快照"语义的来源。
- fork 本身是阻塞的,耗时与进程的页表大小成正比(不是与数据量成正比,但两者相关)。经验值:每 GB 内存约 20ms 的 fork 耗时。10GB 实例 fork 可能阻塞 200ms 以上。
- 内存开销:理论上最坏情况需要双倍内存(如果 fork 后所有页都被改写)。实际取决于写入速率:写入越频繁,被复制的页越多。这是
maxmemory建议只设为物理内存 50%~70% 的原因。 - 必须配置
vm.overcommit_memory = 1,否则内存不足时 fork 会直接失败(见第七章内核参数)。 - THP(透明大页)必须关闭:THP 让内存页从 4KB 变成 2MB,COW 时每次复制 2MB,会让延迟放大数百倍。
# 查看 fork 耗时(微秒)
127.0.0.1:6379> INFO stats
# latest_fork_usec:1523
RDB 优缺点
| 优点 | 缺点 |
|---|---|
| 文件紧凑(二进制 + LZF 压缩),体积远小于 AOF | 可能丢数据:两次快照之间的写入在宕机时全部丢失(最多丢 save 间隔内的数据) |
| 恢复速度快:直接把二进制数据加载进内存,比重放 AOF 命令快几个数量级 | fork 开销大:大内存实例 fork 会造成明显的延迟毛刺,且需要额外内存 |
| 适合备份与灾备:单个文件方便定期归档、异地传输 | 不适合秒级持久化:频繁 BGSAVE 会导致 fork 频繁,性能不可接受 |
| 对主进程性能影响小(只有 fork 瞬间) | 旧版 Redis 的 RDB 格式可能不向后兼容 |
| 主从全量同步天然基于 RDB | 版本升级时需注意格式兼容性 |
5.2 AOF(追加日志)
AOF 记录每一条写命令(以 RESP 协议文本格式),恢复时重新执行这些命令来重建数据集。
写入流程
客户端写命令
↓
① 命令执行(修改内存数据)
↓
② 追加到 aof_buf(AOF 缓冲区,内存)
↓
③ 事件循环末尾调用 write(),把 aof_buf 写入【内核页缓存】
↓
④ 按 appendfsync 策略调用 fsync(),把内核页缓存【真正落盘】
↓
⑤ 文件持续增大 → 达到条件触发【AOF 重写】压缩体积
注意 ③ 和 ④ 是两个不同的步骤:
write()只是交给内核,此时数据还在内存里,机器断电会丢;只有fsync()返回才保证数据真的写到了磁盘介质上。appendfsync控制的正是fsync()的调用频率。
开启 AOF:
appendonly yes
appendfilename "appendonly.aof"
127.0.0.1:6379> CONFIG SET appendonly yes
OK
# 会立即触发一次 AOF 重写,生成包含当前全量数据的基础 AOF 文件
AOF 文件内容示例(RESP 格式,可读):
*2
$6
SELECT
$1
0
*3
$3
SET
$4
name
$8
zhangsan
三种刷盘策略(appendfsync)
通过 appendfsync 控制 AOF 缓冲区刷盘时机:
| 策略 | 行为 |
|---|---|
no |
Redis 不主动刷盘,由操作系统决定,多数 Linux 系统默认每 30 秒将缓冲区数据写入磁盘 |
everysec |
Redis 默认级别。每秒写入一次磁盘;若某次写入超过 1 秒,下一次写入会被延后;该机制在多数数据库中称为 "group commit"(批量提交) |
always |
每次写操作都立即同步到磁盘。数据安全性最高,但因写入次数频繁,对性能影响较大 |
官方建议直接使用默认级别
everysec即可。
| 策略 | 最多丢失数据 | 相对性能 | 适用 |
|---|---|---|---|
always |
理论上 0(单条命令) | 最慢,QPS 可能降到几千 | 金融交易、绝不能丢数据 |
everysec |
1~2 秒 | 接近不开 AOF | 绝大多数生产场景 |
no |
最多 30 秒(由 OS 决定) | 最快 | 纯缓存、丢了可从数据库回填 |
AOF 重写机制
AOF 是追加式的,同一个 key 反复修改会产生大量冗余命令,文件会无限增长。重写(rewrite) 就是根据当前内存中的数据集,生成一份最小化的等价命令集,替换原文件。
例如内存中 counter = 100,AOF 里可能有 100 条 INCR counter,重写后只剩一条 SET counter 100。
手动重写:
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started
自动重写的触发条件(两个条件必须同时满足):
auto-aof-rewrite-percentage 100 # 当前大小 ≥ 上次重写后大小的 (100+100)% 即翻倍
auto-aof-rewrite-min-size 64mb # 且当前大小 ≥ 64MB
触发条件 = (aof_current_size >= auto-aof-rewrite-min-size)
且 (aof_current_size - aof_base_size) / aof_base_size >= auto-aof-rewrite-percentage / 100
查看相关指标:
127.0.0.1:6379> INFO persistence
# aof_enabled:1
# aof_rewrite_in_progress:0
# aof_last_rewrite_time_sec:2
# aof_current_size:104857600
# aof_base_size:52428800
# aof_last_bgrewrite_status:ok
# aof_pending_rewrite:0
重写流程(Redis 4.0 之前):
1. 主进程 fork 出子进程(同样基于 COW)
2. 子进程遍历内存数据,把"重建数据集所需的最少命令"写入【临时 AOF 文件】
3. 重写期间主进程收到的新写命令,同时写入:
① 原 AOF 缓冲区(保证原文件仍然完整)
② 【AOF 重写缓冲区】(aof_rewrite_buf)
4. 子进程写完,通知主进程
5. 主进程把【AOF 重写缓冲区】的内容追加到临时文件末尾
6. rename 临时文件,原子替换旧 AOF 文件
Redis 7.0 的多部分 AOF(Multi-Part AOF):不再需要"重写缓冲区",改为:
appendonlydir/目录下存放 base 文件(RDB 或 AOF 格式的全量基础)+ 若干 incr 文件(增量)+ manifest 清单文件;- 重写时生成新的 base 文件与新的 incr 文件,写完后更新 manifest,删除旧文件;
- 优点:避免了重写缓冲区的内存开销与"追加期间再次阻塞"的问题。
重写期间的注意事项:
no-appendfsync-on-rewrite yes可让主进程在重写期间暂停 fsync,避免磁盘 I/O 争抢导致的延迟毛刺,代价是这段时间最多丢 30 秒数据;- 重写与
BGSAVE不会同时进行(都要 fork,Redis 会让后来者等待); - 重写会消耗额外内存(COW)与磁盘 I/O,建议安排在低峰期或依赖自动触发。
AOF 优缺点
| 优点 | 缺点 |
|---|---|
数据安全性高:everysec 最多丢 1 秒,always 几乎不丢 |
文件体积大:通常是 RDB 的数倍到数十倍(重写后会小很多) |
| 可读性好:RESP 文本格式,误操作后可以手工编辑 AOF 删掉错误命令再恢复 | 恢复速度慢:需要逐条重放命令,大数据集可能需要几分钟到几十分钟 |
| 写入是追加操作,即使断电也不会破坏已写入的部分 | 写性能有损耗(always 尤其明显) |
提供 redis-check-aof --fix 修复截断文件 |
重写同样需要 fork,仍有 COW 内存开销 |
AOF 文件修复:
# 检查 AOF 文件完整性
redis-check-aof /usr/local/redis/data/appendonly.aof
# 修复被截断的 AOF(会截掉末尾不完整的命令,务必先备份)
cp /usr/local/redis/data/appendonly.aof /tmp/appendonly.aof.bak
redis-check-aof --fix /usr/local/redis/data/appendonly.aof
# Redis 7 的多部分 AOF
redis-check-aof --fix /usr/local/redis/data/appendonlydir/appendonly.aof.manifest
# 检查 RDB 文件
redis-check-rdb /usr/local/redis/data/dump.rdb
5.3 RDB 与 AOF 对比及混合持久化
| 对比项 | RDB | AOF |
|---|---|---|
| 存储内容 | 某时刻的全量数据(二进制快照) | 所有写命令(RESP 文本) |
| 文件体积 | 小(压缩) | 大(重写后减小) |
| 数据安全性 | 低(丢两次快照间的数据) | 高(everysec 丢 1 秒) |
| 恢复速度 | 快 | 慢 |
| 对性能影响 | fork 瞬间阻塞,之后几乎无影响 | 每次写都要 write,always 影响显著 |
| 文件可读性 | 不可读(二进制) | 可读(可手工编辑) |
| 适合备份 | 非常适合(单文件、体积小) | 不太适合(体积大) |
| 主从全量同步 | 使用 RDB | 不使用 |
| 触发方式 | SAVE / BGSAVE / save 规则 |
每条写命令自动追加 |
| 默认状态 | 开启 | 关闭 |
混合持久化(aof-use-rdb-preamble,Redis 4.0+):
appendonly yes
aof-use-rdb-preamble yes # 默认已是 yes
开启后,AOF 重写生成的文件结构变成:
┌─────────────────────────────────┐
│ RDB 格式的全量数据(二进制) │ ← 重写时刻的全量快照,加载极快
├─────────────────────────────────┤
│ AOF 格式的增量命令(RESP 文本) │ ← 重写之后的写命令
└─────────────────────────────────┘
优势:既有 RDB 的加载速度(前半部分二进制批量装载),又有 AOF 的数据安全性(后半部分逐条重放最近的命令),文件体积也比纯 AOF 小很多。这是 Redis 4.0+ 的推荐配置。
验证是否为混合格式:
# 混合格式的 AOF 文件开头是 "REDIS" 魔数(与 RDB 相同)
head -c 5 /usr/local/redis/data/appendonly.aof
# REDIS
# 纯 AOF 格式开头是 "*2\r\n$6\r\nSELECT..."
生产配置推荐:
| 场景 | 配置 |
|---|---|
| 纯缓存(数据可从 DB 重建) | 只开 RDB(甚至都关掉),maxmemory + allkeys-lru |
| 一般业务(可容忍丢 1 秒) | RDB + AOF 都开,appendfsync everysec,aof-use-rdb-preamble yes |
| 金融级(不能丢数据) | RDB + AOF,appendfsync always,SSD 磁盘,主从 + 哨兵 |
| 只做备份不要实时性 | 只开 RDB,save 900 1,配合定时脚本归档 |
5.4 数据恢复与加载优先级
Redis 启动时的数据加载逻辑:
启动
↓
appendonly 是否为 yes?
├── 是 → 只加载 AOF 文件(appendonly.aof),【完全忽略 dump.rdb】
└── 否 → 加载 RDB 文件(dump.rdb)
核心结论:AOF 的优先级高于 RDB。因为 AOF 通常包含更完整、更新的数据。
最常见的生产事故:原本只用 RDB,为了提高安全性把
appendonly改成yes后直接重启——由于此时还没有 AOF 文件(或者是个空的),Redis 会加载空 AOF,内存数据全部变空,随后 RDB 也可能被空数据覆盖。
正确开启 AOF 的做法(不重启,热开启):
# 1. 确认当前数据完好
127.0.0.1:6379> DBSIZE
(integer) 138472
# 2. 先备份 RDB
# cp /usr/local/redis/data/dump.rdb /backup/dump.rdb.$(date +%F)
# 3. 动态开启 AOF —— Redis 会立即基于当前内存数据做一次 AOF 重写,
# 生成包含全量数据的 AOF 文件
127.0.0.1:6379> CONFIG SET appendonly yes
OK
# 4. 确认 AOF 已生成且状态正常
127.0.0.1:6379> INFO persistence
# aof_enabled:1
# aof_last_bgrewrite_status:ok
# aof_rewrite_in_progress:0
# 5. 把配置写回文件,保证重启后仍生效
127.0.0.1:6379> CONFIG REWRITE
OK
实操验证:RDB 恢复
# 1. 写入测试数据
redis-cli SET test:rdb "hello-rdb"
redis-cli DBSIZE
# 2. 手动生成快照
redis-cli BGSAVE
redis-cli LASTSAVE
# 3. 确认文件已生成
ls -lh /usr/local/redis/data/dump.rdb
# 4. 关闭 Redis(正常关闭会再落盘一次)
redis-cli shutdown
# 5. 模拟"数据丢失":把 RDB 移走
mv /usr/local/redis/data/dump.rdb /tmp/dump.rdb.bak
# 6. 启动,此时数据是空的
redis-server /usr/local/redis/conf/redis.conf
redis-cli DBSIZE
# (integer) 0
# 7. 恢复:关闭服务 → 放回 RDB → 启动
redis-cli shutdown
cp /tmp/dump.rdb.bak /usr/local/redis/data/dump.rdb
chown redis:redis /usr/local/redis/data/dump.rdb
redis-server /usr/local/redis/conf/redis.conf
# 8. 验证数据回来了
redis-cli GET test:rdb
# "hello-rdb"
redis-cli DBSIZE
关键点:恢复 RDB 必须先停止 Redis。如果在运行中直接覆盖
dump.rdb,Redis 不会重新加载,而且下一次BGSAVE会把你刚放进去的文件再次覆盖掉。
实操验证:AOF 恢复
# 1. 确保 AOF 已开启
redis-cli CONFIG GET appendonly
# 2. 写入数据
redis-cli SET test:aof "hello-aof"
redis-cli SET test:aof2 "world"
# 3. 查看 AOF 文件内容(能看到明文命令)
tail -20 /usr/local/redis/data/appendonly.aof
# 4. 用 kill -9 模拟宕机(跳过正常关闭的落盘)
ps -ef | grep redis-server
kill -9 <pid>
# 5. 重启,AOF 会被重放
redis-server /usr/local/redis/conf/redis.conf
redis-cli GET test:aof
# "hello-aof" —— everysec 策略下,1 秒前的数据都在
备份与迁移
方案 1:定时归档 RDB(最常用)
cat > /usr/local/redis/bin/redis_backup.sh <<'EOF'
#!/bin/bash
# Redis RDB 定时备份脚本
BACKUP_DIR=/backup/redis
DATA_DIR=/usr/local/redis/data
RDB_FILE=dump.rdb
KEEP_DAYS=7
DATE=$(date +%Y%m%d_%H%M%S)
REDIS_CLI="/usr/local/redis/src/redis-cli -a 'yourpassword' --no-auth-warning"
mkdir -p $BACKUP_DIR
# 记录当前 LASTSAVE,触发 BGSAVE 后等待其变化
LAST=$($REDIS_CLI LASTSAVE)
$REDIS_CLI BGSAVE
for i in $(seq 1 120); do
NOW=$($REDIS_CLI LASTSAVE)
if [ "$NOW" != "$LAST" ]; then
break
fi
sleep 1
done
# 拷贝并压缩
cp $DATA_DIR/$RDB_FILE $BACKUP_DIR/dump_$DATE.rdb
gzip $BACKUP_DIR/dump_$DATE.rdb
# 清理过期备份
find $BACKUP_DIR -name "dump_*.rdb.gz" -mtime +$KEEP_DAYS -delete
echo "[$(date)] backup done: dump_$DATE.rdb.gz"
EOF
chmod +x /usr/local/redis/bin/redis_backup.sh
# 加入计划任务:每天凌晨 3 点备份
crontab -e
# 0 3 * * * /usr/local/redis/bin/redis_backup.sh >> /var/log/redis_backup.log 2>&1
方案 2:不落地直接拉取 RDB(在线备份,不影响服务磁盘)
redis-cli -h 192.168.1.220 -p 6379 -a 'pass' --rdb /backup/redis/dump_$(date +%F).rdb
方案 3:用从库做备份(推荐,完全不影响主库)
# 在专门的备份从库上执行 BGSAVE 并归档
# 该从库设置 replica-priority 0,永不被选为主库
redis-cli -h 192.168.1.221 BGSAVE
方案 4:数据迁移
# 4.1 整实例迁移:停服拷贝 RDB
redis-cli -h old shutdown
scp old:/usr/local/redis/data/dump.rdb new:/usr/local/redis/data/
# 在 new 上启动
# 4.2 用主从复制在线迁移(零停机)
# 新实例上执行
redis-cli -h new REPLICAOF old-ip 6379
# 等待 master_link_status:up 且 master_sync_in_progress:0
redis-cli -h new INFO replication
# 切换应用流量到 new,然后断开复制
redis-cli -h new REPLICAOF NO ONE
# 4.3 单个/少量 key 迁移
redis-cli -h old MIGRATE new-ip 6379 "mykey" 0 5000 COPY REPLACE
# 批量迁移多个 key(key 位置留空字符串)
redis-cli -h old MIGRATE new-ip 6379 "" 0 5000 COPY REPLACE KEYS k1 k2 k3
# 4.4 DUMP + RESTORE 手工搬运
DATA=$(redis-cli --no-raw -h old DUMP mykey)
# 实际使用建议用客户端程序处理二进制,shell 下易损坏
# 4.5 使用 redis-shake / redis-migrate-tool 等专业工具做在线双写迁移
备份检查清单:
| 项目 | 要求 |
|---|---|
| 备份频率 | 至少每天一次;重要业务每小时一次 |
| 保留周期 | 本地 7 天,异地 30 天以上 |
| 异地存储 | 必须传到另一台机器 / 对象存储,防止磁盘整体损坏 |
| 恢复演练 | 每季度做一次真实恢复演练——没验证过的备份等于没有备份 |
| 备份校验 | 用 redis-check-rdb 验证文件完整性 |
| 权限 | 备份文件包含全量业务数据,必须限制权限(chmod 600) |
六、主从复制与哨兵
6.1 主从复制(Replication)
作用
| 作用 | 说明 |
|---|---|
| 数据冗余 | 从库是主库的完整副本,主库磁盘损坏时数据仍在 |
| 读写分离 | 写走主库,读分摊到多个从库,提升读吞吐 |
| 故障恢复基础 | 主库宕机时可以把从库提升为主库(哨兵自动完成) |
| 备份卸载 | 在从库上执行 BGSAVE,主库零影响 |
查看当前复制状态
127.0.0.1:6379> INFO replication
临时配置主从(重启失效)
# Redis 5.0 之前
127.0.0.1:6379> SLAVEOF host port
# Redis 5.0 及以后(推荐)
127.0.0.1:6379> REPLICAOF host port
# 示例
127.0.0.1:6379> REPLICAOF 192.168.1.220 6379
OK
# 若主库有密码,还需设置
127.0.0.1:6379> CONFIG SET masterauth 123
OK
# 断开复制,自己变回独立主库(保留已同步的数据)
127.0.0.1:6379> REPLICAOF NO ONE
OK
127.0.0.1:6379> SLAVEOF NO ONE # 旧命令,等效
OK
永久配置主从
编辑 slave 的 redis.conf:
# Redis 5.0 之前的写法
slaveof host port # 改为 master 的 IP 和端口,重启后生效
masterauth 123 # 若 master 开启了 requirepass,需在此配置 master 密码
# Redis 5.0 及以后的写法
replicaof 192.168.1.220 6379
masterauth 123
replica-read-only yes
重启 slave:
/etc/init.d/redis start
# 或
systemctl restart redis
全量同步与增量同步流程
建立复制关系时的完整流程:
从库 主库
│
│─── ① 保存主库地址,建立 socket 连接 ─────────→
│
│─── ② 发送 PING ────────────────────────────→
│←── PONG ──────────────────────────────────
│
│─── ③ 发送 AUTH <masterauth>(若需要) ──────→
│─── ④ 发送 REPLCONF listening-port <port> ──→
│─── ⑤ 发送 REPLCONF capa eof capa psync2 ───→
│
│─── ⑥ 发送 PSYNC <replid> <offset> ─────────→
│ 判断能否部分重同步
│←── +FULLRESYNC <replid> <offset> ───────── (不能 → 全量同步)
│ 执行 BGSAVE 生成 RDB
│ 期间新写命令存入 replication buffer
│←── ⑦ 传输 RDB 文件 ───────────────────────
│ 清空自己的旧数据集,加载 RDB
│←── ⑧ 发送 replication buffer 中的增量命令 ──
│ 执行增量命令,追平主库
│
│══ ⑨ 之后主库持续把写命令通过【命令传播】发给从库 ══
│
│─── ⑩ 从库每秒发送 REPLCONF ACK <offset> ───→ (心跳 + 汇报进度)
三种同步方式:
| 方式 | 触发条件 | 代价 |
|---|---|---|
| 全量同步(full resync) | 首次建立复制;replid 不匹配;断线太久 backlog 已覆盖 |
主库 fork + BGSAVE + 传输整个数据集,代价最大 |
| 部分重同步(partial resync) | 短暂断线重连,且断线期间的数据仍在 repl-backlog 中 |
只传输缺失的那一段命令,代价很小 |
| 命令传播(command propagate) | 正常运行期间 | 主库每执行一条写命令就异步转发给所有从库 |
repl-backlog 与部分重同步
repl-backlog 是主库维护的一个固定大小的环形缓冲区(默认 1MB),保存最近传播给从库的写命令流。
主库复制积压缓冲区(环形,repl-backlog-size = 1mb 默认)
┌──────────────────────────────────────────┐
│ ... offset 10000 ~ offset 11048576 ... │ ← 旧数据被新数据循环覆盖
└──────────────────────────────────────────┘
↑
从库的 offset
从库重连时发送 PSYNC <replid> <offset+1>:
├─ replid 匹配 且 offset 仍在 backlog 范围内 → +CONTINUE,只补发缺失部分
└─ 否则 → +FULLRESYNC,全量同步
关键概念:
| 概念 | 说明 |
|---|---|
master_replid |
主库的 40 位随机复制 ID,标识"一段复制历史" |
master_repl_offset |
主库已传播的字节偏移量,单调递增 |
slave_repl_offset |
从库已接收的偏移量。与主库的差值即"复制延迟" |
second_repl_offset / replid2 |
PSYNC2(Redis 4.0+)用于故障转移后仍能部分重同步 |
backlog 大小的计算:
repl-backlog-size >= 平均写入速率(字节/秒) × 预计最长断线时间(秒) × 安全系数 2
例如写入 2MB/s、网络抖动最长 30 秒,则应设置 2MB × 30 × 2 = 120mb。默认的 1MB 在生产环境几乎总是太小,会导致频繁全量同步(进而 fork 频繁、主库抖动、雪崩)。
repl-backlog-size 128mb
repl-backlog-ttl 3600
PSYNC2 的改进(Redis 4.0+):故障转移后,新主库会保留旧主库的 replid 作为 replid2,这样其他从库切换到新主库时仍可以做部分重同步,避免了整个集群同时全量同步的雪崩。
INFO replication 输出解读
主库输出:
127.0.0.1:6379> INFO replication
# Replication
role:master # 角色:主库
connected_slaves:2 # 已连接的从库数量
slave0:ip=192.168.1.221,port=6379,state=online,offset=1547892,lag=0
slave1:ip=192.168.1.222,port=6379,state=online,offset=1547892,lag=1
master_failover_state:no-failover
master_replid:8e2b1f4c9a3d5e7f1b2c4d6e8f0a1b2c3d4e5f6a
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1547892 # 主库当前复制偏移量
second_repl_offset:-1
repl_backlog_active:1 # backlog 是否启用
repl_backlog_size:134217728 # backlog 大小(字节)
repl_backlog_first_byte_offset:1413675
repl_backlog_histlen:134217
从库字段解读:
| 字段 | 含义 |
|---|---|
state=online |
从库已完成同步,处于正常传播状态。state=send_bulk 表示正在传输 RDB |
offset |
该从库已确认接收的偏移量。与 master_repl_offset 的差值就是复制积压量 |
lag |
从库最后一次发送 REPLCONF ACK 距今的秒数。持续大于 1 需要排查 |
从库输出:
127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:192.168.1.220 # 主库地址
master_port:6379
master_link_status:up # 【最关键】up=连接正常,down=断开
master_last_io_seconds_ago:0 # 距上次与主库通信的秒数
master_sync_in_progress:0 # 是否正在进行全量同步(1=正在同步)
slave_read_repl_offset:1547892
slave_repl_offset:1547892 # 从库偏移量
slave_priority:100 # 选主优先级
slave_read_only:1 # 是否只读
replica_announced:1
connected_slaves:0 # 从库自己的下级从库数(级联复制)
master_failover_state:no-failover
master_replid:8e2b1f4c9a3d5e7f1b2c4d6e8f0a1b2c3d4e5f6a
master_repl_offset:1547892
健康检查要点:
# 1. 链路状态必须是 up
redis-cli -h 192.168.1.221 INFO replication | grep master_link_status
# 2. 计算复制延迟(主从 offset 差值)
MASTER_OFFSET=$(redis-cli -h 192.168.1.220 INFO replication | grep -w master_repl_offset | cut -d: -f2 | tr -d '\r')
SLAVE_OFFSET=$(redis-cli -h 192.168.1.221 INFO replication | grep -w slave_repl_offset | cut -d: -f2 | tr -d '\r')
echo "lag bytes: $((MASTER_OFFSET - SLAVE_OFFSET))"
# 3. 统计全量同步次数(应该很少)
redis-cli -h 192.168.1.220 INFO stats | grep sync_
# sync_full:1 ← 全量同步次数,频繁增长说明 backlog 太小
# sync_partial_ok:5 ← 部分重同步成功次数
# sync_partial_err:0 ← 部分重同步失败次数(失败会退化为全量)
只读从库
replica-read-only yes # 默认值,强烈建议保持
127.0.0.1:6379> SET k v
(error) READONLY You can't write against a read only replica.
为什么必须只读:
- 往从库写的数据不会同步回主库,造成主从数据不一致;
- 下一次全量同步时,从库会清空自己的数据集并加载主库的 RDB,你写进去的数据全部丢失;
- 如果从库上的 key 与主库的 key 冲突,行为不可预测。
唯一的例外:极少数场景下在从库写入纯本地的临时缓存 key(Redis 4.0+ 会给这类 key 打上标记,全量同步时清理)。不推荐。
级联复制(链式复制)
从库自己也可以有从库,形成树状结构,用于减轻主库的复制压力:
Master (192.168.1.220)
│
┌───────┴───────┐
Slave1 Slave2
(.221) (.222)
│
Slave3 ← 从 Slave1 复制,主库无感知
(.223)
配置:
# Slave3 指向 Slave1(而不是 Master)
redis-cli -h 192.168.1.223 REPLICAOF 192.168.1.221 6379
# 在 Slave1 上可以看到它既是 slave 又有 connected_slaves
127.0.0.1:6379> INFO replication
role:slave
master_host:192.168.1.220
connected_slaves:1
slave0:ip=192.168.1.223,port=6379,state=online,offset=1547892,lag=0
| 优点 | 缺点 |
|---|---|
| 主库只需给 1~2 个从库推送数据,fork 与带宽压力大幅降低 | 复制延迟累加:Slave3 的数据比 Slave1 更旧 |
| 适合从库数量很多(10 个以上)的场景 | 中间节点(Slave1)故障会导致其下游全部断链 |
| 可用于跨机房:主机房 1 个中转从库 → 异地多个从库 | 拓扑复杂,运维与排错难度上升 |
中间节点默认也是只读的(
replica-read-only yes),它会把收到的复制流原样转发给下级从库。
读写分离的做法与一致性问题
在 Redis 中,一旦搭建了主从复制关系,即天然满足读写分离:
- master:可读可写;
- slave:默认只读。
客户端侧的实现方式:
# 方式 1:应用代码里维护两个连接池
# 写操作 → master 连接池
# 读操作 → slave 连接池(多个从库间做轮询/随机负载均衡)
# 方式 2:通过哨兵获取地址(推荐,自动感知故障转移)
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
redis-cli -p 26379 SENTINEL replicas mymaster
# 方式 3:用代理层(Twemproxy / Codis / Predixy)统一路由
# 方式 4:客户端库内置支持(如 Lettuce 的 ReadFrom.REPLICA_PREFERRED)
一致性问题(必须理解):
Redis 主从复制是完全异步的:主库执行完写命令立即返回客户端 OK,然后才异步把命令发给从库。因此:
| 问题 | 说明 | 缓解方案 |
|---|---|---|
| 读到旧数据(最终一致) | 写主库后立刻读从库,可能读到写之前的值(毫秒到秒级窗口) | 对一致性敏感的读强制走主库;或写后短时间内的读走主库("读己之写") |
| 主库宕机丢数据 | 已返回 OK 但尚未同步到从库的写入,在故障转移后永久丢失 | min-replicas-to-write + min-replicas-max-lag 限制;或用 WAIT 命令;或改用同步复制方案 |
| 从库延迟过大 | 网络拥塞、从库负载高、大 key 传输时延迟可达秒级甚至分钟级 | 监控 master_repl_offset - slave_repl_offset,超阈值把该从库摘出负载均衡 |
| 从库过期 key 行为 | 从库不会主动删除过期 key,要等主库发来 DEL。3.2+ 从库读取时会判断逻辑过期并返回 nil,但 DBSIZE 仍然计入 |
理解此行为,不要依赖从库的 DBSIZE/KEYS 做精确统计 |
用 WAIT 命令实现"半同步"(阻塞直到指定数量的从库确认):
127.0.0.1:6379> SET important:key "value"
OK
127.0.0.1:6379> WAIT 1 1000
(integer) 1 # 1000ms 内有 1 个从库确认收到;返回值是实际确认的从库数
WAIT numreplicas timeout只保证从库收到并写入了自己的复制流,不保证从库已 fsync 落盘,也不能完全避免故障转移丢数据。它把"异步"变成了"尽力而为的半同步"。
限制主库在从库不健康时拒绝写入:
min-replicas-to-write 1 # 至少 1 个健康从库才接受写
min-replicas-max-lag 10 # "健康"定义:lag <= 10 秒
这样在所有从库都失联时主库会拒绝写入(返回 NOREPLICAS),牺牲可用性换取"数据不会只存在于主库"。
6.2 哨兵(Sentinel)
Sentinel 的作用
Sentinel 进程独立于 Redis 数据节点运行,主要承担三项职责:
- 监控:不断向 master 和 slave 发送
PING,检查节点是否正常运行。 - 提醒:监控到节点异常时,可通过 API 向指定账号或邮箱发送告警。
- 自动故障迁移:当 master 不可用时,自动将一台 slave 提升为新的 master,让其他 slave 改为复制新 master;主从身份调换后,双方配置文件也会被相应修改。
补充职责:
- 配置提供者:客户端连接哨兵询问"当前主库是谁",故障转移后客户端能自动获得新地址,无需改配置或重启。
架构
┌──────────┐ ┌──────────┐ ┌──────────┐
│Sentinel 1│ │Sentinel 2│ │Sentinel 3│ ← 哨兵集群(至少 3 个,奇数)
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ 互相发现、互相监控、共同投票 │
└──────┬───────┴──────┬───────┘
监控 │ │ 监控
┌──────────▼──┐ ┌────▼─────────┐
│ Master │─复制→│ Slave 1 │
│ (.220:6379)│ │ (.221:6379) │
└──────┬──────┘ └──────────────┘
│ 复制
┌──────▼───────┐
│ Slave 2 │
│ (.222:6379) │
└──────────────┘
▲
│ ① 询问主库地址 ② 订阅切换事件
┌──────┴───────┐
│ 客户端 │
└──────────────┘
为什么哨兵至少要 3 个且为奇数:
- 判定"客观下线"与执行故障转移需要多数派(quorum + majority) 同意;
- 只有 1 个哨兵时,哨兵自己就是单点,且无法防止误判;
- 只有 2 个哨兵时,挂掉 1 个后剩下的 1 个无法达到多数派(2/2+1 = 2),无法执行故障转移;
- 3 个哨兵允许挂掉 1 个仍能正常工作;5 个允许挂 2 个。
- 哨兵应部署在不同的物理机上,否则机器宕机会同时带走多个哨兵。
环境准备
- 确定 master 与 slave 的 IP 地址;
- 开放相应防火墙端口(或直接关闭防火墙);
- master 和 slave 上均需部署 Sentinel 进程(也可只在 slave 端部署)。
示例 IP 列表:
| 角色 | IP |
|---|---|
| master | 3.3.3.8 |
| slave | 3.3.3.9 |
CentOS 7 防火墙放行:
firewall-cmd --permanent --add-port=6379/tcp
firewall-cmd --permanent --add-port=26379/tcp
firewall-cmd --reload
# 或直接关闭
systemctl stop firewalld
systemctl disable firewalld
CentOS 6 防火墙放行:
iptables -I INPUT -p tcp --dport 6379 -j ACCEPT
iptables -I INPUT -p tcp --dport 26379 -j ACCEPT
service iptables save
# 或直接关闭
service iptables stop
chkconfig iptables off
master 端 redis.conf
bind 3.3.3.9 # 也可注释掉,允许所有 IP 登录
port 6379 # 默认即可
daemonize yes # 后台运行
logfile logfilename # 日志文件路径
requirepass 123 # 登录 Redis 的密码
masterauth 123 # slave 连接 master 时使用的密码(若开启则两边密码必须一致)
appendonly yes # 开启 AOF(可选)
appendfsync always # 修改写入级别
注意:master 也必须配置
masterauth,因为故障转移后它可能降级为从库,届时需要用这个密码去连接新主库。
slave 端 redis.conf
bind 3.3.3.8 # 也可注释掉,允许所有 IP 登录
port 6379 # 默认即可
daemonize yes # 后台运行
logfile logfilename # 日志文件路径
requirepass 123 # 登录 Redis 的密码
masterauth 123 # 连接 master 的密码
slaveof 3.3.3.8 6379 # 指定 master 地址和端口
slave-serve-stale-data no # slave 不可读(设为 no 后 slave 在同步期间不响应读请求)
appendonly yes # 开启 AOF(可选)
appendfsync always # 修改写入级别
Redis 5.0+ 对应的新参数名为
replicaof与replica-serve-stale-data。
sentinel.conf(master 与 slave 均需配置,也可只配置 slave 端)
port 26379 # Sentinel 监听端口(默认即可)
daemonize yes
logfile '/var/log/redis/sentinel.log' # Sentinel 日志路径
pidfile '/var/run/sentinel.pid'
dir /usr/local/redis/sentinel/ # Sentinel 工作目录
sentinel monitor mymaster 3.3.3.8 6379 1 # 被监控的 master 地址;末尾 1 为判定客观下线所需的 Sentinel 投票数
sentinel auth-pass mymaster 123 # 连接 master 的密码
sentinel down-after-milliseconds mymaster 5000 # master 失联超过 5000ms(5s)则判定为主观下线
sentinel parallel-syncs mymaster 1 # 故障迁移后,同时向新 master 发起同步的 slave 数量
sentinel failover-timeout mymaster 60000 # 故障迁移超时时间(ms),超过则认为迁移失败
sentinel.conf 全部关键配置详解
| 参数 | 说明 |
|---|---|
port 26379 |
哨兵监听端口,默认 26379 |
daemonize yes |
后台运行 |
pidfile |
pid 文件路径 |
logfile |
日志文件路径 |
dir |
工作目录 |
sentinel monitor <name> <ip> <port> <quorum> |
核心配置。name 是主库的逻辑名(客户端用这个名字查询地址);ip:port 是主库地址;quorum 是判定客观下线所需的哨兵票数 |
sentinel auth-pass <name> <password> |
连接被监控主库(及其从库)的密码。主从密码必须一致 |
sentinel auth-user <name> <username> |
ACL 用户名(Redis 6+) |
sentinel down-after-milliseconds <name> <ms> |
哨兵 PING 主库连续失败超过该毫秒数,即判定为主观下线(SDOWN)。默认 30000。设太小易误判(网络抖动就切主),设太大故障发现慢。生产建议 5000~30000 |
sentinel parallel-syncs <name> <n> |
故障转移后,同时允许多少个从库向新主库发起全量同步。设为 1 表示逐个同步——虽然慢,但保证同一时刻只有 1 个从库处于"同步中不可读"状态,服务可用性更好。设大了会让新主库同时 fork 多次、带宽被打满 |
sentinel failover-timeout <name> <ms> |
故障转移的超时时间(默认 180000 = 3 分钟)。含义包括:同一主库两次故障转移的最小间隔(实际是该值的 2 倍)、取消进行中转移的判定时间、等待从库指向新主库的超时等 |
sentinel notification-script <name> <script> |
发生告警级事件时执行的脚本(可用于发邮件/短信/钉钉) |
sentinel client-reconfig-script <name> <script> |
故障转移时执行的脚本,会传入 <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port> 参数,可用于通知 LVS/HAProxy 改后端、更新 DNS 等 |
sentinel deny-scripts-reconfig yes |
禁止运行时通过 SENTINEL SET 修改脚本路径(安全加固) |
requirepass <password> |
哨兵自身的访问密码(哨兵之间也用它互相认证) |
protected-mode no |
哨兵默认也有保护模式,多机部署时需要处理 |
sentinel resolve-hostnames yes |
允许使用主机名而非 IP(Redis 6.2+) |
sentinel announce-ip / sentinel announce-port |
NAT / Docker 环境下宣告的对外地址 |
quorum 的正确理解(最容易搞错的点):
quorum 只用于【判定客观下线】:
至少 quorum 个哨兵认为主库主观下线 → 标记为客观下线(ODOWN)
而【执行故障转移】需要的是哨兵总数的【多数派】(majority),
这个条件与 quorum 无关,无法通过配置绕过。
例:3 个哨兵,quorum = 2
→ 2 个哨兵认为挂了 → 判定 ODOWN
→ 还需要 ≥ 2 个哨兵(3 的多数派)授权 → 才能真正切主
例:3 个哨兵,quorum = 1
→ 1 个哨兵认为挂了就判 ODOWN(容易误判)
→ 但仍需 ≥ 2 个哨兵授权才能切主
→ 所以如果挂了 2 个哨兵,即使 quorum=1 也【无法】完成故障转移
推荐:quorum = (哨兵数 / 2) + 1,即 3 个哨兵设 2,5 个设 3
讲义示例中
sentinel monitor mymaster 3.3.3.8 6379 1的 quorum 为 1,是单哨兵实验环境的配置。生产环境请用 3 个哨兵 + quorum 2。
启动与关闭 Sentinel
启动(两种等价方式):
redis-sentinel sentinel.conf
# 或
redis-server sentinel.conf --sentinel
关闭方式:
pkill -9 redis # 不推荐:会同时杀死 Redis 数据进程
kill -9 <pid> # 推荐:精确杀死 Sentinel 进程
redis-cli -p 26379 shutdown
# 或者使用自定义管理脚本(见附录)
systemd 管理哨兵(CentOS 7+):
# /usr/lib/systemd/system/redis-sentinel.service
[Unit]
Description=Redis Sentinel
After=network.target
[Service]
Type=forking
ExecStart=/usr/local/redis/src/redis-sentinel /usr/local/redis/conf/sentinel.conf
ExecStop=/usr/local/redis/src/redis-cli -p 26379 shutdown
PIDFile=/var/run/sentinel.pid
User=redis
Group=redis
Restart=always
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl start redis-sentinel
systemctl enable redis-sentinel
重要:哨兵会在故障转移后自动改写自己的 sentinel.conf(更新
sentinel monitor中的主库地址、追加sentinel known-replica/known-sentinel条目)。因此:
- 配置文件必须可写,且运行用户有写权限;
- 不要用配置管理工具(Ansible/Puppet)强行覆盖 sentinel.conf,否则会把哨兵的状态记录冲掉;
- 每个哨兵必须有独立的配置文件,不能共用。
主观下线与客观下线
| 概念 | 英文 | 判定方式 |
|---|---|---|
| 主观下线 | SDOWN(Subjectively Down) | 单个哨兵发现某节点在 down-after-milliseconds 内没有有效回复 PING(+PONG / -LOADING / -MASTERDOWN 都算有效),就把它标记为主观下线。这只是"我一个人这么认为" |
| 客观下线 | ODOWN(Objectively Down) | 一个哨兵标记主库 SDOWN 后,会向其他哨兵发送 SENTINEL is-master-down-by-addr 询问。当收到的确认数 ≥ quorum 时,升级为客观下线。这是"大家都这么认为" |
关键区别:
- 只有主库才有"客观下线"的概念。从库和其他哨兵只会被标记 SDOWN,不会触发故障转移。
- ODOWN 是启动故障转移的前提条件。
- 主观下线可能是网络分区导致的误判,客观下线通过多数哨兵确认降低了误判概率。
选举与自动故障转移的完整流程
【阶段 1:故障发现】
① 每个哨兵每 1 秒向 主库/从库/其他哨兵 发送 PING
② 哨兵 A 发现主库在 down-after-milliseconds 内无有效回复
→ 标记主库为 SDOWN(主观下线)
【阶段 2:确认故障】
③ 哨兵 A 向其他哨兵发送 SENTINEL is-master-down-by-addr <ip> <port> <epoch> <runid>
④ 其他哨兵各自根据自己的判断回复"是/否"
⑤ 确认数 ≥ quorum → 主库被标记为 ODOWN(客观下线)
【阶段 3:选举领头哨兵(Leader Election,基于 Raft 思想)】
⑥ 发现 ODOWN 的哨兵发起选举,把 configuration epoch(配置纪元)+1
⑦ 每个哨兵在同一个 epoch 内只能投一票(先到先得)
⑧ 得票数 ≥ max(quorum, 哨兵总数的多数派) 的哨兵成为【领头哨兵】(Leader)
⑨ 若本轮无人达到多数派 → 等随机时间后 epoch+1 重新选举
(只有领头哨兵才能执行故障转移,避免多个哨兵同时切主)
【阶段 4:选择新主库】
⑩ 领头哨兵从存活的从库中筛选与排序:
a) 【过滤】剔除已下线(SDOWN)、断线的从库
b) 【过滤】剔除 down-after-milliseconds * 10 内没有回复过 PING 的从库
c) 【过滤】剔除与旧主库断开时间超过
(down-after-milliseconds * 10 + 主库不可用时长) 的从库
d) 【过滤】剔除 replica-priority = 0 的从库(明确声明不参选)
e) 【排序 1】replica-priority 数值【最小】者优先
f) 【排序 2】优先级相同 → 【复制偏移量 offset 最大】者优先(数据最新、丢失最少)
g) 【排序 3】offset 也相同 → runid【字典序最小】者优先(保证确定性)
【阶段 5:执行切换】
⑪ 领头哨兵向选中的从库发送 SLAVEOF NO ONE,将其提升为新主库
⑫ 等待并确认它的 role 变为 master(通过 INFO 确认)
⑬ 向其余从库发送 SLAVEOF <新主库IP> <新主库端口>
—— 按 parallel-syncs 控制并发数,逐批进行
⑭ 更新自身对该主库的记录,并把 ODOWN 的旧主库也标记为新主库的从库
⑮ 旧主库恢复上线后,被自动降级为新主库的从库(并向新主库同步数据)
【阶段 6:通知】
⑯ 通过 Pub/Sub 频道 +switch-master 广播切换事件
⑰ 客户端收到通知(或下次询问哨兵)后连接新主库
⑱ 若配置了 client-reconfig-script / notification-script,执行脚本
关键的时间参数关系:
故障转移总耗时 ≈ down-after-milliseconds(发现)
+ 选举耗时(通常 < 1 秒)
+ 提升新主库(很快)
+ 其余从库指向新主(受 parallel-syncs 影响)
failover-timeout 的多重含义:
- 同一主库两次故障转移的最小间隔 = failover-timeout * 2
- 从库指向新主库的等待超时
- 取消进行中的故障转移的判定时间
监控哨兵状态
# 连接哨兵
redis-cli -p 26379
# 查看所有被监控的主库
127.0.0.1:26379> SENTINEL masters
# 查看指定主库信息
127.0.0.1:26379> SENTINEL master mymaster
# 查看从库列表(旧命令 SENTINEL slaves)
127.0.0.1:26379> SENTINEL replicas mymaster
127.0.0.1:26379> SENTINEL slaves mymaster
# 查看其他哨兵
127.0.0.1:26379> SENTINEL sentinels mymaster
# 【最重要】获取当前主库地址 —— 客户端就是靠这条命令
127.0.0.1:26379> SENTINEL get-master-addr-by-name mymaster
1) "192.168.1.220"
2) "6379"
# 检查配置是否满足故障转移条件(哨兵数量是否够)
127.0.0.1:26379> SENTINEL ckquorum mymaster
OK 3 usable Sentinels. Quorum and failover authorization can be reached
# 手动强制故障转移(演练用,不需要主库真的挂)
127.0.0.1:26379> SENTINEL failover mymaster
OK
# 重置主库状态(清除已知从库与哨兵记录,重新发现)
127.0.0.1:26379> SENTINEL reset mymaster
# 运行时修改哨兵配置
127.0.0.1:26379> SENTINEL SET mymaster down-after-milliseconds 10000
OK
127.0.0.1:26379> SENTINEL SET mymaster quorum 2
OK
# 动态新增/移除监控目标
127.0.0.1:26379> SENTINEL MONITOR mymaster2 192.168.1.230 6379 2
127.0.0.1:26379> SENTINEL REMOVE mymaster2
# 哨兵自身的 INFO
127.0.0.1:26379> INFO sentinel
# sentinel_masters:1
# sentinel_tilt:0
# sentinel_running_scripts:0
# master0:name=mymaster,status=ok,address=192.168.1.220:6379,slaves=2,sentinels=3
客户端如何通过哨兵获取主库地址
流程:
1. 客户端配置【哨兵地址列表】(而不是主库地址)
sentinels = [("192.168.1.220", 26379),
("192.168.1.221", 26379),
("192.168.1.222", 26379)]
master_name = "mymaster"
2. 依次尝试连接哨兵,执行:
SENTINEL get-master-addr-by-name mymaster
→ 返回当前主库的 ip:port
3. 用返回的地址连接主库,并执行 ROLE 命令验证它确实是 master
(防止哨兵信息滞后,连到了一个刚被降级的节点)
4. 【同时】订阅哨兵的 Pub/Sub 频道以感知切换:
SUBSCRIBE +switch-master
收到消息格式:<master-name> <old-ip> <old-port> <new-ip> <new-port>
5. 收到 +switch-master 后,客户端丢弃旧连接池,重新走步骤 2
哨兵发布的主要事件频道:
| 频道 | 触发时机 |
|---|---|
+switch-master |
主库地址变更(最重要,客户端必须订阅) |
+sdown / -sdown |
节点进入 / 退出主观下线 |
+odown / -odown |
节点进入 / 退出客观下线 |
+failover-state-* |
故障转移各阶段状态变化 |
+slave |
发现新从库 |
+sentinel |
发现新哨兵 |
+slave-reconf-sent / +slave-reconf-done |
从库重配置进度 |
+try-failover / +elected-leader / +failover-end |
故障转移开始 / 选出领头 / 结束 |
* |
订阅全部事件 |
# 实时观察哨兵所有事件(排错利器)
redis-cli -p 26379 PSUBSCRIBE '*'
主流客户端的哨兵配置示例:
# Python (redis-py)
from redis.sentinel import Sentinel
sentinel = Sentinel([('192.168.1.220', 26379),
('192.168.1.221', 26379),
('192.168.1.222', 26379)],
socket_timeout=0.5,
sentinel_kwargs={'password': 'sentinelpass'})
master = sentinel.master_for('mymaster', socket_timeout=0.5, password='123')
slave = sentinel.slave_for('mymaster', socket_timeout=0.5, password='123')
master.set('foo', 'bar')
print(slave.get('foo'))
# Java Spring Boot (application.yml)
# spring:
# redis:
# sentinel:
# master: mymaster
# nodes: 192.168.1.220:26379,192.168.1.221:26379,192.168.1.222:26379
# password: sentinelpass
# password: 123
三节点最小部署完整实操
规划:
| 主机 | Redis 角色 | Redis 端口 | Sentinel 端口 |
|---|---|---|---|
| 192.168.1.220 | master | 6379 | 26379 |
| 192.168.1.221 | slave | 6379 | 26379 |
| 192.168.1.222 | slave | 6379 | 26379 |
统一密码:Redis 123456,哨兵 sentinel123。
步骤 1:三台机器统一环境准备
# 关闭防火墙与 SELinux(实验环境)
systemctl stop firewalld && systemctl disable firewalld
setenforce 0
sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config
# 内核参数
echo 'vm.overcommit_memory = 1' >> /etc/sysctl.conf
echo 'net.core.somaxconn = 1024' >> /etc/sysctl.conf
sysctl -p
echo never > /sys/kernel/mm/transparent_hugepage/enabled
# 目录
mkdir -p /usr/local/redis/{conf,data,log,run}
useradd -M -s /sbin/nologin redis 2>/dev/null
chown -R redis:redis /usr/local/redis
步骤 2:主库(192.168.1.220)配置 /usr/local/redis/conf/redis.conf
bind 0.0.0.0
protected-mode yes
port 6379
daemonize yes
pidfile /usr/local/redis/run/redis_6379.pid
logfile /usr/local/redis/log/redis_6379.log
dir /usr/local/redis/data
dbfilename dump.rdb
requirepass 123456
masterauth 123456 # 关键:为将来降级为从库做准备
save 900 1
save 300 10
save 60 10000
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes
repl-backlog-size 64mb
maxmemory 2gb
maxmemory-policy allkeys-lru
步骤 3:两台从库(.221 / .222)配置
在主库配置的基础上增加:
replicaof 192.168.1.220 6379
replica-read-only yes
replica-serve-stale-data yes
replica-priority 100 # .222 可设 90 让它优先被选为新主
步骤 4:启动三个 Redis 并验证复制
# 三台机器分别执行
redis-server /usr/local/redis/conf/redis.conf
# 在主库上确认两个从库都已连接
redis-cli -a 123456 --no-auth-warning INFO replication
# role:master
# connected_slaves:2
# slave0:ip=192.168.1.221,port=6379,state=online,offset=xxx,lag=0
# slave1:ip=192.168.1.222,port=6379,state=online,offset=xxx,lag=0
# 验证数据同步
redis-cli -h 192.168.1.220 -a 123456 --no-auth-warning SET testkey "hello"
redis-cli -h 192.168.1.221 -a 123456 --no-auth-warning GET testkey
# "hello"
redis-cli -h 192.168.1.222 -a 123456 --no-auth-warning GET testkey
# "hello"
# 验证从库只读
redis-cli -h 192.168.1.221 -a 123456 --no-auth-warning SET k v
# (error) READONLY You can't write against a read only replica.
步骤 5:三台机器配置 /usr/local/redis/conf/sentinel.conf(内容完全相同)
port 26379
daemonize yes
protected-mode no
pidfile /usr/local/redis/run/sentinel_26379.pid
logfile /usr/local/redis/log/sentinel_26379.log
dir /usr/local/redis/data
# 监控名为 mymaster 的主库,quorum = 2(3 个哨兵的多数派)
sentinel monitor mymaster 192.168.1.220 6379 2
sentinel auth-pass mymaster 123456
sentinel down-after-milliseconds mymaster 5000
sentinel parallel-syncs mymaster 1
sentinel failover-timeout mymaster 60000
requirepass sentinel123
sentinel deny-scripts-reconfig yes
注意:
sentinel monitor里写的是主库地址,三台机器都写同一个。哨兵会通过主库的INFO replication自动发现所有从库,并通过主库的__sentinel__:hello频道自动发现其他哨兵,无需手工配置。
步骤 6:启动三个哨兵
# 三台机器分别执行
redis-sentinel /usr/local/redis/conf/sentinel.conf
# 验证
ps -ef | grep sentinel
netstat -tunlp | grep 26379
步骤 7:验证哨兵集群已正确组网
redis-cli -p 26379 -a sentinel123 --no-auth-warning INFO sentinel
# sentinel_masters:1
# master0:name=mymaster,status=ok,address=192.168.1.220:6379,slaves=2,sentinels=3
# ↑ 必须是 2 ↑ 必须是 3
# 确认能达到故障转移条件
redis-cli -p 26379 -a sentinel123 --no-auth-warning SENTINEL ckquorum mymaster
# OK 3 usable Sentinels. Quorum and failover authorization can be reached
# 查看识别到的从库
redis-cli -p 26379 -a sentinel123 --no-auth-warning SENTINEL replicas mymaster
# 查看识别到的其他哨兵
redis-cli -p 26379 -a sentinel123 --no-auth-warning SENTINEL sentinels mymaster
# 查询当前主库地址
redis-cli -p 26379 -a sentinel123 --no-auth-warning SENTINEL get-master-addr-by-name mymaster
# 1) "192.168.1.220"
# 2) "6379"
故障转移验证
方式 1:手动触发(演练)
# 打开一个终端实时观察哨兵事件
redis-cli -p 26379 -a sentinel123 --no-auth-warning PSUBSCRIBE '*'
# 另一个终端触发切换
redis-cli -p 26379 -a sentinel123 --no-auth-warning SENTINEL failover mymaster
# OK
方式 2:模拟真实宕机
# 在主库 .220 上杀掉 Redis 进程(模拟崩溃)
redis-cli -h 192.168.1.220 -a 123456 --no-auth-warning DEBUG SLEEP 30 &
# 或者直接
pkill -9 redis-server
# 或者用防火墙模拟网络分区(更真实)
iptables -A INPUT -p tcp --dport 6379 -j DROP
观察哨兵日志(约 5 秒后开始切换):
tail -f /usr/local/redis/log/sentinel_26379.log
典型日志序列:
+sdown master mymaster 192.168.1.220 6379
+odown master mymaster 192.168.1.220 6379 #quorum 2/2
+new-epoch 1
+try-failover master mymaster 192.168.1.220 6379
+vote-for-leader 7f3a... 1
+elected-leader master mymaster 192.168.1.220 6379
+failover-state-select-slave master mymaster 192.168.1.220 6379
+selected-slave slave 192.168.1.221:6379 ...
+failover-state-send-slaveof-noone slave 192.168.1.221:6379 ...
+failover-state-wait-promotion slave 192.168.1.221:6379 ...
+promoted-slave slave 192.168.1.221:6379 ...
+failover-state-reconf-slaves master mymaster 192.168.1.220 6379
+slave-reconf-sent slave 192.168.1.222:6379 ...
+slave-reconf-inprog slave 192.168.1.222:6379 ...
+slave-reconf-done slave 192.168.1.222:6379 ...
+failover-end master mymaster 192.168.1.220 6379
+switch-master mymaster 192.168.1.220 6379 192.168.1.221 6379
+slave slave 192.168.1.222:6379 ... @ mymaster 192.168.1.221 6379
+slave slave 192.168.1.220:6379 ... @ mymaster 192.168.1.221 6379
验证切换结果:
# 1. 哨兵报告的新主库地址已变化
redis-cli -p 26379 -a sentinel123 --no-auth-warning SENTINEL get-master-addr-by-name mymaster
# 1) "192.168.1.221"
# 2) "6379"
# 2. .221 已成为主库
redis-cli -h 192.168.1.221 -a 123456 --no-auth-warning INFO replication
# role:master
# connected_slaves:1
# slave0:ip=192.168.1.222,port=6379,state=online,...
# 3. .222 已指向新主库
redis-cli -h 192.168.1.222 -a 123456 --no-auth-warning INFO replication
# role:slave
# master_host:192.168.1.221
# master_link_status:up
# 4. 新主库可写
redis-cli -h 192.168.1.221 -a 123456 --no-auth-warning SET afterfailover "ok"
# OK
redis-cli -h 192.168.1.222 -a 123456 --no-auth-warning GET afterfailover
# "ok"
# 5. 哨兵的 sentinel.conf 已被自动改写
grep "sentinel monitor" /usr/local/redis/conf/sentinel.conf
# sentinel monitor mymaster 192.168.1.221 6379 2 ← 地址已更新
恢复旧主库,观察其自动降级为从库:
# 恢复网络(如果用了 iptables)
iptables -D INPUT -p tcp --dport 6379 -j DROP
# 重新启动旧主库
redis-server /usr/local/redis/conf/redis.conf
# 稍等几秒后查看,它已经变成从库了
redis-cli -h 192.168.1.220 -a 123456 --no-auth-warning INFO replication
# role:slave
# master_host:192.168.1.221
# master_link_status:up
# 它的 redis.conf 也被哨兵改写,追加/修改了 replicaof
grep -i replicaof /usr/local/redis/conf/redis.conf
# replicaof 192.168.1.221 6379
这就是 masterauth 必须在所有节点配置的原因:旧主库降级后需要用
masterauth去认证连接新主库,否则会一直master_link_status:down。
哨兵常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
哨兵日志报 -failover-abort-not-elected |
存活哨兵数不足多数派(如 3 个哨兵挂了 2 个) | 保证至少 3 个哨兵且分布在不同物理机;用 SENTINEL ckquorum 提前检查 |
切换后新主库 master_link_status:down |
从库/旧主库没配 masterauth |
所有节点(包括主库)都必须配置 masterauth,且与 requirepass 一致 |
哨兵之间互相发现不了(sentinels=1) |
网络不通;bind 只绑了 127.0.0.1;protected-mode yes 且无密码;防火墙没放行 26379 |
检查 26379 端口连通性;哨兵配置 protected-mode no 或设 requirepass;确认 bind 0.0.0.0 |
只发现 1 个从库(slaves=1) |
从库的 replicaof 没生效;从库密码错;从库上 replica-announce-ip 需要配置(NAT/容器) |
在主库执行 INFO replication 确认从库确实连上了;容器环境配 replica-announce-ip |
| 频繁误切主(脑裂式抖动) | down-after-milliseconds 太小(如 1000),网络抖动就触发;主库因大 key/慢命令/RDB fork 卡住而无法回复 PING |
调大到 5000~30000;治理大 key 与慢查询;关闭 THP;用 no-appendfsync-on-rewrite yes |
| 脑裂导致数据丢失 | 网络分区时旧主库仍在接受写入,恢复后被降级并清空数据去同步新主库,分区期间的写入全部丢失 | 主库配置 min-replicas-to-write 1 + min-replicas-max-lag 10,让失去从库的主库自动拒绝写入 |
| 故障转移后客户端仍连旧主库 | 客户端硬编码了主库 IP,没通过哨兵获取地址 | 客户端必须使用哨兵模式连接,并订阅 +switch-master |
哨兵进入 TILT 模式(sentinel_tilt:1) |
系统时钟跳变(NTP 大幅校时)、进程被长时间挂起、机器负载过高导致定时器严重延迟 | 配置 NTP 使用 slew 模式(ntpd -x)避免时钟跳变;降低机器负载;TILT 模式下哨兵只观察不动作,30 秒后自动恢复 |
| sentinel.conf 被覆盖导致哨兵失忆 | 用配置管理工具强制同步了 sentinel.conf | 把 sentinel.conf 排除在配置管理之外,只在初始化时下发 |
SENTINEL failover 报 NOGOODSLAVE |
所有从库都被过滤掉了(下线、延迟太大、priority=0) | 检查从库状态与 replica-priority;确认至少有 1 个健康从库 |
| 两次故障转移间隔很久无法再切 | failover-timeout * 2 的冷却期未过 |
等待或调小 failover-timeout(不建议低于 30000) |
| 主从数据量大,切换后服务长时间不可用 | parallel-syncs 设置过大,多个从库同时全量同步打满新主库 |
设 parallel-syncs 1;增大 repl-backlog-size 以便走部分重同步 |
哨兵方案的固有局限:
| 局限 | 说明 |
|---|---|
| 不能横向扩展写能力 | 只有一个主库,写入受单机内存与单核 CPU 限制 |
| 不能扩展存储容量 | 数据集必须能装进单机内存 |
| 故障转移期间短暂不可用 | 从发现到切换完成通常几秒到几十秒,期间写入失败 |
| 异步复制仍可能丢数据 | 已 ACK 给客户端但未同步到从库的写入,切主后丢失 |
| 客户端必须支持哨兵协议 | 老旧客户端或直连方式无法受益 |
需要横向扩展写能力与存储容量时,应使用 Redis Cluster(见下一章)。
七、Redis Cluster 集群
7.1 去中心化架构
Redis Cluster 是 Redis 官方的分布式分片方案(Redis 3.0+),核心特点是去中心化:
┌─────────────────────────────────────────────┐
│ Redis Cluster(去中心化) │
│ │
│ Master A Master B Master C │
│ slot 0-5460 slot 5461- slot 10923- │
│ 10922 16383 │
│ │ Gossip 协议互相通信(总线端口)│ │
│ └──────────┬──────────┬─────────┘ │
│ ↓复制 │ ↓复制 │ ↓复制 │
│ Slave A' Slave B' Slave C' │
└─────────────────────────────────────────────┘
↑
客户端(-c 集群模式)
直连任意节点,按需被重定向到正确节点
| 特点 | 说明 |
|---|---|
| 无中心节点 | 没有 proxy、没有配置中心,每个节点都保存完整的集群拓扑信息 |
| 数据自动分片 | 数据按哈希槽分散到多个主节点,写能力与存储容量随节点数线性扩展 |
| 每个分片可带从节点 | 主节点故障时从节点自动提升,无需哨兵 |
| 客户端智能路由 | 客户端缓存"槽 → 节点"映射表,直接连正确的节点,只有映射变化时才被重定向 |
| 最小规模 3 主 | 官方要求至少 3 个主节点(多数派投票需要),生产建议 3 主 3 从起 |
7.2 16384 个哈希槽
Redis Cluster 不使用一致性哈希,而是引入 哈希槽(hash slot) 的概念:整个集群固定有 16384(2¹⁴) 个槽,每个主节点负责其中一部分。
三主集群的默认槽分配:
| 节点 | 槽范围 | 槽数量 |
|---|---|---|
| Master A | 0 ~ 5460 | 5461 |
| Master B | 5461 ~ 10922 | 5462 |
| Master C | 10923 ~ 16383 | 5461 |
为什么是 16384 而不是 65536:
- 心跳包大小:Gossip 心跳包中要携带一个槽位图(bitmap)。16384 bit = 2KB,65536 bit = 8KB。Redis 每秒要发大量心跳,8KB 的包会浪费太多带宽。
- 集群规模上限:Redis 官方不建议集群超过 1000 个节点。16384 个槽对 1000 个节点来说,每节点约 16 个槽,粒度已经足够细。
- 压缩效率:节点数较少时,槽位图有大量连续的 0 或 1,压缩率很高;槽数太多会降低压缩效果。
7.3 槽与键的映射(CRC16)
slot = CRC16(key) mod 16384
CRC16是 16 位循环冗余校验,输出 0~65535;mod 16384等价于取低 14 位(& 0x3FFF);- 相同的 key 永远映射到相同的槽;不同的 key 均匀分散。
# 查看某个 key 属于哪个槽
127.0.0.1:6379> CLUSTER KEYSLOT "user:1001"
(integer) 8093
127.0.0.1:6379> CLUSTER KEYSLOT "user:1002"
(integer) 5254
# 统计某个槽里有多少 key
127.0.0.1:6379> CLUSTER COUNTKEYSINSLOT 8093
(integer) 1
# 列出某个槽里的 key
127.0.0.1:6379> CLUSTER GETKEYSINSLOT 8093 10
1) "user:1001"
hash tag(哈希标签):如果 key 中包含 {...},则只用大括号内的内容计算槽位。这是让多个相关 key 落到同一节点的唯一手段。
127.0.0.1:6379> CLUSTER KEYSLOT "{user1001}:profile"
(integer) 5254
127.0.0.1:6379> CLUSTER KEYSLOT "{user1001}:orders"
(integer) 5254
127.0.0.1:6379> CLUSTER KEYSLOT "{user1001}:cart"
(integer) 5254
# 三个 key 槽位相同 → 位于同一节点 → 可以做多键操作与事务
hash tag 规则:只取第一个
{和它之后第一个}之间的内容;如果{}内为空或没有},则用整个 key。注意:滥用 hash tag(如所有 key 都用同一个 tag)会导致所有数据集中到一个节点,造成数据倾斜和热点,完全失去分片的意义。
7.4 Gossip 协议与总线端口
总线端口(cluster bus port)= 客户端端口 + 10000:
| 用途 | 端口 |
|---|---|
| 客户端连接 | 6379 |
| 集群总线(节点间通信) | 16379 |
| 客户端连接 | 6380 |
| 集群总线 | 16380 |
部署要点:防火墙必须同时放行两个端口。只放行 6379 会导致节点无法互相通信,集群创建时一直卡住无响应。
# CentOS 7
firewall-cmd --permanent --add-port=6379/tcp
firewall-cmd --permanent --add-port=6380/tcp
firewall-cmd --permanent --add-port=16379/tcp
firewall-cmd --permanent --add-port=16380/tcp
firewall-cmd --reload
# CentOS 6
iptables -I INPUT -p tcp -m multiport --dports 6379,6380,16379,16380 -j ACCEPT
service iptables save
Gossip 协议消息类型:
| 消息 | 作用 |
|---|---|
MEET |
邀请新节点加入集群(由 CLUSTER MEET 触发,只需发一次,之后通过 Gossip 扩散) |
PING |
每秒随机挑选几个节点发送,携带自身信息 + 随机几个其他节点的信息(这就是"流言"扩散) |
PONG |
对 PING/MEET 的回复,也用于主动广播配置变更 |
FAIL |
广播某节点已被判定为客观下线(PFAIL → FAIL) |
PUBLISH |
集群内广播 Pub/Sub 消息 |
Gossip 的工作方式:每个节点每 100ms 执行一次集群定时任务,从已知节点中随机挑选 5 个,找出其中"最久未通信"的一个发送 PING;同时确保对超过 cluster-node-timeout / 2 未通信的节点也发 PING。信息通过这种随机传播最终扩散到全集群(最终一致,不是强一致)。
故障判定:
| 状态 | 说明 |
|---|---|
| PFAIL(Possible FAIL) | 单个节点发现某节点在 cluster-node-timeout 内未回复 PONG,标记为疑似下线(类似哨兵的 SDOWN) |
| FAIL | 通过 Gossip 交换,超过半数的主节点都报告某节点 PFAIL,则升级为 FAIL 并广播(类似 ODOWN) |
7.5 主从与故障转移
故障转移流程:
1. 主节点 B 被半数以上主节点标记为 FAIL
2. B 的从节点 B' 发现自己的主节点 FAIL
3. B' 检查资格:
- 与主节点断连时间 < cluster-node-timeout * cluster-replica-validity-factor
(若 factor = 0 则跳过此检查,任何时候都有资格)
4. B' 根据复制偏移量计算延迟:offset 越大(数据越新),等待时间越短
延迟 = 500ms + random(0~500ms) + rank * 1000ms
5. 延迟结束后,B' 把 currentEpoch + 1,向所有【主节点】拉票
(FAILOVER_AUTH_REQUEST)
6. 其他主节点在同一 epoch 内只投一票(FAILOVER_AUTH_ACK)
7. B' 获得【超过半数主节点】的投票 → 成为新主节点
8. B' 接管 B 的所有槽位,广播 PONG 通知全集群更新拓扑
9. 客户端的槽位缓存失效 → 收到 MOVED 后更新映射表
10. 旧主 B 恢复后,自动变成 B' 的从节点
关键要求与配置:
| 要点 | 说明 |
|---|---|
| 至少 3 个主节点 | 投票需要主节点的多数派。2 主集群挂 1 个后无法达到多数派 |
| 只有主节点有投票权 | 从节点不参与投票 |
| 主从必须跨物理机 | 主和它的从在同一台机器上,机器宕机则该分片彻底失联 |
cluster-node-timeout |
故障发现时间,默认 15000ms。太小易误判,太大故障恢复慢 |
cluster-replica-validity-factor |
从节点资格因子。设 0 表示从节点无论断连多久都可参选(可用性优先,但可能丢更多数据) |
cluster-require-full-coverage |
yes 时任一槽无人负责就整体拒绝服务;no 时其他槽仍可用。生产多设 no 以提高可用性 |
cluster-migration-barrier |
从节点自动迁移的门槛,保证不会把某个主节点的最后一个从节点抢走 |
无从节点的主节点故障 → 该分片的槽位无人负责 → cluster_state:fail(若 cluster-require-full-coverage yes)→ 整个集群拒绝服务。因此每个主节点必须至少配 1 个从节点。
7.6 集群搭建完整实操(Redis 5+ / redis-cli --cluster)
Redis 5.0 起,集群管理功能被内置到 redis-cli --cluster,不再需要 Ruby 与 redis-trib.rb。
规划:单机六实例(教学演示)
| 实例 | 端口 | 总线端口 | 角色 |
|---|---|---|---|
| 1 | 7001 | 17001 | Master |
| 2 | 7002 | 17002 | Master |
| 3 | 7003 | 17003 | Master |
| 4 | 7004 | 17004 | Slave |
| 5 | 7005 | 17005 | Slave |
| 6 | 7006 | 17006 | Slave |
生产环境务必分散到至少 3 台物理机,且保证主从不在同一台机器上。
步骤 1:创建目录结构
mkdir -p /usr/local/redis-cluster/{7001,7002,7003,7004,7005,7006}/{conf,data,log,run}
步骤 2:生成六份配置文件
for port in 7001 7002 7003 7004 7005 7006; do
cat > /usr/local/redis-cluster/$port/conf/redis.conf <<EOF
# ===== 基础配置 =====
bind 0.0.0.0
protected-mode no
port $port
daemonize yes
pidfile /usr/local/redis-cluster/$port/run/redis_$port.pid
logfile /usr/local/redis-cluster/$port/log/redis_$port.log
dir /usr/local/redis-cluster/$port/data
dbfilename dump_$port.rdb
# ===== 集群配置(核心)=====
cluster-enabled yes
cluster-config-file nodes-$port.conf
cluster-node-timeout 15000
cluster-replica-validity-factor 10
cluster-migration-barrier 1
cluster-require-full-coverage no
# ===== 持久化 =====
appendonly yes
appendfilename "appendonly_$port.aof"
appendfsync everysec
aof-use-rdb-preamble yes
# ===== 安全(集群模式下 masterauth 与 requirepass 必须同时设置且一致)=====
requirepass 123456
masterauth 123456
# ===== 内存 =====
maxmemory 1gb
maxmemory-policy allkeys-lru
EOF
done
三个关键集群参数:
| 参数 | 作用 |
|---|---|
cluster-enabled yes |
开启集群模式。不开这个,实例就是普通单机 Redis |
cluster-config-file nodes-7001.conf |
集群状态持久化文件,由 Redis 自动维护(记录节点 ID、槽分配、主从关系)。每个实例必须不同名,且不要手工编辑 |
cluster-node-timeout 15000 |
节点失联判定超时(毫秒),同时也影响故障转移的触发速度 |
集群模式下必须同时配置
requirepass与masterauth(且值相同),因为集群中的每个节点既可能是主也可能是从,节点间需要用masterauth互相认证。
步骤 3:启动六个实例
for port in 7001 7002 7003 7004 7005 7006; do
redis-server /usr/local/redis-cluster/$port/conf/redis.conf
done
# 验证六个端口都在监听(客户端端口 + 总线端口)
netstat -tunlp | grep -E '700[1-6]|1700[1-6]'
ps -ef | grep redis-server
# 集群模式的进程后面会带 [cluster] 标记
此时每个节点都是独立的、不认识彼此的集群节点:
redis-cli -p 7001 -a 123456 --no-auth-warning CLUSTER INFO
# cluster_state:fail ← 槽未分配,状态为 fail
# cluster_slots_assigned:0
# cluster_known_nodes:1 ← 只认识自己
步骤 4:创建集群
redis-cli -a 123456 --no-auth-warning --cluster create \
127.0.0.1:7001 \
127.0.0.1:7002 \
127.0.0.1:7003 \
127.0.0.1:7004 \
127.0.0.1:7005 \
127.0.0.1:7006 \
--cluster-replicas 1
参数说明:
| 参数 | 说明 |
|---|---|
--cluster create |
创建集群子命令 |
| 节点列表 | 格式必须是 IP:端口,前 N 个作为主节点,后面的作为从节点 |
--cluster-replicas 1 |
每个主节点配备 1 个从节点。6 个节点 ÷ (1主+1从) = 3 个分片 |
--cluster-yes |
跳过交互确认(自动化脚本用) |
交互式确认输出:
>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 127.0.0.1:7005 to 127.0.0.1:7001
Adding replica 127.0.0.1:7006 to 127.0.0.1:7002
Adding replica 127.0.0.1:7004 to 127.0.0.1:7003
M: a1b2c3... 127.0.0.1:7001
slots:[0-5460] (5461 slots) master
M: d4e5f6... 127.0.0.1:7002
slots:[5461-10922] (5462 slots) master
M: g7h8i9... 127.0.0.1:7003
slots:[10923-16383] (5461 slots) master
S: j1k2l3... 127.0.0.1:7004
replicates g7h8i9...
S: m4n5o6... 127.0.0.1:7005
replicates a1b2c3...
S: p7q8r9... 127.0.0.1:7006
replicates d4e5f6...
Can I set the above configuration? (type 'yes' to accept):
输入 yes 后:
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
....
>>> Performing Cluster Check (using node 127.0.0.1:7001)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
看到 [OK] All 16384 slots covered. 即表示集群创建成功。
步骤 5:查看集群状态
# cluster info:集群整体状态
redis-cli -p 7001 -a 123456 --no-auth-warning CLUSTER INFO
cluster_state:ok # 【最重要】ok 表示集群正常,fail 表示不可用
cluster_slots_assigned:16384 # 已分配的槽数,必须是 16384
cluster_slots_ok:16384 # 状态正常的槽数
cluster_slots_pfail:0 # 疑似失败的槽数
cluster_slots_fail:0 # 已失败的槽数
cluster_known_nodes:6 # 已知节点总数
cluster_size:3 # 【负责槽的主节点数】,即分片数
cluster_current_epoch:6 # 当前配置纪元
cluster_my_epoch:1
cluster_stats_messages_sent:1523
cluster_stats_messages_received:1520
# cluster nodes:节点详情与拓扑
redis-cli -p 7001 -a 123456 --no-auth-warning CLUSTER NODES
a1b2c3... 127.0.0.1:7001@17001 myself,master - 0 0 1 connected 0-5460
d4e5f6... 127.0.0.1:7002@17002 master - 0 1756000001000 2 connected 5461-10922
g7h8i9... 127.0.0.1:7003@17003 master - 0 1756000002000 3 connected 10923-16383
j1k2l3... 127.0.0.1:7004@17004 slave g7h8i9... 0 1756000003000 3 connected
m4n5o6... 127.0.0.1:7005@17005 slave a1b2c3... 0 1756000004000 1 connected
p7q8r9... 127.0.0.1:7006@17006 slave d4e5f6... 0 1756000005000 2 connected
字段含义(从左到右):
| 位置 | 字段 | 说明 |
|---|---|---|
| 1 | node-id | 40 位十六进制节点 ID,节点首次启动时生成,写入 nodes.conf,永不改变 |
| 2 | ip:port@bus-port | 客户端地址 @ 总线端口 |
| 3 | flags | myself(当前连接的节点)、master、slave、fail?(PFAIL)、fail(FAIL)、handshake、noaddr |
| 4 | master | 若是从节点,这里是主节点 ID;主节点为 - |
| 5 | ping-sent | 最近一次发送 PING 的时间戳,0 表示无待回复的 PING |
| 6 | pong-recv | 最近一次收到 PONG 的时间戳 |
| 7 | config-epoch | 配置纪元 |
| 8 | link-state | connected / disconnected |
| 9 | slot | 负责的槽范围,可以是多段。[slot-<-nodeid] 表示正在导入,[slot->-nodeid] 表示正在迁出 |
其他常用集群命令:
# 完整体检(比 CLUSTER INFO 更详细)
redis-cli -a 123456 --no-auth-warning --cluster check 127.0.0.1:7001
# 查看槽分布统计与内存使用
redis-cli -a 123456 --no-auth-warning --cluster info 127.0.0.1:7001
# 只看主节点 / 从节点
redis-cli -p 7001 -a 123456 --no-auth-warning CLUSTER NODES | grep master
redis-cli -p 7001 -a 123456 --no-auth-warning CLUSTER SLOTS
redis-cli -p 7001 -a 123456 --no-auth-warning CLUSTER SHARDS # Redis 7+
# 查看指定主节点的从节点
redis-cli -p 7001 -a 123456 --no-auth-warning CLUSTER REPLICAS a1b2c3...
# 查看自己的节点 ID
redis-cli -p 7001 -a 123456 --no-auth-warning CLUSTER MYID
7.7 集群客户端:-c 参数与 MOVED / ASK 重定向
不加 -c:普通客户端,遇到不属于本节点的 key 直接报错
redis-cli -p 7001 -a 123456 --no-auth-warning
127.0.0.1:7001> set url http://www.baidu.com
(error) MOVED 12704 127.0.0.1:7003
MOVED 12704 127.0.0.1:7003 的含义:url 的槽位是 12704,该槽由 7003 负责,请重新连到 7003 去执行。普通客户端不会自动跳转,只把错误抛给用户。
加 -c:集群模式客户端,自动跟随重定向
redis-cli -c -p 7001 -a 123456 --no-auth-warning
127.0.0.1:7001> set url http://www.baidu.com
-> Redirected to slot [12704] located at 127.0.0.1:7003
OK
127.0.0.1:7003> # 【注意】提示符已经变成 7003,连接被切换过去了
127.0.0.1:7003> get url
"http://www.baidu.com"
127.0.0.1:7003> set name zhangsan
-> Redirected to slot [5798] located at 127.0.0.1:7002
OK
127.0.0.1:7002>
# 查看某个 key 的槽位与所属节点
127.0.0.1:7002> cluster keyslot url
(integer) 12704
MOVED 与 ASK 的区别(面试高频)
| MOVED | ASK | |
|---|---|---|
| 含义 | 槽已经永久属于目标节点 | 槽正在迁移中,这个 key 已经搬到目标节点了 |
| 触发时机 | 槽不在当前节点 | 槽在当前节点但处于 MIGRATING 状态,且该 key 已迁走 |
| 客户端行为 | 更新本地槽映射缓存,之后所有该槽的请求直发新节点 | 不更新缓存,仅本次请求转向目标节点,且必须先发 ASKING 命令 |
| 性质 | 永久重定向(类似 HTTP 301) | 临时重定向(类似 HTTP 302) |
客户端请求 key 属于 slot 100
│
├─ slot 100 由本节点负责 ────────────► 直接执行
│
├─ slot 100 不由本节点负责 ──────────► 返回 MOVED <slot> <ip:port>
│
└─ slot 100 由本节点负责但正在迁出
├─ key 还在本节点 ─────────────► 直接执行
└─ key 已迁走 ────────────────► 返回 ASK <slot> <ip:port>
客户端:ASKING + 重发命令
智能客户端(Smart Client):JedisCluster、Lettuce、redis-py-cluster、go-redis 等在初始化时会执行
CLUSTER SLOTS/CLUSTER SHARDS拉取完整槽映射并缓存在本地,之后直接把请求发到正确节点,零跳转开销;只有收到 MOVED 时才刷新缓存。这是生产环境的标准做法,不要在代理层反复重定向。
7.8 增删节点与槽迁移(reshard)
7.8.1 添加主节点
# 1) 先按前述方式准备并启动新实例 7007(cluster-enabled yes)
mkdir -p /usr/local/redis-cluster/7007/{conf,data,log,run}
# ...复制配置模板并把 7001 全部替换为 7007...
redis-server /usr/local/redis-cluster/7007/conf/redis.conf
# 2) 把新节点加入集群(第一个地址是新节点,第二个是集群中任意已有节点)
redis-cli -a 123456 --no-auth-warning --cluster add-node \
127.0.0.1:7007 127.0.0.1:7001
# 【等价的原生命令】
redis-cli -p 7007 -a 123456 --no-auth-warning CLUSTER MEET 127.0.0.1 7001
此时 7007 已在集群中,但槽数为 0,不承载任何数据:
x1y2z3... 127.0.0.1:7007@17007 master - 0 1756000010000 0 connected
↑ 没有槽范围
7.8.2 迁移槽位(reshard)
redis-cli -a 123456 --no-auth-warning --cluster reshard 127.0.0.1:7001
交互式提问:
How many slots do you want to move (from 1 to 16384)? 4096
← 要迁移多少个槽。四个主节点均分:16384/4 = 4096
What is the receiving node ID? x1y2z3...
← 接收槽的目标节点 ID(新节点 7007 的 ID)
Please enter all the source node IDs.
Type 'all' to use all the nodes as source nodes for the hash slots.
Type 'done' once you entered all the source nodes IDs.
Source node #1: all
← 输入 all 表示从所有现有主节点均匀抽取;也可逐个输入源节点 ID 后以 done 结束
Do you want to proceed with the proposed reshard plan (yes/no)? yes
非交互式(脚本化):
redis-cli -a 123456 --no-auth-warning --cluster reshard 127.0.0.1:7001 \
--cluster-from a1b2c3...,d4e5f6...,g7h8i9... \
--cluster-to x1y2z3... \
--cluster-slots 4096 \
--cluster-yes
自动均衡(推荐):
# 让所有主节点的槽数自动趋于平均
redis-cli -a 123456 --no-auth-warning --cluster rebalance 127.0.0.1:7001
# 按权重分配(性能强的机器多担槽)
redis-cli -a 123456 --no-auth-warning --cluster rebalance 127.0.0.1:7001 \
--cluster-weight a1b2c3...=2 d4e5f6...=1 g7h8i9...=1
# 让新加入的空节点参与均衡(默认空节点不参与)
redis-cli -a 123456 --no-auth-warning --cluster rebalance 127.0.0.1:7001 \
--cluster-use-empty-masters
槽迁移是在线的:迁移期间集群持续对外服务,通过 ASK 重定向保证正确性。但迁移会产生大量
MIGRATE命令(同步阻塞式搬 key),大 key 会造成明显卡顿,务必在业务低峰期操作。
7.8.3 添加从节点
# 方式一:加入时直接指定主节点
redis-cli -a 123456 --no-auth-warning --cluster add-node \
127.0.0.1:7008 127.0.0.1:7001 \
--cluster-slave \
--cluster-master-id x1y2z3...
# 方式二:先作为主节点加入,再转为从节点
redis-cli -a 123456 --no-auth-warning --cluster add-node 127.0.0.1:7008 127.0.0.1:7001
redis-cli -p 7008 -a 123456 --no-auth-warning CLUSTER REPLICATE x1y2z3...
# 不指定 --cluster-master-id 时,会自动挂到从节点最少的主节点下
7.8.4 删除节点
# 【删除从节点】直接删,无需迁移数据
redis-cli -a 123456 --no-auth-warning --cluster del-node 127.0.0.1:7001 <从节点ID>
# 【删除主节点】必须先把槽全部迁走,否则报错:
# [ERR] Node 127.0.0.1:7007 is not empty! Reshard data away and try again.
# 1) 把 7007 的 4096 个槽迁回其他节点
redis-cli -a 123456 --no-auth-warning --cluster reshard 127.0.0.1:7001 \
--cluster-from x1y2z3... \
--cluster-to a1b2c3... \
--cluster-slots 4096 \
--cluster-yes
# 2) 确认槽数为 0 后再删除
redis-cli -a 123456 --no-auth-warning --cluster del-node 127.0.0.1:7001 x1y2z3...
# 3) 被删除的节点会自行 SHUTDOWN 吗?不会,需要手工停止
redis-cli -p 7007 -a 123456 --no-auth-warning shutdown nosave
忘记集群关系(强制踢出):
# 让集群中每个节点都忘记指定节点(60 秒内该节点不会被重新加入)
redis-cli -p 7001 -a 123456 --no-auth-warning CLUSTER FORGET x1y2z3...
# 【注意】必须在所有其他节点上都执行一遍,否则 Gossip 会把它重新传播回来
重置节点(把节点变回全新的独立节点):
redis-cli -p 7007 -a 123456 --no-auth-warning CLUSTER RESET SOFT # 软重置:清集群信息,保留数据
redis-cli -p 7007 -a 123456 --no-auth-warning CLUSTER RESET HARD # 硬重置:同时生成新的 node-id
# 注意:有槽或有数据时 RESET 会失败,需先 FLUSHALL
7.9 故障转移验证
7.9.1 自动故障转移
# 1) 记录当前拓扑:7001(master) ← 7005(slave)
redis-cli -p 7001 -a 123456 --no-auth-warning CLUSTER NODES | grep -E '7001|7005'
# 2) 模拟 7001 宕机
redis-cli -p 7001 -a 123456 --no-auth-warning DEBUG SEGFAULT
# 或
redis-cli -p 7001 -a 123456 --no-auth-warning SHUTDOWN NOSAVE
# 或
kill -9 $(cat /usr/local/redis-cluster/7001/run/redis_7001.pid)
# 3) 等待 cluster-node-timeout(15 秒)+ 选举时间,观察日志
tail -f /usr/local/redis-cluster/7005/log/redis_7005.log
从节点 7005 日志:
# Connection with master lost.
* Caching the disconnected master state.
* MASTER <-> REPLICA sync started
# Cluster state changed: fail
* Marking node a1b2c3... as failing (quorum reached). ← 达成 FAIL 共识
# Start of election delayed for 823 milliseconds ... ← 延迟选举,避免同时发起
# Starting a failover election for epoch 7.
# Failover election won: I'm the new master. ← 赢得选举
# configEpoch set to 7 after successful failover
* Discarding previously cached master state.
# Setting secondary replication ID for ...
# Cluster state changed: ok ← 集群恢复
# 4) 验证:7005 已升为 master,接管了 0-5460 槽
redis-cli -p 7005 -a 123456 --no-auth-warning CLUSTER NODES
# m4n5o6... 127.0.0.1:7005@17005 myself,master - 0 0 7 connected 0-5460
# a1b2c3... 127.0.0.1:7001@17001 master,fail - 1756000100000 1756000085000 1 disconnected
redis-cli -p 7005 -a 123456 --no-auth-warning CLUSTER INFO | grep state
# cluster_state:ok
# 5) 数据仍可正常读写
redis-cli -c -p 7002 -a 123456 --no-auth-warning set name lisi
redis-cli -c -p 7002 -a 123456 --no-auth-warning get name
# 6) 重启原主节点 7001,它会自动作为 7005 的从节点加入
redis-server /usr/local/redis-cluster/7001/conf/redis.conf
redis-cli -p 7001 -a 123456 --no-auth-warning CLUSTER NODES | grep myself
# a1b2c3... 127.0.0.1:7001@17001 myself,slave m4n5o6... 0 0 7 connected
7.9.2 手动故障转移(运维必备,用于滚动升级)
在从节点上执行:
# 标准手动切换:先等主从数据完全同步,再切换(数据零丢失,主库短暂只读)
redis-cli -p 7001 -a 123456 --no-auth-warning CLUSTER FAILOVER
# FORCE:不与主节点协商,直接发起选举(主节点已不可访问但未被判 FAIL 时用)
redis-cli -p 7001 -a 123456 --no-auth-warning CLUSTER FAILOVER FORCE
# TAKEOVER:跳过集群选举投票,单方面提升自己(【危险】可能产生脑裂与数据丢失,仅灾难恢复时用)
redis-cli -p 7001 -a 123456 --no-auth-warning CLUSTER FAILOVER TAKEOVER
| 选项 | 与主协商 | 需要投票 | 数据丢失风险 | 使用场景 |
|---|---|---|---|---|
| 默认 | 是 | 是 | 无 | 主机维护、版本升级(推荐) |
FORCE |
否 | 是 | 极小 | 主节点失联但集群未判 FAIL |
TAKEOVER |
否 | 否 | 有 | 多数主节点已挂,集群无法投票的灾难场景 |
7.9.3 集群不可用的场景(重要)
| 场景 | 集群状态 | 原因 |
|---|---|---|
| 某主节点挂了,但有从节点 | 短暂 fail → ok | 从节点自动升主 |
| 某主节点挂了,且无从节点 | fail(若 cluster-require-full-coverage yes) |
部分槽无人负责 |
| 超过半数主节点挂了 | fail | 无法完成故障转移投票(需要主节点多数派) |
| 某主节点与其从节点同时挂了 | fail | 该分片彻底不可用 |
cluster-require-full-coverage的取舍:
yes(默认):部分槽不可用时整个集群拒绝服务,保证数据视图完整,避免业务读到"假的不存在"。no:只让故障分片的 key 报错(CLUSTERDOWN),其余分片正常服务,可用性更高。生产建议设为no,但业务代码必须能容忍部分 key 不可用。
7.10 集群的限制与 Hash Tag
7.10.1 多键操作的限制
集群模式下,多键命令要求所有 key 落在同一个槽,否则报错:
127.0.0.1:7001> mset k1 v1 k2 v2 k3 v3
(error) CROSSSLOT Keys in request don't hash to the same slot
127.0.0.1:7001> sinterstore dst set1 set2
(error) CROSSSLOT Keys in request don't hash to the same slot
受影响的命令:
| 类别 | 命令 |
|---|---|
| 批量读写 | MSET、MSETNX、MGET |
| 集合运算 | SINTER、SUNION、SDIFF 及其 *STORE 变体 |
| 有序集合 | ZUNIONSTORE、ZINTERSTORE、ZDIFFSTORE、ZRANGESTORE |
| 列表 | RPOPLPUSH、LMOVE、BRPOPLPUSH、BLMOVE |
| 其他 | SMOVE、COPY、RENAME、BITOP、PFMERGE、PFCOUNT(多 key)、GEORADIUS ... STORE |
| 脚本/事务 | Lua 脚本的 KEYS[]、MULTI 事务内的所有 key |
7.10.2 Hash Tag:强制多个 key 落到同一槽
在 key 中用 {} 括起一段内容,Redis 只对第一对 {} 中的内容计算 CRC16:
# 【不使用 hash tag】三个 key 分散在不同槽
127.0.0.1:7001> cluster keyslot user:1001:profile
(integer) 5798
127.0.0.1:7001> cluster keyslot user:1001:orders
(integer) 12045
127.0.0.1:7001> cluster keyslot user:1001:cart
(integer) 3391
# 【使用 hash tag】只对 user:1001 计算槽位,三个 key 必然同槽
127.0.0.1:7001> cluster keyslot {user:1001}:profile
(integer) 8517
127.0.0.1:7001> cluster keyslot {user:1001}:orders
(integer) 8517
127.0.0.1:7001> cluster keyslot {user:1001}:cart
(integer) 8517
# 于是多键操作可用了
127.0.0.1:7001> mset {user:1001}:profile "zhangsan" {user:1001}:cart "book"
OK
127.0.0.1:7001> mget {user:1001}:profile {user:1001}:cart
1) "zhangsan"
2) "book"
Hash Tag 计算规则:
| Key | 参与计算的部分 | 说明 |
|---|---|---|
{user1000}.following |
user1000 |
标准用法 |
foo{}{bar} |
foo{}{bar}(整个 key) |
第一个 {} 内为空 → 忽略 tag |
foo{{bar}}zap |
{bar |
取第一个 { 到第一个 } 之间 |
foo{bar}{zap} |
bar |
只取第一对 |
{}foo |
{}foo |
同上,空 tag 无效 |
Hash Tag 的风险:过度使用会导致数据倾斜——大量 key 挤到同一个槽/节点,形成热点。只在确实需要原子多键操作时使用,且 tag 的取值要足够分散(如用户 ID 而非固定字符串)。
7.10.3 其他集群限制
| 限制 | 说明 | 应对 |
|---|---|---|
| 只支持 0 号数据库 | SELECT 1 报错 (error) ERR SELECT is not allowed in cluster mode |
用 key 前缀做逻辑隔离 |
| 事务只能单槽 | MULTI 内跨槽 key 报 CROSSSLOT |
用 hash tag,或改为多次单节点事务 |
| Lua 脚本只能单槽 | EVAL 的 KEYS 必须同槽 |
用 hash tag |
| 不支持嵌套结构的跨节点操作 | 如 SORT ... BY ... GET 涉及多 key |
业务侧组装 |
| Pub/Sub 全集群广播 | 消息发到任意节点会广播到所有节点,节点多时带宽浪费 | Redis 7 起用 SPUBLISH/SSUBSCRIBE(分片 Pub/Sub) |
KEYS/SCAN/DBSIZE/FLUSHALL 只作用于单节点 |
需要遍历所有主节点 | redis-cli --cluster call 或循环所有节点 |
不支持 SWAPDB、MOVE |
单库限制 | — |
| 客户端必须支持集群协议 | 否则只能靠 MOVED 手动跳 | 用 JedisCluster / Lettuce / redis-py-cluster |
# 在集群所有节点上批量执行同一命令
redis-cli -a 123456 --no-auth-warning --cluster call 127.0.0.1:7001 DBSIZE
redis-cli -a 123456 --no-auth-warning --cluster call 127.0.0.1:7001 INFO memory
# 统计集群总 key 数
for p in 7001 7002 7003; do
redis-cli -p $p -a 123456 --no-auth-warning DBSIZE
done
7.11 三主三从生产部署实战(Redis 3.2 + redis-trib.rb)
本节为旧版本(Redis 3.x / 4.x)的完整原始部署流程。Redis 5.0 之前,集群管理工具是 Ruby 脚本
redis-trib.rb,因此必须先安装 Ruby 与 redis 的 gem 包。流程与前面redis-cli --cluster版本一一对应,运维老系统时仍会用到。
7.11.1 环境说明
| 项目 | 说明 |
|---|---|
| 操作系统 | CentOS 7,内核 3.10.0-693.el7.x86_64 |
| Redis 版本 | redis-3.2.3-x64 |
| Ruby 版本 | ruby-2.5.1 |
| Ruby gem 版本 | redis-4.0.2(或 redis-4.1.0.gem) |
| 集群规模 | 三主三从,共 6 个 Redis 实例 |
| 服务器 | 192.168.1.220、192.168.1.221、192.168.1.222 |
| 实例分布 | 每台主机运行 2 个实例:6379 与 6380 |
| 安装目录 | /opt/truplatform/redis(6379)、/opt/truplatform/redis-6380(6380) |
拓扑设计原则:三台主机各出一个 6379 作为 Master,各出一个 6380 作为 Slave,且 redis-trib.rb 会自动保证从节点不与其主节点在同一台主机上,从而任意单台主机宕机集群仍可用。
192.168.1.220 : 6379 (Master A) 6380 (Slave of B 或 C)
192.168.1.221 : 6379 (Master B) 6380 (Slave of A 或 C)
192.168.1.222 : 6379 (Master C) 6380 (Slave of A 或 B)
7.11.2 安装 Redis(第一个实例 6379)
# 进入解压后的安装包目录,执行安装脚本
cd redis-3.2.3-x64
sh ./redis_install.sh
安装完成后修改配置文件:
vim /opt/truplatform/redis/etc/redis.conf
需要修改的配置项:
| 配置项 | 修改前 | 修改后 | 说明 |
|---|---|---|---|
daemonize |
daemonize no |
daemonize yes |
以后台守护进程方式运行 |
bind |
bind 127.0.0.1 |
bind 0.0.0.0 |
监听所有网卡,否则其他节点无法连接 |
cluster-enabled |
# cluster-enabled yes(注释) |
cluster-enabled yes |
去掉注释,开启集群模式 |
cluster-slave-validity-factor |
cluster-slave-validity-factor 10 |
cluster-slave-validity-factor 0 |
从节点有效性判定因子,设为 0 表示无论从节点与主节点失联多久都可以参与故障转移(提高可用性) |
# /opt/truplatform/redis/etc/redis.conf 关键片段
daemonize yes
bind 0.0.0.0
cluster-enabled yes
cluster-slave-validity-factor 0
cluster-slave-validity-factor(新版名cluster-replica-validity-factor):从节点与主节点断开时间超过cluster-node-timeout × factor时,该从节点不再有资格被提升为主节点。设为0表示取消该限制,任何从节点都能参与选举——可用性优先;保留默认10则是数据一致性优先(避免过于陈旧的从节点上位)。
重启使配置生效:
service redis restart
7.11.3 部署第二个实例(6380)
直接复制整个安装目录,改造成 6380 实例:
cp -rf /opt/truplatform/redis/ /opt/truplatform/redis-6380
修改新实例的配置文件:
vim /opt/truplatform/redis-6380/etc/redis.conf
需要修改的四项(把所有 6379 相关路径与端口改为 6380):
| 配置项 | 修改前 | 修改后 |
|---|---|---|
port |
port 6379 |
port 6380 |
pidfile |
pidfile /var/run/redis_6379.pid |
pidfile /var/run/redis_6380.pid |
logfile |
logfile /opt/truplatform/redis/log/redis_6379.log |
logfile /opt/truplatform/redis-6380/log/redis_6380.log |
dir |
dir /opt/truplatform/redis/redisdb |
dir /opt/truplatform/redis-6380/redisdb |
# /opt/truplatform/redis-6380/etc/redis.conf 关键片段
port 6380
pidfile /var/run/redis_6380.pid
logfile /opt/truplatform/redis-6380/log/redis_6380.log
dir /opt/truplatform/redis-6380/redisdb
集群模式下还要注意
cluster-config-file也应区分(如nodes-6380.conf),否则两个实例会写到同一个 nodes 文件而互相覆盖。由于本例两个实例的dir目录不同,nodes 文件天然隔离。
7.11.4 配置 6380 的启动脚本(CentOS 6/7 SysV 方式)
# 1) 复制 6379 的启动脚本
cp /etc/init.d/redis /etc/init.d/redis-6380
# 2) 修改脚本内的端口、配置文件路径、pid 文件路径
vim /etc/init.d/redis-6380
脚本中需要修改的变量(把 6379 全部替换为 6380):
REDISPORT=6380
EXEC=/opt/truplatform/redis-6380/bin/redis-server
CLIEXEC=/opt/truplatform/redis-6380/bin/redis-cli
PIDFILE=/var/run/redis_6380.pid
CONF="/opt/truplatform/redis-6380/etc/redis.conf"
# 3) 加入系统服务并设置开机自启
chkconfig --add redis-6380
chkconfig redis-6380 on
# 4) 修改新实例目录的属主(否则 redis 用户无权写日志与数据文件)
chown -R redis:redis /opt/truplatform/redis-6380
# 5) 启动 6380 实例
service redis-6380 start
# 6) 验证端口监听(应看到 6380 与集群总线端口 16380)
netstat -tunlp | grep 6380
预期输出:
tcp 0 0 0.0.0.0:6380 0.0.0.0:* LISTEN 12345/redis-server
tcp 0 0 0.0.0.0:16380 0.0.0.0:* LISTEN 12345/redis-server
重要:以上 7.11.2 ~ 7.11.4 的全部步骤,需要在 192.168.1.221 与 192.168.1.222 上分别重复执行一遍,即三台主机上都要有 6379 和 6380 两个已启动的集群模式实例,共 6 个实例,然后才能创建集群。
CentOS 7 下也可以改用 systemd 管理(与 SysV 二选一):
# /etc/systemd/system/redis-6380.service
[Unit]
Description=Redis In-Memory Data Store (port 6380)
After=network.target
[Service]
Type=forking
User=redis
Group=redis
ExecStart=/opt/truplatform/redis-6380/bin/redis-server /opt/truplatform/redis-6380/etc/redis.conf
ExecStop=/opt/truplatform/redis-6380/bin/redis-cli -p 6380 shutdown
PIDFile=/var/run/redis_6380.pid
Restart=always
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now redis-6380
systemctl status redis-6380
| 操作 | CentOS 6(SysV) | CentOS 7(systemd) |
|---|---|---|
| 启动 | service redis-6380 start |
systemctl start redis-6380 |
| 停止 | service redis-6380 stop |
systemctl stop redis-6380 |
| 状态 | service redis-6380 status |
systemctl status redis-6380 |
| 开机自启 | chkconfig redis-6380 on |
systemctl enable redis-6380 |
| 查看自启 | chkconfig --list redis-6380 |
systemctl is-enabled redis-6380 |
7.11.5 安装 Ruby 2.5.1(仅需在其中一台主机上安装)
redis-trib.rb 是 Ruby 脚本,CentOS 7 自带的 Ruby 版本过低(2.0.0),无法满足 redis gem 的要求,需要源码编译新版本。
# 1) 解压并编译安装
tar xf ruby-2.5.1.tar.gz
cd ruby-2.5.1
./configure --prefix=/opt/truplatform/ruby
make && make install
安装 zlib 扩展(gem 安装包时需要解压缩,缺少会报 zlib is not part of the bundle):
cd ruby-2.5.1/ext/zlib
ruby extconf.rb
此时直接 make 会报错,需要手工修改 Makefile,在文件中加入一行:
vim Makefile
top_srcdir = ../..
说明:
extconf.rb生成的 Makefile 中top_srcdir变量取值不正确(指向了绝对路径或为空),导致找不到 Ruby 源码头文件。手工加上top_srcdir = ../..指向 ruby 源码根目录即可。
make && make install
安装 openssl 扩展(gem 从 https 源下载包时需要,缺少会报 Could not find openssl 或 SSL_connect 错误):
# 先安装系统 openssl 开发库
yum install openssl-devel -y
cd ruby-2.5.1/ext/openssl
ruby ./extconf.rb
# 同样需要修改 Makefile,加入 top_srcdir
vim Makefile
top_srcdir = ../..
make && make install
配置 Ruby 环境变量:
cat > /etc/profile.d/ruby.sh <<EOF
export PATH=\$PATH:/opt/truplatform/ruby/bin
EOF
source /etc/profile.d/ruby.sh
# 验证版本
ruby -v
预期输出:
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
# 同时确认 gem 可用
gem -v
which ruby gem
7.11.6 安装 redis 的 Ruby gem 包
方式一:在线安装(服务器能访问外网)
gem install redis
方式二:离线安装(生产环境无外网,推荐)
# 1) 在能上网的机器下载 gem 包
wget https://rubygems.org/downloads/redis-4.1.0.gem
# 2) 上传到服务器后本地安装
gem install --local redis-4.1.0.gem
预期输出:
Successfully installed redis-4.1.0
Parsing documentation for redis-4.1.0
1 gem installed
# 验证 gem 已安装
gem list | grep redis
7.11.7 修改 client.rb(设置默认连接地址与密码)
redis-trib.rb 通过 redis gem 连接各节点。若 Redis 设置了 requirepass,或 gem 默认连 127.0.0.1 导致连不上远程节点,就需要修改 gem 的默认连接参数。
vim /opt/truplatform/ruby/lib/ruby/gems/2.5.0/gems/redis-4.0.2/lib/redis/client.rb
路径中的
2.5.0是 Ruby 的 gems 目录版本号,redis-4.0.2是实际安装的 gem 版本号,请按gem list的结果替换。
找到 DEFAULTS 常量,修改 :host 与 :password 两项:
DEFAULTS = {
:url => lambda { ENV["REDIS_URL"] },
:scheme => "redis",
:host => "0.0.0.0", # 【修改】原为 "127.0.0.1"
:port => 6379,
:path => nil,
:timeout => 5.0,
:password => "redis", # 【修改】原为 nil,改为 Redis 的 requirepass 密码
:db => 0,
:driver => nil,
:id => nil,
:tcp_keepalive => 0,
:reconnect_attempts => 1,
:inherit_socket => false
}
7.11.8 创建集群
在安装了 Ruby 的那台主机上执行 redis-trib.rb:
redis-trib.rb create --replicas 1 \
192.168.1.220:6379 192.168.1.220:6380 \
192.168.1.221:6379 192.168.1.221:6380 \
192.168.1.222:6379 192.168.1.222:6380
参数说明:
| 参数 | 说明 |
|---|---|
create |
创建集群 |
--replicas 1 |
每个主节点分配 1 个从节点。6 个节点 → 3 主 3 从 |
| 节点列表 | 依次列出所有节点的 IP:端口。前 3 个为主节点,后 3 个为从节点 |
redis-trib.rb脚本位于 Redis 安装包的src/redis-trib.rb,可复制到/usr/local/bin/或用绝对路径调用:cp /opt/truplatform/redis/src/redis-trib.rb /usr/local/bin/ chmod +x /usr/local/bin/redis-trib.rb
执行后会打印分配方案并等待确认,输入 yes 继续:
>>> Creating cluster
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
192.168.1.220:6379
192.168.1.221:6379
192.168.1.222:6379
Adding replica 192.168.1.221:6380 to 192.168.1.220:6379
Adding replica 192.168.1.222:6380 to 192.168.1.221:6379
Adding replica 192.168.1.220:6380 to 192.168.1.222:6379
M: xxxx 192.168.1.220:6379
slots:0-5460 (5461 slots) master
M: xxxx 192.168.1.221:6379
slots:5461-10922 (5462 slots) master
M: xxxx 192.168.1.222:6379
slots:10923-16383 (5461 slots) master
S: xxxx 192.168.1.220:6380
replicates 192.168.1.222:6379
S: xxxx 192.168.1.221:6380
replicates 192.168.1.220:6379
S: xxxx 192.168.1.222:6380
replicates 192.168.1.221:6379
Can I set the above configuration? (type 'yes' to accept): yes
注意分配结果:
redis-trib.rb自动把每台主机的 6380 分配给另一台主机的 6379 作为从节点,实现了跨主机的主从分离。
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join....
>>> Performing Cluster Check (using node 192.168.1.220:6379)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
出现 [OK] All 16384 slots covered. 表示集群创建成功。
7.11.9 验证集群
# 连接集群(-c 表示集群模式,-a 指定密码)
redis-cli -c -a redis
查看集群信息:
127.0.0.1:6379> cluster info
cluster_state:ok # 集群状态正常
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6 # 6 个已知节点
cluster_size:3 # 3 个分片
cluster_current_epoch:6
cluster_my_epoch:1
cluster_stats_messages_sent:1234
cluster_stats_messages_received:1230
查看集群节点:
127.0.0.1:6379> cluster nodes
<id> 192.168.1.220:6379 myself,master - 0 0 1 connected 0-5460
<id> 192.168.1.221:6379 master - 0 1552000001000 2 connected 5461-10922
<id> 192.168.1.222:6379 master - 0 1552000002000 3 connected 10923-16383
<id> 192.168.1.221:6380 slave <220:6379的id> 0 1552000003000 4 connected
<id> 192.168.1.222:6380 slave <221:6379的id> 0 1552000004000 5 connected
<id> 192.168.1.220:6380 slave <222:6379的id> 0 1552000005000 6 connected
写入与读取测试:
127.0.0.1:6379> set url http://www.baidu.com
-> Redirected to slot [12704] located at 192.168.1.222:6379
OK
192.168.1.222:6379> get url
"http://www.baidu.com"
由于
url的槽位 12704 属于 192.168.1.222:6379,客户端被自动重定向过去。这正是集群工作正常的标志。
跨节点验证(在 6380 实例上读取同一个 key):
redis-cli -p 6380 -c -a redis
127.0.0.1:6380> get url
-> Redirected to slot [12704] located at 192.168.1.222:6379
"http://www.baidu.com"
在其他两台主机上验证:
# 在 192.168.1.221 上
redis-cli -c -a redis -h 192.168.1.221 -p 6379
192.168.1.221:6379> get url
-> Redirected to slot [12704] located at 192.168.1.222:6379
"http://www.baidu.com"
# 在 192.168.1.222 上
redis-cli -c -a redis -h 192.168.1.222 -p 6380
192.168.1.222:6380> get url
-> Redirected to slot [12704] located at 192.168.1.222:6379
"http://www.baidu.com"
任意节点都能读到相同数据,说明集群搭建成功。
7.11.10 容灾测试
测试一:停掉一台主机上的两个实例
在 192.168.1.221 上同时停掉 6379 与 6380:
service redis stop
service redis-6380 stop
# 确认已停止
netstat -tunlp | grep -E '6379|6380'
ps -ef | grep redis
在其他主机上验证集群:
redis-cli -c -a redis -h 192.168.1.220 -p 6379
192.168.1.220:6379> cluster info
# cluster_state:ok ← 集群依然可用
192.168.1.220:6379> get url
"http://www.baidu.com" ← 数据正常读取
192.168.1.220:6379> set key1 value1
OK ← 写入正常
192.168.1.220:6379> cluster nodes
# 可以看到 192.168.1.221:6379 被标记为 fail
# 原本挂在它下面的从节点 192.168.1.222:6380 已升级为 master 并接管 5461-10922 槽
结论:Redis 集群正常使用。因为 3 个 Master 中只失败了 1 个,未超过半数,集群仍能完成故障转移投票;同时该 Master 的从节点在别的主机上(192.168.1.222:6380),可以顺利升为新 Master 接管槽位。
测试二:每台主机各停掉一个实例
在三台主机上各停掉一个实例(例如 .220 停 6380、.221 停 6379、.222 停 6380),使集群只剩 3 个实例:
# 192.168.1.220
service redis-6380 stop
# 192.168.1.221
service redis stop
# 192.168.1.222
service redis-6380 stop
验证:
redis-cli -c -a redis -h 192.168.1.220 -p 6379
192.168.1.220:6379> cluster info
# cluster_state:ok
192.168.1.220:6379> get url
"http://www.baidu.com"
192.168.1.220:6379> cluster nodes
# 剩余的 3 个实例全部是 master,且共同覆盖了 16384 个槽
结论:Redis 集群正常使用。剩下的 3 个实例会自动全部提升为 Master 并分担全部 16384 个槽位,集群继续对外提供完整服务(但此时已无冗余,再挂任何一个实例集群即不可用)。
容灾测试结论汇总
| 故障场景 | 剩余可用主节点 | 集群状态 | 原因 |
|---|---|---|---|
| 停 1 台主机的全部 2 个实例 | 2/3 + 1 个升主 = 3 | ok | Master 失败数未过半,从节点跨主机可顺利升主 |
| 每台主机各停 1 个实例 | 3 | ok | 剩余实例自动升为 Master,槽位全覆盖 |
| 停 2 台主机的全部实例 | 1/3 | fail | Master 失败过半,无法完成故障转移投票 |
| 某分片主从同时挂 | — | fail | 该分片槽位无人负责 |
核心判定规则:集群要能正常工作,必须满足 ① 存活的主节点超过半数(用于故障转移投票),且 ② 16384 个槽全部有主节点负责。
7.11.11 安装常见错误与解决办法
错误一:redis-trib.rb 连接节点失败(密码/地址问题)
现象:执行 redis-trib.rb create 时报连接被拒绝或认证失败,如:
[ERR] Sorry, can't connect to node 192.168.1.220:6379
/opt/truplatform/ruby/lib/ruby/gems/2.5.0/gems/redis-4.0.2/lib/redis/client.rb:...
NOAUTH Authentication required
原因:redis gem 的 client.rb 中 DEFAULTS 的 :host 默认是 127.0.0.1、:password 默认是 nil,无法连接设置了密码的远程节点。
解决:按 7.11.7 修改 client.rb:
vim /opt/truplatform/ruby/lib/ruby/gems/2.5.0/gems/redis-4.0.2/lib/redis/client.rb
:host => "0.0.0.0",
:password => "redis",
错误二:创建集群时卡在 "Waiting for the cluster to join"
现象:
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join.................................
(一直打点,永不结束)
原因:节点之间的集群总线端口不通。集群总线端口 = 客户端端口 + 10000,即 6379 对应 16379、6380 对应 16380。防火墙拦截了这些端口,或实例未监听。
解决:
# 1) 确认每台主机上两个实例的端口都在监听
netstat -tunlp | grep -E '6379|6380'
# 应能看到 6379、16379、6380、16380 四个监听端口
# 2) 关闭防火墙(CentOS 7)
systemctl stop firewalld
systemctl disable firewalld
# 2') CentOS 6 关闭防火墙
service iptables stop
chkconfig iptables off
# 3) 或者不关防火墙,只放行必要端口(生产推荐)
# CentOS 7
firewall-cmd --permanent --add-port=6379/tcp
firewall-cmd --permanent --add-port=6380/tcp
firewall-cmd --permanent --add-port=16379/tcp
firewall-cmd --permanent --add-port=16380/tcp
firewall-cmd --reload
firewall-cmd --list-ports
# CentOS 6
iptables -A INPUT -p tcp -m multiport --dports 6379,6380,16379,16380 -j ACCEPT
service iptables save
service iptables restart
# 4) 顺便确认 SELinux 未拦截
getenforce
setenforce 0 # 临时关闭
sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config # 永久关闭
# 5) 用 telnet / nc 从其他主机测试连通性
telnet 192.168.1.221 16379
nc -zv 192.168.1.221 16379
错误三:重新创建集群时报错(节点非空)
现象:第一次创建失败后再次执行 redis-trib.rb create,报错:
[ERR] Node 192.168.1.220:6379 is not empty.
Either the node already knows other nodes (check with CLUSTER NODES)
or contains some key in database 0.
原因:上次创建已经在节点上写入了集群配置文件(nodes.conf)和/或数据文件,节点不再是"干净"状态。
解决:清空所有节点的数据目录后重启实例,再重新创建:
# 在每台主机上执行(三台都要做)
service redis stop
service redis-6380 stop
rm -rf /opt/truplatform/redis/redisdb/*
rm -rf /opt/truplatform/redis-6380/redisdb/*
service redis start
service redis-6380 start
redisdb目录(即配置中的dir)下存放着dump.rdb、appendonly.aof和nodes.conf。必须把nodes.conf一起删掉,否则节点启动后仍然"记得"旧的集群关系。
新版本(Redis 5+)的等价清理方式:
redis-cli -p 7001 -a 123456 --no-auth-warning FLUSHALL
redis-cli -p 7001 -a 123456 --no-auth-warning CLUSTER RESET HARD
7.11.12 redis-trib.rb 与 redis-cli --cluster 命令对照
| 功能 | Redis 3/4(redis-trib.rb) | Redis 5+(redis-cli --cluster) |
|---|---|---|
| 创建集群 | redis-trib.rb create --replicas 1 <节点列表> |
redis-cli --cluster create <节点列表> --cluster-replicas 1 |
| 检查集群 | redis-trib.rb check <ip:port> |
redis-cli --cluster check <ip:port> |
| 查看信息 | redis-trib.rb info <ip:port> |
redis-cli --cluster info <ip:port> |
| 添加节点 | redis-trib.rb add-node <新节点> <已有节点> |
redis-cli --cluster add-node <新节点> <已有节点> |
| 添加从节点 | redis-trib.rb add-node --slave --master-id <id> ... |
redis-cli --cluster add-node --cluster-slave --cluster-master-id <id> ... |
| 删除节点 | redis-trib.rb del-node <ip:port> <node-id> |
redis-cli --cluster del-node <ip:port> <node-id> |
| 迁移槽 | redis-trib.rb reshard <ip:port> |
redis-cli --cluster reshard <ip:port> |
| 均衡槽 | redis-trib.rb rebalance <ip:port> |
redis-cli --cluster rebalance <ip:port> |
| 修复槽 | redis-trib.rb fix <ip:port> |
redis-cli --cluster fix <ip:port> |
| 批量执行 | redis-trib.rb call <ip:port> <命令> |
redis-cli --cluster call <ip:port> <命令> |
| 导入数据 | redis-trib.rb import --from <源> <目标> |
redis-cli --cluster import --cluster-from <源> <目标> |
| 依赖 | 需要 Ruby + redis gem | 无需任何外部依赖 |
升级建议:新项目一律使用 Redis 5.0 及以上版本,用
redis-cli --cluster管理集群,彻底摆脱 Ruby 环境依赖。Redis 5.0 中redis-trib.rb仍保留但会提示WARNING: redis-trib.rb is not longer available!,Redis 6 起已彻底移除。
7.12 其他分布式方案:Twemproxy 与 Codis
在 Redis Cluster(3.0,2015 年)成熟之前,业界普遍采用代理层分片方案。了解它们有助于理解老系统架构。
7.12.1 Twemproxy(nutcracker)
Twitter 开源的轻量级代理,用 C 编写。
Client Client Client
│ │ │
└──────────┬──────────┴──────────┬──────────┘
▼ ▼
Twemproxy #1 Twemproxy #2 (通过 LVS/Keepalived 做高可用)
│ │
┌──────────┼──────────┬──────────┘
▼ ▼ ▼
Redis A Redis B Redis C (各自可挂从库 + Sentinel)
核心配置示例:
# /etc/nutcracker/nutcracker.yml(实际为 YAML 格式)
# alpha 为业务池名称
alpha:
listen: 0.0.0.0:22121 # 代理对外监听地址
hash: fnv1a_64 # 哈希算法
distribution: ketama # 分片算法:一致性哈希
auto_eject_hosts: true # 自动剔除故障节点
redis: true # 后端为 Redis(false 表示 Memcached)
redis_auth: password # 后端 Redis 密码
server_retry_timeout: 2000 # 剔除后重试间隔(毫秒)
server_failure_limit: 3 # 连续失败几次判定为故障
timeout: 400 # 后端请求超时(毫秒)
preconnect: true
servers:
- 192.168.1.220:6379:1 redis-a # 格式:ip:port:权重 名称
- 192.168.1.221:6379:1 redis-b
- 192.168.1.222:6379:1 redis-c
# 启动
nutcracker -c /etc/nutcracker/nutcracker.yml -d -o /var/log/nutcracker.log
# 常用参数
# -d 后台运行 -c 配置文件 -o 日志文件 -s 状态端口(默认 22222,输出 JSON 统计)
# -t 检查配置文件语法
nutcracker -t -c /etc/nutcracker/nutcracker.yml
# 查看统计信息
curl http://127.0.0.1:22222
| 优点 | 缺点 |
|---|---|
| 轻量、稳定、消耗资源少 | 不支持在线扩容(增删节点导致大量 key 重新分布) |
| 客户端无感知,用普通 Redis 客户端即可 | 不支持多键操作、事务、Pub/Sub 等命令 |
| 支持连接池,减少后端连接数 | 多一层网络转发,延迟增加约 20% |
| 自动剔除故障节点 | 代理本身是单点,需自行做高可用 |
| 已基本停止维护 |
7.12.2 Codis
豌豆荚开源的分布式 Redis 方案,Go 编写,支持在线数据迁移,是 Twemproxy 的最大痛点的解决方案。
Client
│
▼
┌───── Codis-Proxy (多个,无状态) ─────┐
│ │
▼ ▼
Codis-Group 1 Codis-Group 2
(Master + Slave) (Master + Slave)
▲ ▲
└────────── Codis-Dashboard ───────────┘
│
ZooKeeper / etcd(存元数据)
│
Codis-FE(Web 管理界面)
组件说明:
| 组件 | 作用 |
|---|---|
| Codis-Proxy | 无状态代理,实现 Redis 协议,客户端直连它。可部署多个,前端用 LVS/HAProxy 负载均衡 |
| Codis-Server | 修改过的 Redis 分支,增加了槽(slot)与数据迁移相关命令 |
| Codis-Dashboard | 集群管理工具,负责槽分配、数据迁移、节点增删。同一集群同时只能有一个 Dashboard |
| Codis-FE | Web 管理界面,可管理多个集群 |
| Codis-Group | 一组主从 Redis,相当于 Redis Cluster 的一个分片 |
| ZooKeeper/etcd | 存储集群元数据(槽与 Group 的映射关系),Proxy 从这里同步路由表 |
Codis 使用 1024 个槽(Redis Cluster 是 16384 个)。
| 优点 | 缺点 |
|---|---|
| 支持在线扩缩容与平滑数据迁移(迁移过程业务无感知) | 架构组件多,运维复杂度高 |
| 提供 Web 管理界面,运维友好 | 依赖 ZooKeeper/etcd 等外部组件 |
| Proxy 无状态,易于水平扩展 | 使用魔改的 Redis 分支,版本跟进滞后 |
| 从 Twemproxy 迁移方便 | 多一层代理,延迟增加 |
| 支持的命令比 Twemproxy 多 | 社区活跃度已下降 |
7.12.3 三种方案选型对比
| 维度 | Twemproxy | Codis | Redis Cluster |
|---|---|---|---|
| 架构 | 代理分片 | 代理 + 中心化管理 | 去中心化(无代理) |
| 槽数量 | 一致性哈希(无固定槽) | 1024 | 16384 |
| 在线扩容 | 不支持 | 支持 | 支持 |
| 数据迁移 | 需停机重分布 | 平滑迁移 | reshard 在线迁移 |
| 高可用 | 需配合 Sentinel | Dashboard + 哨兵 | 内置故障转移 |
| 外部依赖 | 无(但需 LVS) | ZooKeeper/etcd | 无 |
| 客户端 | 普通客户端 | 普通客户端 | 需支持集群协议的客户端 |
| 网络跳数 | 2(多一跳代理) | 2 | 1(直连) |
| 运维界面 | 无 | 有 Web UI | 无(可用第三方) |
| 官方支持 | 停止维护 | 社区维护 | Redis 官方 |
| 适用场景 | 遗留系统 | 需要平滑扩容且团队有 Go 运维能力 | 新项目首选 |
结论:新项目一律用 Redis Cluster。它是官方方案,无外部依赖、无代理跳数、内置故障转移,生态与客户端支持最完善。只有在维护遗留系统,或必须让老客户端零改造接入分片集群时,才考虑代理方案。
八、性能优化
8.1 内存优化与 INFO memory 解读
redis-cli -a 123456 --no-auth-warning INFO memory
used_memory:1073741824 # Redis 分配器分配的内存总量(字节)
used_memory_human:1.00G # 人类可读格式
used_memory_rss:1181116006 # 【重要】操作系统视角的物理内存占用(RSS)
used_memory_rss_human:1.10G
used_memory_peak:1288490188 # 【重要】历史内存使用峰值
used_memory_peak_human:1.20G
used_memory_peak_perc:83.33% # 当前占峰值的百分比
used_memory_lua:37888 # Lua 引擎占用内存
used_memory_scripts:0 # 缓存的 Lua 脚本占用
used_memory_dataset:1063741824 # 【重要】纯数据占用(used_memory - 开销)
used_memory_dataset_perc:99.07% # 数据占比
allocator_frag_ratio:1.03 # 【重要】分配器碎片率
allocator_frag_bytes:32212254
total_system_memory:8589934592 # 系统总内存
maxmemory:2147483648 # maxmemory 配置值
maxmemory_human:2.00G
maxmemory_policy:allkeys-lru # 淘汰策略
mem_fragmentation_ratio:1.10 # 【最重要】内存碎片率 = used_memory_rss / used_memory
mem_allocator:jemalloc-5.1.0 # 内存分配器
mem_clients_normal:49694 # 普通客户端占用内存
mem_clients_slaves:0 # 从库客户端占用内存
mem_replication_backlog:1048576 # 复制积压缓冲区占用
mem_aof_buffer:8192 # AOF 缓冲区占用
lazyfree_pending_objects:0 # 待异步释放的对象数
mem_fragmentation_ratio 判读:
| 取值 | 含义 | 处理 |
|---|---|---|
| 约 1.0 ~ 1.5 | 正常 | 无需处理 |
| > 1.5 | 碎片严重,有大量内存被分配器持有但未使用 | 开启 activedefrag yes 自动碎片整理;或在低峰期重启实例(有持久化保障) |
| < 1.0 | 内存已被换出到磁盘(Swap),性能会急剧下降 | 立即排查:检查 free -m、vmstat,降低 maxmemory、加内存、关闭 Swap |
自动碎片整理(Redis 4.0+,需 jemalloc):
# redis.conf
activedefrag yes # 开启主动碎片整理
active-defrag-ignore-bytes 100mb # 碎片超过 100MB 才开始整理
active-defrag-threshold-lower 10 # 碎片率超过 10% 开始整理
active-defrag-threshold-upper 100 # 碎片率达到 100% 时用最大力度整理
active-defrag-cycle-min 1 # 整理占用 CPU 的最小百分比
active-defrag-cycle-max 25 # 整理占用 CPU 的最大百分比
active-defrag-max-scan-fields 1000 # 每次扫描的最大字段数(针对大 Hash/Set)
# 运行时开启
redis-cli CONFIG SET activedefrag yes
# 查看整理效果
redis-cli INFO memory | grep -E 'frag|active_defrag'
内存优化实用技巧:
| 技巧 | 说明 | 效果 |
|---|---|---|
| 缩短 key 名 | user:1001:name → u:1001:n |
key 本身也占内存,百万级 key 可省几十 MB |
| 用 Hash 代替大量独立 String | SET u:1:name x; SET u:1:age 20 → HSET u:1 name x age 20 |
一个小 Hash(ziplist 编码)比多个 String 省 5~10 倍内存 |
| 控制在 ziplist/listpack 范围内 | 让集合元素数与元素长度低于阈值 | 触发紧凑编码,内存占用大幅下降 |
| 用整数做值 | 整数会被共享(0~9999 共享对象池)或用 int 编码 | 省去 SDS 结构开销 |
| 用 Bitmap/HyperLogLog 代替 Set | 统计场景 | 内存从 GB 级降到 MB/KB 级 |
| 设置合理的 TTL | 所有缓存 key 都应有过期时间 | 避免无用数据长期驻留 |
| 压缩 value | 大文本用 gzip/snappy 压缩后再存 | 视数据可压缩 3~10 倍(但增加 CPU) |
| 关闭不需要的持久化缓冲 | 只做缓存时可关 AOF | 省下 mem_aof_buffer |
调低 maxmemory-clients(Redis 7+) |
限制客户端缓冲区总量 | 防止客户端拖垮内存 |
# 查看单个 key 占用的内存(字节,Redis 4.0+)
redis-cli MEMORY USAGE user:1001
redis-cli MEMORY USAGE mybighash SAMPLES 0 # SAMPLES 0 表示精确统计所有字段
# 内存使用总体报告与优化建议
redis-cli MEMORY DOCTOR
redis-cli MEMORY STATS
# 查看 key 的底层编码,确认是否命中紧凑编码
redis-cli OBJECT ENCODING myhash
redis-cli OBJECT REFCOUNT counter # 共享整数对象的引用计数
redis-cli OBJECT FREQ user:1001 # LFU 策略下的访问频率(需 maxmemory-policy 为 lfu)
redis-cli OBJECT IDLETIME user:1001 # 空闲时间(秒),LRU 策略下有效
8.2 大 key 与热 key 的发现与治理
8.2.1 什么是大 key,为什么危险
| 类型 | 大 key 的判定标准(经验值) |
|---|---|
| String | value 超过 10KB |
| Hash / List / Set / ZSet | 元素个数超过 5000,或总大小超过 10MB |
大 key 的危害:
| 危害 | 原因 |
|---|---|
| 阻塞主线程 | Redis 单线程,操作大 key(如 HGETALL 一个 10 万字段的 Hash)耗时数百毫秒,期间所有请求排队 |
| 删除时卡顿 | DEL 一个大 key 需要释放大量内存,同步阻塞(应改用 UNLINK) |
| 过期时卡顿 | 大 key 到期被删除时同样阻塞(开启 lazyfree-lazy-expire yes) |
| 网络拥塞 | 一次返回 10MB 数据,千兆网卡瞬时被打满 |
| 集群数据倾斜 | 大 key 只能落在一个槽,导致该节点内存远高于其他节点 |
| 迁移失败 | MIGRATE 是同步操作,大 key 迁移超时导致 reshard 失败 |
| 主从同步延迟 | 大 key 的写命令在从库重放耗时长 |
8.2.2 发现大 key
# 【方法一】redis-cli --bigkeys:扫描全库,找出每种类型的最大 key
# 原理:用 SCAN 遍历 + TYPE + 对应的 STRLEN/HLEN/LLEN/SCARD/ZCARD
# 优点:不阻塞(基于 SCAN);缺点:只能找出每种类型的 Top1,且元素多≠内存大
redis-cli -a 123456 --no-auth-warning --bigkeys
# 加 -i 参数控制扫描速度,每 100 次 SCAN 休眠 0.1 秒,降低对生产的影响
redis-cli -a 123456 --no-auth-warning --bigkeys -i 0.1
输出示例:
# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).
[00.00%] Biggest string found so far 'session:abc' with 3 bytes
[12.50%] Biggest hash found so far 'user:1001' with 5 fields
[45.00%] Biggest list found so far 'msg:queue' with 128000 items
[78.00%] Biggest zset found so far 'rank:global' with 50000 members
-------- summary -------
Sampled 100000 keys in the keyspace!
Total key length in bytes is 1500000 (avg len 15.00)
Biggest string found 'bigdata' has 10485760 bytes
Biggest hash found 'user:1001' has 5 fields
Biggest list found 'msg:queue' has 128000 items
Biggest set found 'tags' has 2000 members
Biggest zset found 'rank:global' has 50000 members
40000 strings with 20971520 bytes (40.00% of keys, avg size 524.29)
30000 hashs with 150000 fields (30.00% of keys, avg size 5.00)
...
# 【方法二】--memkeys(Redis 6.0+):按实际内存占用排序,比 --bigkeys 更准
redis-cli -a 123456 --no-auth-warning --memkeys
redis-cli -a 123456 --no-auth-warning --memkeys-samples 0 # 精确采样
# 【方法三】离线分析 RDB 文件(对生产零影响,推荐)
pip install rdbtools python-lzf
rdb -c memory /var/lib/redis/dump.rdb --bytes 10240 -f /tmp/bigkeys.csv
# 输出 CSV:database,type,key,size_in_bytes,encoding,num_elements,len_largest_element
sort -t, -k4 -rn /tmp/bigkeys.csv | head -50
# 生成内存报告
rdb --command memory --largest 100 /var/lib/redis/dump.rdb
# 【方法四】在集群中逐个节点扫描
for node in 7001 7002 7003; do
echo "===== node $node ====="
redis-cli -p $node -a 123456 --no-auth-warning --bigkeys
done
# 【方法五】用 SCAN 自行脚本化统计
redis-cli -a 123456 --no-auth-warning --scan --pattern 'user:*' | while read k; do
echo "$(redis-cli -a 123456 --no-auth-warning MEMORY USAGE "$k") $k"
done | sort -rn | head -20
8.2.3 治理大 key
| 场景 | 拆分方案 |
|---|---|
| 大 Hash(10 万字段) | 按字段 hash 取模拆成 100 个小 Hash:user:1001:0 ~ user:1001:99 |
| 大 List(消息队列) | 按业务分多个队列,或改用 Stream / 专业 MQ(Kafka、RocketMQ) |
| 大 Set(用户标签) | 按时间/业务维度拆分;纯统计场景改用 Bitmap 或 HyperLogLog |
| 大 ZSet(全局排行榜) | 只保留 Top N,用 ZREMRANGEBYRANK rank 0 -1001 定期裁剪 |
| 大 String(大 JSON) | 拆成 Hash 按字段存取;或压缩后存储;或改存对象存储只在 Redis 放引用 |
# 【安全删除大 key】
# ① 直接用 UNLINK 异步删除(Redis 4.0+,首选)
UNLINK bigkey
# ② 老版本:分批删除,避免一次性阻塞
# Hash:分批 HDEL
redis-cli -a 123456 --no-auth-warning HSCAN bighash 0 COUNT 100
# List:分批 LTRIM / RPOP
LTRIM biglist 0 -1001 # 每次只留 1000 个,循环执行
# Set:分批 SREM(配合 SSCAN)
# ZSet:分批 ZREMRANGEBYRANK
ZREMRANGEBYRANK bigzset 0 999 # 每次删 1000 个,循环执行
# ③ 开启惰性删除,让过期与淘汰也走异步
CONFIG SET lazyfree-lazy-expire yes
CONFIG SET lazyfree-lazy-eviction yes
CONFIG SET lazyfree-lazy-server-del yes
CONFIG SET replica-lazy-flush yes
8.2.4 发现与治理热 key
热 key:短时间内访问量远超其他 key 的 key(如秒杀商品、明星微博)。危害是单个节点 CPU/网卡被打满,集群分片也无法分担(同一个 key 只在一个节点)。
# 【方法一】--hotkeys(需 maxmemory-policy 为 allkeys-lfu 或 volatile-lfu)
redis-cli CONFIG SET maxmemory-policy allkeys-lfu
redis-cli -a 123456 --no-auth-warning --hotkeys
# 【方法二】MONITOR 抓取实时命令流(【危险】性能损耗大,只能短时间使用)
redis-cli -a 123456 --no-auth-warning MONITOR | head -10000 > /tmp/mon.log
awk '{print $4}' /tmp/mon.log | sort | uniq -c | sort -rn | head -20
# 【方法三】OBJECT FREQ 查看指定 key 的访问频率(LFU 模式下)
redis-cli OBJECT FREQ hot:product:1001
# 【方法四】开启 key 空间通知 + 业务埋点统计(生产推荐)
# 【方法五】用 tcpdump 抓包离线分析(对 Redis 零影响)
tcpdump -i eth0 port 6379 -w /tmp/redis.pcap -c 100000
热 key 解决方案:
| 方案 | 做法 |
|---|---|
| 多级缓存 | 在应用进程内加本地缓存(Caffeine/Guava,TTL 几秒),绝大部分请求不到 Redis |
| key 打散 | 把 hot:key 复制成 hot:key:1 ~ hot:key:N(N 个副本分布在不同节点),读时随机选一个 |
| 读写分离 | 给该分片多加从节点,读请求分摊到从库 |
| 限流降级 | 对该 key 的访问加令牌桶限流,超出直接返回兜底数据 |
| 合并请求 | 应用层用单飞(singleflight)合并同一时刻对同一 key 的并发请求 |
8.3 慢查询日志(SLOWLOG)
Redis 会把执行时间超过阈值的命令记录到内存中的慢日志队列(不写磁盘,重启丢失)。
# redis.conf
slowlog-log-slower-than 10000 # 阈值:微秒。10000 微秒 = 10 毫秒
# 设为 0 → 记录所有命令(仅调试用)
# 设为负数 → 关闭慢日志
slowlog-max-len 128 # 慢日志最多保留多少条(超出后丢弃最旧的)
注意:
slowlog-log-slower-than统计的是命令实际执行时间,不包含客户端网络传输、排队等待时间。所以客户端感知的耗时可能远大于慢日志中的时间(比如前面有个大 key 操作把它堵住了)。
# 运行时调整(生产建议阈值设为 10000,即 10ms)
redis-cli CONFIG SET slowlog-log-slower-than 10000
redis-cli CONFIG SET slowlog-max-len 1000
# 查看慢日志
redis-cli SLOWLOG GET # 默认返回 10 条
redis-cli SLOWLOG GET 50 # 返回最近 50 条
redis-cli SLOWLOG GET -1 # 返回全部(Redis 7+)
# 查看慢日志条数
redis-cli SLOWLOG LEN
# 清空慢日志
redis-cli SLOWLOG RESET
# 查看 SLOWLOG 帮助
redis-cli SLOWLOG HELP
输出格式解读:
1) 1) (integer) 14 # ① 慢日志唯一 ID(自增)
2) (integer) 1756000000 # ② 命令执行完成的 UNIX 时间戳
3) (integer) 45231 # ③ 【执行耗时,微秒】45231us = 45ms
4) 1) "KEYS" # ④ 命令与参数数组
2) "user:*"
5) "192.168.1.100:52344" # ⑤ 客户端地址与端口(Redis 4.0+)
6) "app-worker-1" # ⑥ 客户端名称(CLIENT SETNAME 设置的,Redis 4.0+)
命令参数超过 31 个时只记录前 31 个,最后一个替换为
... (N more arguments);单个参数超过 128 字节时截断为...(M more bytes)。
慢日志中最常见的元凶:
| 命令 | 原因 | 替代方案 |
|---|---|---|
KEYS * |
O(N) 全量遍历 | SCAN |
HGETALL / LRANGE 0 -1 / SMEMBERS |
一次取出大 key 全部元素 | HSCAN/SSCAN,或分页 LRANGE 0 99 |
DEL 大 key |
同步释放大量内存 | UNLINK |
FLUSHALL / FLUSHDB |
清空全库 | 加 ASYNC 参数 |
SORT |
复杂排序 O(N+M*log(M)) | 业务侧排序,或用 ZSet |
ZUNIONSTORE / SINTERSTORE |
大集合运算 | 拆小、预计算 |
| Lua 脚本 | 脚本内有循环遍历大集合 | 拆分脚本,控制单次处理量 |
SAVE |
前台阻塞式 RDB | BGSAVE |
DEBUG SLEEP |
显式阻塞 | 只用于测试 |
延迟监控(Latency Monitor)——比慢日志更全面:
latency-monitor-threshold 100 # 单位毫秒,超过 100ms 的事件被记录。0 为关闭
redis-cli CONFIG SET latency-monitor-threshold 100
redis-cli LATENCY LATEST # 最近各类延迟事件:事件名、时间戳、最近一次耗时、最大耗时
redis-cli LATENCY HISTORY command # 某类事件的历史记录
redis-cli LATENCY RESET # 重置
redis-cli LATENCY DOCTOR # 【推荐】自动分析并给出人类可读的诊断与建议
redis-cli LATENCY GRAPH command # ASCII 图形化展示延迟分布
# 常见事件类型:command(命令执行)、fork(fork 子进程)、rdb-unlink-temp-file、
# aof-write、aof-fsync-always、aof-rewrite-diff-write、expire-cycle、
# eviction-del、eviction-cycle
# 从客户端侧测量端到端延迟
redis-cli --latency # 持续采样:min/max/avg(毫秒)
redis-cli --latency-history # 每 15 秒输出一段统计
redis-cli --latency-dist # 彩色分布图(Redis 4.0+)
redis-cli --intrinsic-latency 100 # 测量【操作系统本身】的固有延迟(跑 100 秒)
8.4 客户端连接管理(CLIENT)
# 【CLIENT LIST】列出所有客户端连接
redis-cli CLIENT LIST
id=15 addr=192.168.1.100:52344 laddr=192.168.1.220:6379 fd=8 name=app-1 age=3600 idle=0
flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=26 qbuf-free=32742 argv-mem=10
multi-mem=0 tot-net-in=120 tot-net-out=340 rbs=1024 rbp=0 obl=0 oll=0 omem=0
tot-mem=61466 events=r cmd=client|list user=default redir=-1 resp=2
关键字段:
| 字段 | 说明 |
|---|---|
id |
客户端唯一 ID(单调递增) |
addr |
客户端地址:端口 |
laddr |
本端(服务端)地址:端口 |
fd |
socket 文件描述符 |
name |
客户端名称(CLIENT SETNAME 设置) |
age |
连接已建立的秒数 |
idle |
空闲秒数,排查僵尸连接看这个 |
flags |
N 普通、M 主库、S 从库、O MONITOR、x 事务中、b 阻塞中、d 脏事务、c 回复后关闭、U Unix socket |
db |
当前选中的数据库编号 |
sub/psub |
订阅的频道数/模式数 |
multi |
事务中已入队的命令数,-1 表示不在事务中 |
watch |
WATCH 的 key 数量 |
qbuf / qbuf-free |
查询缓冲区已用/剩余字节(异常大说明客户端发了超大命令) |
obl / oll / omem |
输出缓冲区:固定缓冲已用字节 / 回复链表长度 / 输出缓冲区占用内存(omem 大是内存暴涨的常见原因) |
tot-mem |
该连接总占用内存 |
events |
监听的事件:r 可读、w 可写 |
cmd |
最后执行的命令(排查问题最有用的字段) |
user |
ACL 用户名(Redis 6.0+) |
resp |
协议版本 2 或 3 |
# 【CLIENT INFO】只看当前连接的信息(Redis 6.2+)
redis-cli CLIENT INFO
# 【CLIENT ID】当前连接 ID
redis-cli CLIENT ID
# 【CLIENT SETNAME / GETNAME】给连接命名,便于排查
redis-cli CLIENT SETNAME order-service
redis-cli CLIENT GETNAME
# 【CLIENT KILL】杀掉连接
redis-cli CLIENT KILL 192.168.1.100:52344 # 老语法:按 addr
redis-cli CLIENT KILL ID 15 # 按客户端 ID
redis-cli CLIENT KILL ADDR 192.168.1.100:52344 # 按客户端地址
redis-cli CLIENT KILL LADDR 192.168.1.220:6379 # 按本端地址(杀掉某个监听地址的全部连接)
redis-cli CLIENT KILL TYPE normal # 按类型:normal/master/replica/pubsub
redis-cli CLIENT KILL USER appuser # 按 ACL 用户
redis-cli CLIENT KILL SKIPME no # 连自己也杀(默认 yes 不杀自己)
redis-cli CLIENT KILL MAXAGE 3600 # 杀掉存活超过 1 小时的连接(Redis 7.4+)
# 一键清理空闲超过 1 小时的连接
redis-cli CLIENT LIST | awk -F'[ =]' '$8>3600 {print $2}' | \
xargs -I{} redis-cli CLIENT KILL ID {}
# 【CLIENT PAUSE】暂停所有客户端 N 毫秒(用于主从切换等需要短暂静默的场景)
redis-cli CLIENT PAUSE 5000 # 暂停 5 秒(读写都暂停)
redis-cli CLIENT PAUSE 5000 WRITE # 只暂停写(Redis 6.2+)
redis-cli CLIENT PAUSE 5000 ALL
redis-cli CLIENT UNPAUSE # 提前解除暂停
# 【CLIENT NO-EVICT / NO-TOUCH】(Redis 7+)
redis-cli CLIENT NO-EVICT on # 该连接的缓冲区不参与客户端内存淘汰
redis-cli CLIENT NO-TOUCH on # 该连接的读操作不更新 key 的 LRU/LFU(运维工具连接应开启)
# 【CLIENT UNBLOCK】解除某个客户端的阻塞(BLPOP 等)
redis-cli CLIENT UNBLOCK 15 # 让阻塞命令返回 nil(超时效果)
redis-cli CLIENT UNBLOCK 15 ERROR # 让阻塞命令返回错误
连接数相关配置与排查:
redis-cli INFO clients
connected_clients:52 # 【重要】当前连接数(不含从库连接)
cluster_connections:12 # 集群总线连接数
maxclients:10000 # 上限
client_recent_max_input_buffer:20480 # 最近最大输入缓冲区
client_recent_max_output_buffer:0 # 最近最大输出缓冲区
blocked_clients:2 # 【重要】被 BLPOP/BRPOP/WAIT 等阻塞的客户端数
tracking_clients:0
pubsub_clients:0
watching_clients:0
total_watched_keys:0
clients_in_timeout_table:2
# 客户端输出缓冲区限制(防止慢消费者拖垮内存)
client-output-buffer-limit normal 0 0 0 # 普通客户端:不限制
client-output-buffer-limit replica 256mb 64mb 60 # 从库:硬限制 256MB,60 秒内持续超 64MB 则断开
client-output-buffer-limit pubsub 32mb 8mb 60 # 订阅者:硬限 32MB,软限 8MB/60 秒
maxmemory-clients 5% # Redis 7+:所有客户端缓冲区总量上限(可用 1g 或百分比)
timeout 300 # 空闲客户端超时断开(秒),0 为不断开
tcp-keepalive 300 # TCP 层探活,及时清理半开连接
8.5 Pipeline(管道)与批量操作
8.5.1 为什么需要 Pipeline
一次 Redis 命令的耗时 = 网络往返时间(RTT)+ 命令执行时间。命令执行通常只要几微秒,而 RTT 可能是 0.2~1 毫秒(同机房),也就是说绝大部分时间花在网络往返上。
【普通模式】1 万次 SET,每次一个 RTT
Client ──SET k1 v1──► Server RTT = 0.5ms
Client ◄────OK────── Server
Client ──SET k2 v2──► Server RTT = 0.5ms
Client ◄────OK────── Server
...
总耗时 ≈ 10000 × 0.5ms = 5000ms = 5 秒
【Pipeline 模式】1 万次 SET 打包成 100 批,每批 100 条
Client ──SET k1 v1; SET k2 v2; ... SET k100 v100──► Server RTT = 0.5ms
Client ◄────OK; OK; ... OK──────────────────────── Server
...
总耗时 ≈ 100 × 0.5ms + 执行时间 ≈ 100ms ← 【提升约 50 倍】
8.5.2 使用 Pipeline
# 【方式一】redis-cli --pipe:从标准输入批量导入(内部用 RESP 协议,效率最高)
cat > /tmp/cmds.txt <<'EOF'
SET k1 v1
SET k2 v2
SET k3 v3
EOF
cat /tmp/cmds.txt | redis-cli -a 123456 --no-auth-warning --pipe
# 输出:
# All data transferred. Waiting for the last reply...
# Last reply received from server.
# errors: 0, replies: 3
# 【方式二】生成 100 万条数据的性能测试
for i in $(seq 1 1000000); do
echo "SET key:$i value:$i"
done | redis-cli -a 123456 --no-auth-warning --pipe
# 更快的生成方式(awk)
awk 'BEGIN{for(i=1;i<=1000000;i++) printf "SET key:%d value:%d\r\n", i, i}' \
| redis-cli -a 123456 --no-auth-warning --pipe
# 【方式三】shell 里用括号一次性送多条(走 redis-cli 的交互模式)
(echo -en "PING\r\nSET k v\r\nGET k\r\n"; sleep 1) | nc 127.0.0.1 6379
# 【方式四】redis-benchmark 的 -P 参数测试 pipeline 效果
redis-benchmark -a 123456 -t set,get -n 100000 -q # 不用 pipeline
redis-benchmark -a 123456 -t set,get -n 100000 -P 16 -q # 每批 16 条
效果对比(示例数据):
# 不用 pipeline
SET: 82000.34 requests per second
GET: 85000.21 requests per second
# -P 16
SET: 610000.75 requests per second ← 约 7 倍提升
GET: 680000.50 requests per second
8.5.3 Pipeline 使用注意事项
| 注意点 | 说明 |
|---|---|
| 不是原子的 | Pipeline 只是批量发送,中间可能插入其他客户端的命令。需要原子性用 MULTI/EXEC 或 Lua |
| 批次不要太大 | 一批 100~1000 条为宜。太大会占用服务端输出缓冲区内存,也增加单次阻塞时间 |
| 不能有依赖关系 | 后面的命令不能依赖前面命令的返回值(结果统一在最后返回) |
| 集群模式受限 | 一批命令中的 key 可能分散在不同节点。智能客户端会按节点自动拆分成多个 pipeline |
| 错误不中断 | 某条命令出错,其余命令继续执行,错误在结果数组对应位置返回 |
Pipeline vs MULTI/EXEC vs Lua:
| 特性 | Pipeline | MULTI/EXEC | Lua 脚本 |
|---|---|---|---|
| 减少 RTT | 是 | 是 | 是 |
| 原子性 | 否 | 是(不被打断) | 是 |
| 可读中间结果 | 否 | 否 | 是 |
| 有条件逻辑 | 否 | 否 | 是 |
| 服务端缓存 | 否 | 否 | 是(EVALSHA) |
| 失败回滚 | 否 | 否 | 否 |
其他减少 RTT 的批量命令:
MSET k1 v1 k2 v2 k3 v3 # 批量写 String
MGET k1 k2 k3 # 批量读 String
HMSET h f1 v1 f2 v2 # 批量写 Hash 字段(Redis 4.0+ 推荐用 HSET 多参数形式)
HSET h f1 v1 f2 v2 f3 v3
HMGET h f1 f2 f3 # 批量读 Hash 字段
SADD s m1 m2 m3 # 批量加集合成员
ZADD z 1 a 2 b 3 c # 批量加有序集合成员
LPUSH l v1 v2 v3 # 批量入队
DEL k1 k2 k3 # 批量删除
UNLINK k1 k2 k3 # 批量异步删除
EXISTS k1 k2 k3 # 返回存在的个数(Redis 3.0.3+ 支持多 key)
8.6 避免阻塞主线程
Redis 是单线程执行命令,任何一条慢命令都会让所有其他请求排队。
8.6.1 禁用/慎用的命令清单
| 命令 | 复杂度 | 危害 | 替代方案 |
|---|---|---|---|
KEYS pattern |
O(N) | 遍历全部 key,百万级 key 阻塞数秒 | SCAN cursor MATCH p COUNT 100 |
FLUSHALL / FLUSHDB |
O(N) | 清空全库,同步释放内存 | 加 ASYNC:FLUSHALL ASYNC |
SMEMBERS |
O(N) | 返回大集合全部成员 | SSCAN |
HGETALL / HKEYS / HVALS |
O(N) | 返回大 Hash 全部内容 | HSCAN,或只取需要的字段 HMGET |
LRANGE key 0 -1 |
O(N) | 返回整个列表 | 分页:LRANGE key 0 99 |
ZRANGE key 0 -1 |
O(N) | 返回整个有序集合 | 分页 + ZSCAN |
SORT |
O(N+M*log(M)) | CPU 密集 | 业务侧排序,或改用 ZSet |
SUNION/SINTER/SDIFF |
O(N*M) | 大集合运算 | 预计算结果并缓存,或改在业务侧算 |
DEL bigkey |
O(N) | 同步释放大量内存 | UNLINK |
SAVE |
O(N) | 前台阻塞式生成 RDB | BGSAVE |
DEBUG SLEEP |
— | 显式阻塞 | 只用于测试 |
MONITOR |
— | 输出全部命令流,CPU 与网络暴涨 | 短时间使用后立即退出 |
SCRIPT/EVAL 长脚本 |
取决于脚本 | 脚本执行期间完全阻塞 | 拆分脚本,单次处理量控制在毫秒级 |
BGREWRITEAOF/BGSAVE |
fork 开销 | fork 时短暂阻塞(内存越大越久) | 低峰期执行;关闭 THP |
用 rename-command 在生产环境彻底禁用危险命令:
# redis.conf —— 重命名为空字符串即禁用
rename-command KEYS ""
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""
rename-command SHUTDOWN ""
rename-command DEBUG ""
rename-command MONITOR ""
rename-command SAVE ""
# 或重命名为只有运维知道的名字(保留应急使用能力)
rename-command CONFIG "CONFIG_a8f5f167f44f4964e6c998dee827110c"
rename-command FLUSHALL "FLUSHALL_a8f5f167f44f4964e6c998dee827110c"
Redis 6.0+ 用 ACL 更优雅地限制:
# 创建一个只能读写业务 key、不能执行危险命令的用户
redis-cli ACL SETUSER appuser on '>StrongP@ss123' '~app:*' '+@read' '+@write' '-@dangerous'
redis-cli ACL SETUSER appuser on '>pass' allkeys +@all -flushall -flushdb -keys -config -debug
redis-cli ACL LIST
redis-cli ACL WHOAMI
redis-cli ACL CAT # 查看所有命令分类
redis-cli ACL CAT dangerous # 查看 dangerous 类包含哪些命令
redis-cli ACL GETUSER appuser
redis-cli ACL DELUSER appuser
redis-cli ACL SAVE # 持久化到 aclfile
redis-cli ACL LOAD
redis-cli ACL LOG # 查看被拒绝的操作记录(排查权限问题)
# redis.conf 中定义 ACL
user default on nopass ~* +@all
user appuser on >StrongP@ss123 ~app:* +@read +@write -@dangerous
aclfile /etc/redis/users.acl # 或用独立文件管理
8.6.2 其他阻塞源
| 阻塞源 | 原因 | 优化 |
|---|---|---|
| fork(BGSAVE/BGREWRITEAOF/全量同步) | 复制页表,内存越大越慢。10GB 实例约 200ms | 单实例内存控制在 10GB 以内;关闭 THP;用物理机而非虚拟机(KVM fork 更慢) |
| AOF fsync(appendfsync always) | 每条命令都刷盘,磁盘 IO 成瓶颈 | 改为 everysec;用 SSD |
| AOF 重写期间的 fsync | 主进程 fsync 与子进程写盘竞争 | no-appendfsync-on-rewrite yes |
| 大 key 过期/淘汰 | 同步释放内存 | 开启 lazyfree-lazy-expire/lazyfree-lazy-eviction |
| 集中过期 | 同一秒大量 key 到期,主动过期循环占满 CPU | TTL 加随机抖动:EXPIRE key $((3600 + RANDOM % 600)) |
| 内存交换(Swap) | 访问被换出的页需要磁盘 IO,延迟从微秒变毫秒 | 关闭 Swap 或设 vm.swappiness=1;maxmemory 留足余量 |
| 网络带宽打满 | 大 value + 高 QPS | 减小 value;用压缩;万兆网卡 |
| CPU 竞争 / 绑核不当 | 与其他进程抢 CPU | Redis 独占物理机;用 taskset/server_cpulist 绑核 |
transparent_hugepage |
内存写时复制粒度从 4KB 变 2MB,fork 后写放大 | 必须关闭 |
8.7 操作系统与内核参数调优
8.7.1 vm.overcommit_memory(必改)
警告信息:
WARNING overcommit_memory is set to 0! Background save may fail under low memory
condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf
and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to
take effect.
原因:BGSAVE/BGREWRITEAOF 通过 fork() 创建子进程。虽然有写时复制(COW),子进程实际不会真的用掉一份内存,但内核在 overcommit_memory=0 的启发式判断下,会认为"申请的内存超过剩余物理内存"而拒绝 fork,导致后台保存失败。
| 取值 | 策略 | 说明 |
|---|---|---|
0 |
启发式(默认) | 内核估算,明显过大的申请会被拒绝 → 可能导致 fork 失败 |
1 |
总是允许 | 允许超额申请,只要还有地址空间就成功 → Redis 推荐 |
2 |
严格控制 | 总申请量不得超过 swap + 物理内存 × overcommit_ratio |
# 临时生效
sysctl vm.overcommit_memory=1
# 永久生效
echo 'vm.overcommit_memory = 1' >> /etc/sysctl.conf
sysctl -p
# 验证
cat /proc/sys/vm/overcommit_memory
sysctl -a | grep overcommit
8.7.2 关闭透明大页 THP(必改)
警告信息:
WARNING you have Transparent Huge Pages (THP) support enabled in your kernel.
This will create latency and memory usage issues with Redis. To fix this issue
run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as
root, and add it to your /etc/rc.local in order to retain the setting after a
reboot. Redis must be restarted after THP is disabled.
原因:THP 把内存页从 4KB 变成 2MB。fork 之后主进程一旦写入,COW 复制的粒度就从 4KB 变成 2MB(放大 512 倍),导致:① 内存占用暴涨;② 每次写操作的延迟明显增加。
# 查看当前状态
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
# 【CentOS 7 永久关闭方式一】写入 rc.local
cat >> /etc/rc.local <<'EOF'
if test -f /sys/kernel/mm/transparent_hugepage/enabled; then
echo never > /sys/kernel/mm/transparent_hugepage/enabled
fi
if test -f /sys/kernel/mm/transparent_hugepage/defrag; then
echo never > /sys/kernel/mm/transparent_hugepage/defrag
fi
EOF
chmod +x /etc/rc.d/rc.local # CentOS 7 必须给 rc.local 加执行权限
# 【CentOS 7 永久关闭方式二】修改内核启动参数(更彻底,需重启)
grubby --update-kernel=ALL --args="transparent_hugepage=never"
# 或手工编辑 /etc/default/grub,在 GRUB_CMDLINE_LINUX 中加 transparent_hugepage=never
grub2-mkconfig -o /boot/grub2/grub.cfg
reboot
# 【CentOS 7 永久关闭方式三】systemd 服务
cat > /etc/systemd/system/disable-thp.service <<'EOF'
[Unit]
Description=Disable Transparent Huge Pages (THP)
Before=redis.service
[Service]
Type=oneshot
ExecStart=/bin/sh -c "echo never > /sys/kernel/mm/transparent_hugepage/enabled && echo never > /sys/kernel/mm/transparent_hugepage/defrag"
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now disable-thp
# 【CentOS 6 永久关闭】
echo never > /sys/kernel/mm/redhat_transparent_hugepage/enabled
echo 'echo never > /sys/kernel/mm/redhat_transparent_hugepage/enabled' >> /etc/rc.local
关闭 THP 后必须重启 Redis 进程,因为已分配的大页不会自动拆分。
8.7.3 net.core.somaxconn(必改)
警告信息:
WARNING: The TCP backlog setting of 511 cannot be enforced because
/proc/sys/net/core/somaxconn is set to the lower value of 128.
原因:redis.conf 里的 tcp-backlog 511 是已完成三次握手但还未被 accept 的连接队列长度,但它受内核 net.core.somaxconn 的上限限制。默认 128 在高并发短连接场景下会导致连接被丢弃、客户端超时。
# 临时生效
sysctl net.core.somaxconn=1024
# 永久生效
cat >> /etc/sysctl.conf <<'EOF'
# TCP accept 队列长度(配合 redis.conf 的 tcp-backlog)
net.core.somaxconn = 1024
# SYN 半连接队列长度
net.ipv4.tcp_max_syn_backlog = 8192
# 网卡收包队列
net.core.netdev_max_backlog = 8192
# 允许 TIME_WAIT 连接复用
net.ipv4.tcp_tw_reuse = 1
# TIME_WAIT 最大数量
net.ipv4.tcp_max_tw_buckets = 20000
# FIN_WAIT2 超时
net.ipv4.tcp_fin_timeout = 15
# 本地端口范围(客户端侧高并发需要)
net.ipv4.ip_local_port_range = 1024 65000
EOF
sysctl -p
# 验证
cat /proc/sys/net/core/somaxconn
ss -lnt | grep 6379 # Send-Q 列显示 backlog 大小
注意:
net.ipv4.tcp_tw_recycle在 NAT 环境下会导致连接异常,Linux 4.12 起已移除,不要开启。
8.7.4 Swap 与 vm.swappiness
Redis 数据全在内存,一旦被换出到磁盘,访问延迟从微秒级变成毫秒级,性能断崖式下跌。
# 查看 swap 使用情况
free -m
swapon -s
cat /proc/meminfo | grep -i swap
# 查看 Redis 进程实际被换出多少(最直接的排查手段)
redis_pid=$(pgrep -f 'redis-server.*6379' | head -1)
cat /proc/$redis_pid/smaps | grep -i swap | awk '{sum+=$2} END {print sum" kB"}'
# 或逐段查看
grep -A1 -i swap /proc/$redis_pid/smaps | head -40
# 【方案一】完全关闭 swap(内存充足时推荐)
swapoff -a
sed -i '/swap/s/^/#/' /etc/fstab # 注释掉 fstab 中的 swap 行
# 【方案二】保留 swap 但尽量不用(更稳妥,避免 OOM Killer 直接杀进程)
sysctl vm.swappiness=1 # 【不要设为 0】0 在新内核下会让 OOM Killer 更激进
echo 'vm.swappiness = 1' >> /etc/sysctl.conf
sysctl -p
vm.swappiness |
行为 |
|---|---|
0 |
尽最大努力不 swap,内存不足时直接触发 OOM Killer 杀进程 |
1 |
几乎不 swap,但极端情况下仍允许,避免 OOM —— Redis 推荐 |
60 |
默认值,倾向于换出不活跃页 |
100 |
积极换出 |
防止 Redis 被 OOM Killer 杀掉:
# 降低 Redis 进程被 OOM Killer 选中的概率(-1000 ~ 1000,越小越不容易被杀)
echo -900 > /proc/$redis_pid/oom_score_adj
cat /proc/$redis_pid/oom_score
# 查看 OOM 杀进程记录
dmesg | grep -i 'killed process'
grep -i 'out of memory' /var/log/messages
# systemd 方式(推荐):在 unit 文件的 [Service] 段加
# OOMScoreAdjust=-900
# redis.conf 中让 Redis 自己管理 oom_score_adj(Redis 6.0+)
oom-score-adj no # no / yes / relative / absolute
oom-score-adj-values 0 200 800 # 分别对应 master / replica / bgchild 进程
# 让后台子进程(bgchild)最先被杀,保护主进程
8.7.5 文件描述符限制
# 查看当前限制
ulimit -n
cat /proc/$redis_pid/limits | grep 'open files'
# 【永久修改】/etc/security/limits.conf
cat >> /etc/security/limits.conf <<'EOF'
redis soft nofile 65535
redis hard nofile 65535
* soft nofile 65535
* hard nofile 65535
EOF
# CentOS 7 还需检查(会覆盖 limits.conf 中的 * 通配)
vim /etc/security/limits.d/20-nproc.conf
# 【systemd 管理的服务不受 limits.conf 约束】必须在 unit 文件中指定
# /etc/systemd/system/redis.service 的 [Service] 段:
# LimitNOFILE=65535
systemctl daemon-reload
systemctl restart redis
cat /proc/$(pgrep -f redis-server | head -1)/limits | grep 'open files'
Redis 需要的文件描述符数 ≈
maxclients+ 32(预留给持久化、监听 socket、集群总线等)。若ulimit -n小于此值,Redis 启动时会自动下调 maxclients 并打印警告:# You requested maxclients of 10000 requiring at least 10032 max file descriptors. # Server can't set maximum open files to 10032 because of OS error: Operation not permitted. # Current maximum open files is 4096. maxclients has been reduced to 4064 ...
8.7.6 CPU 绑核与多线程 I/O
# redis.conf(Redis 6.0+):把不同类型的线程绑到不同 CPU
server_cpulist 0-3 # 主线程 + I/O 线程使用 CPU 0~3
bio_cpulist 4,5 # 后台 I/O 线程(惰性删除、AOF fsync)
aof_rewrite_cpulist 6,7 # AOF 重写子进程
bgsave_cpulist 6,7 # RDB 保存子进程
# 多线程 I/O(Redis 6.0+):只并行处理网络读写与协议解析,命令执行仍是单线程
io-threads 4 # 建议 = CPU 核数 - 1,且不超过 8。设为 1 表示关闭
io-threads-do-reads yes # 同时用多线程处理读(默认只处理写)
# 也可以用 taskset 从外部绑核
taskset -c 0-3 redis-server /etc/redis/redis.conf
taskset -cp $redis_pid # 查看进程绑核情况
# 关闭 CPU 节能模式(避免降频造成延迟抖动)
cpupower frequency-set -g performance
# 或
for c in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do echo performance > $c; done
# NUMA 架构下避免跨节点访问内存
numactl --hardware
numactl --cpunodebind=0 --membind=0 redis-server /etc/redis/redis.conf
8.7.7 系统调优检查清单(上线前逐项确认)
| # | 项目 | 命令/配置 | 期望值 |
|---|---|---|---|
| 1 | 内存超分 | sysctl vm.overcommit_memory |
1 |
| 2 | 透明大页 | cat /sys/kernel/mm/transparent_hugepage/enabled |
never |
| 3 | accept 队列 | sysctl net.core.somaxconn |
>= 1024 |
| 4 | swap 倾向 | sysctl vm.swappiness |
1(或关闭 swap) |
| 5 | 文件描述符 | ulimit -n / LimitNOFILE |
>= 65535 |
| 6 | OOM 保护 | /proc/<pid>/oom_score_adj |
负值(如 -900) |
| 7 | maxmemory | redis-cli CONFIG GET maxmemory |
物理内存的 50%~70% |
| 8 | 淘汰策略 | redis-cli CONFIG GET maxmemory-policy |
缓存用 allkeys-lru;持久存储用 noeviction |
| 9 | 危险命令 | rename-command / ACL |
KEYS、FLUSHALL、CONFIG 已禁用 |
| 10 | 密码 | requirepass |
已设置且为强密码 |
| 11 | 绑定地址 | bind |
内网 IP,不要 0.0.0.0 暴露公网 |
| 12 | 运行用户 | ps -ef \| grep redis |
非 root 的 redis 用户 |
| 13 | 慢日志 | slowlog-log-slower-than |
10000(10ms) |
| 14 | 持久化 | appendonly / save |
按业务需求,缓存可全关 |
| 15 | NTP 时间同步 | chronyc sources / ntpq -p |
已同步(哨兵与集群依赖时间判定) |
九、缓存设计的经典问题
9.1 缓存穿透(Cache Penetration)
定义:查询一个数据库中根本不存在的数据。缓存中没有,请求每次都打到数据库。
请求 id=-1 → Redis 查不到 → 查数据库 → 数据库也没有 → 不写缓存 → 返回空
下一个请求 id=-1 → 重复上述过程 ...
【结果】缓存完全失去保护作用,数据库被打穿
典型场景:黑客用不存在的 ID(如 id=-1、随机 UUID)恶意刷接口。
解决方案:
| 方案 | 做法 | 优点 | 缺点 |
|---|---|---|---|
| ① 缓存空值 | 查库为空时也写入缓存,值为特殊标记(如 "" 或 "NULL"),TTL 设短(30秒~5分钟) |
简单有效 | 占额外内存;数据真的新增时有短暂不一致(需在写库后删除空缓存) |
| ② 布隆过滤器 | 启动时把所有存在的 key 加载到布隆过滤器,请求先过过滤器,不存在直接返回 | 内存极省,拦截率高 | 有误判(判在可能不在,判不在一定不在);不支持删除(需用 Counting Bloom 或定期重建) |
| ③ 参数校验 | 接口层校验 ID 格式、范围(如 ID 必须为正整数) | 零成本 | 只能拦截明显非法的请求 |
| ④ 限流/黑名单 | 对同一 IP/用户的异常请求限流,识别后加黑名单 | 防恶意攻击 | 需要额外的风控组件 |
# 【方案一】缓存空值
SET product:9999 "" EX 60 NX # 空值只缓存 60 秒
# 【方案二】布隆过滤器(需要 RedisBloom 模块)
# 安装模块
# 在 redis.conf 中:loadmodule /usr/lib/redis/modules/redisbloom.so
BF.RESERVE products 0.001 1000000 # 误判率 0.1%,预计元素 100 万
BF.ADD products 1001 # 添加单个
BF.MADD products 1002 1003 1004 # 批量添加
BF.EXISTS products 9999 # 返回 0 → 一定不存在,直接返回
(integer) 0
BF.MEXISTS products 1001 9999 # 批量判断
BF.INFO products # 查看容量、误判率、已插入数量
# 用 Bitmap 手工实现简易布隆过滤器(无 RedisBloom 模块时)
SETBIT bloom:products 12345 1
GETBIT bloom:products 12345
布隆过滤器原理:一个长位数组 + k 个哈希函数。插入时把 k 个哈希位置置 1;查询时若任一位置为 0 则一定不存在,全为 1 则可能存在(存在误判)。位数组长度 m 与元素数 n、误判率 p 的关系:m = -n·ln(p) / (ln2)²。100 万元素、1% 误判率约需 1.2MB。
9.2 缓存击穿(Hotspot Invalid / Cache Breakdown)
定义:某个热点 key 恰好过期的瞬间,大量并发请求同时发现缓存失效,全部涌向数据库。
时刻 T: hot:product:1001 过期
时刻 T+1ms:1 万个并发请求同时查缓存 → 全部 miss
时刻 T+2ms:1 万个请求同时查数据库 → 【数据库瞬时被压垮】
与雪崩的区别:击穿是单个热点 key失效;雪崩是大量 key 同时失效或Redis 整体宕机。
解决方案:
| 方案 | 做法 |
|---|---|
| ① 互斥锁(分布式锁) | 只允许第一个请求去查数据库并回填缓存,其余请求短暂等待后重试读缓存 |
| ② 逻辑过期(不设物理 TTL) | value 中存一个逻辑过期时间字段。读到逻辑过期时,返回旧数据并异步开线程去更新缓存(保证可用性,牺牲一致性) |
| ③ 热点数据永不过期 | 对已知的极热 key 不设 TTL,改由后台定时任务或数据变更时主动更新 |
| ④ 提前预热 + 续期 | 监控 key 剩余 TTL,即将过期时后台自动续期并刷新 |
# 【方案一】互斥锁实现(Shell 演示逻辑,实际在应用代码中)
# 1) 尝试读缓存
VALUE=$(redis-cli GET product:1001)
if [ -z "$VALUE" ]; then
# 2) 缓存未命中,尝试获取重建锁
LOCK=$(redis-cli SET lock:product:1001 "$(uuidgen)" NX EX 10)
if [ "$LOCK" == "OK" ]; then
# 3) 获得锁:查数据库 → 写缓存 → 释放锁
DATA=$(query_db 1001)
redis-cli SET product:1001 "$DATA" EX 1800
redis-cli DEL lock:product:1001 # 生产中应用 Lua 校验 value 后删除
else
# 4) 没抢到锁:短暂 sleep 后重试读缓存
sleep 0.05
VALUE=$(redis-cli GET product:1001)
fi
fi
# 【方案二】逻辑过期:value 中带过期时间戳
redis-cli SET product:1001 '{"data":{...},"expireAt":1756000000}'
# 应用读到后比较 expireAt 与当前时间:
# 未过期 → 直接返回
# 已过期 → 返回旧值 + 提交异步任务重建缓存(重建前先抢锁,避免重复重建)
9.3 缓存雪崩(Cache Avalanche)
定义:大量缓存在同一时间失效,或 Redis 集群整体宕机,导致所有请求直接打到数据库,数据库不堪重负后连锁引发整个系统崩溃。
两种成因与对策:
成因一:大量 key 同时过期
【错误做法】批量预热时统一设置 TTL 为 3600 秒
→ 3600 秒后这批 key 在同一秒全部失效
→ 所有请求同时打到数据库
| 对策 | 做法 |
|---|---|
| TTL 加随机抖动 | EXPIRE key $((3600 + RANDOM % 600)),把失效时间打散到 10 分钟窗口内 |
| 分级过期 | 热数据 TTL 长、冷数据 TTL 短,避免集中 |
| 永不过期 + 主动更新 | 核心数据不设 TTL,由数据变更时主动刷新 |
# 批量写入时打散 TTL
for i in $(seq 1 10000); do
ttl=$((3600 + RANDOM % 600))
echo "SET product:$i value$i EX $ttl"
done | redis-cli --pipe
成因二:Redis 服务整体不可用
| 对策 | 层次 | 做法 |
|---|---|---|
| 高可用架构 | 事前 | 主从 + 哨兵,或 Redis Cluster;跨机房部署 |
| 多级缓存 | 事前 | 本地缓存(Caffeine/Guava)+ Redis 二级缓存,Redis 挂了本地缓存还能扛一阵 |
| 持久化 + 快速恢复 | 事前 | 开启 AOF/RDB,宕机重启后能快速恢复热数据,避免"冷启动雪崩" |
| 服务降级与熔断 | 事中 | Redis 不可用时直接返回兜底默认数据或友好提示,不透传到数据库(Sentinel/Hystrix/Resilience4j) |
| 请求限流 | 事中 | 数据库前加限流,只放行数据库能承受的 QPS,其余快速失败 |
| 数据库连接池隔离 | 事中 | 不同业务用独立连接池,防止一个业务打满连接影响全局 |
| 缓存预热 | 事后 | 重启后先由后台任务批量加载热点数据,再放开流量 |
# 【缓存预热】重启后从数据库批量灌入热点数据
mysql -N -e "SELECT id, name FROM product WHERE hot=1" mydb | \
awk -F'\t' '{printf "SET product:%s \"%s\" EX %d\n", $1, $2, 3600+int(rand()*600)}' | \
redis-cli -a 123456 --no-auth-warning --pipe
# 【验证 Redis 重启后热数据是否加载完成】
redis-cli DBSIZE
redis-cli INFO persistence | grep -E 'loading|rdb_last_load'
9.4 三大问题对比总结
| 问题 | 成因 | key 是否存在 | 影响范围 | 核心解法 |
|---|---|---|---|---|
| 缓存穿透 | 查询数据库中不存在的数据 | 数据库中不存在 | 持续的、恶意的 | 布隆过滤器 + 缓存空值 |
| 缓存击穿 | 单个热点 key 过期瞬间高并发 | 存在,但缓存刚失效 | 瞬时、单 key | 互斥锁 + 逻辑过期 |
| 缓存雪崩 | 大量 key 同时失效或 Redis 宕机 | 存在 | 大面积、系统性 | TTL 随机化 + 高可用 + 降级限流 |
9.5 缓存与数据库的一致性
根本矛盾:缓存和数据库是两个独立系统,无法在无分布式事务的情况下保证强一致,只能追求最终一致。
9.5.1 四种更新策略对比
| 策略 | 操作顺序 | 问题 | 评价 |
|---|---|---|---|
| ① 先更新缓存,再更新数据库 | SET cache → UPDATE db |
数据库更新失败则缓存是脏数据;且缓存无意义地承担了写压力 | 不可用 |
| ② 先更新数据库,再更新缓存 | UPDATE db → SET cache |
并发写时可能出现"后写的数据库、先写的缓存"导致缓存是旧值;写多读少时浪费(更新了却没人读) | 不推荐 |
| ③ 先删缓存,再更新数据库 | DEL cache → UPDATE db |
高并发下问题明显:A 删缓存后还没更新库,B 读到旧值并回填缓存 → 缓存长期是旧值 | 需配合延迟双删 |
| ④ 先更新数据库,再删缓存 | UPDATE db → DEL cache |
极小概率不一致(读线程在"查库后、回填前"被更新线程插入),概率极低 | 推荐(Cache Aside Pattern) |
9.5.2 Cache Aside Pattern(旁路缓存,业界标准)
【读操作】
1. 读缓存
2. 命中 → 直接返回
3. 未命中 → 查数据库 → 写入缓存(设置 TTL)→ 返回
【写操作】
1. 更新数据库
2. 删除缓存(不是更新缓存!)
为什么是"删除"而不是"更新"缓存:
- 避免并发覆盖:两个写请求的"更新缓存"步骤顺序可能与"更新数据库"顺序相反,产生永久脏数据。
- 懒加载更经济:写多读少的数据,更新缓存后可能一次都没被读,纯属浪费。
- 计算型缓存:若缓存值是多表聚合结果,更新缓存需要重新计算,成本高。
为什么是"先更新数据库,后删缓存":
【假设反过来:先删缓存,后更新数据库】—— 不一致概率高
时刻1 线程A(写):DEL cache
时刻2 线程B(读):GET cache → miss
时刻3 线程B(读):查数据库 → 得到【旧值】
时刻4 线程B(读):SET cache = 旧值
时刻5 线程A(写):UPDATE db = 新值
【结果】缓存中是旧值,且会一直存在到 TTL 到期 —— 时间窗口是【整个 TTL】
【推荐:先更新数据库,后删缓存】—— 不一致概率极低
时刻1 线程B(读):GET cache → miss
时刻2 线程B(读):查数据库 → 得到旧值
时刻3 线程A(写):UPDATE db = 新值
时刻4 线程A(写):DEL cache
时刻5 线程B(读):SET cache = 旧值 ← 只有这个极窄的交错顺序才会出错
【要求】读操作的"查库"必须早于写操作的"更新库",且读操作的"回填"必须晚于写操作的"删缓存"
由于【写数据库比读数据库慢得多】,这个窗口极难命中
9.5.3 增强方案
| 方案 | 做法 | 适用 |
|---|---|---|
| 延迟双删 | DEL cache → UPDATE db → sleep 几百毫秒 → DEL cache |
主从架构下解决"从库延迟导致回填旧值" |
| 删除失败重试 | 删缓存失败时投递到消息队列重试,或写入本地重试表由定时任务补偿 | 保证删除最终成功 |
| 订阅 binlog(推荐) | Canal/Debezium 订阅 MySQL binlog,解析变更后异步删除对应缓存 | 业务代码零侵入,最优雅 |
| 设置合理 TTL(兜底) | 无论如何都给缓存设 TTL,最坏情况下 TTL 到期自动一致 | 必备兜底手段 |
| 读写都加分布式锁 | 强一致场景下,读写同一数据都需先获取锁 | 性能损失大,仅用于金额等强一致场景 |
| 读请求走主库 | 强一致读绕过缓存与从库,直读主库 | 关键路径(如下单前的库存校验) |
# 【延迟双删】示例
redis-cli DEL product:1001 # ① 先删缓存
mysql -e "UPDATE product SET price=99 WHERE id=1001" # ② 更新数据库
sleep 0.5 # ③ 等待主从同步完成(延迟时间 > 主从延迟)
redis-cli DEL product:1001 # ④ 再删一次,清掉可能被回填的旧值
# 【订阅 binlog 方案架构】
# MySQL binlog → Canal Server → MQ(Kafka/RocketMQ) → 消费者 → DEL Redis 缓存
#
# 优势:
# 1. 业务代码完全不用关心缓存删除
# 2. MQ 天然提供重试与顺序保证
# 3. 可以同时更新多个缓存/搜索引擎/数仓
一致性等级的选择:绝大多数业务(商品详情、用户资料、文章内容)用 Cache Aside + 合理 TTL + binlog 兜底删除就足够了。只有金额、库存、余额等场景才需要强一致——这类数据应该直接读数据库或使用数据库的事务与行锁,而不是硬要塞进缓存。
十、安全加固
10.1 未授权访问漏洞(必须理解的经典漏洞)
漏洞成因:Redis 默认配置下 ① 绑定 0.0.0.0、② 无密码、③ 以 root 运行。攻击者可以直接连上 Redis,利用其持久化功能向任意路径写入任意内容,从而获得服务器控制权。
10.1.1 风险原理一:篡改 SSH 授权文件
成立条件:Redis 以 root 运行、无密码、可从外部访问。
原理说明:Redis 的 CONFIG SET 可以在运行时修改 dir(数据目录)和 dbfilename(持久化文件名),而 SAVE 会把内存数据按这两个设置写到磁盘。两者组合起来,就等价于以 Redis 进程的身份向任意路径写入一个内容部分可控的文件。若 Redis 以 root 运行,可写范围就包括 /root/.ssh/、/var/spool/cron/、Web 站点目录等敏感位置,攻击者据此可追加 SSH 授权密钥、写入计划任务或落下 Web 后门,从而取得服务器控制权。
写出的文件里会混有 RDB 的二进制头尾,但 authorized_keys、crontab 这类按行解析、忽略无法识别的行的文件格式,只要其中有一行合法就会生效——这是该漏洞能够利用成功的关键。
⚠️ 本节只说明风险成因与自查方法,不提供可直接运行的利用步骤。如需验证,只能在你自己拥有授权的隔离测试环境中进行;对未授权的系统实施此类操作是违法行为。
10.1.2 如何自查是否已被利用
一旦发现 Redis 曾以 root 身份裸奔在公网上,就应当按下面的清单逐项排查,不要只改配置了事:
# 1. 检查 root 与各用户的 SSH 授权文件是否被追加了陌生公钥
cat /root/.ssh/authorized_keys
for d in /home/*; do echo "== $d"; cat $d/.ssh/authorized_keys 2>/dev/null; done
# 2. 检查计划任务是否被写入异常条目(尤其是带 curl/wget/bash -i 的行)
crontab -l -u root
ls -l /var/spool/cron/ /etc/cron.d/
cat /etc/crontab
# 3. 检查 Redis 当前的持久化路径是否被改到了敏感目录
redis-cli config get dir
redis-cli config get dbfilename
# 4. 检查 Redis 里是否存在攻击者残留的异常 key
redis-cli --scan --pattern '*' | head -50
# 5. 检查异常进程、异常网络连接与挖矿迹象
ps aux --sort=-%cpu | head -15
ss -antp | grep ESTAB
top -bn1 | head -15
# 6. 检查最近新增或修改的可疑文件
find /root /home /tmp /var/tmp -mtime -7 -type f 2>/dev/null | head -50
# 7. 检查是否有新增的可疑用户与 UID 为 0 的账号
awk -F: '$3==0{print}' /etc/passwd
lastlog | head -20
若确认已被入侵,改密码和加防火墙都不足以清除后门。正确做法是:隔离该主机 → 取证留存 → 备份业务数据 → 重装系统后再从干净备份恢复数据,同时轮换所有可能泄露的密钥与凭据。
10.2 加固清单
下面十项按收益从高到低排列。其中第 5 项(低权限用户运行)与第 8 项(防火墙)是阻断危害的根本手段,密码只是第一道门。
| 序号 | 加固项 | 关键作用 | 优先级 |
|---|---|---|---|
| 1 | requirepass 设强密码 |
阻断匿名连接 | 必做 |
| 2 | bind 绑定内网地址 |
缩小可达面 | 必做 |
| 3 | protected-mode yes |
兜底防护(3.2+) | 必做 |
| 4 | 修改默认端口 | 躲开批量扫描 | 建议 |
| 5 | 低权限用户运行 | 限制任意文件写入的破坏范围 | 必做 |
| 6 | rename-command 屏蔽危险命令 |
降低误操作与被利用风险 | 建议 |
| 7 | 数据目录选址与权限 | 防止写入敏感路径 | 必做 |
| 8 | 防火墙 / 安全组白名单 | 只允许应用服务器访问 | 必做 |
| 9 | ACL 用户体系(6.0+) | 最小权限、按业务隔离 | 建议 |
| 10 | TLS 加密传输(6.0+) | 防止明文抓包 | 按需 |
10.2.1 requirepass:设置访问密码
# redis.conf
# 【必须】设置访问密码,长度建议 32 位以上的随机串
requirepass K7xQ2mR9vLpZ3nT8wY4bC6dF1gH5jS0a
密码强度要求:
| 要求 | 说明 |
|---|---|
| 长度足够 | 至少 20 位,推荐 32~64 位。Redis 单机每秒可承受十万级请求,短密码在内网可被瞬间暴破 |
| 完全随机 | 用 openssl rand/pwgen 生成,不要用有语义的词 |
| 禁止弱口令 | 123456、redis、root、admin、password、公司名、端口号、与其他系统复用的密码,一律禁止 |
| 定期轮换 | 配合 CONFIG SET requirepass 可不重启热改,改完立刻同步更新应用配置与 masterauth |
生成随机密码:
# 方式一:openssl(推荐)
openssl rand -base64 48 | tr -d '/+=' | cut -c1-40
# 方式二:/dev/urandom
tr -dc 'A-Za-z0-9' </dev/urandom | head -c 40; echo
# 方式三:pwgen(需 yum install -y pwgen)
pwgen -s 40 1
运行时热修改密码(不重启):
redis-cli -a 'OldPassword' --no-auth-warning CONFIG SET requirepass 'NewLongRandomPassword'
# 【注意】CONFIG SET 只改内存中的配置,重启即失效
# 必须同时改配置文件,或执行 CONFIG REWRITE 把当前配置写回 redis.conf
redis-cli -a 'NewLongRandomPassword' --no-auth-warning CONFIG REWRITE
requirepass在配置文件里是明文,这是必须重点保护的一环:# 配置文件属主改为 redis,权限收到 600,只有 redis 用户可读 chown redis:redis /etc/redis/redis.conf chmod 600 /etc/redis/redis.conf ls -l /etc/redis/redis.conf # -rw------- 1 redis redis 62KB ... /etc/redis/redis.conf同理,命令行里的
-a password会出现在ps -ef输出和~/.bash_history里,任何本机用户都能看到。安全的做法:# 1) 用 REDISCLI_AUTH 环境变量(Redis 5.0+),不出现在命令行参数中 export REDISCLI_AUTH='YourPassword' redis-cli PING # 2) 或者交互式登录后再 AUTH redis-cli 127.0.0.1:6379> AUTH YourPassword OK # 3) --no-auth-warning 只是抑制"命令行传密码不安全"的告警,不解决泄露问题
主从与集群环境下密码要成套配置:
# 从节点必须配置 masterauth,否则无法与主节点同步
masterauth K7xQ2mR9vLpZ3nT8wY4bC6dF1gH5jS0a
requirepass K7xQ2mR9vLpZ3nT8wY4bC6dF1gH5jS0a
# Redis 6.0+ 若主节点启用了 ACL,从节点还需指定用户名
masteruser replica-user
# 哨兵端需要单独配置连接主节点的密码
sentinel auth-pass mymaster K7xQ2mR9vLpZ3nT8wY4bC6dF1gH5jS0a
# Redis 5.0+ 哨兵自身也可设密码
requirepass SentinelOwnPassword
集群模式下
requirepass与masterauth必须同时设置且值相同,因为集群中任一节点都可能被提升为主或降级为从,节点间需要互相认证。
10.2.2 bind:绑定监听地址
bind 决定 Redis 监听在哪些网卡地址上,是收缩攻击面的第一道物理隔离。
# 【最安全】只允许本机访问:应用与 Redis 同机部署时的首选
bind 127.0.0.1
# 【常用】同时监听本地回环与内网网卡(多个地址空格分隔)
bind 127.0.0.1 192.168.1.10
# 【IPv6 环境】
bind 127.0.0.1 ::1
# 【危险】监听所有网卡,等价于注释掉 bind
# bind 0.0.0.0
| 写法 | 效果 | 评价 |
|---|---|---|
bind 127.0.0.1 |
只能本机连接 | 最安全,应用与 Redis 同机时首选 |
bind 127.0.0.1 <内网IP> |
本机 + 内网可连 | 生产推荐。务必带上 127.0.0.1,否则本机运维脚本、健康检查、redis-cli 默认连接都会失败 |
bind 0.0.0.0 / 注释掉 |
监听全部网卡 | 禁止用于生产,是未授权访问漏洞的首要成因 |
取舍要点:
- 只写内网 IP 不写
127.0.0.1是常见踩坑:redis-cli不带-h时默认连127.0.0.1,会得到Connection refused;很多监控采集脚本、Zabbix Agent 也默认走回环地址。 - 绑定的 IP 必须在本机真实存在,否则启动失败并报
Could not create server TCP listening socket 192.168.1.10:6379: bind: Cannot assign requested address。云主机上要绑内网 IP(ip addr看到的那个),不是弹性公网 IP。 - 容器 / Kubernetes 场景:容器网络地址是动态的,通常只能
bind 0.0.0.0。此时必须靠requirepass+ NetworkPolicy / 安全组 / 不发布端口到宿主机来兜底,绝不能因为"容器里"就省掉密码。 - Unix Socket 方案:应用与 Redis 同机时可彻底不监听 TCP,性能还高约 20%~30%。
# 完全关闭 TCP 监听,只用 Unix Socket(同机部署的最强隔离)
port 0
unixsocket /var/run/redis/redis.sock
unixsocketperm 700
redis-cli -s /var/run/redis/redis.sock
10.2.3 protected-mode:保护模式
版本说明:protected-mode 是 Redis 3.2 引入的参数,正是为了缓解大规模爆发的未授权访问事件。Redis 3.0 及更早版本没有这个参数,只能靠 bind + requirepass + 防火墙自行防护。
# 【默认值即为 yes,不要改成 no】
protected-mode yes
生效逻辑:
protected-mode yes 且 ① 没有配置任何 bind
且 ② 没有设置 requirepass
↓
只接受来自 127.0.0.1 / ::1 / Unix Socket 的连接
外部连接虽能建立 TCP,但执行任何命令都返回:
(error) DENIED Redis is running in protected mode because protected
mode is enabled, no bind address was specified, no authentication
password is requested to clients. ...
| 条件 | protected-mode 是否拦截外部连接 |
|---|---|
| 未配 bind + 未设密码 | 拦截(只能本机访问) |
| 未配 bind + 已设密码 | 不拦截(认为管理员已明确授权) |
| 已配 bind + 未设密码 | 不拦截(认为管理员已明确授权) |
| 已配 bind + 已设密码 | 不拦截 |
关键理解:
protected-mode只是给"完全裸配置"兜底的安全网,它不是防火墙。一旦你设置了requirepass或写了bind,它就自动让路。因此绝不能因为开了 protected-mode 就认为安全——真正的防护依然是密码 + bind + 防火墙 + 低权限用户四件套。
集群模式下的特殊情况:
# 集群节点之间需要通过内网互相通信,若未设 bind,需要显式关闭保护模式
# 但更好的做法是:保留 protected-mode yes,同时配置 bind 与 requirepass
protected-mode no # 仅在已有防火墙严格白名单时才这样配
10.2.4 修改默认端口
6379 是全网扫描器的首要目标端口,改成一个高位随机端口能过滤掉绝大部分自动化扫描("安全性提升"来自减少噪音,不是真正的防护,属于纵深防御中的一层)。
# 换成 10000~65535 之间的非常见端口
port 26380
# 改完后所有客户端都要带 -p
redis-cli -p 26380 -a 'YourPassword' --no-auth-warning PING
# 修改端口后需同步调整(漏改任何一项都会导致故障)
# 1) 应用配置里的 Redis 端口
# 2) 从节点的 replicaof / slaveof 中的主节点端口
# 3) 哨兵配置的 sentinel monitor 端口
# 4) 集群的 cluster-config-file 与总线端口(总线端口 = 端口 + 10000)
# 5) 防火墙放行规则
# 6) 监控采集配置(Zabbix / redis_exporter)
# 7) SELinux 端口标签(CentOS 7 且 SELinux 为 enforcing 时)
semanage port -a -t redis_port_t -p tcp 26380
CentOS 6 与 7 差异:CentOS 6 默认 SELinux 也可能是 enforcing,但
redis_port_t类型可能不存在(Redis 未被 EPEL 的策略覆盖),此时若 SELinux 拦截,需要用audit2allow生成本地策略模块或临时setenforce 0定位问题。CentOS 7 上semanage(属于policycoreutils-python包)是标准做法。
10.2.5 用低权限专用用户运行(最关键的一条)
这是整个安全加固中收益最高的一项。 密码可能泄露、防火墙可能被临时放开,但只要 Redis 不是 root 运行,"任意文件写入"的破坏范围就被限制在 redis 用户可写的目录里——写不了 /root/.ssh/authorized_keys,写不了 /var/spool/cron/root,写不了 /etc,漏洞的危害等级直接从"服务器失陷"降到"Redis 数据受损"。
# 1) 创建专用系统用户:无登录 shell、无家目录、不可交互登录
useradd -r -s /sbin/nologin -M redis
# -r 创建系统用户(UID < 1000,不占用普通用户 UID 段)
# -s /sbin/nologin 禁止登录
# -M 不创建家目录
# 验证
id redis
# uid=996(redis) gid=994(redis) groups=994(redis)
grep redis /etc/passwd
# redis:x:996:994::/home/redis:/sbin/nologin
# 2) 创建目录并设置属主与权限
mkdir -p /var/lib/redis /var/log/redis /var/run/redis /etc/redis
chown -R redis:redis /var/lib/redis /var/log/redis /var/run/redis
chown redis:redis /etc/redis/redis.conf
# 数据目录 750:属主可读写执行,同组可读执行,其他用户完全无权限
chmod 750 /var/lib/redis
chmod 750 /var/log/redis
# 配置文件 600:含明文密码,只有属主可读
chmod 600 /etc/redis/redis.conf
ls -ld /var/lib/redis /var/log/redis
# drwxr-x--- 2 redis redis 4096 ... /var/lib/redis
# drwxr-x--- 2 redis redis 4096 ... /var/log/redis
systemd 单元文件(CentOS 7+)中指定运行用户:
# /usr/lib/systemd/system/redis.service
[Unit]
Description=Redis In-Memory Data Store
After=network.target
[Service]
Type=notify
# 【核心】以 redis 用户与组运行,而不是 root
User=redis
Group=redis
ExecStart=/usr/local/bin/redis-server /etc/redis/redis.conf --supervised systemd
ExecStop=/usr/local/bin/redis-cli -p 6379 shutdown
Restart=always
RuntimeDirectory=redis
RuntimeDirectoryMode=0750
# 【进一步收紧】systemd 沙箱选项(CentOS 7 的 systemd 219 已支持下列大部分)
NoNewPrivileges=yes # 禁止进程提权(禁用 setuid)
PrivateTmp=yes # 独立的 /tmp 命名空间
ProtectSystem=full # /usr /boot /etc 只读
ProtectHome=yes # /home /root 不可见,直接封死 authorized_keys 写入
ReadWritePaths=/var/lib/redis /var/log/redis
LimitNOFILE=65535 # 文件描述符上限,必须与 maxclients 匹配
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable redis
systemctl restart redis
# 【验证】确认运行用户不是 root —— 这一步必须做
ps -ef | grep [r]edis-server
# redis 12345 1 0 10:00 ? 00:00:03 /usr/local/bin/redis-server 127.0.0.1:6379
# ↑ 第一列必须是 redis,如果是 root 说明加固没生效
CentOS 6(SysV init)中指定运行用户:
CentOS 6 没有 systemd,用 /etc/init.d/redis 脚本管理,需要用 su 或 daemon --user 降权启动:
# /etc/init.d/redis 中的启动行
# 【错误】直接启动会以 root 运行
# $EXEC $CONF
# 【正确】切换到 redis 用户启动
daemon --user redis $EXEC $CONF
# 或
su -s /bin/bash redis -c "$EXEC $CONF"
# CentOS 6 注册开机自启
chkconfig --add redis
chkconfig redis on
chkconfig --list redis
# CentOS 7 对应命令
systemctl enable redis
systemctl is-enabled redis
已经用 root 跑了怎么改:先
systemctl stop redis,再把dir、logfile、pidfile指向的目录属主全部chown -R redis:redis(漏改任何一个都会导致启动后无法写 RDB,进而触发MISCONF错误),然后修改 systemd 的User=,最后daemon-reload+ 重启并用ps -ef验证。
10.2.6 rename-command:重命名或禁用危险命令
rename-command 把命令改名,只有知道新名字的人才能调用;改成空字符串 "" 即彻底禁用该命令。
# 【彻底禁用】改名为空字符串
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command KEYS ""
rename-command DEBUG ""
# 【改名保留】只有知道新名字的运维能用(新名字要足够随机,不要用 CONFIG2 之类)
rename-command CONFIG CONFIG_a7f3e9b1c5d2
rename-command SHUTDOWN SHUTDOWN_a7f3e9b1c5d2
各命令的取舍分析:
| 命令 | 危险点 | 建议处理 | 屏蔽后的副作用 |
|---|---|---|---|
FLUSHALL |
清空所有 db,一条命令毁掉全部数据 | 禁用("") |
需要清库时只能改配置重启,或用 --scan + DEL 批量删 |
FLUSHDB |
清空当前 db | 禁用("") |
同上 |
KEYS |
全量遍历,O(N) 阻塞单线程,百万级 key 会卡死数秒 | 禁用("") |
改用 SCAN / redis-cli --scan(渐进式、不阻塞) |
CONFIG |
可改 dir、dbfilename、requirepass,是未授权访问漏洞的核心利用点 |
改名(保留运维能力) | 监控采集若用 CONFIG GET maxmemory 会失败,需改用 INFO;CONFIG SET 热改参数、CONFIG REWRITE 都要用新名字 |
EVAL / EVALSHA / SCRIPT |
可执行任意 Lua 脚本,历史上多次出现沙箱逃逸 CVE;写得差的脚本会长时间阻塞 | 若业务不用 Lua → 禁用;若用了(如分布式锁)→ 保留 | 禁用会导致 Redisson 等框架的分布式锁、限流器直接不可用 |
DEBUG |
DEBUG SEGFAULT 直接让进程崩溃,DEBUG SLEEP 可阻塞服务 |
禁用("") |
失去 DEBUG OBJECT 排查编码方式的能力,改用 OBJECT ENCODING |
SHUTDOWN |
直接关停实例;带 NOSAVE 时还会丢数据 |
改名 | 停服务改用 systemctl stop redis(发 SIGTERM,Redis 会优雅落盘) |
SLAVEOF / REPLICAOF |
可把生产实例变成攻击者实例的从库,数据被整体覆盖清空 | 改名 | 主从切换需用新名字或改配置重启 |
MONITOR |
打印所有实时命令,吞吐下降 50% 以上,还会泄露业务数据 | 改名 | 排查时用新名字,且必须限时使用 |
MIGRATE |
可把数据搬到任意外部 Redis,数据外泄通道 | 非迁移期禁用 | 数据迁移时临时开放 |
BGREWRITEAOF / BGSAVE |
频繁调用引发大量 fork,内存翻倍 | 保留(运维必需) | — |
完整的生产加固片段:
# ===== 危险命令屏蔽(生产环境模板)=====
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command KEYS ""
rename-command DEBUG ""
rename-command MIGRATE ""
rename-command CONFIG CONFIG_9f2a7c4e
rename-command SHUTDOWN SHUTDOWN_9f2a7c4e
rename-command SLAVEOF SLAVEOF_9f2a7c4e
rename-command REPLICAOF REPLICAOF_9f2a7c4e
rename-command MONITOR MONITOR_9f2a7c4e
⚠️ 屏蔽后运维自己也用不了,这是最容易被忽略的代价,落地前务必确认:
rename-command只能写在配置文件里,不能通过CONFIG SET动态设置,改完必须重启实例才生效——生产环境要安排窗口,主从架构下先改从库、切换、再改原主库。- 集群模式下不要重命名集群相关命令(
CLUSTER、MIGRATE、ASKING),否则redis-cli --cluster的 reshard/rebalance 会直接失败。- 主从两端的 rename 配置必须完全一致,否则从库执行主库传来的命令时会报
unknown command,复制中断。- 把新名字记录到运维文档与密码管理系统,否则人员交接后无人知道
CONFIG被改成了什么,等于永久失去了热改配置的能力。- 监控与备份脚本要同步适配。典型踩坑:Zabbix 模板用
CONFIG GET maxmemory采集内存上限,CONFIG改名后该项监控静默失效。rename-command不是权限系统。Redis 6.0+ 有了 ACL 后,优先用 ACL 做细粒度授权(见 10.2.9),rename-command定位为"防误操作 + 给老版本兜底"。
10.2.7 数据目录选址与权限
# 【推荐】专用数据目录,不在任何 Web 站点目录、不在 /tmp、不在用户家目录下
dir /var/lib/redis
# 日志与 pid 也用专用目录
logfile /var/log/redis/redis.log
pidfile /var/run/redis/redis_6379.pid
选址禁忌:
| 禁止设置为 | 原因 |
|---|---|
/var/www/html、/usr/share/nginx/html 等 Web 站点目录 |
一旦被未授权访问,攻击者可写入 webshell 并通过 HTTP 直接执行 |
/tmp、/var/tmp |
全局可写;systemd-tmpfiles 会定期清理,RDB 文件可能被自动删除 |
/root、/home/xxx |
与 SSH 密钥、.bashrc 等敏感文件同目录 |
./(相对路径,默认值) |
实际落盘位置取决于启动时的工作目录,行为不可预测,重启后可能找不到旧数据 |
| NFS / 网络文件系统 | fsync 语义不可靠,BGSAVE/AOF 可能损坏;且延迟高 |
# 确认当前实际使用的数据目录
redis-cli -a 'pwd' --no-auth-warning CONFIG GET dir
ls -l /var/lib/redis/
# -rw------- 1 redis redis 1.2M ... dump.rdb ← RDB 文件权限应为 600
# -rw------- 1 redis redis 3.4M ... appendonly.aof
# Redis 6.2+ 可直接指定持久化文件的权限,默认 0600
rdb-file-permissions 0600
数据目录所在分区剩余空间必须大于内存中数据量的 1.5 倍,否则
BGSAVE写不下去会失败,进而导致所有写命令返回MISCONF错误(详见第十二章)。
10.2.8 防火墙与网络隔离
Redis 不应该出现在任何公网可达的地址上。 网络层白名单是比密码更硬的一道防线。
# ===== CentOS 7 firewalld:只放行指定应用服务器 =====
# 【推荐】用 rich rule 精确到源 IP
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.20/32" port protocol="tcp" port="6379" accept'
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.21/32" port protocol="tcp" port="6379" accept'
firewall-cmd --reload
firewall-cmd --list-rich-rules
# 【或者】用 zone + source 的方式管理一组可信主机
firewall-cmd --permanent --new-zone=redis-clients
firewall-cmd --permanent --zone=redis-clients --add-source=192.168.1.0/24
firewall-cmd --permanent --zone=redis-clients --add-port=6379/tcp
firewall-cmd --reload
# 【错误示范】千万不要这样:等于向全世界开放
# firewall-cmd --permanent --add-port=6379/tcp
# ===== CentOS 6 iptables =====
# 只允许两台应用服务器 + 本机访问 6379
iptables -A INPUT -p tcp --dport 6379 -s 192.168.1.20 -j ACCEPT
iptables -A INPUT -p tcp --dport 6379 -s 192.168.1.21 -j ACCEPT
iptables -A INPUT -p tcp --dport 6379 -s 127.0.0.1 -j ACCEPT
iptables -A INPUT -p tcp --dport 6379 -j DROP
# 集群模式还要放行总线端口 16379(= 6379 + 10000)
iptables -A INPUT -p tcp --dport 16379 -s 192.168.1.0/24 -j ACCEPT
service iptables save # 保存到 /etc/sysconfig/iptables
service iptables restart
iptables -L -n --line-numbers
详见 07-03-防火墙与SELinux.md
云环境的安全组与 VPC 要点:
| 要点 | 说明 |
|---|---|
| 安全组入方向不要开 0.0.0.0/0 | 这是云上 Redis 被入侵的第一大原因。源地址必须填应用服务器的内网 IP 或安全组 ID |
| 实例不要绑定公网 IP | Redis 节点放在 VPC 私有子网,不分配弹性公网 IP;需要外网访问时通过跳板机 / VPN / 云厂商的私有链路 |
| 用"安全组引用"而非 IP 段 | 阿里云/AWS 支持"允许来自 sg-app 安全组的实例访问",应用扩容换 IP 时无需改规则 |
| 跨可用区但同 VPC | 主从/集群节点分布在不同可用区提升可用性,但保持在同一 VPC 内网通信,不走公网 |
| NAT 网关只做出方向 | Redis 节点需要出网拉包时用 NAT 网关,NAT 不提供入方向映射,天然安全 |
| 托管版 Redis | 云数据库 Redis 版默认在 VPC 内,务必确认没有开启"公网连接地址";开启了要配白名单 |
| 审计与告警 | 打开云平台的安全组变更审计,对"6379 端口对 0.0.0.0/0 放开"配置合规告警规则 |
# 自查:本机是否有公网 IP 且 Redis 监听在其上
ip addr | grep -w inet
ss -tlnp | grep redis-server
# 期望输出只有内网地址与 127.0.0.1:
# LISTEN 0 511 127.0.0.1:6379 0.0.0.0:* users:(("redis-server",pid=12345,fd=6))
# LISTEN 0 511 192.168.1.10:6379 0.0.0.0:* users:(("redis-server",pid=12345,fd=7))
# 从公网机器验证(应该超时或拒绝)
# telnet <公网IP> 6379
# nc -zv -w3 <公网IP> 6379
10.2.9 ACL 用户体系(Redis 6.0+)
版本说明:ACL(Access Control List)是 Redis 6.0 引入的功能。
| 版本 | 权限能力 |
|---|---|
| Redis 5.x 及更早 | 只有一个全局密码 requirepass,密码正确即拥有全部权限。想做隔离只能靠多实例 + 不同密码 + rename-command |
| Redis 6.0+ | 支持多用户,可按命令、命令类别、key 模式、Pub/Sub 频道授权 |
| Redis 7.0+ | 新增选择器(selector),支持一个用户拥有多组独立的权限规则;ACL DRYRUN 可预演命令是否被允许 |
ACL SETUSER 语法
ACL SETUSER <username> [规则 ...]
| 规则 | 含义 |
|---|---|
on / off |
启用 / 禁用该用户(off 的用户无法认证) |
>password |
添加一个明文密码 |
<password |
移除一个密码 |
#<sha256hex> |
以 SHA-256 哈希形式添加密码(配置文件里不留明文) |
nopass |
该用户无需密码(危险,仅测试用) |
resetpass |
清空该用户的全部密码 |
~<pattern> |
允许访问匹配 pattern 的 key,如 ~cache:* |
%R~<pattern> |
只读该 key 模式(Redis 7.0+) |
%W~<pattern> |
只写该 key 模式(Redis 7.0+) |
allkeys / ~* |
允许访问所有 key |
resetkeys |
清空已授权的 key 模式 |
+<command> |
允许执行某命令,如 +get、+set |
-<command> |
禁止执行某命令 |
+<cmd>\|<subcmd> |
只允许某子命令,如 +config\|get(允许读配置但不允许改) |
+@<category> |
允许整个命令类别,如 +@read |
-@<category> |
禁止整个命令类别,如 -@dangerous |
allcommands / +@all |
允许所有命令 |
nocommands / -@all |
禁止所有命令(新建用户的默认状态) |
&<pattern> |
允许订阅/发布匹配的频道(Redis 6.2+) |
allchannels / &* |
允许所有频道 |
reset |
把用户重置为 off -@all resetkeys resetpass 的初始状态 |
命令类别(category)
# 列出全部命令类别
redis-cli ACL CAT
# 查看某个类别包含哪些命令
redis-cli ACL CAT dangerous
redis-cli ACL CAT read
| 类别 | 含义 |
|---|---|
@read |
所有读命令(GET、MGET、HGETALL、LRANGE、SMEMBERS、ZRANGE 等) |
@write |
所有写命令(SET、DEL、HSET、LPUSH、INCR 等) |
@keyspace |
key 空间操作(DEL、EXPIRE、RENAME、TYPE、SCAN、KEYS、DBSIZE、FLUSHALL 等) |
@string / @list / @hash / @set / @sortedset / @bitmap / @hyperloglog / @geo / @stream |
按数据类型划分 |
@admin |
管理命令(CONFIG、SHUTDOWN、SLAVEOF、CLIENT KILL、DEBUG、MONITOR、ACL 等),默认不给业务用户 |
@dangerous |
危险命令(FLUSHALL、FLUSHDB、KEYS、CONFIG、SHUTDOWN、DEBUG、MONITOR、SWAPDB、RESTORE、MIGRATE、CLUSTER、SORT 等),必须 -@dangerous |
@slow |
时间复杂度可能很高的命令(KEYS、SMEMBERS、SORT、LRANGE 全量等) |
@fast |
O(1) 或 O(log N) 的快命令 |
@blocking |
会阻塞的命令(BLPOP、BRPOP、BLMOVE、XREAD BLOCK、WAIT) |
@pubsub |
发布订阅相关 |
@transaction |
MULTI、EXEC、DISCARD、WATCH |
@scripting |
EVAL、EVALSHA、SCRIPT、FUNCTION |
@connection |
AUTH、PING、SELECT、HELLO、CLIENT、RESET |
@keyspace+@admin 之外的 @all |
全部命令 |
典型授权示例
# ===== 1) 只读用户:给报表/BI 系统用 =====
redis-cli ACL SETUSER reporter on '>Rep0rt_Str0ng_Pass_2026' \
'~*' '+@read' '-@dangerous'
# on 启用
# >... 设置密码
# ~* 可访问所有 key
# +@read 允许所有读命令
# -@dangerous 剔除 KEYS、SORT 等危险的读命令(顺序在后者生效,起到剪掉的作用)
# ===== 2) 业务用户:只能操作自己业务前缀下的 key =====
redis-cli ACL SETUSER app-order on '>0rd3r_Svc_Pass_LongRandom' \
'~order:*' '~cart:*' '+@read' '+@write' '-@dangerous' '-@admin'
# 只能读写 order: 与 cart: 前缀的 key,其他 key 一律 NOPERM
# ===== 3) 缓存服务用户:额外允许过期与计数 =====
redis-cli ACL SETUSER app-cache on '>C4ch3_Svc_Pass_LongRandom' \
'~cache:*' '+@read' '+@write' '+@keyspace' '-@dangerous' '-flushall' '-flushdb' '-keys'
# ===== 4) 监控用户:只能看信息,不能读业务数据 =====
redis-cli ACL SETUSER monitor on '>M0nit0r_Pass_LongRandom' \
'-@all' '+info' '+ping' '+client|info' '+config|get' '+latency' '+slowlog|get' '+slowlog|len'
# 注意 +config|get 只放开 CONFIG GET 子命令,CONFIG SET 依然被禁
# ===== 5) 复制专用用户:给从节点的 masteruser 用 =====
redis-cli ACL SETUSER replica-user on '>Repl_Pass_LongRandom' \
'-@all' '+psync' '+replconf' '+ping'
# ===== 6) 运维管理员 =====
redis-cli ACL SETUSER dba on '>DBA_Pass_VeryLongRandom' '~*' '&*' '+@all'
规则的生效顺序是"从左到右依次叠加",后面的规则会覆盖前面的。所以
+@all -@dangerous与-@dangerous +@all的结果完全不同:前者是"全部命令但排除危险的",后者是"全部命令"(-@dangerous被随后的+@all抵消)。排除类规则一定要写在后面。
ACL 查询与调试命令
# 列出所有用户及其完整规则(一行一个用户,格式与 ACL SETUSER 参数一致)
redis-cli ACL LIST
# 1) "user default on nopass sanitize-payload ~* &* +@all"
# 2) "user app-order on #a3f5... ~order:* ~cart:* resetchannels -@all +@read +@write"
# 只列出用户名
redis-cli ACL USERS
# 1) "default"
# 2) "app-order"
# 3) "monitor"
# 查看当前连接是以哪个用户认证的(排查权限问题第一步)
redis-cli -u redis://app-order:0rd3r_Svc_Pass_LongRandom@127.0.0.1:6379 ACL WHOAMI
# "app-order"
# 查看某用户权限的结构化详情
redis-cli ACL GETUSER app-order
# 1) "flags" 2) 1) "on" 2) "sanitize-payload"
# 3) "passwords" 4) 1) "a3f5c8...(SHA256 哈希,不显示明文)"
# 5) "commands" 6) "-@all +@read +@write"
# 7) "keys" 8) "~order:* ~cart:*"
# 9) "channels" 10) ""
# 11) "selectors" 12) (empty array) ← Redis 7.0+
# 生成一个安全的随机密码(Redis 自带,输出 64 位十六进制)
redis-cli ACL GENPASS
# "8f2a1c9e7b3d5a4f6c8e0b2d4f6a8c0e1b3d5f7a9c1e3b5d7f9a1c3e5b7d9f1a"
redis-cli ACL GENPASS 256 # 指定位数(bit)
# 删除用户(正在使用该用户的连接会被立即断开)
redis-cli ACL DELUSER app-order
# 预演:检查某用户执行某命令是否会被允许(Redis 7.0+,排查利器)
redis-cli ACL DRYRUN app-order GET order:1001
# "OK"
redis-cli ACL DRYRUN app-order GET user:1001
# "This user has no permissions to access the 'user:1001' key"
redis-cli ACL DRYRUN app-order FLUSHALL
# "This user has no permissions to run the 'flushall' command"
acllog:认证与授权失败日志
# 查看最近的 ACL 违规记录(认证失败、越权访问 key、越权执行命令)
redis-cli ACL LOG
# 1) 1) "count" 2) (integer) 3 ← 同类事件发生次数
# 3) "reason" 4) "command" ← auth/command/key/channel
# 5) "context" 6) "toplevel" ← toplevel/multi/lua
# 7) "object" 8) "flushall" ← 被拒的命令名或 key 名
# 9) "username" 10) "app-order"
# 11) "age-seconds" 12) "12.478"
# 13) "client-info" 14) "id=8 addr=192.168.1.20:51234 name= age=30 ..."
# 15) "entry-id" 16) (integer) 5 ← Redis 7.0+
# 17) "timestamp-created" 18) (integer) 1756000000000
# 19) "timestamp-last-updated" 20) (integer) 1756000012000
# 只看最近 10 条
redis-cli ACL LOG 10
# 清空日志
redis-cli ACL LOG RESET
reason 取值 |
含义 |
|---|---|
auth |
认证失败(密码错误或用户不存在)——大量出现说明可能正在被暴破 |
command |
用户尝试执行未授权的命令 |
key |
用户尝试访问未授权的 key |
channel |
用户尝试订阅/发布未授权的频道 |
# 控制 acllog 保留的条目数
acllog-max-len 128
把
ACL LOG接入监控:reason=auth的条目在短时间内快速增长,是暴力破解的强信号,应当配置告警。
default 用户的处理
Redis 6.0 之后仍保留一个名为 default 的特殊用户,用于兼容老客户端:
默认状态:user default on nopass ~* &* +@all
含义:启用、无需密码、可访问所有 key 与频道、可执行所有命令
| 场景 | default 用户的状态 |
|---|---|
未配置 requirepass、未配置 ACL |
on nopass +@all —— 任何人连上就是超级管理员 |
配置了 requirepass xxx |
等价于 user default on >xxx ~* &* +@all。requirepass 本质就是给 default 用户设密码 |
| 配置了 ACL 且要求严格隔离 | 应当收紧或禁用 default |
# 【推荐做法之一】保留 default 但设强密码并去掉危险命令
redis-cli ACL SETUSER default on '>Very_Long_Random_Default_Pass' '~*' '&*' '+@all' '-@dangerous'
# 【推荐做法之二】彻底禁用 default,强制所有客户端显式带用户名
redis-cli ACL SETUSER default off
# 此后 redis-cli -a 'pass' 会失败,必须用:
redis-cli --user app-order --pass '0rd3r_Svc_Pass_LongRandom' --no-auth-warning
# 或 AUTH 的双参数形式(Redis 6.0+)
redis-cli
127.0.0.1:6379> AUTH app-order 0rd3r_Svc_Pass_LongRandom
OK
⚠️ 禁用
default前必须确认所有客户端都支持"用户名+密码"认证(即 RESP 的AUTH user pass两参数形式)。Jedis 3.x+、Lettuce 5.2+、redis-py 3.4+、go-redis v8+ 均支持;过老的客户端只会发单参数AUTH pass,禁用 default 后会直接连不上。同时注意:哨兵与主从复制也依赖认证,禁用 default 时必须给从节点配置masteruser+masterauth。
ACL 与 requirepass 的关系
requirepass <pass> ≡ ACL SETUSER default on >:pass ~* &* +@all
两者是同一件事的两种写法:
· requirepass 是 default 用户密码的"简写"
· 两处都配时,以【后加载/后执行的那个】为准
· CONFIG SET requirepass 会直接改写 default 用户的密码
· ACL SETUSER default resetpass 会让 requirepass 失效(变成需要 nopass 或无法认证)
| 对比项 | requirepass |
ACL |
|---|---|---|
| 版本要求 | 全版本 | 6.0+ |
| 粒度 | 全局单一密码,全有或全无 | 多用户 / 按命令 / 按 key / 按频道 |
| 审计 | 无 | ACL LOG 可追溯用户与违规行为 |
| 适用 | 小规模、单业务实例 | 多业务共用实例、需要最小权限 |
| 建议 | 两者配合:requirepass 保证老客户端可用,ACL 给新业务做隔离 |
ACL 的持久化
ACL 规则通过 ACL SETUSER 在运行时创建后只存在于内存中,重启即丢失,必须持久化。有两种方式,二者互斥,不能同时用:
# ===== 方式一:直接写进 redis.conf(适合用户数少、变动不频繁)=====
user default on >MainPassword ~* &* +@all
user app-order on >0rd3r_Pass ~order:* ~cart:* +@read +@write -@dangerous
user monitor on >M0nit0r_Pass -@all +info +ping +client|info +config|get
# 修改后把内存中的 ACL 写回 redis.conf
redis-cli CONFIG REWRITE
# ===== 方式二:使用独立的 aclfile(推荐,适合用户多、需要热加载)=====
aclfile /etc/redis/users.acl
# /etc/redis/users.acl 的格式(每行一个 user,与 ACL LIST 输出一致)
cat > /etc/redis/users.acl <<'EOF'
user default on #a3f5c8e1... ~* &* +@all
user app-order on #b7d9e2f4... ~order:* ~cart:* +@read +@write -@dangerous
user monitor on #c1e3a5b7... -@all +info +ping +config|get
EOF
chown redis:redis /etc/redis/users.acl
chmod 600 /etc/redis/users.acl # 【必须】含密码哈希
# 把内存中的 ACL 保存到 aclfile
redis-cli ACL SAVE
# 从 aclfile 重新加载(热生效,无需重启;语法错误会整体回滚,不会半生效)
redis-cli ACL LOAD
| 命令 | 作用 | 前提 |
|---|---|---|
ACL SAVE |
内存 ACL → aclfile |
必须配置了 aclfile,否则报错 |
ACL LOAD |
aclfile → 内存 ACL |
同上;用于多节点统一分发 ACL 后热加载 |
CONFIG REWRITE |
内存配置(含 user 行)→ redis.conf |
仅在未配置 aclfile 时用于持久化 ACL |
生产建议:用
aclfile+ 配置管理工具(Ansible/SaltStack)统一分发,然后各节点执行ACL LOAD热加载,避免重启。主从与集群的所有节点 ACL 必须保持一致,否则故障转移后新主节点上的用户不存在,业务会大面积WRONGPASS。用#<sha256hex>形式写密码可以避免文件中出现明文(哈希值可由echo -n 'pass' | sha256sum生成)。
10.2.10 TLS 加密传输(Redis 6.0+)
版本说明:Redis 6.0 起官方内置 TLS 支持。此前只能通过 stunnel / spiped 等外部工具做隧道加密,或依赖 VPC 内网隔离。
未加密时,AUTH 命令的密码与所有业务数据都是明文在网络上传输,同网段抓包即可获取。以下场景必须上 TLS:跨机房 / 跨 VPC 通信、混合云、需满足等保或 PCI-DSS 合规要求、多租户共享网络。
# 源码编译时必须显式开启 TLS(默认不编译)
yum install -y openssl-devel
cd /usr/local/src/redis-7.2.4
make BUILD_TLS=yes
# 验证是否支持
./src/redis-server --tls-port 0 --version
# 用官方脚本一键生成测试证书(生产请用企业 CA 或 Let's Encrypt 签发)
cd /usr/local/src/redis-7.2.4
./utils/gen-test-certs.sh
ls tests/tls/
# ca.crt ca.key redis.crt redis.key redis.dh client.crt client.key
# ===== redis.conf TLS 配置 =====
# 【关键取舍】port 0 表示关闭明文端口,只接受 TLS 连接(最安全)
port 0
tls-port 6379
# 服务端证书与私钥
tls-cert-file /etc/redis/tls/redis.crt
tls-key-file /etc/redis/tls/redis.key
tls-key-file-pass secretPassphrase # 私钥有口令时填(6.2+)
# CA 证书:用于校验客户端证书
tls-ca-cert-file /etc/redis/tls/ca.crt
# 或指定 CA 目录
# tls-ca-cert-dir /etc/redis/tls/ca
# DH 参数(可选,用于 DHE 密钥交换)
tls-dh-params-file /etc/redis/tls/redis.dh
# 【重要】是否强制校验客户端证书(双向 TLS)
tls-auth-clients yes # yes=必须提供客户端证书(默认)
# no=不要求客户端证书(单向 TLS,仅加密不认证)
# optional=有则校验(6.2+)
# 主从复制与集群总线也走 TLS(主从两端必须同时开)
tls-replication yes
tls-cluster yes
# 协议版本:禁用不安全的旧版本
tls-protocols "TLSv1.2 TLSv1.3"
# 加密套件
tls-ciphers DEFAULT:!MEDIUM:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK
tls-ciphersuites TLS_CHACHA20_POLY1305_SHA256:TLS_AES_256_GCM_SHA384 # TLS 1.3 专用
tls-prefer-server-ciphers yes
# 会话复用:提升握手性能
tls-session-caching yes
tls-session-cache-size 20480
tls-session-cache-timeout 300
# 证书文件权限:私钥必须 600 且属主为 redis
mkdir -p /etc/redis/tls
cp tests/tls/{ca.crt,redis.crt,redis.key,redis.dh} /etc/redis/tls/
chown -R redis:redis /etc/redis/tls
chmod 700 /etc/redis/tls
chmod 600 /etc/redis/tls/redis.key
# ===== 客户端连接 TLS 实例 =====
redis-cli --tls \
--cert /etc/redis/tls/client.crt \
--key /etc/redis/tls/client.key \
--cacert /etc/redis/tls/ca.crt \
-h 192.168.1.10 -p 6379 -a 'YourPassword' --no-auth-warning PING
# PONG
# 单向 TLS(tls-auth-clients no)时只需 CA
redis-cli --tls --cacert /etc/redis/tls/ca.crt -h 192.168.1.10 -p 6379 PING
# 跳过证书校验(仅调试用,生产禁止)
redis-cli --tls --insecure -h 192.168.1.10 -p 6379 PING
# 用 openssl 验证握手与证书链
openssl s_client -connect 192.168.1.10:6379 -CAfile /etc/redis/tls/ca.crt </dev/null
# 从节点连接 TLS 主节点时,从节点自身也需要配置客户端证书
tls-replication yes
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 要点小结:
| 要点 | 说明 |
|---|---|
| 性能代价 | 加解密带来约 10%~30% 的吞吐下降与额外延迟。可通过开启会话复用、选用 AES-NI 硬件加速的 CPU、优先使用 TLS 1.3 来缓解 |
port 0 是关键 |
只加 tls-port 而不把 port 设为 0,明文端口依然开放,等于没加固 |
| 证书有效期与轮换 | 到期会导致全站连不上。必须监控证书剩余天数(建议 30 天告警),并准备好热替换流程(换完文件后重启或 CONFIG SET tls-cert-file + CONFIG SET tls-key-file,Redis 6.2+ 支持热加载) |
| 哨兵也要配 | 哨兵与主从节点通信同样需要 TLS 配置,否则监控链路失败导致误判故障转移 |
| 集群总线 | tls-cluster yes 后总线端口也走 TLS,所有节点必须同时开启,不能混用 |
| 旧版替代方案 | Redis 5.x 及更早只能用 stunnel(在 Redis 前后各架一个 TLS 隧道端点)或直接依赖内网/VPN 隔离 |
10.2.11 及时升级版本与关注已知漏洞
Redis 历史上出现过多个高危 CVE,运行老版本本身就是风险。
| 漏洞类型 | 影响 | 修复方向 |
|---|---|---|
| Lua 沙箱逃逸 | 通过 EVAL 执行的 Lua 脚本突破沙箱,读写宿主文件或执行代码(如 CVE-2022-24736、CVE-2022-24735 等一系列脚本引擎问题) |
升级到官方修复版本;业务不用 Lua 时 rename-command EVAL "" |
| 整数溢出 / 堆溢出 | 超大参数触发内存破坏,可能导致崩溃或 RCE(如 32 位平台上 SETRANGE、BITFIELD、STRALGO 相关问题) |
升级;避免使用 32 位构建;限制 proto-max-bulk-len |
RESTORE/DUMP 反序列化 |
恶意构造的序列化载荷可破坏内存结构 | 升级;开启 sanitize-dump-payload yes(6.0+);不给业务用户 +restore 权限 |
| 认证绕过 / 权限提升 | 特定 ACL 或 SUBSCRIBE 场景下越权 |
升级;用 ACL DRYRUN 复核权限 |
| Sentinel / Cluster 协议问题 | 伪造节点信息干扰集群拓扑 | 升级;总线端口严格白名单;开启 tls-cluster |
# 加固 RESTORE 反序列化(Redis 6.0+)
# no(默认)/ yes(总是校验,安全但有性能开销)/ clients(只校验来自普通客户端的载荷,推荐)
sanitize-dump-payload clients
# 限制单个请求/回复的最大体积,缓解超大参数类漏洞与内存打爆
proto-max-bulk-len 512mb
client-query-buffer-limit 1gb
版本维护建议:
# 查看当前版本
redis-cli INFO server | grep -E 'redis_version|redis_git_sha1|os|arch_bits'
# redis_version:7.2.4
# arch_bits:64 ← 必须是 64,32 位构建有额外的溢出风险
redis-server --version
| 建议 | 说明 |
|---|---|
| 只用官方稳定版 | 从 redis.io 或 GitHub Releases 获取;发行版仓库(CentOS 7 base 的 redis 3.2)版本过老,至少要用 EPEL 或 Remi 源,最好源码编译新版 |
| 跟进小版本补丁 | 例如 7.2.x 系列的 patch 版本通常只含 bug 与安全修复,升级风险低、收益高 |
| 订阅安全公告 | 关注 Redis 官方 GitHub 的 Security Advisories、redis.io/docs/latest/operate/oss_and_stack/ 的发布说明、CVE 数据库与所用发行版的安全通告 |
| 建立升级流程 | 主从架构下:先升级从库 → 验证 → 手动切换(SENTINEL FAILOVER)→ 升级原主库。集群逐节点滚动升级,每次只动一个节点并等 cluster_state:ok |
| 注意跨大版本兼容 | RDB 文件格式向前兼容但不向后兼容:新版能读旧 RDB,旧版读不了新版 RDB。降级需要重新导数据 |
| 淘汰 EOL 版本 | Redis 4.x/5.x 已停止维护,6.0 亦已进入维护末期。生产环境不要再新建 5.x 及以下实例 |
绝不要把 Redis 直接暴露到公网。 这条规则优先于本节所有其他措施:即便密码强度足够、命令都改了名、ACL 也配好了,暴露在公网上仍然意味着要独自承受所有 0-day 与暴力破解。正确架构是:Redis 只在内网可达,外部访问一律经由应用服务端转发。
10.3 加固效果验证
加固不是改完配置就结束,必须逐项验证生效。下面按"网络可达性 → 认证 → 命令权限 → 运行身份 → 文件权限"的顺序做回归测试。
10.3.1 网络可达性验证
# ===== 第一步:在 Redis 本机确认监听地址 =====
ss -tlnp | grep -E 'redis|6379|26380'
# 期望:只有 127.0.0.1 与内网 IP,绝不能出现 0.0.0.0 或公网 IP
# LISTEN 0 511 127.0.0.1:26380 0.0.0.0:* users:(("redis-server",pid=1234,fd=6))
# LISTEN 0 511 192.168.1.10:26380 0.0.0.0:* users:(("redis-server",pid=1234,fd=7))
# CentOS 6 用 netstat(iproute 版本较老,ss 输出格式不同)
netstat -tunlp | grep redis
# ===== 第二步:从外网机器验证【应该连不上】=====
# 在一台公网主机上执行,期望超时或 Connection refused
nc -zv -w3 <公网IP> 26380
# nc: connect to <公网IP> port 26380 (tcp) failed: Connection refused ← 正确
# 若返回 succeeded,说明防火墙/安全组配置有问题,立即处理
telnet <公网IP> 26380
# Trying <公网IP>...
# telnet: connect to address <公网IP>: Connection timed out ← 正确
# ===== 第三步:从【非白名单】内网机器验证(应该被防火墙拒绝)=====
redis-cli -h 192.168.1.10 -p 26380 PING
# Could not connect to Redis at 192.168.1.10:26380: Connection refused ← 正确
# ===== 第四步:从【白名单】应用服务器验证(应该能连上)=====
redis-cli -h 192.168.1.10 -p 26380 -a 'YourPassword' --no-auth-warning PING
# PONG ← 正确
10.3.2 认证验证
# ===== 无密码连接【必须被拒绝】=====
redis-cli -h 192.168.1.10 -p 26380 PING
# (error) NOAUTH Authentication required. ← 正确,说明密码生效
redis-cli -h 192.168.1.10 -p 26380 GET anykey
# (error) NOAUTH Authentication required. ← 正确
# ===== 错误密码【必须被拒绝】=====
redis-cli -h 192.168.1.10 -p 26380 -a 'wrongpass' --no-auth-warning PING
# (error) WRONGPASS invalid username-password pair or user is disabled. ← 正确(6.0+)
# 5.x 及更早的报错为:(error) ERR invalid password
# ===== 正确密码可用 =====
redis-cli -h 192.168.1.10 -p 26380 -a 'YourPassword' --no-auth-warning PING
# PONG
# ===== ACL 用户越权验证(Redis 6.0+)=====
# app-order 只被授权了 ~order:* ~cart:*
redis-cli --user app-order --pass '0rd3r_Pass' --no-auth-warning GET order:1001
# (nil) ← 允许访问(key 不存在返回 nil,不是权限错误)
redis-cli --user app-order --pass '0rd3r_Pass' --no-auth-warning GET user:1001
# (error) NOPERM ... no permissions to access one of the keys used as arguments ← 正确
redis-cli --user app-order --pass '0rd3r_Pass' --no-auth-warning FLUSHALL
# (error) NOPERM ... has no permissions to run the 'flushall' command ← 正确
# 验证完后检查 acllog 是否记录了这些违规尝试
redis-cli -a 'YourPassword' --no-auth-warning ACL LOG 5
10.3.3 危险命令屏蔽验证
export REDISCLI_AUTH='YourPassword'
R="redis-cli -h 192.168.1.10 -p 26380"
# 被置空禁用的命令:报 unknown command
$R FLUSHALL
# (error) ERR unknown command 'FLUSHALL', with args beginning with: ← 正确
$R KEYS '*'
# (error) ERR unknown command 'KEYS', with args beginning with: '*' ← 正确
$R DEBUG SEGFAULT
# (error) ERR unknown command 'DEBUG', with args beginning with: ← 正确
# 被改名的命令:原名不可用,新名可用
$R CONFIG GET maxmemory
# (error) ERR unknown command 'CONFIG' ← 正确
$R CONFIG_9f2a7c4e GET maxmemory
# 1) "maxmemory"
# 2) "4294967296" ← 新名字可用
# 确认替代方案可用:用 SCAN 代替 KEYS
$R --scan --pattern 'order:*' | head -20
10.3.4 运行身份与文件权限验证
# ===== 确认运行用户不是 root(最关键的一项验证)=====
ps -ef | grep [r]edis-server
# redis 1234 1 0 10:00 ? 00:00:12 /usr/local/bin/redis-server 127.0.0.1:26380
# 更严格的确认方式:直接读 /proc
PID=$(pgrep -x redis-server)
grep -E '^(Uid|Gid)' /proc/$PID/status
# Uid: 996 996 996 996 ← 必须不是 0
# Gid: 994 994 994 994
ps -o user,group,pid,cmd -p $PID
# systemd 单元里确认 User=(CentOS 7+)
systemctl show redis -p User -p Group
# User=redis
# Group=redis
systemctl cat redis | grep -E '^User|^Group'
# ===== 文件与目录权限验证 =====
ls -l /etc/redis/redis.conf
# -rw------- 1 redis redis 62K ... redis.conf ← 必须 600
ls -ld /var/lib/redis /var/log/redis
# drwxr-x--- 2 redis redis 4096 ... /var/lib/redis ← 750
# drwxr-x--- 2 redis redis 4096 ... /var/log/redis
ls -l /var/lib/redis/
# -rw------- 1 redis redis 1.2M ... dump.rdb ← 600
# 反向验证:普通用户读不到配置文件与数据文件
su - someuser -c 'cat /etc/redis/redis.conf'
# cat: /etc/redis/redis.conf: Permission denied ← 正确
# 验证 redis 用户确实无法登录
grep '^redis' /etc/passwd
# redis:x:996:994::/home/redis:/sbin/nologin ← shell 必须是 nologin/false
su - redis
# This account is currently not available. ← 正确
10.3.5 端口扫描自查
# ===== 在内网跳板机上扫描 Redis 主机,确认只开放了必要端口 =====
yum install -y nmap
# 扫描常见 Redis 端口与自定义端口
nmap -p 6379,16379,26379,26380 192.168.1.10
# 全端口扫描(确认没有遗漏的调试端口/多实例端口)
nmap -p- -T4 192.168.1.10
# 服务指纹识别:确认 Redis 端口是否会向未认证客户端泄露版本号
nmap -sV -p 26380 192.168.1.10
# 26380/tcp open redis Redis key-value store 7.2.4
# ↑ 若能识别出版本,说明 INFO 在未认证时可读(正常情况下有密码时读不到,
# nmap 只能通过 PING 的响应特征判断这是 Redis)
# 用 nmap 官方脚本做基线检查
nmap --script redis-info -p 26380 192.168.1.10
# ===== 自查本机是否有"忘记关"的多余 Redis 实例 =====
ps -ef | grep [r]edis
ss -tlnp | grep redis-server
# 常见问题:测试时启的临时实例(默认 6379、无密码、bind 0.0.0.0)忘记停掉
10.3.6 安全基线自查表
上线前逐项打勾,任何一项为"否"都不允许投产。
| # | 检查项 | 验证命令 | 期望结果 |
|---|---|---|---|
| 1 | 设置了强密码(≥20 位随机) | CONFIG GET requirepass(或改名后的命令) |
非空且足够复杂 |
| 2 | 无密码连接被拒绝 | redis-cli -h <ip> -p <port> PING |
NOAUTH Authentication required |
| 3 | bind 未监听 0.0.0.0 |
ss -tlnp \| grep redis |
只有 127.0.0.1 与内网 IP |
| 4 | protected-mode 为 yes(3.2+) |
CONFIG GET protected-mode |
yes |
| 5 | 端口已改为非 6379 | CONFIG GET port |
非默认端口 |
| 6 | 运行用户不是 root | ps -ef \| grep [r]edis-server |
首列为 redis |
| 7 | redis 用户不可登录 | grep ^redis /etc/passwd |
shell 为 nologin |
| 8 | 配置文件权限 600 | ls -l /etc/redis/redis.conf |
-rw------- |
| 9 | 数据目录权限 750、属主 redis | ls -ld /var/lib/redis |
drwxr-x--- redis redis |
| 10 | 数据目录不在 Web 目录//tmp/家目录 |
CONFIG GET dir |
/var/lib/redis 之类专用目录 |
| 11 | 危险命令已屏蔽/改名 | FLUSHALL、KEYS *、CONFIG GET |
unknown command |
| 12 | 防火墙只放行应用服务器 IP | firewall-cmd --list-rich-rules / iptables -L -n |
精确源 IP,非 0.0.0.0/0 |
| 13 | 公网不可达 | 外网机 nc -zv <公网IP> <port> |
超时或拒绝 |
| 14 | 未绑定公网 IP / 云安全组无 0.0.0.0/0 |
云控制台 + ip addr |
仅内网 |
| 15 | ACL 用户按最小权限配置(6.0+) | ACL LIST + ACL DRYRUN |
越权操作返回 NOPERM |
| 16 | default 用户已收紧或禁用(6.0+) |
ACL GETUSER default |
无 nopass,或为 off |
| 17 | ACL 已持久化 | cat /etc/redis/users.acl 或 redis.conf 中 user 行 |
重启后规则仍在 |
| 18 | 跨网络通信启用 TLS(6.0+,按需) | redis-cli --tls ... PING |
PONG;明文端口为 0 |
| 19 | 版本为受支持的稳定版且为 64 位 | INFO server \| grep -E 'version\|arch_bits' |
≥6.2,arch_bits:64 |
| 20 | 主从/集群密码与 ACL 各节点一致 | 各节点 INFO replication、ACL LIST |
复制正常、规则一致 |
| 21 | masterauth 已配置(有主从时) |
CONFIG GET masterauth |
与主库 requirepass 一致 |
| 22 | 日志中无异常认证失败 | grep -i 'wrong\|denied\|auth' /var/log/redis/redis.log、ACL LOG |
无大量失败记录 |
| 23 | 未被利用痕迹(见 10.1.2) | authorized_keys、crontab -l、异常进程 |
干净 |
| 24 | 监控与告警已接入 | Zabbix / Prometheus 面板 | 内存、命中率、连接数有数据 |
加固口诀:密码要长、地址要窄、身份要低、命令要少、网络要关、版本要新、日志要看。
十一、监控与运维
INFO 是 Redis 运维的核心入口——内存、持久化、复制、命中率、慢命令的判断依据几乎全在这里。本章逐段解读 INFO 输出,给出告警阈值建议,并整理常用运维命令与数据迁移方案。
# 全部信息
redis-cli -a 'pwd' --no-auth-warning INFO
# 只看某一段(推荐,输出简洁)
redis-cli INFO server
redis-cli INFO clients
redis-cli INFO memory
redis-cli INFO persistence
redis-cli INFO stats
redis-cli INFO replication
redis-cli INFO cpu
redis-cli INFO commandstats
redis-cli INFO latencystats # Redis 7.0+
redis-cli INFO keyspace
redis-cli INFO cluster
redis-cli INFO everything # 包含 commandstats/latencystats 等默认不输出的段(6.2+)
# 提取单个指标(脚本常用写法:注意要去掉行尾的 \r)
redis-cli INFO memory | grep -w used_memory_human | cut -d: -f2 | tr -d '\r'
redis-cli INFO memory | awk -F: '/^used_memory:/{print $2}' | tr -d '\r'
11.1 INFO server:实例基本信息
redis-cli INFO server
# Server
redis_version:7.2.4 # 【重点】Redis 版本号
redis_git_sha1:00000000 # Git 提交号,官方发布版一般为 0
redis_git_dirty:0 # 是否有未提交修改编译(0=干净)
redis_build_id:5a3e4e3a2f1b8c9d
redis_mode:standalone # 运行模式:standalone / sentinel / cluster
os:Linux 3.10.0-1160.el7.x86_64 x86_64 # 操作系统与内核版本
arch_bits:64 # 【重点】32 或 64 位。32 位有 4GB 内存上限且有溢出风险
multiplexing_api:epoll # I/O 多路复用实现:Linux 上应为 epoll
atomicvar_api:c11-builtin
gcc_version:4.8.5
process_id:12345 # 【重点】主进程 PID
process_supervised:systemd # 由谁托管(6.0+)
run_id:9c1f7a3e5b8d2c4f6a0e1b3d5f7a9c1e3b5d7f9a # 【重点】实例运行 ID
tcp_port:6379 # 监听端口
server_time_usec:1756000000123456
uptime_in_seconds:864000 # 【重点】已运行秒数
uptime_in_days:10 # 【重点】已运行天数
hz:10 # 后台任务频率(次/秒)
configured_hz:10
lru_clock:1234567
executable:/usr/local/bin/redis-server # 可执行文件路径
config_file:/etc/redis/redis.conf # 【重点】当前加载的配置文件路径
io_threads_active:0 # I/O 多线程是否启用(6.0+)
listener0:name=tcp,bind=127.0.0.1,bind=-::1,port=6379 # 7.0+
| 字段 | 运维意义 |
|---|---|
redis_version |
决定可用特性的第一依据:ACL 需 6.0+、protected-mode 需 3.2+、redis-cli --cluster 需 5.0+、--memkeys 需 6.0+、latencystats 需 7.0+、OBJECT FREQ 需 4.0+。排障时先看版本,很多"命令不存在"就是版本不够 |
redis_mode |
确认是单机、哨兵还是集群。误把集群实例当单机连(不加 -c)会大量 MOVED 报错 |
process_id |
用于 /proc/<pid>/ 下查 status(Uid/Gid、内存)、smaps、limits(fd 上限)、fd(打开的文件数),是把 Redis 指标与操作系统指标关联起来的桥梁 |
uptime_in_seconds / uptime_in_days |
短于预期就是异常信号:说明实例近期重启过(OOM Killer 杀掉、进程崩溃、被人手动重启)。必须结合日志确认重启原因,因为未开持久化时重启等于全部数据丢失,会引发缓存雪崩。也是判断"指标是否有足够采样时间"的前提——刚启动几分钟的实例,命中率等累计型指标没有参考价值 |
run_id |
40 位随机 ID,每次重启都会变。主从复制的部分重同步依赖它:从库记录的 master_replid 与主库 run_id 不匹配时,会退化为全量同步。监控上 run_id 变化 = 实例重启过 |
arch_bits |
生产必须为 64 |
config_file |
多实例部署时确认"改的是不是正在生效的那个配置文件",避免改错文件后百思不解 |
hz |
后台任务(过期 key 清理、客户端超时检查、增量 rehash)的执行频率,默认 10。调高(如 100)能让过期 key 清理更及时,但会增加 CPU 占用 |
process_supervised |
确认是否由 systemd 托管;no 说明是裸启动,异常退出后不会自动重启 |
# 结合 process_id 查看操作系统层面的信息
PID=$(redis-cli INFO server | awk -F: '/^process_id:/{print $2}' | tr -d '\r')
cat /proc/$PID/limits | grep -i 'open files' # fd 上限,需 > maxclients + 32
ls /proc/$PID/fd | wc -l # 当前已打开的 fd 数
grep VmRSS /proc/$PID/status # 进程实际物理内存占用
11.2 INFO clients:客户端连接
redis-cli INFO clients
# Clients
connected_clients:128 # 【重点】当前客户端连接数(不含从库连接)
cluster_connections:0 # 集群总线连接数(7.0+)
maxclients:10000 # 最大连接数上限
client_recent_max_input_buffer:20480 # 【重点】近期客户端输入缓冲区最大值(字节)
client_recent_max_output_buffer:0 # 【重点】近期客户端输出缓冲区最大值(字节)
blocked_clients:3 # 【重点】被阻塞命令挂起的客户端数
tracking_clients:0 # 使用客户端缓存(Client Side Caching)的连接数(6.0+)
pubsub_clients:2 # 订阅类连接数(7.4+)
watching_clients:0 # 正在 WATCH 的连接数(7.4+)
clients_in_timeout_table:3 # 处于超时表中的客户端数
total_watched_keys:0
total_blocking_keys:1 # 7.0+
total_blocking_keys_on_nokey:0
| 字段 | 判读要点 |
|---|---|
connected_clients |
与 maxclients 对比看使用率。接近上限时新连接会被拒绝并返回 ERR max number of clients reached。持续偏高且远超业务实际并发,通常说明:① 应用没用连接池,每次操作新建连接;② 连接池配置过大(微服务实例数 × 池大小超出预期);③ 连接泄漏(用完不归还)。告警阈值建议:> maxclients 的 80% |
maxclients |
由配置项 maxclients 决定(默认 10000),但实际上限受进程 fd 限制约束。Redis 启动时会尝试把 fd 上限提到 maxclients + 32,若系统不允许则自动下调 maxclients 并在日志中警告:You requested maxclients of 10000 requiring at least 10032 max file descriptors. Redis can't set maximum open files to 10032 because of OS error... Current maximum open files is 4096. maxclients has been reduced to 4064 |
blocked_clients |
执行 BLPOP/BRPOP/BLMOVE/BRPOPLPUSH/WAIT/XREAD BLOCK 而挂起等待的客户端数。用队列的业务这个值不为 0 是正常的(消费者在等新消息)。但突然大幅增长说明生产端停了、或消费逻辑卡住。注意:阻塞的客户端不占用 CPU,不会阻塞其他命令 |
client_recent_max_input_buffer |
近期客户端输入缓冲区峰值。异常增大(如上百 MB)说明有客户端发送了超大命令:MSET 一次塞几万个 key、HMSET 巨型 hash、pipeline 一次发几十万条命令、或写入了超大 value。输入缓冲区无法通过 client-output-buffer-limit 限制,只能靠 client-query-buffer-limit(默认 1GB)兜底;单个客户端的输入缓冲区打到 1GB 会被强制断开 |
client_recent_max_output_buffer |
近期客户端输出缓冲区峰值。增大说明有客户端在拉取大量数据:KEYS *、HGETALL 大 hash、LRANGE key 0 -1 大列表、SMEMBERS 大集合,或 MONITOR/订阅端消费太慢。超过 client-output-buffer-limit 限制的客户端会被杀掉 |
tracking_clients |
开启了服务端辅助的客户端缓存(RESP3 的 CLIENT TRACKING)的连接数 |
# ===== 连接与缓冲区相关配置 =====
maxclients 10000
# 客户端空闲超时(秒),0 表示永不超时。
# 【建议】设置 300~600,回收因网络异常残留的半死连接;但用了连接池的应用要注意
# 池中空闲连接被服务端断开后,客户端首次使用会报错(多数客户端有重连机制)
timeout 300
# TCP keepalive(秒),默认 300。用于探测对端已消失的连接
tcp-keepalive 300
# TCP 半连接队列长度,需与内核 net.core.somaxconn 匹配
tcp-backlog 511
# 输出缓冲区限制:<class> <hard limit> <soft limit> <soft seconds>
# normal 普通客户端:默认全 0 即不限制(普通客户端是拉取型,不会无限堆积)
client-output-buffer-limit normal 0 0 0
# replica 从库:超过 256MB 立即断开,或超过 64MB 持续 60 秒断开
# 【重点】主从复制中断反复重连、日志里出现 "client scheduled to be closed
# ASAP for overcoming of output buffer limits" 就要调大这一项
client-output-buffer-limit replica 256mb 64mb 60
# pubsub 订阅端:消费慢会被断开
client-output-buffer-limit pubsub 32mb 8mb 60
# 单客户端查询缓冲区上限(1GB),防止单个客户端发送超大命令打爆内存
client-query-buffer-limit 1gb
# 单个请求/回复的最大体积
proto-max-bulk-len 512mb
# 【连接数使用率速算】
redis-cli INFO clients | grep -E 'connected_clients|maxclients|blocked_clients'
# 结合系统 fd 上限一起看
PID=$(pgrep -x redis-server); cat /proc/$PID/limits | grep 'Max open files'
ulimit -n
# 【CentOS 7】需要同时改 /etc/security/limits.conf 与 systemd 单元的 LimitNOFILE
# 【CentOS 6】只需 /etc/security/limits.conf(无 systemd)
11.3 INFO memory:内存使用(最重要的一段)
redis-cli INFO memory
# Memory
used_memory:2147483648 # 【核心】Redis 分配器分配的内存总量(字节)
used_memory_human:2.00G # 【核心】上一项的人类可读形式
used_memory_rss:2415919104 # 【核心】操作系统视角的进程物理内存(RSS)
used_memory_rss_human:2.25G
used_memory_peak:3221225472 # 【核心】历史内存使用峰值
used_memory_peak_human:3.00G
used_memory_peak_perc:66.67% # 当前内存 / 历史峰值
used_memory_overhead:104857600 # 管理开销(客户端缓冲、复制积压、各种元数据)
used_memory_startup:786432 # 启动时的空实例基础开销
used_memory_dataset:2042626048 # 纯数据占用 = used_memory - overhead
used_memory_dataset_perc:95.12% # 数据占比
allocator_allocated:2150000000 # 分配器已分配
allocator_active:2200000000 # 分配器活跃页
allocator_resident:2400000000 # 分配器常驻内存
allocator_frag_ratio:1.02 # 分配器碎片率
allocator_frag_bytes:50000000
allocator_rss_ratio:1.09
allocator_rss_bytes:200000000
rss_overhead_ratio:1.00
rss_overhead_bytes:15919104
total_system_memory:8589934592 # 系统总内存
total_system_memory_human:8.00G
used_memory_lua:37888 # Lua 引擎占用
used_memory_vm_eval:37888 # 7.0+
used_memory_scripts:0
used_memory_functions:184 # 7.0+
number_of_cached_scripts:12
maxmemory:4294967296 # 【核心】内存上限(字节),0 = 不限制
maxmemory_human:4.00G # 【核心】
maxmemory_policy:allkeys-lru # 【核心】达到上限后的淘汰策略
mem_fragmentation_ratio:1.13 # 【核心】内存碎片率
mem_fragmentation_bytes:268435456
mem_not_counted_for_evict:0
mem_replication_backlog:1048576 # 复制积压缓冲区
mem_total_replication_buffers:2097152 # 7.0+
mem_clients_slaves:17408 # 从库客户端缓冲区
mem_clients_normal:1671168 # 普通客户端缓冲区
mem_cluster_links:0 # 7.0+
mem_aof_buffer:0 # AOF 缓冲区
mem_allocator:jemalloc-5.3.0 # 【核心】内存分配器
mem_overhead_db_hashtable_lut:1024 # 7.4+
lazyfree_pending_objects:0 # 待异步释放的对象数
lazyfreed_objects:0
11.3.1 核心内存指标判读
| 指标 | 含义与判读标准 |
|---|---|
used_memory |
Redis 自己统计的内存用量,即内存分配器(jemalloc/libc)为数据、缓冲区、元数据分配的字节总数。这是 maxmemory 判断是否触发淘汰的依据。注意:它不包含分配器碎片,也不包含进程代码段与栈,所以通常小于操作系统看到的 RSS |
used_memory_human |
used_memory 的可读形式(2.00G)。人工排查时看这一项,写监控脚本时用 used_memory 的原始字节数(human 值带单位无法直接比较) |
used_memory_rss |
Resident Set Size,操作系统视角下进程实际占用的物理内存,等于 ps aux 的 RSS 列与 top 的 RES 列。包含:数据 + 碎片 + 代码段 + 栈 + 各类缓冲。监控主机内存是否吃紧要看这一项,不是 used_memory |
used_memory_peak |
历史最高内存使用量(从实例启动或上次 CONFIG RESETSTAT 起)。这是容量规划的关键依据:maxmemory 必须大于峰值,否则峰值时段会触发大规模淘汰。峰值远高于当前值(used_memory_peak_perc 很低)说明有过突发写入或大 key,需要追查 |
used_memory_overhead |
非数据开销:所有客户端的输入输出缓冲区 + 复制积压缓冲区 + AOF 缓冲区 + 各 db 的字典与过期字典结构 + used_memory_startup。这一项异常大(如超过总内存 20%)通常是连接数过多或有慢消费客户端 |
used_memory_dataset |
used_memory - used_memory_overhead,真实业务数据占用。做容量规划时用这个值除以 key 数量得到"平均每 key 开销" |
maxmemory |
内存使用上限。0 表示不限制——这是生产环境的重大隐患:数据无限增长直到耗尽物理内存,然后被 Linux OOM Killer 杀掉,或开始使用 swap 导致性能雪崩。必须显式设置 |
maxmemory_policy |
达到 maxmemory 后的淘汰策略。默认 noeviction 意味着写命令直接报 OOM command not allowed when used memory > 'maxmemory'。纯缓存场景应设为 allkeys-lru 或 allkeys-lfu;有持久化数据不能丢时用 volatile-lru(只淘汰设了 TTL 的 key)并配合监控 |
mem_allocator |
使用的内存分配器。jemalloc 是官方推荐与默认(碎片控制好,支持 activedefrag 主动碎片整理)。若显示 libc,说明编译时用了 make MALLOC=libc(通常是为绕过缺少 jemalloc/jemalloc.h 的编译报错),此时碎片率会明显更高,且 activedefrag 不可用。tcmalloc 也可能出现在部分发行版包中 |
total_system_memory |
主机总内存,用于计算"Redis 占主机内存的比例" |
used_memory_lua / used_memory_scripts |
Lua 脚本占用。持续增长说明脚本没有复用:应用每次用 EVAL 发送完整脚本文本,Redis 会缓存每个不同的脚本。正确做法是 SCRIPT LOAD 一次 + EVALSHA 复用,或定期 SCRIPT FLUSH |
11.3.2 mem_fragmentation_ratio:碎片率的判读标准
mem_fragmentation_ratio = used_memory_rss / used_memory
这是内存段最需要盯的比值,判读标准如下:
| 取值范围 | 含义 | 处理动作 |
|---|---|---|
< 1 |
RSS 小于 Redis 自认为使用的内存 → 说明部分内存被换出到了 swap。这是最危险的信号 | 立即处理:内存访问一旦落到磁盘,延迟从纳秒级变成毫秒级,Redis 会出现严重的性能雪崩且难以自愈。排查步骤见下方 |
1 ~ 1.5 |
正常范围,少量碎片是分配器的正常行为 | 无需处理。理想值约 1.0 ~ 1.1 |
> 1.5 |
碎片较多,说明有超过 50% 的内存被浪费在碎片上 | 需要处理:开启 activedefrag,或安排重启 |
> 2 |
碎片严重 | 尽快处理,可能已浪费一半以上内存 |
| 极大值(如 10+) | 通常是实例刚启动、数据量极小(分母很小导致比值虚高),或刚执行过大批量删除 | 数据量小时该指标无意义,先看绝对值 |
< 1 的排查与处理(swap 问题):
# 1) 确认 Redis 进程是否真的在用 swap
PID=$(pgrep -x redis-server)
grep -E 'VmSwap|VmRSS' /proc/$PID/status
# VmSwap: 524288 kB ← 不为 0 就是在用 swap,必须处理
# 更精确地统计每个内存段的 swap 使用
awk '/^Swap:/{s+=$2} END{print s" kB swapped"}' /proc/$PID/smaps
# 2) 看系统整体 swap 使用与内存水位
free -h
vmstat 1 5
# si/so 两列(swap in/out)持续非 0 说明正在频繁换页
# 3) 看内核参数
cat /proc/sys/vm/swappiness # 建议 1(不是 0,0 在部分内核上会更容易触发 OOM Kill)
cat /proc/sys/vm/overcommit_memory # 必须为 1,否则 fork 可能失败
# 4) 处理方式
sysctl -w vm.swappiness=1
echo 'vm.swappiness = 1' >> /etc/sysctl.conf
sysctl -p
# 根治:调低 maxmemory 让 Redis 内存 + 系统预留 < 物理内存,或加内存 / 拆分实例
redis-cli CONFIG SET maxmemory 3gb
redis-cli CONFIG REWRITE
# 已经被换出的内存不会自动换回,通常需要在业务低峰重启实例才能彻底恢复
不要为了防 swap 而直接
swapoff -a。完全关闭 swap 后,内存吃紧时 Linux 会直接触发 OOM Killer 杀掉 Redis 进程(进程被杀比变慢更严重)。正确做法是保留少量 swap +vm.swappiness=1,同时用maxmemory从源头限制。
> 1.5 的处理(碎片整理):
碎片产生的原因:Redis 频繁 SET/DEL 大小不一的 value 后,jemalloc 释放的内存块无法归还给操作系统(因为同一内存页上还有其他活跃对象),形成"空洞"。大量删除 key 或大 key 被删除后,碎片率通常会明显上升。
# ===== 主动碎片整理(Redis 4.0+,且 mem_allocator 必须是 jemalloc)=====
activedefrag yes
# 碎片占用达到 100MB 才开始整理
active-defrag-ignore-bytes 100mb
# 碎片率达到 10% 时启动整理
active-defrag-threshold-lower 10
# 碎片率达到 100% 时全力整理
active-defrag-threshold-upper 100
# 整理占用的 CPU 时间下限(%)—— 轻度整理时的 CPU 配额
active-defrag-cycle-min 5
# 整理占用的 CPU 时间上限(%)—— 全力整理时的 CPU 配额
active-defrag-cycle-max 75
# 单次扫描处理的最大字段数(针对大 hash/set/zset/list)
active-defrag-max-scan-fields 1000
# 运行时开启(无需重启,可立即观察效果)
redis-cli CONFIG SET activedefrag yes
redis-cli CONFIG SET active-defrag-ignore-bytes 100mb
redis-cli CONFIG SET active-defrag-threshold-lower 10
# 观察碎片率变化(每 2 秒刷新)
watch -n2 "redis-cli INFO memory | grep -E 'mem_fragmentation_ratio|used_memory_human|used_memory_rss_human'"
# 确认分配器支持
redis-cli INFO memory | grep mem_allocator
# mem_allocator:jemalloc-5.3.0 ← 必须是 jemalloc,libc 不支持 activedefrag
| 处理手段 | 说明 |
|---|---|
activedefrag yes |
首选。在线整理,无需重启,代价是额外 CPU 占用。整理期间 active-defrag-cycle-max 控制上限,建议生产先设 25~50 避免抢占过多 CPU |
| 重启实例 | 最彻底(重启后从 RDB/AOF 加载,内存紧凑排列)。主从架构下的无损做法:先在从库重启 → 数据同步完成 → 主动切换 → 再重启原主库 |
| 改用 jemalloc | 若 mem_allocator 是 libc,重新编译时不要加 MALLOC=libc。缺 jemalloc/jemalloc.h 头文件时应安装依赖或用源码包自带的 deps/jemalloc,而不是退化到 libc |
| 减少大 key 增删 | 从业务侧治理:避免把大集合整体删除重建,改用增量更新 |
MEMORY PURGE |
让 jemalloc 尽力把空闲页归还操作系统(4.0+),效果有限但零风险,可先试 |
redis-cli MEMORY PURGE
redis-cli MEMORY DOCTOR
11.3.3 maxmemory 与淘汰策略
# ===== 内存上限与淘汰 =====
# 【必须显式设置】建议为物理内存的 50%~70%(见 11.9 容量规划)
maxmemory 4gb
# 淘汰策略
maxmemory-policy allkeys-lru
# LRU/LFU/TTL 采样精度:默认 5,调到 10 更接近真实 LRU 但更耗 CPU
maxmemory-samples 5
# 从库是否独立执行淘汰(5.0+)。默认 no:从库不自行淘汰,等主库的 DEL 同步过来,
# 这样才能保证主从数据一致。改成 yes 会导致主从不一致,仅特殊场景使用
replica-ignore-maxmemory yes
# 淘汰时是否统计从库输出缓冲区与 AOF 缓冲区(6.0+)
# no(默认)表示这些缓冲区不计入淘汰判断,避免误淘汰
| 策略 | 行为 | 适用场景 |
|---|---|---|
noeviction |
默认值。不淘汰,写命令直接报 OOM command not allowed 错误,读命令仍可用 |
持久化存储用途(数据不能丢),但必须严密监控内存 |
allkeys-lru |
在所有 key 中淘汰最久未使用的 | 纯缓存场景首选 |
allkeys-lfu |
在所有 key 中淘汰访问频率最低的(4.0+) | 有明显热点数据、访问频率差异大的缓存 |
allkeys-random |
在所有 key 中随机淘汰 | 访问模式均匀、无热点时 |
volatile-lru |
只在设置了 TTL 的 key 中按 LRU 淘汰 | 缓存与持久数据混存 |
volatile-lfu |
只在设置了 TTL 的 key 中按 LFU 淘汰(4.0+) | 同上,有热点时 |
volatile-random |
只在设置了 TTL 的 key 中随机淘汰 | 同上 |
volatile-ttl |
只在设置了 TTL 的 key 中淘汰剩余存活时间最短的 | 希望短命数据先走 |
⚠️
volatile-*策略的陷阱:如果没有任何 key 设置了 TTL*,`volatile-` 的行为退化为noeviction**,即照样报 OOM。混存场景下必须确保缓存类 key 都设了 TTL。
# 热改内存上限与策略(立即生效)
redis-cli CONFIG SET maxmemory 4gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
redis-cli CONFIG REWRITE # 写回配置文件,否则重启失效
# 内存使用率计算(脚本写法)
U=$(redis-cli INFO memory | awk -F: '/^used_memory:/{print $2}' | tr -d '\r')
M=$(redis-cli INFO memory | awk -F: '/^maxmemory:/{print $2}' | tr -d '\r')
[ "$M" -gt 0 ] && echo "内存使用率: $(awk "BEGIN{printf \"%.2f%%\", $U*100/$M}")"
11.4 INFO persistence:持久化状态
redis-cli INFO persistence
# Persistence
loading:0 # 是否正在从磁盘加载数据(1=启动加载中,此时只能执行部分命令)
async_loading:0 # 7.0+,无盘复制时的异步加载
current_cow_peak:0 # fork 子进程的 copy-on-write 峰值
current_cow_size:0 # 当前 COW 内存大小
current_fork_perc:0.00 # 当前 fork 任务的进度百分比
current_save_keys_processed:0
current_save_keys_total:0
rdb_changes_since_last_save:15234 # 【重点】距上次成功落盘的写操作次数
rdb_bgsave_in_progress:0 # 【重点】是否正在执行 BGSAVE(1=进行中)
rdb_last_save_time:1756000000 # 上次成功保存的 Unix 时间戳
rdb_last_bgsave_status:ok # 【核心】上次 BGSAVE 结果:ok / err
rdb_last_bgsave_time_sec:3 # 上次 BGSAVE 耗时(秒)
rdb_current_bgsave_time_sec:-1 # 当前 BGSAVE 已耗时,-1 表示没在执行
rdb_saves:128 # 7.0+,累计保存次数
rdb_last_cow_size:8388608 # 上次 BGSAVE 的 COW 内存开销
rdb_last_load_keys_expired:0 # 7.0+
rdb_last_load_keys_loaded:100000 # 7.0+
aof_enabled:1 # 【重点】AOF 是否开启(1=开启)
aof_rewrite_in_progress:0 # 【重点】是否正在重写 AOF
aof_rewrite_scheduled:0 # 是否有排队等待的重写任务
aof_last_rewrite_time_sec:5 # 上次重写耗时(秒)
aof_current_rewrite_time_sec:-1 # 当前重写已耗时
aof_last_bgrewrite_status:ok # 【核心】上次 AOF 重写结果:ok / err
aof_rewrites:12 # 7.0+,累计重写次数
aof_rewrites_consecutive_failures:0 # 7.0+,连续失败次数
aof_last_write_status:ok # 【核心】上次 AOF 写盘结果:ok / err
aof_last_cow_size:4194304
module_fork_in_progress:0
module_fork_last_cow_size:0
# 以下字段仅在 aof_enabled:1 时出现
aof_base_size:104857600 # 上次重写后的 AOF 大小(字节)
aof_current_size:157286400 # 当前 AOF 大小(字节)
aof_pending_rewrite:0 # 是否有延迟到 BGSAVE 结束后执行的重写
aof_buffer_length:0 # AOF 缓冲区长度
aof_pending_bio_fsync:0 # 后台线程待 fsync 的任务数
aof_delayed_fsync:12 # 【重点】被延迟的 fsync 次数
11.4.1 RDB 相关指标
| 指标 | 判读要点 |
|---|---|
rdb_changes_since_last_save |
距上次成功持久化以来累积的写操作次数,即"如果此刻宕机会丢多少次写操作"。这是衡量持久化风险最直观的指标。值很大(如上百万)说明:① save 条件配得太松;② BGSAVE 一直在失败;③ 长时间没有触发保存。只开 AOF 不开 RDB 时这个值一直增长是正常的(AOF 才是主要持久化手段),此时应改看 aof_* 指标 |
rdb_bgsave_in_progress |
1 表示正在 fork 子进程写 RDB。此期间:① 内存可能因 COW 显著上涨(最坏翻倍);② 若又收到 BGREWRITEAOF 会被排队(aof_rewrite_scheduled:1);③ 主库正好要给新从库做全量同步时会复用这次 BGSAVE。持续为 1 且 rdb_current_bgsave_time_sec 很大,说明数据量大或磁盘 I/O 慢 |
rdb_last_bgsave_status |
必须监控的核心项。err 表示上次后台保存失败,这会直接导致所有写命令返回 MISCONF Redis is configured to save RDB snapshots but is currently unable to persist to disk——业务整体不可写。常见原因:磁盘满、目录权限不对(改了 dir 却没 chown redis)、fork 失败(内存不足 + overcommit_memory=0) |
rdb_last_bgsave_time_sec |
上次 BGSAVE 耗时。耗时越长,fork 期间的 COW 内存膨胀与延迟抖动风险越大。超过 10 秒就要考虑拆分实例或换更快的磁盘 |
rdb_last_cow_size |
上次 BGSAVE 期间因 copy-on-write 额外占用的内存。这是"为什么内存突然涨了一截"的直接答案,也是规划内存冗余的依据 |
loading |
1 表示实例正在从 RDB/AOF 加载数据到内存。此期间大部分命令会返回 LOADING Redis is loading the dataset in memory。数据量大时可能持续数分钟,是"重启后业务报错"的常见原因 |
11.4.2 AOF 相关指标
| 指标 | 判读要点 |
|---|---|
aof_enabled |
1=开启 AOF,0=未开启。生产环境只开 RDB 的话,宕机会丢失从上次快照到宕机的全部数据,重要业务应至少开 AOF(appendonly yes + appendfsync everysec) |
aof_rewrite_in_progress |
1 表示正在后台重写 AOF(fork 子进程把内存快照写成新 AOF)。同样有 COW 内存膨胀风险。注意 BGSAVE 与 BGREWRITEAOF 不会同时执行,后来者会被排队 |
aof_rewrite_scheduled |
1 表示有重写请求正在排队等待当前的 BGSAVE 结束 |
aof_last_bgrewrite_status |
必须监控。err 表示上次 AOF 重写失败。持续失败会导致 AOF 文件无限增长直至撑满磁盘 |
aof_last_write_status |
必须监控。err 表示上次向 AOF 文件写入失败(磁盘满、只读文件系统、权限问题)。这同样会触发 MISCONF 错误使实例不可写——注意这里的 MISCONF 提示语与 RDB 版本略有不同,但机制一致 |
aof_rewrites_consecutive_failures |
连续重写失败次数(7.0+)。> 0 就应告警 |
aof_base_size / aof_current_size |
重写基准大小与当前大小。二者的比值决定自动重写是否触发:(current - base) / base × 100% >= auto-aof-rewrite-percentage 且 current >= auto-aof-rewrite-min-size |
aof_delayed_fsync |
被延迟的 fsync 次数。appendfsync everysec 时,若上一次 fsync 还没完成,Redis 会推迟本次写入。这个值持续增长说明磁盘 I/O 已经跟不上写入速度,是磁盘瓶颈的明确信号,会直接表现为写命令延迟抖动。处理方向:换 SSD、降低 appendfsync 严格程度、拆分实例分摊写压力 |
MISCONF的联系要点(必须理解的因果链):磁盘满 / dir 权限不对 / fork 失败 ↓ BGSAVE 或 AOF 写入失败 ↓ rdb_last_bgsave_status:err 或 aof_last_write_status:err ↓ Redis 认为"配置了持久化但持久化不了",为了保护数据【主动拒绝所有写命令】 ↓ 客户端收到:(error) MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commands that may modify the data set are disabled... ↓ 业务表现:读正常、写全挂,且【重启也不会好】,因为根因在磁盘/权限因此监控这三个
status字段的优先级等同于监控内存:任一为err都必须立即告警到人。
# 持久化健康一键自查
redis-cli INFO persistence | grep -E 'rdb_last_bgsave_status|aof_last_write_status|aof_last_bgrewrite_status|rdb_changes_since_last_save|aof_delayed_fsync|rdb_bgsave_in_progress|aof_rewrite_in_progress'
# 三个 status 任一为 err 就报警的检查脚本
for k in rdb_last_bgsave_status aof_last_write_status aof_last_bgrewrite_status; do
v=$(redis-cli INFO persistence | awk -F: -v K="^$k:" '$0 ~ K {print $2}' | tr -d '\r')
[ -n "$v" ] && [ "$v" != "ok" ] && echo "【告警】$k = $v"
done
# 配合系统层面确认根因
df -h /var/lib/redis # 磁盘剩余空间
ls -ld /var/lib/redis # 目录属主是否为 redis
cat /proc/sys/vm/overcommit_memory # 应为 1
tail -50 /var/log/redis/redis.log # Redis 自己的报错最直接
11.5 INFO stats:运行统计
redis-cli INFO stats
# Stats
total_connections_received:1052341 # 【重点】累计接受的连接数
total_commands_processed:892341521 # 【重点】累计处理的命令数
instantaneous_ops_per_sec:12580 # 【核心】当前每秒操作数(QPS)
total_net_input_bytes:52428800000 # 累计入网流量(字节)
total_net_output_bytes:104857600000 # 累计出网流量(字节)
total_net_repl_input_bytes:0 # 7.0+,复制入网流量
total_net_repl_output_bytes:1048576000 # 7.0+,复制出网流量
instantaneous_input_kbps:1024.50 # 当前入网速率(KB/s)
instantaneous_output_kbps:2048.75 # 当前出网速率(KB/s)
instantaneous_input_repl_kbps:0.00
instantaneous_output_repl_kbps:512.00
rejected_connections:0 # 【核心】被拒绝的连接数
sync_full:2 # 【重点】全量同步次数
sync_partial_ok:15 # 【重点】部分重同步成功次数
sync_partial_err:1 # 【重点】部分重同步失败次数
expired_keys:1523400 # 【重点】累计过期删除的 key 数
expired_stale_perc:0.10
expired_time_cap_reached_count:0
expired_subkeys:0
evicted_keys:0 # 【核心】累计因内存不足被淘汰的 key 数
evicted_clients:0 # 7.0+,因内存被踢掉的客户端数
evicted_scripts:0 # 7.4+
total_eviction_exceeded_time:0
current_eviction_exceeded_time:0
keyspace_hits:85234120 # 【核心】命中次数
keyspace_misses:4512300 # 【核心】未命中次数
pubsub_channels:5 # 当前订阅的频道数
pubsub_patterns:2 # 当前订阅的模式数
pubsubshard_channels:0 # 7.0+
latest_fork_usec:12500 # 【重点】最近一次 fork 耗时(微秒)
total_forks:140 # 7.0+
migrate_cached_sockets:0
slave_expires_tracked_keys:0
active_defrag_hits:0 # 主动碎片整理相关统计
active_defrag_misses:0
active_defrag_key_hits:0
active_defrag_key_misses:0
total_active_defrag_time:0
current_active_defrag_time:0
tracking_total_keys:0
tracking_total_items:0
tracking_total_prefixes:0
unexpected_error_replies:0
total_error_replies:1250 # 6.2+,累计错误回复数
dump_payload_sanitizations:0
total_reads_processed:892500000
total_writes_processed:892300000
io_threaded_reads_processed:0
io_threaded_writes_processed:0
client_query_buffer_limit_disconnections:0 # 7.0+
client_output_buffer_limit_disconnections:0
reply_buffer_shrinks:1024
reply_buffer_expands:1048
eventloop_cycles:15234000 # 7.0+
eventloop_duration_sum:8523400000 # 7.0+
instantaneous_eventloop_cycles_per_sec:12800
instantaneous_eventloop_duration_usec:45
11.5.1 连接与吞吐指标
| 指标 | 判读要点 |
|---|---|
total_connections_received |
自启动(或上次 CONFIG RESETSTAT)以来累计接受的连接数。关键用法是看它的增长速率:用 (本次 - 上次) / 采样间隔 得到"每秒新建连接数"。正常使用连接池的应用,这个速率应该接近 0;若达到每秒几百上千,说明应用没用连接池或连接池失效——频繁 TCP 三次握手 + AUTH 会显著推高延迟与 CPU |
total_commands_processed |
累计处理的命令总数。同样看增长速率,即平均 QPS。与 total_connections_received 的比值(命令数/连接数)可判断连接复用程度:比值很小(如 < 10)说明连接用完就扔 |
instantaneous_ops_per_sec |
当前瞬时 QPS,Redis 内部按最近若干次采样计算。这是容量评估与异常发现的核心指标:突然掉到 0 → 业务侧断了或实例卡住;突然翻几倍 → 流量异常、缓存击穿导致的重试风暴、或有人在跑批。单实例参考量级:普通命令 8 万~10 万 QPS(千兆网卡与网络往返通常先成为瓶颈) |
instantaneous_input_kbps / instantaneous_output_kbps |
瞬时网络吞吐。*output 远大于 input 且数值很大(如接近百兆),常见于 `KEYS 、HGETALL大 hash、SMEMBERS` 大集合,或大 value 被高频读取**。千兆网卡上限约 125 MB/s = 128000 KB/s,达到 70% 就要警惕 |
rejected_connections |
被拒绝的连接数。只要 > 0 就说明连接数已达 maxclients 上限,新连接被直接关闭并返回 ERR max number of clients reached。这是硬故障级别的指标,必须告警。处理:调大 maxclients(同时调大系统 fd 上限)、修正应用连接池配置、排查连接泄漏 |
latest_fork_usec |
最近一次 fork 操作的耗时(微秒)。fork 期间主进程完全阻塞,所以这个值直接等于一次延迟尖刺的时长。经验值:每 GB 内存约 20~50 毫秒(普通物理机),10GB 实例一次 fork 可能阻塞 200~500 毫秒。latest_fork_usec > 1000000(1 秒)说明问题严重,需要拆分实例或关闭不必要的持久化。虚拟机(尤其是 Xen)上 fork 会比物理机慢很多 |
total_error_replies |
累计返回错误的次数(6.2+)。增长速率突增说明业务有大量报错,配合 INFO errorstats(6.2+)可看到具体错误类型分布 |
# INFO errorstats:按错误类型统计(Redis 6.2+),排查报错风暴的利器
redis-cli INFO errorstats
# errorstat_NOAUTH:count=1523 ← 有客户端没带密码
# errorstat_WRONGTYPE:count=42 ← 业务代码 key 类型用错
# errorstat_OOM:count=8 ← 内存打满了
# errorstat_MISCONF:count=125 ← 持久化失败导致写被拒
# errorstat_NOPERM:count=3 ← ACL 越权
# errorstat_MOVED:count=98000 ← 客户端没用集群模式
11.5.2 命中率:keyspace_hits 与 keyspace_misses
【命中率计算公式】
命中率 = keyspace_hits / (keyspace_hits + keyspace_misses) × 100%
以上面的示例数据为例:
85234120 / (85234120 + 4512300) × 100% = 94.97%
| 指标 | 含义 |
|---|---|
keyspace_hits |
在主键空间中成功找到 key 的读操作次数 |
keyspace_misses |
在主键空间中没有找到 key 的读操作次数 |
# 命中率计算脚本
H=$(redis-cli INFO stats | awk -F: '/^keyspace_hits:/{print $2}' | tr -d '\r')
M=$(redis-cli INFO stats | awk -F: '/^keyspace_misses:/{print $2}' | tr -d '\r')
awk "BEGIN{t=$H+$M; if(t>0) printf \"命中率: %.2f%% (hits=%d misses=%d)\n\", $H*100/t, $H, $M; else print \"暂无读请求\"}"
# 只算【最近一段时间】的命中率(更有意义):先重置统计,等一段时间再算
redis-cli CONFIG RESETSTAT # 重置所有 INFO stats 与 commandstats 计数
sleep 300
redis-cli INFO stats | grep -E 'keyspace_hits|keyspace_misses'
| 命中率区间 | 判读 | 处理方向 |
|---|---|---|
| ≥ 95% | 优秀,缓存设计合理 | 保持 |
| 90% ~ 95% | 良好 | 可优化 TTL 与预热策略 |
| 80% ~ 90% | 偏低,需关注 | 检查 TTL 是否过短、缓存容量是否不足(看 evicted_keys) |
| < 80% | 有问题 | 见下方原因清单 |
命中率过低的常见原因:
| 原因 | 排查方式 |
|---|---|
| 缓存穿透:大量请求查询不存在的数据(可能是恶意扫描) | MONITOR 短时抽样看 miss 的 key 是否有规律(如自增 ID 探测);对策见第九章 9.1(空值缓存 + 布隆过滤器) |
| TTL 设置过短:数据刚写入就过期 | 看 expired_keys 增长速率是否远高于业务变更频率 |
| 内存不足导致大量淘汰 | 看 evicted_keys 是否持续增长 |
缓存未预热:实例刚重启(结合 uptime_in_seconds 判断) |
上线前做缓存预热 |
| 业务本身低命中:如按用户维度的长尾查询 | 属正常,不必强求高命中率 |
统计口径误解:hits/misses 只统计读操作,纯写入型实例(如队列、计数器)该指标无意义 |
区分实例用途再看指标 |
注意:
keyspace_hits/misses是自启动累计值,会被历史数据"稀释"。监控系统应采集增量差值计算区间命中率,而不是直接用累计值——否则命中率突然下降的故障会被历史高命中率掩盖。
11.5.3 过期与淘汰:expired_keys 与 evicted_keys
| 指标 | 含义与判读 |
|---|---|
expired_keys |
累计因 TTL 到期而被删除的 key 数。这是正常且健康的行为。增长速率应大致与业务写入速率匹配。突然出现巨大跳变说明一批 key 同时到期(见 11.5.4 的抖动问题) |
evicted_keys |
累计因内存达到 maxmemory 而被淘汰的 key 数。这个值持续增长就明确说明内存不足——Redis 正在被迫删除还没到期的数据来腾空间。纯缓存实例少量淘汰可接受(说明 maxmemory 正好用满,是设计预期);但作为存储用途的实例,evicted_keys > 0 意味着数据在丢失 |
evicted_clients |
因内存压力被强制断开的客户端数(7.0+,配合 maxmemory-clients) |
expired_stale_perc |
估算的"已过期但还未被清理"的 key 占比。偏高说明惰性+定期删除跟不上,可考虑调高 hz |
total_eviction_exceeded_time |
累计处于"内存超限"状态的时间(毫秒,6.0+)。> 0 说明确实发生过内存超限 |
# 【evicted_keys 增长率监控】—— 这是内存不足最直接的证据
E1=$(redis-cli INFO stats | awk -F: '/^evicted_keys:/{print $2}' | tr -d '\r')
sleep 60
E2=$(redis-cli INFO stats | awk -F: '/^evicted_keys:/{print $2}' | tr -d '\r')
echo "过去 60 秒淘汰了 $((E2 - E1)) 个 key"
# 结论:
# = 0 内存充足
# < 100/分 轻微,纯缓存场景可接受
# > 1000/分 【严重】内存严重不足,命中率会同步下降,需扩容或排查大 key
evicted_keys 持续增长的处理路径:
evicted_keys 持续增长
│
├─ 1) 内存真的不够 ──► 调大 maxmemory / 加物理内存 / 拆分实例 / 上集群
│
├─ 2) 有大 key 占了大头 ──► redis-cli --bigkeys / --memkeys 定位后拆分
│
├─ 3) key 没设 TTL,只增不减 ──► 排查是否有忘记设过期时间的 key
│ redis-cli --scan --pattern 'xxx:*' | head -100 | xargs -n1 redis-cli TTL
│ (返回 -1 表示永不过期)
│
├─ 4) 碎片率过高,实际可用内存被浪费 ──► 见 11.3.2,开 activedefrag
│
└─ 5) 淘汰策略不合适 ──► 缓存场景改 allkeys-lru/allkeys-lfu,
volatile-* 在无 TTL key 时会退化为 noeviction
11.5.4 主从同步质量:sync_full 与 sync_partial
| 指标 | 判读要点 |
|---|---|
sync_full |
全量同步次数。全量同步的代价很高:主库 fork + 生成 RDB + 网络传输整个数据集 + 从库清空后重新加载。正常情况下,这个值应该只等于从库的个数(每个从库首次接入时全量一次)。持续增长是明确的异常信号 |
sync_partial_ok |
部分重同步成功次数。网络短暂闪断后,从库凭 replid + 偏移量请求增量补齐,主库能从复制积压缓冲区(repl-backlog)里找到对应数据。这是我们希望看到的结果 |
sync_partial_err |
部分重同步失败次数。请求了增量但主库缓冲区里已经没有对应数据了,只能退化为全量同步。这个值增长就意味着 sync_full 也会跟着增长 |
【同步质量的判读组合】
sync_full ≈ 从库数量, sync_partial_err = 0 → 健康
sync_partial_ok 增长, sync_partial_err = 0 → 有网络闪断但都增量补齐了,可接受
sync_partial_err 增长, sync_full 跟着增长 → 【问题】复制积压缓冲区太小
sync_full 频繁增长(每小时多次) → 【严重】主从反复重连,会周期性
拖垮主库(每次都 fork + 传全量)
sync_full 频繁增长的原因与处理:
| 原因 | 处理 |
|---|---|
repl-backlog-size 太小(默认仅 1MB) |
调大到 64MB~256MB。计算公式:backlog ≥ 平均写入速率(字节/秒) × 可容忍断连秒数 |
client-output-buffer-limit replica 太小 |
主库给从库的输出缓冲区超限时会主动断开从库,日志出现 client scheduled to be closed ASAP。调大到 256mb 64mb 60 |
| 网络不稳定 / 跨机房高延迟 | 排查网络;跨机房复制要加大超时与缓冲区 |
repl-timeout 过小 |
大数据量全量同步耗时超过 repl-timeout(默认 60 秒)会中断并重试,形成死循环。调大到 300 或更多 |
| 从库反复重启 | 看从库 uptime_in_seconds 与 run_id 是否变化;从库 OOM 被杀是常见原因 |
主库 run_id 变了(主库重启过) |
4.0 之前主库重启必然导致全量同步;4.0+ 支持 repl-diskless-load 与基于 replid2 的重启后部分重同步 |
# ===== 复制健壮性配置 =====
# 复制积压缓冲区:断连期间保存写命令,够大才能走部分重同步
repl-backlog-size 128mb
# 所有从库都断开后,积压缓冲区保留多久(秒),0 表示永不释放
repl-backlog-ttl 3600
# 复制超时(秒),必须大于全量同步的预计耗时
repl-timeout 300
# 从库输出缓冲区限制(超限会被主库断开,导致全量同步)
client-output-buffer-limit replica 256mb 64mb 60
# 无盘复制:主库直接把 RDB 流式发给从库,不落盘(磁盘慢但网络快时有优势)
repl-diskless-sync yes
repl-diskless-sync-delay 5
# 从库无盘加载(4.0+):边收边加载,省一次落盘
repl-diskless-load disabled # disabled / on-empty-db / swapdb
# 是否禁用 TCP_NODELAY(no 表示低延迟优先,yes 表示省带宽)
repl-disable-tcp-nodelay no
# 同步质量一键查看
redis-cli INFO stats | grep -E 'sync_full|sync_partial_ok|sync_partial_err'
redis-cli INFO replication
redis-cli CONFIG GET repl-backlog-size
grep -iE 'sync|backlog|MASTER|SLAVE' /var/log/redis/redis.log | tail -30
11.6 INFO replication:复制状态
主节点视角:
redis-cli INFO replication
# Replication
role:master # 【核心】角色:master / slave
connected_slaves:2 # 【核心】已连接的从库数量
slave0:ip=192.168.1.11,port=6379,state=online,offset=1523400,lag=0
slave1:ip=192.168.1.12,port=6379,state=online,offset=1523380,lag=1
# ↑ 每个从库一行:IP、端口、状态、复制偏移量、延迟(秒)
master_failover_state:no-failover # 7.0+,故障转移状态
master_replid:8f2a1c9e7b3d5a4f6c8e0b2d4f6a8c0e1b3d5f7a # 复制 ID
master_replid2:0000000000000000000000000000000000000000 # 上一个复制 ID(故障转移后用)
master_repl_offset:1523400 # 【核心】主库当前复制偏移量
second_repl_offset:-1
repl_backlog_active:1 # 复制积压缓冲区是否启用
repl_backlog_size:134217728 # 积压缓冲区大小(字节)
repl_backlog_first_byte_offset:1389182
repl_backlog_histlen:134218 # 积压缓冲区中实际数据长度
从节点视角:
# Replication
role:slave
master_host:192.168.1.10 # 主库地址
master_port:6379
master_link_status:up # 【核心】与主库的连接状态:up / down
master_last_io_seconds_ago:0 # 【重点】距上次与主库通信的秒数
master_sync_in_progress:0 # 【重点】是否正在进行全量同步
slave_read_repl_offset:1523400
slave_repl_offset:1523400 # 【核心】从库已复制到的偏移量
slave_priority:100 # 哨兵选主优先级,越小越优先,0=永不当主
slave_read_only:1 # 【重点】是否只读
replica_announced:1 # 6.2+
connected_slaves:0 # 级联复制时这里会有下级从库
master_failover_state:no-failover
master_replid:8f2a1c9e7b3d5a4f6c8e0b2d4f6a8c0e1b3d5f7a
master_repl_offset:1523400
...
# 正在全量同步时会额外出现以下字段
master_sync_total_bytes:1073741824 # 需要传输的总字节数(7.0+)
master_sync_read_bytes:536870912 # 已接收的字节数
master_sync_left_bytes:536870912 # 剩余字节数
master_sync_perc:50.00 # 同步进度百分比
master_sync_last_io_seconds_ago:0
# 与主库断开时会出现
master_link_down_since_seconds:35 # 【重点】断开已持续多少秒
| 指标 | 判读要点 |
|---|---|
role |
确认角色。故障转移后要及时核对:应用误把请求写到从库会报 READONLY You can't write against a read only replica |
connected_slaves |
主库上实际连接的从库数。少于预期就说明有从库掉线 |
slaveN:...state= |
online 表示正常传输增量;send_bulk 表示正在传 RDB(全量同步中);wait_bgsave 表示在等 BGSAVE 完成 |
slaveN:...lag= |
该从库最近一次响应 REPLCONF ACK 距今的秒数。持续 > 1 说明网络或从库处理能力有问题 |
master_repl_offset(主)与 slave_repl_offset(从) |
两者之差就是主从数据延迟的字节数,这是衡量主从延迟最准确的方式。差值持续增大说明从库跟不上(网络带宽不足、从库磁盘或 CPU 瓶颈、从库上跑了慢命令) |
master_link_status |
从库最重要的指标。up=正常,down=已断开。down 必须立即告警,此时从库数据停止更新,读写分离架构下会读到越来越旧的数据。排查见第十二章 |
master_last_io_seconds_ago |
距上次与主库通信的秒数。正常应为 0~1(主库每秒发 PING)。接近或超过 repl-timeout 就要出事 |
master_sync_in_progress |
1 表示正在全量同步。此期间从库的行为取决于 replica-serve-stale-data:yes(默认)→ 返回旧数据;no → 除少数命令外全部返回 SYNC with master in progress 错误 |
slave_read_only |
1(默认)表示从库只读。改成 0 极其危险:写入从库的数据不会同步给主库,也会在下次全量同步时被清空,造成"数据神秘消失" |
slave_priority |
哨兵选主时的优先级。设为 0 的从库永远不会被提升为主(适合专门做备份/统计的从库) |
master_link_down_since_seconds |
断连持续时间。超过 repl-backlog 能覆盖的时长后,重连必然触发全量同步 |
# 主从延迟计算(在主库上执行)
MO=$(redis-cli -h 192.168.1.10 INFO replication | awk -F: '/^master_repl_offset:/{print $2}' | tr -d '\r')
SO=$(redis-cli -h 192.168.1.11 INFO replication | awk -F: '/^slave_repl_offset:/{print $2}' | tr -d '\r')
echo "主从延迟: $((MO - SO)) 字节"
# 判读:
# 0 完全同步
# < 1MB 正常
# 持续 > 10MB 【告警】从库跟不上,读写分离会读到明显过期数据
# 用 WAIT 命令确认写入已被 N 个从库确认(3.0+)
redis-cli SET k v
redis-cli WAIT 1 1000 # 等待至少 1 个从库确认,最多等 1000 毫秒
# (integer) 1 ← 返回实际确认的从库数
# ===== 复制安全性配置 =====
# 从库只读(默认 yes,不要改)
replica-read-only yes
# 全量同步期间从库是否响应读请求
replica-serve-stale-data yes
# 哨兵选主优先级
replica-priority 100
# 【重要】主库至少要有 N 个从库且延迟不超过 M 秒,才接受写请求
# 这是防止"脑裂导致数据丢失"的关键配置
min-replicas-to-write 1
min-replicas-max-lag 10
min-replicas-to-write与min-replicas-max-lag(5.0 之前叫min-slaves-to-write/min-slaves-max-lag)的作用:当满足条件的从库数量不足时,主库主动拒绝写入并返回NOREPLICAS Not enough good replicas to write。这样在网络分区导致主库被隔离时,主库不会继续接受写入,避免故障转移后这批写入丢失。代价是可用性下降,需按业务权衡。
11.7 INFO cpu:CPU 使用
# CPU
used_cpu_sys:12345.678900 # 主进程消耗的系统态 CPU 时间(秒)
used_cpu_user:23456.789012 # 主进程消耗的用户态 CPU 时间(秒)
used_cpu_sys_children:234.567890 # 子进程(BGSAVE/BGREWRITEAOF)的系统态 CPU
used_cpu_user_children:345.678901 # 子进程的用户态 CPU
used_cpu_sys_main_thread:12000.111111 # 主线程系统态 CPU(7.0+)
used_cpu_user_main_thread:23000.222222 # 主线程用户态 CPU(7.0+)
| 判读方式 | 说明 |
|---|---|
| 看增长速率而非绝对值 | 这些是累计 CPU 秒数。(本次 - 上次) / 采样间隔 × 100% 得到该期间的 CPU 使用率 |
| Redis 主线程是单线程的 | 单核使用率接近 100% 就是瓶颈(无论机器有多少核)。此时必须靠拆分实例 / 上集群来水平扩展,加 CPU 核数对主线程无效 |
used_cpu_user 高 |
命令执行本身消耗大:可能有大量 O(N) 命令(SORT、ZUNIONSTORE、大 LRANGE)、复杂 Lua 脚本、或纯粹 QPS 太高 |
used_cpu_sys 高 |
系统调用消耗大:通常是连接数过多(大量 epoll 事件与读写系统调用)、频繁新建连接、或网络包过多。检查是否用了连接池、是否可以用 Pipeline 合并请求 |
| *`_children` 高** | 持久化子进程消耗。频繁 BGSAVE/BGREWRITEAOF 会占用大量 CPU,还会因 COW 触发大量页错误。可放宽 save 条件与 auto-aof-rewrite-percentage |
activedefrag 的影响 |
开启主动碎片整理后 CPU 会明显上升(由 active-defrag-cycle-max 控制上限),属预期行为 |
# Redis 进程的实时 CPU 占用(操作系统视角)
PID=$(pgrep -x redis-server)
top -p $PID -bn1 | tail -3
pidstat -p $PID 1 5 # 需 yum install -y sysstat
# 看主线程与后台线程的 CPU 分布
top -H -p $PID -bn1 | head -15
# 从 INFO 计算区间 CPU 使用率
read s1 u1 <<<$(redis-cli INFO cpu | awk -F: '/^used_cpu_sys:/{s=$2} /^used_cpu_user:/{u=$2} END{print s, u}')
sleep 10
read s2 u2 <<<$(redis-cli INFO cpu | awk -F: '/^used_cpu_sys:/{s=$2} /^used_cpu_user:/{u=$2} END{print s, u}')
awk "BEGIN{printf \"sys: %.1f%% user: %.1f%%\n\", ($s2-$s1)*10, ($u2-$u1)*10}"
# ===== I/O 多线程(Redis 6.0+)=====
# 【注意】只并行化网络 I/O 的读写与协议解析,【命令执行仍然是单线程的】
# 因此它解决的是"网络处理成为瓶颈"的问题,不能提升单个命令的执行速度
io-threads 4 # 建议不超过物理核数,一般 4 个足够;1 表示关闭
io-threads-do-reads yes # 是否也用多线程处理读(默认 no,只多线程写回复)
多线程的适用判断:
used_cpu_sys占比高、单核已满而 QPS 上不去、value 较大(网络传输量大)时,开启io-threads有效;如果瓶颈在命令本身(有慢命令、大 key),开多线程没有帮助。用INFO stats的io_threaded_reads_processed/io_threaded_writes_processed确认是否真的走了多线程路径。
11.8 INFO commandstats:按命令统计(找出慢命令)
这一段默认不在 INFO 输出中,必须显式指定:
redis-cli INFO commandstats
# Commandstats
cmdstat_get:calls=52341200,usec=41872960,usec_per_call=0.80,rejected_calls=0,failed_calls=0
cmdstat_set:calls=15234100,usec=18280920,usec_per_call=1.20,rejected_calls=0,failed_calls=0
cmdstat_hgetall:calls=1523400,usec=12187200,usec_per_call=8.00,rejected_calls=0,failed_calls=0
cmdstat_keys:calls=12,usec=18000000,usec_per_call=1500000.00,rejected_calls=0,failed_calls=0
cmdstat_lrange:calls=52300,usec=15690000,usec_per_call=300.00,rejected_calls=0,failed_calls=0
cmdstat_smembers:calls=8200,usec=4100000,usec_per_call=500.00,rejected_calls=0,failed_calls=0
cmdstat_expire:calls=15234000,usec=7617000,usec_per_call=0.50,rejected_calls=0,failed_calls=0
cmdstat_info:calls=8640,usec=8640000,usec_per_call=1000.00,rejected_calls=0,failed_calls=0
cmdstat_eval:calls=125000,usec=6250000,usec_per_call=50.00,rejected_calls=0,failed_calls=0
cmdstat_config|get:calls=120,usec=24000,usec_per_call=200.00,rejected_calls=0,failed_calls=0
| 字段 | 含义 |
|---|---|
calls |
该命令被调用的总次数。用于分析业务命令构成:哪些命令是主力、哪些是意外出现的 |
usec |
该命令累计消耗的总微秒数。usec 占总时间的比例反映"哪个命令吃掉了最多 CPU 时间" |
usec_per_call |
平均每次调用耗时(微秒),这是找慢命令的核心字段 |
rejected_calls |
被拒绝执行的次数(6.2+):命令不存在、参数个数错误、ACL 拒绝等在执行前就被挡掉的调用 |
failed_calls |
执行过程中失败的次数(6.2+):如 WRONGTYPE、OOM 等执行时报错的调用 |
usec_per_call 判读标准:
| 取值 | 判读 |
|---|---|
| < 10 微秒 | 正常。GET/SET/INCR/EXPIRE 等 O(1) 命令应在此区间 |
| 10 ~ 100 微秒 | 可接受。小规模的 HGETALL、LRANGE、ZRANGE 在此区间 |
| 100 ~ 1000 微秒 | 偏慢,需要关注。通常意味着操作的集合较大,或 value 体积大 |
| > 1000 微秒(1 毫秒) | 有问题。单线程模型下,一个 1 毫秒的命令会让排在它后面的所有命令都多等 1 毫秒 |
| > 100000 微秒(100 毫秒) | 严重阻塞。上例中 cmdstat_keys 的 usec_per_call=1500000(1.5 秒!)就是典型的 KEYS * 拖死生产库 |
# ===== 找出平均耗时最高的命令(排障第一步)=====
redis-cli INFO commandstats | sed 's/^cmdstat_//' | \
awk -F'[:,=]' '{print $6, $2, $4}' | sort -rn | head -15 | \
awk 'BEGIN{printf "%-14s %-14s %s\n","平均耗时(us)","命令","调用次数"} {printf "%-14s %-14s %s\n",$1,$2,$3}'
# ===== 找出累计消耗 CPU 时间最多的命令(谁吃掉了最多资源)=====
redis-cli INFO commandstats | sed 's/^cmdstat_//' | \
awk -F'[:,=]' '{print $4, $1, $2, $6}' | sort -rn | head -15
# ===== 找出调用次数最多的命令(业务画像)=====
redis-cli INFO commandstats | sed 's/^cmdstat_//' | \
awk -F'[:,=]' '{print $2, $1}' | sort -rn | head -15
# ===== 检查是否有不该出现的危险命令 =====
redis-cli INFO commandstats | grep -iE 'cmdstat_(keys|flushall|flushdb|monitor|debug|hgetall|smembers|sort)'
# 生产环境出现 keys/flushall/monitor 的调用记录,必须追查是谁在用
# ===== 检查是否有大量失败调用 =====
redis-cli INFO commandstats | grep -vE 'rejected_calls=0,failed_calls=0'
# ===== 重置统计,只看接下来一段时间的数据(推荐做法)=====
redis-cli CONFIG RESETSTAT
sleep 300
redis-cli INFO commandstats
CONFIG RESETSTAT的重要性:commandstats是自启动累计的,一次误操作的KEYS *会永久留在平均值里,而正在发生的问题可能被海量正常请求稀释。排障时先CONFIG RESETSTAT,等待 3~5 分钟再看,得到的才是当前真实画像。注意该命令会同时重置INFO stats的所有计数(命中率、连接数统计等),执行前先记录一份快照。
11.9 INFO latencystats:命令延迟分布(Redis 7.0+)
版本说明:latencystats 段是 Redis 7.0 新增的,提供按命令的延迟百分位分布,比 commandstats 的平均值更能反映真实体验(平均值会掩盖长尾)。
redis-cli INFO latencystats
# Latencystats
latency_percentiles_usec_get:p50=0.897,p99=1.993,p99.9=8.031
latency_percentiles_usec_set:p50=1.003,p99=3.007,p99.9=15.039
latency_percentiles_usec_hgetall:p50=6.015,p99=45.087,p99.9=210.943
latency_percentiles_usec_lrange:p50=120.831,p99=980.991,p99.9=4227.071
latency_percentiles_usec_eval:p50=42.111,p99=180.223,p99.9=1523.711
| 百分位 | 含义与用法 |
|---|---|
p50(中位数) |
一半的请求快于这个值。反映典型体验 |
p99 |
99% 的请求快于这个值。这是 SLA 通常约定的指标,1% 的慢请求会被用户感知 |
p99.9 |
99.9% 分位。长尾延迟,反映最差情况。p99.9 远高于 p99(如 10 倍以上)说明存在偶发阻塞:fork、大 key 操作、AOF fsync 阻塞、swap |
# 找出 p99 最高的命令
redis-cli INFO latencystats | sed 's/^latency_percentiles_usec_//' | \
awk -F'[:,]' '{split($3,a,"="); print a[2], $1}' | sort -rn | head -10
# 配套的延迟百分位配置
redis-cli CONFIG GET latency-tracking
redis-cli CONFIG GET latency-tracking-info-percentiles
# ===== 延迟追踪配置(Redis 7.0+)=====
latency-tracking yes # 开启延迟百分位追踪(默认 yes)
latency-tracking-info-percentiles 50 99 99.9 # 输出哪些百分位
# 重置延迟统计
redis-cli CONFIG RESETSTAT
对比
commandstats与latencystats:commandstats的usec_per_call是平均值,一个 1 秒的慢请求混在 100 万个 0.5 微秒的快请求里,平均值只会涨 1 微秒,几乎看不出来;而latencystats的p99.9会明确暴露它。排查"偶发卡顿"必须看latencystats或SLOWLOG,不要只看平均值。 Redis 6.x 及更早只能靠SLOWLOG+--latency-history观察。
11.10 INFO keyspace:各数据库的 key 统计
redis-cli INFO keyspace
# Keyspace
db0:keys=1523400,expires=1420000,avg_ttl=3542100,subexpiry=0
db1:keys=52300,expires=0,avg_ttl=0
db2:keys=128,expires=128,avg_ttl=600000
| 字段 | 含义与判读 |
|---|---|
keys |
该 db 中的 key 总数。快速掌握数据规模;结合 used_memory_dataset / keys 可算出平均每 key 内存开销 |
expires |
设置了过期时间(TTL)的 key 数量 |
avg_ttl |
这些 key 的平均剩余存活时间(毫秒)。注意这是 Redis 用采样估算的近似值,不是精确值;expires=0 时该值为 0 |
subexpiry |
带字段级过期的 key 数(7.4+ 的 hash field TTL 特性) |
核心判读:keys 与 expires 的差值
永不过期的 key 数 = keys - expires
上例 db0:1523400 - 1420000 = 103400 个 key 没有 TTL
| 情况 | 判读 |
|---|---|
expires ≈ keys |
健康的缓存实例,绝大部分 key 都会自动清理 |
expires 远小于 keys |
危险信号:大量 key 永不过期,内存只增不减,最终必然触发淘汰或 OOM。这是"内存持续增长"最常见的根因(见第十二章) |
expires = 0 |
该 db 中没有任何 key 设过期时间。若淘汰策略是 volatile-*,内存满时会退化为 noeviction 直接报 OOM |
| 多个 db 都有数据 | 单机可用 SELECT 切换 db 做业务隔离,但集群模式只支持 db0,将来迁移到集群时会有改造成本。不推荐用多 db 做隔离,应该用 key 前缀 |
# 查看 key 总数(DBSIZE 只统计当前 db,INFO keyspace 看全部)
redis-cli DBSIZE
redis-cli -n 1 DBSIZE # 指定 db1
redis-cli INFO keyspace
# 【找出没有 TTL 的 key】—— 内存持续增长时必查
redis-cli --scan --pattern 'session:*' | head -200 | while read k; do
t=$(redis-cli TTL "$k")
[ "$t" = "-1" ] && echo "无 TTL: $k"
done
# TTL 返回值:-1 = key 存在但没有过期时间;-2 = key 不存在;>0 = 剩余秒数
# 按业务前缀统计 key 数量分布(找出哪个业务的 key 最多)
redis-cli --scan | awk -F: '{print $1}' | sort | uniq -c | sort -rn | head -20
# 计算平均每 key 内存开销
K=$(redis-cli INFO keyspace | awk -F'[=,]' '/^db0:/{print $2}')
D=$(redis-cli INFO memory | awk -F: '/^used_memory_dataset:/{print $2}' | tr -d '\r')
awk "BEGIN{printf \"平均每 key 占用: %.1f 字节\n\", $D/$K}"
⚠️
--scan而不是KEYS:redis-cli --scan底层用SCAN游标渐进遍历,每次只取一小批,不阻塞 Redis;KEYS *是一次性 O(N) 遍历,百万级 key 会阻塞数秒,生产环境等同于故障。
11.11 关键监控指标与告警阈值建议
下表是可直接落地到 Zabbix / Prometheus 告警规则的建议值。阈值需按业务实际情况微调:纯缓存实例可放宽淘汰相关阈值,存储型实例则要收紧。
11.11.1 内存类
| 指标 | 计算方式 | 警告阈值 | 严重阈值 | 含义与动作 |
|---|---|---|---|---|
| 内存使用率 | used_memory / maxmemory × 100% |
> 80% | > 90% | 接近上限即将触发淘汰。动作:扩容 maxmemory、清理大 key、拆分实例 |
| RSS 占主机内存比 | used_memory_rss / total_system_memory × 100% |
> 70% | > 85% | 主机内存吃紧,有 OOM Killer 与 swap 风险 |
mem_fragmentation_ratio |
used_memory_rss / used_memory |
> 1.5 | > 2.0 或 < 1 | > 1.5 碎片浪费严重 → 开 activedefrag;< 1 表示已用 swap,属最高优先级故障 |
used_memory_peak_perc |
used_memory / used_memory_peak |
— | — | 参考项,判断当前是否处于峰值时段 |
used_memory_overhead 占比 |
used_memory_overhead / used_memory |
> 20% | > 40% | 非数据开销过大 → 连接数过多或有慢消费客户端 |
evicted_keys 增长率 |
每分钟增量 | > 100/分钟 | > 1000/分钟 | 内存不足正在丢数据。存储型实例任何非零增长都应告警 |
maxmemory 是否为 0 |
CONFIG GET maxmemory |
— | = 0 即告警 | 未设上限,随时可能耗尽主机内存 |
| 进程 swap 用量 | /proc/<pid>/status 的 VmSwap |
> 0 | > 100MB | 性能雪崩前兆,立即处理 |
11.11.2 性能与吞吐类
| 指标 | 计算方式 | 警告阈值 | 严重阈值 | 含义与动作 |
|---|---|---|---|---|
| 缓存命中率 | hits / (hits + misses) × 100%(用增量计算) |
< 90% | < 80% | 缓存效果差 → 查穿透、TTL、淘汰。纯写入型实例忽略此项 |
instantaneous_ops_per_sec |
直接读取 | > 50000 或环比突增 100% | > 80000 | 接近单实例上限;突增可能是缓存击穿引发的重试风暴 |
| QPS 突降 | 环比下降 > 80% | 告警 | 降为 0 | 业务侧中断或实例卡死 |
| 平均延迟 | redis-cli --latency 的 avg |
> 1ms | > 10ms | 内网正常应 < 0.5ms |
p99 延迟 |
latencystats(7.0+) |
> 5ms | > 20ms | 长尾延迟,用户可感知 |
| 慢查询数量 | SLOWLOG LEN 的增量 |
> 10/分钟 | > 100/分钟 | 有慢命令在拖累整体延迟 |
| 单核 CPU 使用率 | used_cpu_user + used_cpu_sys 增量 |
> 70% | > 90% | 单线程已成瓶颈,只能拆分或上集群 |
latest_fork_usec |
直接读取 | > 500000(0.5s) | > 1000000(1s) | fork 阻塞时间过长,是延迟尖刺的根因 |
| 网络出带宽 | instantaneous_output_kbps |
> 千兆的 60% | > 80% | 可能有大 key 被高频读取 |
total_error_replies 增长率 |
每分钟增量(6.2+) | > 10/分钟 | > 100/分钟 | 配合 INFO errorstats 定位错误类型 |
11.11.3 连接类
| 指标 | 计算方式 | 警告阈值 | 严重阈值 | 含义与动作 |
|---|---|---|---|---|
| 连接数使用率 | connected_clients / maxclients × 100% |
> 70% | > 85% | 即将打满 → 调大 maxclients + 系统 fd,或修连接池 |
rejected_connections |
累计值的增量 | > 0 即告警 | 持续增长 | 已经在拒绝新连接了,属硬故障 |
| 新建连接速率 | total_connections_received 每秒增量 |
> 100/秒 | > 500/秒 | 应用没用连接池 |
blocked_clients |
直接读取 | 环比突增 | 持续增长不回落 | 队列消费者卡住或生产端异常 |
client_recent_max_input_buffer |
直接读取 | > 10MB | > 100MB | 有客户端发送超大命令 |
client_recent_max_output_buffer |
直接读取 | > 10MB | > 100MB | 有客户端拉取超大结果集 |
| 进程 fd 使用率 | ls /proc/<pid>/fd \| wc -l ÷ fd 上限 |
> 70% | > 85% | 需调整 LimitNOFILE / limits.conf |
11.11.4 持久化类
| 指标 | 判断条件 | 级别 | 动作 |
|---|---|---|---|
rdb_last_bgsave_status |
≠ ok |
严重(P0) | 立即查磁盘空间、目录权限、overcommit_memory。不修复则所有写命令返回 MISCONF |
aof_last_write_status |
≠ ok |
严重(P0) | 同上 |
aof_last_bgrewrite_status |
≠ ok |
严重 | AOF 会无限增长撑满磁盘 |
aof_rewrites_consecutive_failures |
> 0(7.0+) | 严重 | 同上 |
rdb_changes_since_last_save |
> 100000(且开了 RDB) | 警告 | 落盘不及时,宕机将丢大量数据 |
| 距上次保存时长 | now - rdb_last_save_time > 1 小时 |
警告 | save 条件过松或保存一直失败 |
aof_delayed_fsync |
增量 > 10/分钟 | 警告 | 磁盘 I/O 跟不上写入,换 SSD 或拆实例 |
rdb_last_bgsave_time_sec |
> 10 秒 | 警告 | 数据量大或磁盘慢,fork 期间风险高 |
| 数据目录磁盘使用率 | df > 80% |
警告 | > 90% 严重:磁盘满会直接引发 MISCONF |
loading |
= 1 且持续 > 5 分钟 | 警告 | 启动加载时间过长,业务会持续报 LOADING |
11.11.5 复制与集群类
| 指标 | 判断条件 | 级别 | 动作 |
|---|---|---|---|
master_link_status(从库) |
≠ up |
严重(P0) | 从库数据已停止更新,读写分离会读到旧数据 |
master_last_io_seconds_ago |
> 10 秒 | 严重 | 复制链路异常 |
| 主从偏移量差 | master_repl_offset - slave_repl_offset |
> 1MB 警告,> 10MB 严重 | 从库跟不上,检查网络与从库负载 |
slaveN:...lag |
> 1 秒 | 警告 | 该从库响应慢 |
connected_slaves |
< 预期从库数 | 严重 | 有从库掉线,冗余度下降 |
sync_full 增长 |
任何非首次接入的增长 | 警告 | 频繁全量同步会周期性拖垮主库 → 调大 repl-backlog-size 与 client-output-buffer-limit replica |
sync_partial_err 增长 |
> 0 | 警告 | 部分重同步失败,必然退化为全量 |
role 变化 |
master ↔ slave 切换 | 严重 | 发生了故障转移,需确认应用是否已连到新主 |
run_id 变化 |
变了 | 严重 | 实例重启过,未开持久化则数据全丢 |
uptime_in_seconds |
< 300 秒 | 严重 | 实例刚重启,需查原因(OOM Kill?崩溃?) |
cluster_state(集群) |
≠ ok |
严重(P0) | 集群整体不可用 |
cluster_slots_assigned |
≠ 16384 | 严重 | 有槽未分配,部分 key 不可访问 |
cluster_slots_fail / pfail |
> 0 | 严重 | 有节点疑似或已确认失效 |
cluster_known_nodes |
< 预期节点数 | 严重 | 节点掉出集群 |
| 哨兵数量 | 存活哨兵 < quorum |
严重 | 无法完成故障转移,高可用形同虚设 |
11.11.6 安全类
| 指标 | 判断条件 | 级别 | 动作 |
|---|---|---|---|
ACL LOG 中 reason=auth 增长 |
短时间内快速增长(6.0+) | 严重 | 疑似暴力破解,检查来源 IP 并加固防火墙 |
errorstat_NOAUTH 增长 |
持续增长(6.2+) | 警告 | 有客户端未带密码,或有人在探测 |
errorstat_NOPERM 增长 |
> 0 | 警告 | 有 ACL 越权尝试 |
| 监听地址 | 出现 0.0.0.0 或公网 IP |
严重 | 立即修正 bind 与防火墙 |
| 运行用户 | 不是 redis |
严重 | 按 10.2.5 整改 |
| TLS 证书剩余有效期 | < 30 天 | 警告 | < 7 天严重:到期会导致全站连不上 |
# ===== 一个可直接放进 crontab 的巡检脚本骨架 =====
#!/bin/bash
# Redis 健康巡检:输出异常项,无异常时不输出(配合 cron 的邮件通知)
export REDISCLI_AUTH='YourPassword'
R="redis-cli -h 127.0.0.1 -p 6379"
INFO=$($R INFO)
get() { echo "$INFO" | awk -F: -v K="^$1:" '$0 ~ K {print $2}' | tr -d '\r'; }
USED=$(get used_memory); MAX=$(get maxmemory)
[ "$MAX" -gt 0 ] && awk "BEGIN{r=$USED*100/$MAX; if(r>80) printf \"[WARN] 内存使用率 %.1f%%\n\", r}"
FRAG=$(get mem_fragmentation_ratio)
awk "BEGIN{if($FRAG>1.5) print \"[WARN] 碎片率 $FRAG > 1.5\"; if($FRAG<1) print \"[CRIT] 碎片率 $FRAG < 1,已使用 swap\"}"
for k in rdb_last_bgsave_status aof_last_write_status aof_last_bgrewrite_status; do
v=$(get $k); [ -n "$v" ] && [ "$v" != "ok" ] && echo "[CRIT] $k=$v (将导致 MISCONF,写入不可用)"
done
REJ=$(get rejected_connections); [ "$REJ" -gt 0 ] && echo "[CRIT] rejected_connections=$REJ 连接数已打满"
CC=$(get connected_clients); MC=$(get maxclients)
awk "BEGIN{if($CC*100/$MC>70) printf \"[WARN] 连接数使用率 %.1f%%\n\", $CC*100/$MC}"
H=$(get keyspace_hits); M=$(get keyspace_misses)
awk "BEGIN{t=$H+$M; if(t>10000 && $H*100/t<90) printf \"[WARN] 命中率 %.2f%%\n\", $H*100/t}"
UP=$(get uptime_in_seconds); [ "$UP" -lt 300 ] && echo "[CRIT] 实例 $UP 秒前重启过,请查原因"
ROLE=$(get role)
[ "$ROLE" = "slave" ] && [ "$(get master_link_status)" != "up" ] && echo "[CRIT] 主从链路 down"
df -h /var/lib/redis | awk 'NR==2 && int($5)>80 {print "[WARN] 数据目录磁盘使用率 "$5}'
11.12 监控方案简述
11.12.1 Zabbix 监控 Redis
Zabbix 从 4.4 起提供官方 Redis 模板(基于 Zabbix Agent 2 的 redis 插件),无需自己写脚本。
# ===== 方案一:Zabbix Agent 2(推荐,Zabbix 4.4+)=====
yum install -y zabbix-agent2
# /etc/zabbix/zabbix_agent2.d/plugins.d/redis.conf
Plugins.Redis.Sessions.prod.Uri=tcp://127.0.0.1:6379
Plugins.Redis.Sessions.prod.Password=YourPassword
Plugins.Redis.Timeout=3
systemctl restart zabbix-agent2
# 在 Zabbix Web 端给主机关联模板 "Redis by Zabbix agent 2"
# 模板自带:内存、命中率、连接数、持久化状态、复制状态、慢日志等约 60 个监控项与触发器
# 手动验证取值
zabbix_get -s 127.0.0.1 -k 'redis.info[tcp://127.0.0.1:6379,,memory]'
zabbix_get -s 127.0.0.1 -k 'redis.ping[tcp://127.0.0.1:6379]'
# ===== 方案二:自定义 UserParameter(老版本 Agent 或需要定制指标时)=====
cat > /etc/zabbix/zabbix_agentd.d/userparameter_redis.conf <<'EOF'
# 通用取值:redis.info[段名,字段名]
UserParameter=redis.info[*],redis-cli -h 127.0.0.1 -p 6379 -a "$(cat /etc/zabbix/.redis_pass)" --no-auth-warning INFO $1 2>/dev/null | awk -F: -v K="^$2:" '$0 ~ K {gsub(/\r/,"",$2); print $2}'
# 存活检测
UserParameter=redis.ping,redis-cli -h 127.0.0.1 -p 6379 -a "$(cat /etc/zabbix/.redis_pass)" --no-auth-warning PING 2>/dev/null | grep -c PONG
# 命中率(计算型指标)
UserParameter=redis.hitrate,redis-cli -h 127.0.0.1 -p 6379 -a "$(cat /etc/zabbix/.redis_pass)" --no-auth-warning INFO stats 2>/dev/null | awk -F: '/^keyspace_hits:/{h=$2}/^keyspace_misses:/{m=$2}END{t=h+m; if(t>0) printf "%.2f", h*100/t; else print 0}'
EOF
# 密码文件权限收紧,避免出现在 ps 输出中
echo 'YourPassword' > /etc/zabbix/.redis_pass
chown zabbix:zabbix /etc/zabbix/.redis_pass
chmod 400 /etc/zabbix/.redis_pass
systemctl restart zabbix-agent # CentOS 7
# service zabbix-agent restart # CentOS 6
# 验证
zabbix_get -s 127.0.0.1 -k 'redis.info[memory,used_memory]'
zabbix_get -s 127.0.0.1 -k 'redis.hitrate'
zabbix_get -s 127.0.0.1 -k 'redis.ping'
建议给监控单独建一个 ACL 用户(Redis 6.0+),只放开
INFO、PING、CONFIG GET、SLOWLOG GET等只读命令,见 10.2.9 的示例 4。这样即使 Zabbix Agent 所在主机被攻破,也拿不到业务数据。
详见 08-05-Zabbix监控.md
11.12.2 Prometheus + redis_exporter + Grafana
# ===== 1) 部署 redis_exporter =====
wget https://github.com/oliver006/redis_exporter/releases/download/v1.62.0/redis_exporter-v1.62.0.linux-amd64.tar.gz
tar xf redis_exporter-v1.62.0.linux-amd64.tar.gz
cp redis_exporter-v1.62.0.linux-amd64/redis_exporter /usr/local/bin/
chmod +x /usr/local/bin/redis_exporter
# /usr/lib/systemd/system/redis_exporter.service
[Unit]
Description=Redis Exporter for Prometheus
After=network.target
[Service]
User=prometheus
Group=prometheus
# 密码通过环境变量传入,避免出现在命令行与 ps 输出中
Environment="REDIS_PASSWORD=YourPassword"
ExecStart=/usr/local/bin/redis_exporter \
--redis.addr=redis://127.0.0.1:6379 \
--web.listen-address=:9121 \
--include-system-metrics \
--check-keys=order:*,cache:* \
--check-key-groups=order:*,session:*
Restart=always
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now redis_exporter
# 验证指标输出
curl -s http://127.0.0.1:9121/metrics | grep -E '^redis_(up|memory_used_bytes|connected_clients)'
| 常用启动参数 | 作用 |
|---|---|
--redis.addr |
Redis 地址,多实例可用逗号分隔,或用 --redis.addr= 留空 + 通过 target 参数动态指定 |
--redis.password / REDIS_PASSWORD |
密码(优先用环境变量) |
--redis.user |
ACL 用户名(Redis 6.0+) |
--web.listen-address |
暴露指标的监听地址,默认 :9121 |
--include-system-metrics |
额外采集所在主机的系统指标 |
--check-keys |
指定 key 模式,导出这些 key 的大小与内存占用(redis_key_size),注意模式过宽会引入性能开销 |
--check-key-groups |
按前缀分组统计 key 数量与内存 |
--is-tile38 / --is-cluster |
特殊模式 |
--skip-tls-verification |
TLS 环境下跳过证书校验(仅测试用) |
--script |
执行自定义 Lua 脚本采集业务指标 |
# ===== 2) Prometheus 抓取配置 /etc/prometheus/prometheus.yml =====
scrape_configs:
- job_name: 'redis'
scrape_interval: 15s
static_configs:
- targets:
- '192.168.1.10:9121'
- '192.168.1.11:9121'
labels:
env: 'prod'
# 【多实例方案】一个 exporter 采集多个 Redis(通过 target 参数转发)
- job_name: 'redis-multi'
static_configs:
- targets:
- 'redis://192.168.1.10:6379'
- 'redis://192.168.1.11:6379'
- 'redis://192.168.1.12:7001'
metrics_path: /scrape
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: 192.168.1.100:9121 # exporter 的地址
# ===== 3) 告警规则 /etc/prometheus/rules/redis.yml =====
groups:
- name: redis
rules:
- alert: RedisDown
expr: redis_up == 0
for: 1m
labels: { severity: critical }
annotations:
summary: "Redis 实例 {{ $labels.instance }} 不可用"
- alert: RedisMemoryHigh
expr: redis_memory_used_bytes / redis_memory_max_bytes * 100 > 85
for: 5m
labels: { severity: warning }
annotations:
summary: "{{ $labels.instance }} 内存使用率 {{ $value | printf \"%.1f\" }}%"
- alert: RedisLowHitRate
expr: |
rate(redis_keyspace_hits_total[5m])
/ (rate(redis_keyspace_hits_total[5m]) + rate(redis_keyspace_misses_total[5m])) * 100 < 85
for: 10m
labels: { severity: warning }
annotations:
summary: "{{ $labels.instance }} 命中率仅 {{ $value | printf \"%.1f\" }}%"
- alert: RedisEvictingKeys
expr: rate(redis_evicted_keys_total[5m]) * 60 > 100
for: 5m
labels: { severity: warning }
annotations:
summary: "{{ $labels.instance }} 每分钟淘汰 {{ $value | printf \"%.0f\" }} 个 key,内存不足"
- alert: RedisRdbSaveFailed
expr: redis_rdb_last_bgsave_status != 1
for: 1m
labels: { severity: critical }
annotations:
summary: "{{ $labels.instance }} RDB 保存失败,写命令将返回 MISCONF"
- alert: RedisReplicationBroken
expr: redis_master_link_up == 0
for: 1m
labels: { severity: critical }
annotations:
summary: "{{ $labels.instance }} 主从复制中断"
- alert: RedisRejectedConnections
expr: rate(redis_rejected_connections_total[5m]) > 0
for: 2m
labels: { severity: critical }
annotations:
summary: "{{ $labels.instance }} 连接数已打满,正在拒绝新连接"
- alert: RedisFragmentationHigh
expr: redis_memory_fragmentation_ratio > 1.5
for: 15m
labels: { severity: warning }
annotations:
summary: "{{ $labels.instance }} 碎片率 {{ $value | printf \"%.2f\" }}"
- alert: RedisUsingSwap
expr: redis_memory_fragmentation_ratio < 1
for: 5m
labels: { severity: critical }
annotations:
summary: "{{ $labels.instance }} 碎片率 < 1,内存已被换出到 swap"
# ===== 4) Grafana 面板 =====
# 直接导入官方社区面板 ID:
# 763 Redis Dashboard for Prometheus Redis Exporter 1.x(最常用)
# 11835 Redis Dashboard(含集群视图)
# 14091 Redis Cluster Dashboard
# 操作:Grafana → Dashboards → Import → 填入 ID → 选择 Prometheus 数据源
| 监控方案 | 优点 | 适用场景 |
|---|---|---|
| Zabbix | 一体化(采集+存储+告警+通知)、官方模板开箱可用、适合传统运维体系、对主机与网络设备监控更强 | 已有 Zabbix 体系、实例数量中等、团队熟悉 Zabbix |
| Prometheus + Grafana | 指标维度丰富、PromQL 灵活、图表美观、天然适配容器与 Kubernetes、服务发现自动化 | 云原生环境、大规模实例、需要自定义复杂告警表达式 |
redis-cli --stat |
零部署、实时 | 临时排障,不能持久化历史数据 |
| 云厂商托管监控 | 免运维、与实例深度集成 | 使用云数据库 Redis 版时 |
# 【临时排障】redis-cli --stat:一行一秒的实时概览
redis-cli --stat
# ------- data ------ --------------------- load -------------------- - child -
# keys mem clients blocked requests connections
# 1523400 2.00G 128 0 892341521 (+1258) 1052341
# ↑key数 ↑内存 ↑连接 ↑阻塞 ↑累计命令(+本秒增量) ↑累计连接
redis-cli --stat -i 2 # 每 2 秒刷新一次
11.13 常用运维命令
11.13.1 DBSIZE:查看 key 数量
# 当前 db 的 key 总数(O(1),直接读计数器,不遍历,可放心在生产使用)
redis-cli DBSIZE
# (integer) 1523400
# 指定 db
redis-cli -n 1 DBSIZE
# 集群模式下 DBSIZE 只返回【当前连接节点】的 key 数,需遍历所有主节点求和
for p in 7001 7002 7003; do
echo -n "7001..$p: "; redis-cli -p $p DBSIZE
done
# 或用集群工具直接看全局分布
redis-cli --cluster call 127.0.0.1:7001 DBSIZE
DBSIZE与KEYS *的区别:DBSIZE是 O(1)*,直接读取字典中维护的元素计数;`KEYS ` 是 O(N) 全量遍历。只想知道数量就用DBSIZE。** 注意DBSIZE包含"已过期但还没被清理"的 key,与实际可访问的 key 数可能有微小差异。
11.13.2 CLIENT:客户端连接管理
# 列出所有客户端连接
redis-cli CLIENT LIST
id=1024 addr=192.168.1.20:51234 laddr=192.168.1.10:6379 fd=8 name=order-svc age=3600 idle=0 flags=N db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=26 qbuf-free=20448 argv-mem=10 multi-mem=0 tot-net-in=1024 tot-net-out=2048 rbs=1024 rbp=0 obl=0 oll=0 omem=0 tot-mem=20512 events=r cmd=get user=app-order redir=-1 resp=2 lib-name=jedis lib-ver=4.4.3
id=1025 addr=192.168.1.11:52345 laddr=192.168.1.10:6379 fd=9 name= age=86400 idle=0 flags=S db=0 ... cmd=psync user=default
id=1026 addr=127.0.0.1:53456 laddr=127.0.0.1:6379 fd=10 name= age=5 idle=5 flags=O db=0 ... cmd=monitor user=default
| 字段 | 含义 |
|---|---|
id |
连接的唯一自增 ID,CLIENT KILL ID 用它精确定位 |
addr |
客户端地址 IP:端口。排查"是哪台机器在搞事"的关键字段 |
laddr |
本端(服务端)地址(6.2+),多网卡时可看出走的哪块网卡 |
fd |
该连接占用的文件描述符编号 |
name |
客户端自设的名字(通过 CLIENT SETNAME)。强烈建议应用设置,排障时能立刻知道是哪个服务 |
age |
连接已建立的总秒数。用连接池的应用这个值会很大(长连接),很小说明是新建连接 |
idle |
连接空闲秒数(多久没发命令)。idle 很大的连接是清理候选;超过 timeout 配置会被自动断开 |
flags |
连接类型:N=普通客户端、S=从库、M=主库(本机是从库时)、O=正在执行 MONITOR、x=在事务中、b=被阻塞、d=脏事务(WATCH 的 key 被改过)、c=回复后即关闭、u=不可被淘汰、t=开启了 tracking、A=尽快关闭 |
db |
当前选中的数据库编号 |
sub / psub / ssub |
订阅的频道数 / 模式数 / 分片频道数 |
multi |
事务中已排队的命令数,-1 表示不在事务中 |
watch |
WATCH 的 key 数量(6.2+) |
qbuf / qbuf-free |
查询(输入)缓冲区已用 / 剩余字节数。qbuf 很大说明该客户端发了超大命令 |
obl / oll / omem |
输出缓冲区:obl=固定缓冲区已用字节、oll=输出链表长度(对象个数)、omem=输出缓冲区占用的内存字节数。omem 很大是"内存被客户端缓冲区吃掉"的直接证据,常见于慢消费的订阅端、执行 MONITOR 的连接、拉大结果集的客户端 |
tot-mem |
该连接占用的总内存 |
tot-net-in / tot-net-out |
该连接累计收发字节数(6.2+),可找出流量大户 |
cmd |
该连接最近执行的命令。这是定位"谁在执行 KEYS / MONITOR / 慢命令"的最直接字段 |
user |
该连接认证使用的 ACL 用户名(6.0+) |
resp |
使用的协议版本 2 或 3(7.0+) |
lib-name / lib-ver |
客户端库名称与版本(7.2+),排查客户端 Bug 时有用 |
# ===== CLIENT 子命令 =====
# 查看当前连接自身的信息(6.2+)
redis-cli CLIENT INFO
# 获取/设置当前连接的名字
redis-cli CLIENT GETNAME
redis-cli CLIENT SETNAME my-debug-session
# 获取当前连接的 ID
redis-cli CLIENT ID
# 只看某类连接(6.2+):normal / master / replica / pubsub
redis-cli CLIENT LIST TYPE replica
redis-cli CLIENT LIST TYPE pubsub
# 按 ID 过滤(6.2+)
redis-cli CLIENT LIST ID 1024 1025
# 统计连接来源分布(找出连接大户)
redis-cli CLIENT LIST | awk '{print $2}' | cut -d= -f2 | cut -d: -f1 | sort | uniq -c | sort -rn
# 找出输出缓冲区占用最大的连接(内存被谁吃了)
redis-cli CLIENT LIST | tr ' ' '\n' | grep -E '^(id|addr|omem|cmd)=' | paste - - - - | sort -t= -k3 -rn | head -10
# 找出空闲时间最长的连接
redis-cli CLIENT LIST | awk '{for(i=1;i<=NF;i++) if($i ~ /^idle=/) {split($i,a,"="); print a[2], $2, $NF}}' | sort -rn | head -10
# 找出正在执行 MONITOR 的连接(必须及时杀掉)
redis-cli CLIENT LIST | grep 'cmd=monitor'
# ===== CLIENT KILL:断开客户端连接 =====
# 【旧语法】按 addr 断开(3.2 之前只支持这一种)
redis-cli CLIENT KILL 192.168.1.20:51234
# 【新语法,2.8.12+】支持多种过滤条件,返回被杀的连接数
redis-cli CLIENT KILL ID 1024 # 按连接 ID(最精确)
redis-cli CLIENT KILL ADDR 192.168.1.20:51234 # 按客户端地址
redis-cli CLIENT KILL LADDR 192.168.1.10:6379 # 按本端地址(6.2+)
redis-cli CLIENT KILL TYPE normal # 按类型:normal/master/replica/pubsub
redis-cli CLIENT KILL USER app-order # 按 ACL 用户(6.0+)
redis-cli CLIENT KILL SKIPME yes # 是否跳过当前连接(默认 yes)
redis-cli CLIENT KILL MAXAGE 3600 # 杀掉存活超过 N 秒的连接(7.4+)
# 组合条件(AND 关系):杀掉 app-order 用户的所有普通连接
redis-cli CLIENT KILL TYPE normal USER app-order
# (integer) 12 ← 返回被断开的连接数
# 【典型场景】干掉执行 MONITOR 忘记退出的连接
ID=$(redis-cli CLIENT LIST | grep 'cmd=monitor' | awk '{print $1}' | cut -d= -f2)
[ -n "$ID" ] && redis-cli CLIENT KILL ID $ID
# 【典型场景】干掉 omem 过大的慢消费客户端(正在吃内存)
redis-cli CLIENT LIST | awk -F'[ =]' '{for(i=1;i<=NF;i++) if($i=="omem" && $(i+1)>10485760) print}'
⚠️
CLIENT KILL会让被断开的客户端收到连接重置错误。多数客户端库有自动重连机制,影响很小;但正在执行事务或阻塞命令的连接被杀会导致业务报错。生产环境优先精确到ID,不要用TYPE normal一把全杀。
# ===== CLIENT PAUSE:临时暂停处理客户端命令 =====
redis-cli CLIENT PAUSE 5000 # 暂停 5000 毫秒(所有命令都被挂起)
redis-cli CLIENT PAUSE 5000 WRITE # 只暂停写命令(6.2+),读仍可用
redis-cli CLIENT PAUSE 5000 ALL # 暂停全部(默认行为)
redis-cli CLIENT UNPAUSE # 立即解除暂停(6.2+)
# 【用途】手动主从切换时短暂冻结写入,等从库追平偏移量后再切,避免数据丢失
# ===== CLIENT NO-EVICT:保护连接不被内存压力驱逐(Redis 7.0+)=====
# Redis 7.0 引入 maxmemory-clients:当所有客户端缓冲区总占用超过该值时,
# Redis 会【主动断开占用内存最多的客户端】以自保(对应 INFO 的 evicted_clients)
redis-cli CONFIG SET maxmemory-clients 1gb # 也可写百分比如 5%,0 表示不限制
# 把【当前连接】标记为不可被驱逐 —— 保护关键的运维/监控连接
redis-cli CLIENT NO-EVICT on
redis-cli CLIENT NO-EVICT off
# ===== CLIENT NO-TOUCH:读取 key 时不更新其 LRU/LFU 信息(Redis 7.2+)=====
# 【用途】运维排查时读取 key 不会污染淘汰算法的热度统计
redis-cli CLIENT NO-TOUCH on
redis-cli CLIENT NO-TOUCH off
# ===== CLIENT UNPAUSE / RESET =====
redis-cli CLIENT RESET # 重置连接状态(退出事务、取消订阅、恢复 default 用户,6.2+)
版本说明:
CLIENT NO-EVICT与maxmemory-clients是 Redis 7.0+ 的特性;CLIENT NO-TOUCH是 7.2+;CLIENT INFO、CLIENT UNPAUSE、CLIENT KILL LADDR是 6.2+;CLIENT KILL USER是 6.0+(依赖 ACL)。老版本上只能用CLIENT LIST+CLIENT KILL ADDR。
11.13.3 MEMORY:内存分析命令族
# ===== MEMORY USAGE:查看单个 key 占用的内存(字节,Redis 4.0+)=====
redis-cli MEMORY USAGE user:1001
# (integer) 128
# 返回值包含:key 本身的字符串 + value 的数据结构 + 字典条目的元数据开销
# 对集合类型只采样部分元素估算(默认采样 5 个),可指定采样数
redis-cli MEMORY USAGE big:hash SAMPLES 100 # 采样 100 个字段,更准
redis-cli MEMORY USAGE big:hash SAMPLES 0 # 0 = 全量精确计算(【大 key 上会阻塞】)
# 【批量找大 key】遍历指定前缀,按内存排序
redis-cli --scan --pattern 'cache:*' | head -1000 | while read k; do
echo "$(redis-cli MEMORY USAGE "$k") $k"
done | sort -rn | head -20
# ===== MEMORY DOCTOR:内存健康诊断(Redis 4.0+)=====
redis-cli MEMORY DOCTOR
# 【健康时的输出】
Sam, I detected a few issues in this Redis instance memory implants:
* Peak memory: In the past this instance used more than 150% the memory that is
currently using. The allocator is normally not able to release memory after a
peak, so you can expect to see a big fragmentation ratio, however this is
actually harmless and is only due to the memory peak, and if the Redis
instance Resident Set Size (RSS) is currently bigger than expected, the
allocator will be able to reuse memory later.
* High allocator fragmentation: This instance has an allocator external
fragmentation greater than 1.1. This problem is usually due to a large peak
memory (check if there is a peak memory entry above in the report)...
I'm here just for mental support buddy!
# 【完全健康时】
Hi Sam, I can't find any memory issue in your instance. I can only account for
what occurs on this base.
# 【数据量太小无法诊断时】
Hi Sam, this instance is empty or is using very little memory, my engines are
not powerful enough to draw any conclusion.
MEMORY DOCTOR 会检查这些问题:内存峰值远高于当前值、碎片率过高、RSS 与 used_memory 差距过大、客户端输出缓冲区占用过多、复制积压缓冲区过大、Lua 脚本缓存过多。它给的是方向性建议,具体数值还要看 INFO memory。
# ===== MEMORY STATS:详细内存分布(Redis 4.0+)=====
redis-cli MEMORY STATS
1) "peak.allocated" 2) (integer) 3221225472 # 分配器历史峰值
3) "total.allocated" 4) (integer) 2147483648 # 当前已分配总量
5) "startup.allocated" 6) (integer) 786432 # 启动基础开销
7) "replication.backlog" 8) (integer) 134217728 # 复制积压缓冲区
9) "clients.slaves" 10) (integer) 17408 # 从库客户端缓冲区
11) "clients.normal" 12) (integer) 1671168 # 普通客户端缓冲区
13) "cluster.links" 14) (integer) 0
15) "aof.buffer" 16) (integer) 0 # AOF 缓冲区
17) "lua.caches" 18) (integer) 37888 # Lua 脚本缓存
19) "functions.caches" 20) (integer) 184
21) "db.0" 22) 1) "overhead.hashtable.main" 2) (integer) 82616000
3) "overhead.hashtable.expires" 4) (integer) 45440000
5) "overhead.hashtable.slot-to-keys" 6) (integer) 0
23) "overhead.db.hashtable.lut" 24) (integer) 1024
25) "overhead.total" 26) (integer) 264921088 # 非数据开销总计
27) "keys.count" 28) (integer) 1523400 # key 总数
29) "keys.bytes-per-key" 30) (integer) 1234 # 【重点】平均每 key 字节数
31) "dataset.bytes" 32) (integer) 1882562560 # 纯数据字节数
33) "dataset.percentage" 34) "98.76" # 数据占比
35) "peak.percentage" 36) "66.67"
37) "allocator.allocated" 38) (integer) 2150000000
39) "allocator.active" 40) (integer) 2200000000
41) "allocator.resident" 42) (integer) 2400000000
43) "allocator-fragmentation.ratio" 44) "1.02"
45) "allocator-fragmentation.bytes" 46) (integer) 50000000
47) "allocator-rss.ratio" 48) "1.09"
49) "rss-overhead.ratio" 50) "1.00"
51) "fragmentation" 52) "1.13" # 总碎片率
53) "fragmentation.bytes" 54) (integer) 268435456
| 关注项 | 用途 |
|---|---|
keys.bytes-per-key |
平均每 key 占用字节数,容量规划的直接依据。异常大说明有大 key |
overhead.hashtable.main / .expires |
主字典与过期字典的元数据开销。key 数量极多时这部分会很可观(每个 key 约 50~100 字节的字典开销),是"key 太多太碎"的量化证据 |
clients.normal / clients.slaves |
客户端缓冲区占用。异常大要用 CLIENT LIST 找 omem 大的连接 |
replication.backlog |
复制积压缓冲区实际占用,与 repl-backlog-size 配置对应 |
dataset.percentage |
数据占比。偏低(如 < 80%)说明开销吃掉了太多内存 |
# 让 jemalloc 尽量把空闲页归还操作系统(Redis 4.0+)
redis-cli MEMORY PURGE
# 查看内存分配器的详细统计(jemalloc 原生输出,非常长)
redis-cli MEMORY MALLOC-STATS | head -40
11.13.4 LATENCY:延迟监控命令族
# ===== 前置配置:必须先设置延迟监控阈值,否则不会采集任何数据 =====
# 单位毫秒。0 表示【关闭延迟监控】(这是默认值)
# 建议设为 100(只记录超过 100ms 的事件),排障时可临时调到 10
latency-monitor-threshold 100
# 运行时开启(无需重启)
redis-cli CONFIG SET latency-monitor-threshold 100
redis-cli CONFIG GET latency-monitor-threshold
# ===== LATENCY LATEST:各事件的最新与最大延迟 =====
redis-cli LATENCY LATEST
1) 1) "command" # 事件名称
2) (integer) 1756000000 # 最近一次发生的 Unix 时间戳
3) (integer) 350 # 【最近一次】的延迟(毫秒)
4) (integer) 1520 # 【历史最大】延迟(毫秒)
2) 1) "fork"
2) (integer) 1755999000
3) (integer) 180
4) (integer) 620
3) 1) "expire-cycle"
2) (integer) 1755998500
3) (integer) 120
4) (integer) 210
| 事件名 | 含义与常见原因 |
|---|---|
command |
单条命令执行耗时超阈值。最常见,说明有慢命令(大 key 操作、KEYS、复杂 Lua) |
fast-command |
本应是 O(1)/O(log N) 的"快命令"也超了阈值。这更严重,通常说明系统层面有问题(swap、CPU 争抢、内存带宽饱和) |
fork |
fork 子进程耗时(BGSAVE/BGREWRITEAOF)。与内存量成正比,虚拟机上更慢 |
expire-cycle |
过期 key 清理周期耗时长。说明有大量 key 同时到期(见第十二章的过期抖动问题) |
eviction-cycle / eviction-del |
淘汰周期与淘汰删除耗时。说明内存不足正在大量淘汰 |
aof-write / aof-write-alone / aof-write-active-child / aof-write-pending-fsync |
AOF 写盘相关阻塞。指向磁盘 I/O 瓶颈。aof-write-active-child 表示写 AOF 时正好有子进程在跑(可尝试 no-appendfsync-on-rewrite yes) |
aof-fsync-always |
appendfsync always 导致的每次写都 fsync 阻塞。改为 everysec |
aof-rename / aof-rewrite-diff-write |
AOF 重写收尾阶段的阻塞 |
rdb-unlink-temp-file |
删除 RDB 临时文件的阻塞(大文件删除慢) |
active-defrag-cycle |
主动碎片整理周期耗时。可调低 active-defrag-cycle-max |
hz-timer / while-blocked-cron |
后台定时任务耗时 |
# ===== LATENCY HISTORY:某事件的历史采样时间序列 =====
redis-cli LATENCY HISTORY command
1) 1) (integer) 1755999100 # 时间戳
2) (integer) 120 # 延迟(毫秒)
2) 1) (integer) 1755999400
2) (integer) 350
3) 1) (integer) 1756000000
2) (integer) 280
# 最多保留 160 个采样点,用于观察【是持续性的还是偶发尖刺】
redis-cli LATENCY HISTORY fork
redis-cli LATENCY HISTORY expire-cycle
# ===== LATENCY RESET:清空延迟采样数据 =====
redis-cli LATENCY RESET # 清空所有事件
# (integer) 3 ← 返回被清空的事件数
redis-cli LATENCY RESET command fork # 只清空指定事件
# 【用途】排障时先 RESET,做完一次压测或等待一段时间再看,避免历史数据干扰
# ===== LATENCY DOCTOR:延迟诊断报告(带处理建议)=====
redis-cli LATENCY DOCTOR
Dave, I have observed the system, and here is my advice:
1. command: 12 latency spikes (average 285ms, mean deviation 95ms,
period 45.00 sec). Worst all time event 1520ms.
I have a few advices for you:
- Check your Slow Log to understand what are the commands you are running
which are too slow to execute. Please check
https://redis.io/commands/slowlog for more information.
- Deleting, expiring or evicting (because of maxmemory policy) large objects
is a blocking operation. If you have very large objects that are often
deleted, consider using the UNLINK command instead of DEL.
- The fork() system call is slow. Check the latest_fork_usec field in INFO.
# 【无延迟事件时】
Dave, no latency spike was observed during the lifetime of this Redis instance,
not in the slightest. I honestly think you ought to sit down calmly, take a
stress pill, and think things over.
# ===== LATENCY GRAPH:ASCII 延迟图(直观看趋势)=====
redis-cli LATENCY GRAPH command
command - high 1520 ms, low 85 ms (all time high 1520 ms)
--------------------------------------------------------------------------------
#
_ ||
_ _ || |||
_ _ || || _ ||| ||||
_____|||_|||_________|||_______|||_______|||________||||_______|||||
11109887766554433221100
mmmmmmmmmmmmmmmmmmmmmss
--latency与LATENCY命令族的区别:redis-cli --latency是客户端侧测量(不停发PING测往返时间,包含网络延迟);LATENCY LATEST/HISTORY是服务端侧记录(Redis 自己感知到的内部阻塞事件,不含网络)。两者要结合看:客户端延迟高但服务端无事件 → 问题在网络或客户端;两边都高 → 问题在 Redis 内部。
11.13.5 SLOWLOG:慢查询日志
# ===== 配置 =====
# 慢查询阈值(微秒)。默认 10000 = 10 毫秒
# 0 记录所有命令(【仅用于极短时间的调试】,会严重影响性能)
# < 0 完全关闭慢查询日志
# 建议生产设为 10000(10ms),要求严格的场景设 5000(5ms)
slowlog-log-slower-than 10000
# 慢查询日志最多保留的条数(超出后丢弃最老的)。每条会占用少量内存
slowlog-max-len 128
# 运行时调整(无需重启,排障常用)
redis-cli CONFIG SET slowlog-log-slower-than 5000
redis-cli CONFIG SET slowlog-max-len 1000
redis-cli CONFIG GET slowlog-*
# ===== SLOWLOG GET:读取慢查询记录 =====
redis-cli SLOWLOG GET # 默认返回 10 条
redis-cli SLOWLOG GET 20 # 返回最近 20 条
redis-cli SLOWLOG GET -1 # 返回全部(6.2+)
1) 1) (integer) 128 # ① 慢日志唯一自增 ID
2) (integer) 1756000000 # ② 命令执行的 Unix 时间戳
3) (integer) 1520000 # ③ 【执行耗时,微秒】= 1.52 秒
4) 1) "KEYS" # ④ 命令与参数数组
2) "user:*"
5) "192.168.1.20:51234" # ⑤ 客户端地址(4.0+)
6) "report-svc" # ⑥ 客户端名称(4.0+)
2) 1) (integer) 127
2) (integer) 1755999800
3) (integer) 85000 # 85 毫秒
4) 1) "HGETALL"
2) "big:hash:1001"
5) "192.168.1.21:52345"
6) ""
3) 1) (integer) 126
2) (integer) 1755999700
3) (integer) 42000
4) 1) "LRANGE"
2) "queue:log"
3) "0"
4) "-1" # 【问题】LRANGE 0 -1 取全量
5) "192.168.1.22:53456"
6) "worker-1"
| 字段 | 说明 |
|---|---|
| ① ID | 自增唯一 ID。监控系统可记录"上次读到的 ID",实现增量采集不重复告警 |
| ② 时间戳 | date -d @1756000000 转成可读时间,用于与业务事件对时 |
| ③ 耗时(微秒) | 只统计命令的实际执行时间,不包含网络传输时间与客户端排队等待时间 |
| ④ 命令与参数 | 参数超过 32 个时会截断为 ... (N more arguments);单个参数超过 128 字节时截断为 ...(M more bytes) |
| ⑤ 客户端地址 | 定位问题来源机器(4.0+) |
| ⑥ 客户端名称 | 通过 CLIENT SETNAME 设置的名字(4.0+),这是为什么建议应用一定要设名字 |
# ===== SLOWLOG LEN:当前慢日志条数 =====
redis-cli SLOWLOG LEN
# (integer) 42
# 【监控用法】采集这个值的增量:每分钟新增 > 10 条就告警
# ===== SLOWLOG RESET:清空慢日志 =====
redis-cli SLOWLOG RESET
# OK
# 【用途】排障时先清空,观察一段时间后再看,得到的都是当前问题
# ===== SLOWLOG HELP =====
redis-cli SLOWLOG HELP
# ===== 实用分析脚本 =====
# 1) 按耗时排序,输出可读格式
redis-cli --no-raw SLOWLOG GET 100 | paste - - - - - - | \
awk -F'\t' '{print $3, $4, $5, $6}' | sort -rn | head -20
# 2) 统计哪个命令出现在慢日志中最多
redis-cli SLOWLOG GET 128 | grep -A1 '^[0-9]*)' | grep -oE '"[A-Z]+"' | sort | uniq -c | sort -rn
# 3) 统计慢查询来源 IP 分布
redis-cli SLOWLOG GET 128 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+' | \
cut -d: -f1 | sort | uniq -c | sort -rn
# 4) 定时归档慢日志到文件(避免被新记录覆盖丢失)
cat > /usr/local/bin/redis_slowlog_archive.sh <<'EOF'
#!/bin/bash
export REDISCLI_AUTH='YourPassword'
LOG=/var/log/redis/slowlog-$(date +%Y%m%d).log
{ echo "===== $(date '+%F %T') ====="; redis-cli SLOWLOG GET -1; } >> $LOG
redis-cli SLOWLOG RESET
EOF
chmod +x /usr/local/bin/redis_slowlog_archive.sh
# 每 5 分钟归档一次
echo '*/5 * * * * /usr/local/bin/redis_slowlog_archive.sh' >> /var/spool/cron/root
慢日志的局限:① 只记录执行阶段,一个命令因为排在慢命令后面而等了 1 秒,它自己不会进慢日志(所以客户端感知的延迟可能远高于慢日志显示的);② 只保留
slowlog-max-len条,高并发下会被快速覆盖,必须定时归档;③ 重启后清空。排查延迟问题要把SLOWLOG+LATENCY+--latency-history+commandstats/latencystats四者结合看。
11.13.6 MONITOR:实时命令流(危险,慎用)
redis-cli MONITOR
OK
1756000000.123456 [0 192.168.1.20:51234] "GET" "user:1001"
1756000000.124001 [0 192.168.1.20:51234] "SET" "session:abc" "{...}" "EX" "1800"
1756000000.125100 [0 192.168.1.21:52345] "HGETALL" "product:2002"
1756000000.126200 [0 lua] "GET" "lock:order:3003"
# ↑时间戳 ↑db ↑客户端地址 ↑命令与参数
# 来自 Lua 脚本内部执行的命令,客户端标识显示为 lua
⚠️ MONITOR 的严重性能代价
MONITOR是 Redis 最危险的运维命令之一,绝不能长时间开着,更不能写进监控脚本。
代价 说明 吞吐下降 50% 以上 官方基准测试:单个 MONITOR客户端会使整体吞吐下降约 50%;开多个客户端时下降更多。原因是每条命令都要额外序列化成文本并推送给所有 MONITOR 连接输出缓冲区打爆内存 高 QPS 下输出速度远超网络发送速度,数据在服务端输出缓冲区堆积。 CLIENT LIST中该连接的omem会飞速上涨,可能直接把实例内存打满触发 OOM敏感数据泄露 输出包含所有命令的完整参数——业务数据、 AUTH之外的凭据、用户隐私全都在里面。落到日志文件里就是数据泄露事故磁盘被写满 redis-cli MONITOR > /tmp/mon.log在高 QPS 下每分钟可产生数 GB,很容易把磁盘写满,进而引发MISCONF无法被 SLOWLOG 记录 MONITOR自身不计入慢日志,问题不易被发现正确的使用姿势:
# 【铁律】必须限时!用 timeout 强制在 N 秒后退出 timeout 5 redis-cli MONITOR > /tmp/redis_monitor.txt # 或者限制行数,够了就断开 redis-cli MONITOR | head -2000 > /tmp/redis_monitor.txt # 【首选】在【从库】上执行,不影响主库业务 timeout 10 redis-cli -h 192.168.1.11 MONITOR | head -5000 > /tmp/mon.txt # 用完立即确认没有残留的 MONITOR 连接 redis-cli CLIENT LIST | grep 'cmd=monitor' # 有残留就立刻杀掉 ID=$(redis-cli CLIENT LIST | grep 'cmd=monitor' | awk '{print $1}' | cut -d= -f2) [ -n "$ID" ] && redis-cli CLIENT KILL ID $ID # 分析采样结果:统计命令频次 awk '{print $4}' /tmp/redis_monitor.txt | sort | uniq -c | sort -rn | head -20 # 找出被访问最多的 key(热 key 的粗略定位) awk '{print $5}' /tmp/redis_monitor.txt | sort | uniq -c | sort -rn | head -20 # 统计各客户端 IP 的请求量 awk '{print $3}' /tmp/redis_monitor.txt | tr -d '[]' | sort | uniq -c | sort -rn # 用完立刻删除采样文件(含敏感数据) shred -u /tmp/redis_monitor.txt替代方案(优先选择这些,不要动 MONITOR):
目的 替代命令 找慢命令 SLOWLOG GET+INFO commandstats找热 key redis-cli --hotkeys(需maxmemory-policy为 LFU)、OBJECT FREQ找大 key redis-cli --bigkeys/--memkeys看命令构成 INFO commandstats看延迟分布 INFO latencystats(7.0+)、LATENCY LATEST看实时概览 redis-cli --stat审计谁改了数据 键空间通知( notify-keyspace-events)+ 订阅生产环境建议直接
rename-command MONITOR MONITOR_随机串,防止有人误用。
11.13.7 DEBUG:调试命令
# ===== DEBUG SLEEP:让 Redis 主线程睡眠 N 秒(阻塞所有命令)=====
redis-cli DEBUG SLEEP 0 # 0 秒,用于测试连通性
redis-cli DEBUG SLEEP 3 # 【阻塞 3 秒!】期间所有其他客户端的命令全部挂起
# 【用途】① 验证客户端的超时与重试逻辑是否正确
# ② 模拟 Redis 卡顿,验证业务降级预案
# ③ 验证哨兵的故障判定(睡眠超过 down-after-milliseconds 会被判为主观下线)
# ⚠️ 【绝对不要在生产环境执行】,它就是一次人为制造的故障
# ===== DEBUG OBJECT:查看 key 的底层存储细节 =====
redis-cli DEBUG OBJECT user:1001
Value at:0x7f8b2c003a40 refcount:1 encoding:embstr serializedlength:24 lru:12345678 lru_seconds_idle:120
| 字段 | 含义 |
|---|---|
Value at |
value 在内存中的地址 |
refcount |
引用计数。共享整数对象(0~9999)的 refcount 会是很大的值(2147483647 表示共享对象) |
encoding |
底层编码方式:int/embstr/raw(string)、listpack/quicklist(list,7.0+)、ziplist/linkedlist(list,7.0 之前)、listpack/hashtable(hash,7.0+)、ziplist/hashtable(hash,7.0 之前)、intset/listpack/hashtable(set)、listpack/skiplist(zset)、stream |
serializedlength |
序列化后的长度(字节)。注意这不等于内存占用(它是压缩序列化后的大小,用 MEMORY USAGE 才能得到实际内存),但可用于粗略比较大小 |
lru |
LRU 时钟值 |
lru_seconds_idle |
该 key 空闲了多少秒(多久没被访问)。用于找冷数据 |
ql_nodes / ql_avg_node |
quicklist 的节点数与平均节点长度(list 类型专有) |
# 【推荐用 OBJECT 代替 DEBUG OBJECT】—— DEBUG 通常已被 rename-command 禁用
redis-cli OBJECT ENCODING user:1001 # 只看编码方式(最常用)
# "embstr"
redis-cli OBJECT REFCOUNT user:1001 # 引用计数
redis-cli OBJECT IDLETIME user:1001 # 空闲秒数(LRU 策略下有效)
redis-cli OBJECT FREQ user:1001 # 访问频率(4.0+,需 maxmemory-policy 为 *-lfu)
redis-cli OBJECT HELP
# 其他 DEBUG 子命令
redis-cli DEBUG JMAP # 无实际作用的占位命令
redis-cli DEBUG SET-ACTIVE-EXPIRE 0 # 关闭主动过期(仅测试用)
redis-cli DEBUG STRINGMATCH-LEN '*' key
redis-cli DEBUG QUICKLIST-PACKED-THRESHOLD 1k
redis-cli DEBUG JMAP
redis-cli DEBUG SEGFAULT # 【立即崩溃!】仅用于测试崩溃报告,生产禁用
redis-cli DEBUG CHANGE-REPL-ID # 修改复制 ID(会强制下次全量同步)
⚠️
DEBUG命令族在生产环境应当通过rename-command DEBUG ""直接禁用(见 10.2.6)。DEBUG SEGFAULT会让进程立刻崩溃,DEBUG SLEEP会阻塞整个实例,都是可以被用来做拒绝服务攻击的命令。日常排查用OBJECT命令族即可,能力基本覆盖。
11.13.8 redis-cli 的运维参数
redis-cli 自带一批运维专用参数,是排障与巡检的主力工具。
# ===== --bigkeys:扫描并统计各类型的最大 key(Redis 3.0.3+)=====
redis-cli --bigkeys
redis-cli -h 192.168.1.10 -a 'pwd' --no-auth-warning --bigkeys
redis-cli --bigkeys -i 0.1 # 每扫 100 个 key 休眠 0.1 秒,降低对生产的影响
# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).
[00.00%] Biggest string found so far '"config:global"' with 3 bytes
[12.50%] Biggest list found so far '"queue:log"' with 1520000 items
[25.00%] Biggest hash found so far '"user:profile:1001"' with 82000 fields
[50.00%] Biggest set found so far '"tags:all"' with 520000 members
[75.00%] Biggest zset found so far '"rank:global"' with 1200000 members
-------- summary -------
Sampled 1523400 keys in the keyspace!
Total key length in bytes is 24374400 (avg len 16.00)
Biggest string found '"config:global"' has 3 bytes
Biggest list found '"queue:log"' has 1520000 items ← 【严重大 key】
Biggest hash found '"user:profile:1001"' has 82000 fields ← 【严重大 key】
Biggest set found '"tags:all"' has 520000 members ← 【严重大 key】
Biggest zset found '"rank:global"' has 1200000 members ← 【严重大 key】
1420000 strings with 22720000 bytes (93.21% of keys, avg size 16.00)
12 lists with 1523400 items (00.00% of keys, avg size 126950.00)
100000 hashs with 8200000 fields (06.56% of keys, avg size 82.00)
120 sets with 520000 members (00.01% of keys, avg size 4333.33)
268 zsets with 1200000 members (00.02% of keys, avg size 4477.61)
输出解读要点:
| 要点 | 说明 |
|---|---|
底层用 SCAN,不阻塞 |
渐进式遍历,生产环境可以使用,但仍建议在业务低峰期跑,或加 -i 0.1 降速 |
| "大"的口径是"元素个数",不是内存 | 对 list/hash/set/zset,--bigkeys 统计的是元素数量(用 LLEN/HLEN/SCARD/ZCARD);只有 string 统计的是字节数(STRLEN)。一个 10 个字段但每个字段 10MB 的 hash,--bigkeys 不会认为它大 —— 这种情况要用 --memkeys |
| 每类型只报一个最大的 | 它只保留每种类型的冠军,不会列出所有大 key。要全量分析需自己写脚本 |
avg size 很有价值 |
平均元素数远高于预期,说明这类 key 普遍设计不当 |
| 大 key 的判定标准 | string > 10KB、集合类元素数 > 5000 就要关注;> 10000 必须治理。原因:单线程模型下,DEL/HGETALL/SMEMBERS 大 key 会阻塞数十甚至上百毫秒 |
# ===== --memkeys:按【实际内存占用】扫描最大 key(Redis 6.0+)=====
# 这是 --bigkeys 的补充:用 MEMORY USAGE 而不是元素个数来衡量
redis-cli --memkeys
redis-cli --memkeys-samples 0 # 精确计算(0=不采样,【大 key 上较慢】)
redis-cli --memkeys-samples 10 # 每个集合采样 10 个元素估算(默认 5)
[00.00%] Biggest string found so far '"html:page:home"' with 1048576 bytes
[25.00%] Biggest hash found so far '"user:profile:1001"' with 8388608 bytes
[50.00%] Biggest list found so far '"queue:log"' with 104857600 bytes ← 100MB!
-------- summary -------
Sampled 1523400 keys in the keyspace!
Total key length in bytes is 24374400 (avg len 16.00)
Biggest string found '"html:page:home"' has 1048576 bytes
Biggest hash found '"user:profile:1001"' has 8388608 bytes
Biggest list found '"queue:log"' has 104857600 bytes
# ===== --hotkeys:找出访问频率最高的 key(Redis 4.0+)=====
# 【前置条件】maxmemory-policy 必须是 allkeys-lfu 或 volatile-lfu,
# 否则 Redis 不维护访问频率信息,命令会直接报错
redis-cli CONFIG SET maxmemory-policy allkeys-lfu
redis-cli --hotkeys
# Scanning the entire keyspace to find hot keys as well as
# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).
[00.00%] Hot key '"product:hot:1001"' found so far with counter 255
[12.50%] Hot key '"config:global"' found so far with counter 254
[25.00%] Hot key '"session:token:abc"' found so far with counter 210
-------- summary -------
Sampled 1523400 keys in the keyspace!
hot key found with counter: 255 keyname: product:hot:1001
hot key found with counter: 254 keyname: config:global
hot key found with counter: 210 keyname: session:token:abc
| 要点 | 说明 |
|---|---|
counter 是 LFU 计数器,不是访问次数 |
LFU 计数器采用对数递增 + 定期衰减,最大值 255。多个 key 都到 255 时无法区分谁更热 |
counter 会衰减 |
由 lfu-decay-time(默认 1 分钟)控制衰减速度,所以它反映的是近期热度 |
| 报错处理 | 若报 ERR An LFU maxmemory policy is not selected,说明策略不对,需先切到 *-lfu |
| 配套命令 | OBJECT FREQ <key> 可查单个 key 的 LFU 计数器 |
| 替代方案 | 也可用短时 MONITOR 采样统计(见 11.13.6),或在客户端埋点统计 |
# LFU 相关配置
maxmemory-policy allkeys-lfu
lfu-log-factor 10 # 对数因子,越大计数器增长越慢(能区分更高频的访问)
lfu-decay-time 1 # 衰减时间(分钟),0 表示每次访问都衰减
# ===== --scan:渐进式遍历所有 key(KEYS 的安全替代)=====
redis-cli --scan # 遍历全部 key
redis-cli --scan | wc -l # 统计 key 总数(不如 DBSIZE 快,但可配合过滤)
redis-cli --scan --pattern 'user:*' # 按模式过滤
redis-cli --scan --pattern 'session:*' --count 1000 # 每次 SCAN 返回约 1000 个(默认 10)
redis-cli --scan --pattern 'cache:*' > /tmp/keys.txt
# 结合 TYPE 分类统计
redis-cli --scan | head -1000 | while read k; do redis-cli TYPE "$k"; done | sort | uniq -c
# 【批量删除的正确做法】用 --scan + xargs UNLINK(异步删除,不阻塞)
redis-cli --scan --pattern 'tmp:*' | xargs -L 100 redis-cli UNLINK
# UNLINK(4.0+)把实际释放内存的工作交给后台线程,比 DEL 安全得多
# 老版本只能用 DEL,删除大 key 时会阻塞:
# redis-cli --scan --pattern 'tmp:*' | xargs -L 100 redis-cli DEL
# 【注意】SCAN 的保证:
# · 保证:遍历开始前就存在、且遍历结束时仍存在的 key,一定会被返回至少一次
# · 不保证:不会重复返回(可能同一个 key 返回多次,处理逻辑要幂等)
# · 不保证:遍历过程中新增的 key 一定被返回
# ===== --stat:实时状态概览(见 11.12.2)=====
redis-cli --stat
redis-cli --stat -i 2
# ===== --latency:测量客户端到服务端的延迟 =====
redis-cli --latency
# min: 0, max: 3, avg: 0.19 (1427 samples)
# ↑最小 ↑最大 ↑平均(毫秒) ↑采样数
# 【判读】内网环境 avg 应 < 0.5ms;> 1ms 需排查;> 10ms 是严重问题
# 【注意】它测的是 PING 的完整往返时间,【包含网络延迟】,
# 所以要在【应用服务器上】执行才有意义,在 Redis 本机执行只测得本地回环
redis-cli -h 192.168.1.10 --latency # 从应用服务器测远端 Redis
# ===== --latency-history:分段输出延迟历史 =====
redis-cli --latency-history
# min: 0, max: 3, avg: 0.19 (1427 samples) -- 15.01 seconds range
# min: 0, max: 25, avg: 0.35 (1401 samples) -- 15.00 seconds range ← max 突增
# min: 0, max: 2, avg: 0.18 (1432 samples) -- 15.01 seconds range
redis-cli --latency-history -i 5 # 每 5 秒输出一段(默认 15 秒)
# 【用途】长时间挂着观察延迟波动,定位【周期性尖刺】。
# 若发现每隔一段时间 max 就飙高,对照 BGSAVE 时间点,通常是 fork 造成的
# ===== --latency-dist:延迟分布谱图(彩色 ASCII)=====
redis-cli --latency-dist
# 用不同颜色的字符展示延迟分布直方图,能直观看出是否有长尾
# ===== --intrinsic-latency:测量系统固有延迟(不连接 Redis)=====
redis-cli --intrinsic-latency 100 # 测试 100 秒
# Max latency so far: 1 microseconds.
# Max latency so far: 15 microseconds.
# Max latency so far: 3200 microseconds. ← 【系统层面就有 3.2ms 的抖动】
#
# 1085774 total runs (avg latency: 0.9211 microseconds / 921.10 nanoseconds per run).
# Worst run took 3474x longer than the average latency.
# 【用途】排除 Redis 自身因素,测量【操作系统与硬件本身】的调度抖动。
# 这个值就是 Redis 延迟的【理论下限】——如果它本身就有几毫秒,
# 说明问题在主机层面(CPU 争抢、虚拟化开销、透明大页、节能模式),
# 优化 Redis 配置是没用的。
# 【必须在 Redis 服务器本机上执行】,且该命令会占满一个 CPU 核
# ===== --rdb:远程备份 RDB 文件(在线热备)=====
redis-cli --rdb /backup/dump-$(date +%F-%H%M).rdb
redis-cli -h 192.168.1.10 -p 6379 -a 'pwd' --no-auth-warning \
--rdb /backup/redis-192.168.1.10-$(date +%F).rdb
SYNC sent to master, writing 1073741824 bytes to '/backup/dump-2026-08-22-1030.rdb'
Transfer finished with success.
| 要点 | 说明 |
|---|---|
| 原理 | redis-cli 伪装成一个从库,向主库发送 SYNC,主库执行 BGSAVE 并把 RDB 流式传回,客户端落盘 |
| 代价 | 会在主库触发一次 BGSAVE(fork),有 COW 内存膨胀与延迟尖刺的风险。大实例应在从库上执行 |
| 优势 | 无需登录服务器、无需关心 dir 配置、可直接备份到远程机器,是最方便的在线冷备手段 |
| 验证备份 | 备份完成后必须校验:redis-check-rdb /backup/dump.rdb |
| 常见报错 | Server closed the connection → 通常是 repl-timeout 太小或网络中断;Wrong signature trying to load DB from file → 传输不完整 |
# 备份 + 校验 + 压缩的完整流程
BAK=/backup/redis-$(date +%F-%H%M).rdb
redis-cli -h 192.168.1.11 --rdb $BAK # 在从库上做,不影响主库
redis-check-rdb $BAK # 校验完整性
gzip $BAK # 压缩归档
find /backup -name 'redis-*.rdb.gz' -mtime +7 -delete # 清理 7 天前的备份
# ===== --pipe:批量导入数据(极高吞吐)=====
# 【适用】初始化数据、缓存预热、大批量导入。比逐条发命令快 10~100 倍
# 【要求】输入必须是【RESP 协议格式】,不是普通命令文本
# 方式一:用 redis-cli 自己生成 RESP(最简单)
cat > /tmp/cmds.txt <<'EOF'
SET user:1 zhangsan
SET user:2 lisi
SET user:3 wangwu
EOF
cat /tmp/cmds.txt | redis-cli --pipe
# 【注意】纯文本命令也能被 --pipe 接受(内联命令格式),但每行必须是完整命令
# 方式二:用脚本生成标准 RESP(推荐,可靠且支持二进制安全)
python3 - <<'PY' > /tmp/data.resp
def resp(*args):
out = "*%d\r\n" % len(args)
for a in args:
a = str(a)
out += "$%d\r\n%s\r\n" % (len(a), a)
return out
import sys
for i in range(1, 1000001):
sys.stdout.write(resp("SET", "user:%d" % i, "value-%d" % i))
sys.stdout.write(resp("EXPIRE", "user:%d" % i, 86400))
PY
redis-cli --pipe < /tmp/data.resp
# 方式三:awk 生成
awk 'BEGIN{for(i=1;i<=100000;i++) printf "*3\r\n$3\r\nSET\r\n$%d\r\nk:%d\r\n$%d\r\nv:%d\r\n", length("k:"i), i, length("v:"i), i}' \
| redis-cli --pipe
All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 2000000
# ↑错误数(【必须是 0】) ↑服务端回复数
--pipe 要点 |
说明 |
|---|---|
| 性能 | 通过一次性发送大量命令、批量读取回复,绕过每条命令的往返延迟,可达每秒数十万条 |
errors 必须为 0 |
非 0 说明有命令执行失败(语法错、类型错、OOM),需检查输入数据 |
--pipe-timeout |
等待最后回复的超时秒数(默认 30),大批量导入时可调大:--pipe-timeout 120 |
| 内存冲击 | 一次导入千万级 key 会让内存瞬间暴涨,导入前先确认 maxmemory 足够,否则会触发大量淘汰 |
| 集群模式 | --pipe 不支持自动重定向,集群环境下需先按槽位把数据分组,再分别导入对应节点;或改用 --cluster import |
| 格式要求 | RESP 格式错误会导致协议错乱,之后所有命令都失败。生成脚本务必用 \r\n 而不是 \n |
# ===== 其他常用 redis-cli 参数 =====
redis-cli -h 192.168.1.10 -p 6379 -a 'pwd' -n 2 # -n 指定 db
redis-cli --user app-order --pass 'pwd' # ACL 用户名密码(6.0+)
redis-cli -u redis://user:pass@192.168.1.10:6379/0 # URI 形式连接
redis-cli --no-auth-warning # 抑制命令行传密码的告警
redis-cli -r 10 -i 1 INFO stats # 重复执行 10 次,间隔 1 秒
redis-cli -r 100 -i 0.1 PING # 简易压测
redis-cli -x SET mykey < /tmp/value.txt # 从标准输入读最后一个参数
redis-cli --raw GET mykey # 原始输出(【中文不转义,见第十二章】)
redis-cli --no-raw GET mykey # 强制转义输出
redis-cli --csv LRANGE mylist 0 -1 # CSV 格式输出
redis-cli --json GET mykey # JSON 格式输出(7.0+)
redis-cli --eval /path/script.lua key1 key2 , arg1 # 执行 Lua 脚本(注意逗号两边有空格)
redis-cli --cluster help # 集群管理子命令帮助(5.0+)
redis-cli --show-hints # 显示命令提示
redis-cli -t 5 PING # 连接/读写超时 5 秒
redis-cli --connect-timeout 3 -h 192.168.1.10 PING # 仅连接超时
11.14 数据迁移与容量规划
11.14.1 DUMP 与 RESTORE:单 key 序列化搬运
# DUMP:把 key 的值序列化为二进制字符串(含类型、编码、版本校验)
redis-cli DUMP user:1001
# "\x00\bzhangsan\x0b\x00\xb2\x9c\xf1\x8e\xd4\x9a\x1c\x8d"
# ↑类型 ↑数据 ↑RDB版本(2字节) + CRC64校验(8字节)
# RESTORE:从序列化数据还原 key
# 语法:RESTORE key ttl serialized-value [REPLACE] [ABSTTL] [IDLETIME s] [FREQ f]
redis-cli RESTORE user:1001:copy 0 "\x00\bzhangsan\x0b\x00\xb2\x9c\xf1\x8e\xd4\x9a\x1c\x8d"
# OK
| 参数 | 说明 |
|---|---|
ttl |
新 key 的过期时间(毫秒)。0 表示永不过期 |
REPLACE |
目标 key 已存在时覆盖(3.0+)。不加则报 BUSYKEY Target key name already exists |
ABSTTL |
ttl 参数按绝对 Unix 时间戳(毫秒)解释(5.0+),迁移时可保持原过期时刻 |
IDLETIME |
设置初始空闲时间(5.0+) |
FREQ |
设置初始 LFU 计数器(5.0+) |
# 【实用脚本】跨实例复制单个 key(含 TTL)
migrate_key() {
local key=$1 src=$2 dst=$3
local ttl=$(redis-cli -h $src PTTL "$key")
[ "$ttl" -lt 0 ] && ttl=0 # -1(无TTL) / -2(不存在) 都转成 0
local val=$(redis-cli -h $src --no-raw DUMP "$key")
redis-cli -h $dst RESTORE "$key" "$ttl" "$val" REPLACE
}
# 【注意】shell 中传递二进制数据容易出错(NUL 字节、转义问题),
# 生产环境请用 Python/Go 客户端处理,或直接用 MIGRATE 命令
# Python 版本(可靠)
python3 - <<'PY'
import redis
src = redis.Redis(host='192.168.1.10', port=6379, password='pwd')
dst = redis.Redis(host='192.168.1.20', port=6379, password='pwd')
for key in src.scan_iter(match='user:*', count=1000):
ttl = src.pttl(key)
data = src.dump(key)
if data:
dst.restore(key, ttl if ttl and ttl > 0 else 0, data, replace=True)
print("done")
PY
DUMP/RESTORE的版本兼容性:序列化数据里带有 RDB 版本号。目标实例的 RDB 版本必须 ≥ 源实例,否则RESTORE报ERR DUMP payload version or checksum are wrong。也就是说可以从低版本迁到高版本,反之不行。跨大版本迁移前务必先用一个 key 做验证。
11.14.2 MIGRATE:原子迁移到另一实例
MIGRATE 在服务端直接建立到目标实例的连接完成搬运,比 DUMP+RESTORE 更可靠、更快(无需把数据经过客户端)。
# 语法:
# MIGRATE host port key destination-db timeout [COPY] [REPLACE]
# [AUTH password | AUTH2 username password] [KEYS key [key ...]]
# ===== 迁移单个 key =====
redis-cli -h 192.168.1.10 MIGRATE 192.168.1.20 6379 user:1001 0 5000
# OK ← 成功
# NOKEY ← 源实例上不存在该 key
# ===== 迁移多个 key(3.0.6+)=====
# 注意:key 位置必须传空字符串 "",然后用 KEYS 指定多个 key
redis-cli -h 192.168.1.10 MIGRATE 192.168.1.20 6379 "" 0 5000 \
KEYS user:1001 user:1002 user:1003
# ===== 目标实例有密码时 =====
redis-cli -h 192.168.1.10 MIGRATE 192.168.1.20 6379 "" 0 5000 \
AUTH 'TargetPassword' KEYS user:1001 user:1002
# Redis 6.0+ 的 ACL 用户名+密码形式
redis-cli -h 192.168.1.10 MIGRATE 192.168.1.20 6379 "" 0 5000 \
AUTH2 app-order 'TargetPassword' KEYS user:1001
# ===== 批量迁移某前缀的所有 key =====
redis-cli -h 192.168.1.10 --scan --pattern 'user:*' | \
xargs -L 50 sh -c 'redis-cli -h 192.168.1.10 MIGRATE 192.168.1.20 6379 "" 0 5000 REPLACE KEYS "$@"' _
| 参数 | 说明 |
|---|---|
destination-db |
目标实例的 db 编号。集群模式必须为 0 |
timeout |
单次迁移的超时(毫秒)。大 key 需要调大,如 60000 |
COPY |
保留源实例上的 key(默认行为是迁移后删除源 key) |
REPLACE |
目标已存在同名 key 时覆盖。不加则报 BUSYKEY |
KEYS ... |
一次迁移多个 key(3.0.6+)。同一批 key 在集群模式下必须属于同一个槽 |
⚠️
MIGRATE是阻塞命令:迁移期间源实例与目标实例的主线程都被阻塞(直到该批 key 传输完成或超时)。迁移大 key 会造成明显卡顿,必须:① 小批量(每批 10~50 个 key);② 业务低峰执行;③ 大 key 单独处理并调大timeout;④ 迁移前用--bigkeys找出大 key。这也是集群 reshard 慢的根本原因。
11.14.3 主从切换法在线迁移(推荐的整实例迁移方案)
这是生产环境迁移整个实例的标准做法:把新实例作为旧实例的从库同步全部数据,追平后切换应用连接,几乎零停机。
# ===== 场景:把 192.168.1.10:6379 的数据迁移到 192.168.1.20:6379 =====
# ---------- 步骤 1:准备新实例 ----------
# 在 192.168.1.20 上部署 Redis,配置要点:
# · 版本 >= 源实例(RDB 向前兼容)
# · maxmemory 不小于源实例的 used_memory_peak
# · 与源实例一致的 requirepass / rename-command / ACL 配置
# · 内存足够容纳全量数据 + 同步期间的缓冲
redis-cli -h 192.168.1.20 -a 'pwd' --no-auth-warning PING
redis-cli -h 192.168.1.20 DBSIZE # 必须是 0,新实例要干净
# ---------- 步骤 2:调大源实例的复制相关参数,避免同步中断 ----------
redis-cli -h 192.168.1.10 CONFIG SET repl-backlog-size 256mb
redis-cli -h 192.168.1.10 CONFIG SET client-output-buffer-limit "replica 512mb 128mb 120"
redis-cli -h 192.168.1.10 CONFIG SET repl-timeout 600
# ---------- 步骤 3:让新实例成为旧实例的从库,开始同步 ----------
redis-cli -h 192.168.1.20 CONFIG SET masterauth 'SourcePassword'
redis-cli -h 192.168.1.20 REPLICAOF 192.168.1.10 6379
# Redis 5.0 之前用 SLAVEOF:
# redis-cli -h 192.168.1.20 SLAVEOF 192.168.1.10 6379
# ---------- 步骤 4:监控同步进度,等待追平 ----------
watch -n2 "redis-cli -h 192.168.1.20 INFO replication | grep -E 'master_link_status|master_sync_in_progress|master_sync_perc|slave_repl_offset'"
# 等待条件(三者必须同时满足):
# master_link_status:up
# master_sync_in_progress:0
# 主从偏移量差 ≈ 0
# 精确比对偏移量
MO=$(redis-cli -h 192.168.1.10 INFO replication | awk -F: '/^master_repl_offset:/{print $2}' | tr -d '\r')
SO=$(redis-cli -h 192.168.1.20 INFO replication | awk -F: '/^slave_repl_offset:/{print $2}' | tr -d '\r')
echo "偏移量差: $((MO - SO)) 字节"
# 比对 key 数量(应该一致)
redis-cli -h 192.168.1.10 DBSIZE
redis-cli -h 192.168.1.20 DBSIZE
# ---------- 步骤 5:切换(业务低峰期执行)----------
# 5.1 【可选但推荐】暂停源实例的写入,确保数据完全一致
redis-cli -h 192.168.1.10 CLIENT PAUSE 10000 WRITE # 6.2+ 支持只暂停写
# 5.2 确认偏移量完全一致
redis-cli -h 192.168.1.10 INFO replication | grep master_repl_offset
redis-cli -h 192.168.1.20 INFO replication | grep slave_repl_offset
# 5.3 把新实例提升为主库(断开复制关系,保留数据)
redis-cli -h 192.168.1.20 REPLICAOF NO ONE
# 【关键】REPLICAOF NO ONE 只断开复制,【不会清空数据】
# 5.4 确认新实例已是可写的主库
redis-cli -h 192.168.1.20 INFO replication | grep role
# role:master
redis-cli -h 192.168.1.20 SET migrate:test ok
# OK
# 5.5 切换应用配置指向新实例(改配置中心 / DNS / VIP,然后滚动重启应用)
# 若用了 VIP 或域名,只需切 VIP/DNS,应用无需改动,这是最平滑的方式
# ---------- 步骤 6:验证与善后 ----------
redis-cli -h 192.168.1.20 DBSIZE # key 数量对得上
redis-cli -h 192.168.1.20 INFO clients # 确认应用连接已切过来
redis-cli -h 192.168.1.10 INFO clients # 旧实例连接数应降为 0
# 抽样比对数据
for k in $(redis-cli -h 192.168.1.20 --scan | head -20); do
a=$(redis-cli -h 192.168.1.10 --no-raw GET "$k" 2>/dev/null)
b=$(redis-cli -h 192.168.1.20 --no-raw GET "$k" 2>/dev/null)
[ "$a" != "$b" ] && echo "不一致: $k"
done
# 观察 1~2 天确认无问题后,再下线旧实例
redis-cli -h 192.168.1.10 --rdb /backup/final-backup.rdb # 留一份最终备份
systemctl stop redis # 在旧机器上执行
| 优点 | 缺点 / 注意 |
|---|---|
| 几乎零停机,只在切换瞬间有秒级影响 | 需要新旧实例网络互通 |
| 数据完整,包含 TTL 与所有类型 | 同步期间源实例有 fork 与网络开销 |
| 可随时回滚(切换前旧实例数据完好) | 目标版本必须 ≥ 源版本 |
| 全程可观测(偏移量、key 数) | 切换后要确认应用连接确实迁移了 |
回滚预案:若切换后发现问题,可反向操作——让旧实例
REPLICAOF 192.168.1.20 6379追平新实例的增量,然后再切回。切换前务必确认旧实例还在、数据还在,不要急着停。
11.14.4 RDB 文件冷迁移
适用于允许停机、或新旧实例网络不通(只能靠文件拷贝)的场景。
# ---------- 源实例 ----------
# 1) 触发一次完整落盘(BGSAVE 后台执行,不阻塞)
redis-cli -h 192.168.1.10 BGSAVE
# 等待完成
while [ "$(redis-cli -h 192.168.1.10 INFO persistence | awk -F: '/^rdb_bgsave_in_progress:/{print $2}' | tr -d '\r')" = "1" ]; do
echo "BGSAVE 进行中..."; sleep 2
done
redis-cli -h 192.168.1.10 INFO persistence | grep -E 'rdb_last_bgsave_status|rdb_last_save_time'
# 必须确认 rdb_last_bgsave_status:ok
# 2) 找到 RDB 文件位置
redis-cli -h 192.168.1.10 CONFIG GET dir
redis-cli -h 192.168.1.10 CONFIG GET dbfilename
# 3) 校验并传输
redis-check-rdb /var/lib/redis/dump.rdb
scp /var/lib/redis/dump.rdb root@192.168.1.20:/tmp/
# ---------- 目标实例 ----------
# 4) 停止目标实例(【必须先停】,运行中替换 RDB 会在退出时被覆盖)
systemctl stop redis
# 5) 替换文件并修正权限(【最容易漏的一步】)
cp /tmp/dump.rdb /var/lib/redis/dump.rdb
chown redis:redis /var/lib/redis/dump.rdb
chmod 600 /var/lib/redis/dump.rdb
# 6) 【关键】若目标实例开启了 AOF,必须处理,否则启动时会优先加载 AOF 而忽略 RDB!
# 方案 A:先关 AOF 启动,加载 RDB 后再动态开 AOF
sed -i 's/^appendonly yes/appendonly no/' /etc/redis/redis.conf
systemctl start redis
redis-cli DBSIZE # 确认数据已加载
redis-cli CONFIG SET appendonly yes # 动态开启 AOF,会立即触发一次重写
redis-cli CONFIG REWRITE
# 方案 B(Redis 7.0+ 用 multi-part AOF):直接删除 appenddirname 目录后启动
# rm -rf /var/lib/redis/appendonlydir
# 7) 验证
redis-cli DBSIZE
redis-cli INFO keyspace
tail -30 /var/log/redis/redis.log
⚠️ 冷迁移最常见的三个坑:① 忘记
chown redis:redis→ 启动后无法读取或后续无法写入,触发MISCONF;② 目标实例开着 AOF → AOF 优先级高于 RDB,导入的 RDB 被完全忽略,DBSIZE为 0 让人百思不解;③ 在实例运行时替换 RDB 文件 → Redis 退出时会再写一次 RDB,把你刚拷进去的文件覆盖掉。
11.14.5 第三方迁移工具
| 工具 | 说明 |
|---|---|
| redis-shake | 阿里云开源,目前最主流的 Redis 迁移工具(Go 编写)。支持 sync(全量+增量实时同步)、scan(用 SCAN 逐 key 迁移)、restore(从 RDB 文件恢复)、dump(导出 RDB)等模式。支持单机 ↔ 集群 ↔ 云托管版之间任意方向迁移,支持跨版本、支持数据过滤(按 db、key 前缀、类型)。2.x 用 TOML 配置,1.x 用 conf 文件 |
| redis-port | 由 Codis 团队开发(Go 编写),功能类似:sync(实时同步)、dump(导出)、restore(导入)、decode(解析 RDB 为可读格式)。redis-shake 早期就是基于 redis-port 发展而来。目前维护活跃度低于 redis-shake |
| redis-migrate-tool | 唯品会开源(C 编写),支持从 Twemproxy/Codis/单机/集群迁移到集群,支持数据校验 |
| rdbtools | Python 工具,用于离线解析 RDB 文件:导出为 JSON/CSV、生成内存占用报告(可精确分析每个 key 的内存构成)。做容量分析非常好用,但不做迁移 |
| 云厂商 DTS | 阿里云 DTS、腾讯云 DTS 等,托管的数据传输服务,适合上云迁移,图形化操作、自带校验 |
# ===== redis-shake 2.x 使用示例 =====
wget https://github.com/tair-opensource/RedisShake/releases/download/v4.2.1/redis-shake-linux-amd64.tar.gz
tar xf redis-shake-linux-amd64.tar.gz
cd redis-shake-linux-amd64
# shake.toml —— 全量 + 增量实时同步(sync 模式)
[sync_reader]
cluster = false # 源端是否为集群
address = "192.168.1.10:6379"
username = "" # ACL 用户名(6.0+)
password = "SourcePassword"
tls = false
sync_rdb = true # 同步全量 RDB
sync_aof = true # 同步增量命令流
[redis_writer]
cluster = false # 目标端是否为集群
address = "192.168.1.20:6379"
username = ""
password = "TargetPassword"
tls = false
[advanced]
dir = "data"
ncpu = 4
log_level = "info"
log_file = "shake.log"
# 目标 key 已存在时的策略:rewrite(覆盖)/ skip(跳过)/ panic(报错退出)
# 在 [filter] 或 writer 段按版本配置
# shake.toml —— 用 SCAN 逐 key 迁移(scan 模式,适合云托管版不支持 PSYNC 时)
[scan_reader]
cluster = false
address = "192.168.1.10:6379"
password = "SourcePassword"
ksn = false # 是否同时订阅键空间通知捕获增量
count = 1000 # 每次 SCAN 的 COUNT
[redis_writer]
address = "192.168.1.20:6379"
password = "TargetPassword"
[filter]
allow_keys = [] # 只迁移指定 key
allow_key_prefix = ["user:", "order:"] # 按前缀过滤
block_key_prefix = ["tmp:", "temp:"] # 排除前缀
allow_db = [0] # 只迁移 db0
allow_command = []
block_command = []
# 启动迁移
./redis-shake shake.toml
# 观察进度(日志中会输出已同步的 key 数与偏移量)
tail -f shake.log
# sync_reader: read_count=1523400, entry_count=1523400, ...
# sync 模式会持续运行做增量同步,切换应用后再手动停止(Ctrl+C)
# ===== rdbtools 做内存分析 =====
pip install rdbtools python-lzf
# 生成每个 key 的内存报告(CSV)
rdb -c memory /var/lib/redis/dump.rdb --bytes 1024 -f /tmp/memory.csv
# database,type,key,size_in_bytes,encoding,num_elements,len_largest_element,expiry
head -20 /tmp/memory.csv
# 按内存排序找出最大的 key
sort -t, -k4 -rn /tmp/memory.csv | head -20
# 导出为 JSON
rdb --command json /var/lib/redis/dump.rdb > /tmp/dump.json
# 只看某个 db 与某个 key 模式
rdb -c memory dump.rdb -d 0 -k '^user:' -f /tmp/user_memory.csv
11.14.6 集群扩缩容与重分片
集群的扩容(--cluster add-node + --cluster reshard)、缩容(--cluster del-node)、自动均衡(--cluster rebalance)以及槽迁移的完整操作步骤与交互输出,详见第七章 7.8 节「增删节点与槽迁移(reshard)」。
# 常用命令速查(详细说明见 7.8)
redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7001 # 加主节点
redis-cli --cluster add-node 127.0.0.1:7008 127.0.0.1:7001 \
--cluster-slave --cluster-master-id <masterID> # 加从节点
redis-cli --cluster reshard 127.0.0.1:7001 # 手动重分片
redis-cli --cluster rebalance 127.0.0.1:7001 # 自动均衡
redis-cli --cluster del-node 127.0.0.1:7001 <nodeID> # 删节点(需先迁空槽)
redis-cli --cluster check 127.0.0.1:7001 # 体检
redis-cli --cluster fix 127.0.0.1:7001 # 修复槽状态不一致
redis-cli --cluster import 127.0.0.1:7001 --cluster-from 192.168.1.10:6379 # 从单机导入集群
集群扩缩容的关键提醒:①
reshard底层用MIGRATE,是阻塞式的,大 key 会造成明显卡顿,务必先用--bigkeys排查并在低峰执行;② 删除节点前必须先把它的槽全部迁走(--cluster reshard把 slots 迁到其他节点),否则会报is not empty;③ 每一步操作后都要--cluster check确认[OK] All 16384 slots covered;④ 缩容要先删从节点再删主节点。
11.14.7 不同版本间迁移的兼容性
| 迁移方向 | 可行性 | 说明 |
|---|---|---|
| 低版本 → 高版本 | ✅ 支持 | RDB 格式向前兼容,新版本能读旧版本的 RDB 与 DUMP 载荷 |
| 高版本 → 低版本 | ❌ 不支持 | 旧版本读新 RDB 会报 Can't handle RDB format version 11 或 DUMP payload version or checksum are wrong。只能用逐 key 迁移(SCAN + 按类型重建,如 redis-shake 的 scan 模式),且新版本独有的数据类型(Stream 需 5.0+、hash field TTL 需 7.4+)无法降级 |
| 主从复制方式迁移 | 要求目标 ≥ 源 | 从库版本低于主库时,PSYNC 可能失败或数据不兼容 |
跨大版本迁移的具体注意点:
| 版本变化 | 注意事项 |
|---|---|
| → 4.0 | RDB 版本升到 8;新增 UNLINK、MEMORY 命令族、activedefrag、LFU 策略、混合持久化(aof-use-rdb-preamble)。开启混合持久化后的 AOF 文件,4.0 之前的版本无法解析 |
| → 5.0 | 新增 Stream 类型(旧版本无法承接 Stream 数据);SLAVEOF 更名为 REPLICAOF(旧名仍可用);redis-trib.rb 被 redis-cli --cluster 取代 |
| → 6.0 | 新增 ACL、TLS、RESP3 协议、I/O 多线程。迁移后若启用 ACL,所有客户端配置都要检查是否支持用户名认证;sanitize-dump-payload 新增 |
| → 6.2 | CLIENT INFO/UNPAUSE、COPY 命令、GETDEL/GETEX、ZRANGESTORE、SMISMEMBER |
| → 7.0 | AOF 改为 multi-part AOF(appendonlydir 目录 + manifest 文件),AOF 文件结构完全变了,7.0 的 AOF 目录旧版本不认;ziplist 全面改为 listpack;新增 Functions、ACL 选择器、latencystats、CLIENT NO-EVICT |
| → 7.2 | CLIENT NO-TOUCH;部分命令行为微调 |
| → 7.4 / 8.0 | hash field 级 TTL(HEXPIRE 等);新的数据结构与性能优化 |
# 迁移前的兼容性检查清单
# 1) 比对版本
redis-cli -h 源 INFO server | grep redis_version
redis-cli -h 目标 INFO server | grep redis_version
# 2) 检查源实例是否使用了目标版本不支持的数据类型
redis-cli -h 源 --scan | head -5000 | while read k; do redis-cli -h 源 TYPE "$k"; done | sort -u
# 出现 stream 时,目标必须 >= 5.0
# 3) 用一个 key 做 DUMP/RESTORE 兼容性验证(最直接)
V=$(redis-cli -h 源 --no-raw DUMP anykey)
redis-cli -h 目标 RESTORE compat:test 0 "$V"
# 报 "DUMP payload version or checksum are wrong" 就说明版本不兼容
# 4) 检查配置项是否在目标版本存在(老参数可能被移除)
redis-cli -h 目标 CONFIG GET appendfsync
redis-cli -h 目标 CONFIG GET activedefrag
# 报 (empty array) 说明该版本没有这个参数
11.14.8 容量规划与内存估算
估算公式:
【基础估算】
总内存 ≈ key 数量 × (key 平均字节数 + value 平均字节数 + 单 key 固定开销)
其中【单 key 固定开销】约 50~100 字节,包括:
· redisObject 结构:16 字节
· 主字典 dictEntry:约 24~32 字节(key 指针 + value 指针 + next 指针)
· SDS 头部:3~17 字节(取决于长度)
· 过期字典 dictEntry(设了 TTL 时):额外约 24~32 字节
· 字典桶数组的摊销开销:约 8~16 字节
【实测法(推荐,比公式准)】
1) 在测试实例上写入 N 条真实业务数据(N 取 10 万以上)
2) used_memory_dataset ÷ N = 平均每 key 实际占用
3) 乘以预期 key 总量得到数据部分内存
【直接读取现有实例的统计值】
redis-cli MEMORY STATS | grep -A1 'keys.bytes-per-key'
# 实测法的完整操作
# 1) 准备一个干净的测试实例
redis-cli -n 15 FLUSHDB
B=$(redis-cli INFO memory | awk -F: '/^used_memory:/{print $2}' | tr -d '\r')
# 2) 写入 10 万条真实结构的数据
for i in $(seq 1 100000); do
echo "HSET user:$i name zhangsan$i age 28 city beijing email u$i@example.com"
done | redis-cli -n 15 --pipe
# 3) 计算
A=$(redis-cli INFO memory | awk -F: '/^used_memory:/{print $2}' | tr -d '\r')
awk "BEGIN{printf \"10万条占用: %.2f MB,平均每 key: %.1f 字节\n\", ($A-$B)/1048576, ($A-$B)/100000}"
# 4) 推算:若线上预期 5000 万 key
awk "BEGIN{printf \"5000万 key 预估: %.2f GB\n\", ($A-$B)/100000*50000000/1073741824}"
各数据类型的额外开销:
| 类型 | 小数据时的编码(省内存) | 超过阈值后的编码(开销大) | 额外开销说明 |
|---|---|---|---|
| String | int(8 字节,整数且 < 10000 时还会共享对象)embstr(≤ 44 字节,redisObject 与 SDS 连续分配,一次 malloc) |
raw(> 44 字节,两次 malloc) |
embstr → raw 的分界是 44 字节。value 一旦被 APPEND/SETRANGE 修改过,就永久变成 raw,即使长度很短 |
| Hash | listpack(7.0+)/ ziplist(7.0 前):紧凑连续存储,无指针开销,约每字段 (字段长+值长+3) 字节 |
hashtable:每字段约 50~90 字节开销 |
阈值:hash-max-listpack-entries 128(字段数)、hash-max-listpack-value 64(值长度)。超任一阈值就转 hashtable,内存可能暴涨 5~10 倍 |
| List | listpack(7.0+)/ ziplist(7.0 前) |
quicklist(ziplist 组成的双向链表) |
阈值:list-max-listpack-size 128。每个 quicklist 节点有约 11 字节头部 + 前后指针 |
| Set | intset(全为整数且 ≤ 512 个,最省内存,每元素仅 2/4/8 字节)listpack(7.2+,非整数小集合) |
hashtable:每元素约 50~70 字节 |
阈值:set-max-intset-entries 512、set-max-listpack-entries 128。只存整数的 set 用 intset 极省内存,混入一个非整数就退化 |
| ZSet | listpack(7.0+)/ ziplist(7.0 前):member 与 score 交替存放 |
skiplist + hashtable:每元素约 90~130 字节(跳表节点 + 字典条目) |
阈值:zset-max-listpack-entries 128、zset-max-listpack-value 64。ZSet 是内存开销最大的类型 |
| Bitmap | 底层就是 String | — | 每 bit 1/8 字节。SETBIT key 1000000 1 会直接分配 125KB(按最大偏移分配整段) |
| HyperLogLog | 稀疏表示(元素少时仅几十字节) | 稠密表示:固定 12KB | 基数超过约 3000 后转稠密,之后恒定 12KB,误差约 0.81% |
| Stream | listpack 组成的 radix tree | — | 每条消息约 100+ 字节;必须用 XTRIM/MAXLEN 限长,否则无限增长 |
# 查看当前编码,确认是否处于省内存的紧凑编码
redis-cli OBJECT ENCODING user:1001
redis-cli CONFIG GET hash-max-listpack-entries
redis-cli CONFIG GET '*max-listpack*'
redis-cli CONFIG GET set-max-intset-entries
# 【优化技巧】适度调大阈值,让更多 key 保持紧凑编码
# 代价:紧凑编码是 O(N) 查找,元素太多时 CPU 会上升。建议不超过 1000
redis-cli CONFIG SET hash-max-listpack-entries 512
redis-cli CONFIG SET zset-max-listpack-entries 256
redis-cli CONFIG SET list-max-listpack-size 256
redis-cli CONFIG SET set-max-intset-entries 1024
redis-cli CONFIG REWRITE
必须预留的内存冗余:
| 冗余项 | 需要预留的量 | 原因 |
|---|---|---|
| fork 的 COW 开销 | 最坏情况 = 数据量 × 100%,实测通常 10%~50% | BGSAVE/BGREWRITEAOF 时子进程与父进程共享内存页,父进程修改的页会被复制。写入越频繁、持续时间越长,复制的页越多。看 rdb_last_cow_size 得到实测值 |
| 客户端缓冲区 | 连接数 × 平均缓冲区,通常 几十 MB ~ 几 GB | 看 INFO memory 的 mem_clients_normal 与 mem_clients_slaves |
| 复制积压缓冲区 | = repl-backlog-size(建议 64~256MB) |
固定占用 |
| AOF 缓冲区与重写缓冲区 | 通常几十 MB | 看 mem_aof_buffer |
| 内存碎片 | 10%~30% | 碎片率 1.1~1.3 是常态 |
| 操作系统与其他进程 | 至少 1~2GB | 系统本身、监控 Agent、日志等 |
maxmemory 该设成物理内存的多少:
| 场景 | 建议 maxmemory |
说明 |
|---|---|---|
| 开启了 RDB 或 AOF 持久化 | 物理内存的 50%~60% | 必须给 fork 的 COW 留出足够空间。8GB 机器建议 maxmemory 4gb。这是最保守也最安全的配置 |
| 纯缓存,完全关闭持久化 | 物理内存的 70%~80% | 没有 fork,只需留出客户端缓冲、碎片与系统开销。8GB 机器可设 maxmemory 6gb |
| 写入极不频繁(COW 很少) | 可放宽到 65%~70% | 需实测 rdb_last_cow_size 确认 |
| 单机多实例 | 各实例 maxmemory 之和 ≤ 物理内存的 50% |
多个实例可能同时 fork,风险叠加 |
| 有从库的主库 | 再额外扣除 repl-backlog-size 与从库缓冲区 |
全量同步期间开销更大 |
# 实例规格建议
# 【单实例内存不要超过 10GB】,理由:
# · fork 耗时与内存量成正比:10GB 实例一次 fork 可能阻塞 200~500ms
# · 全量同步传输时间长,容易触发 repl-timeout
# · 重启加载时间长(GB 级数据加载需要数十秒到数分钟,期间返回 LOADING)
# · 故障影响面大
# 【超过 10GB 就应该考虑】:拆分为多个实例,或上 Redis Cluster 做水平分片
# 容量规划检查
redis-cli INFO memory | grep -E 'used_memory_human|used_memory_peak_human|maxmemory_human|total_system_memory_human'
redis-cli INFO persistence | grep rdb_last_cow_size
free -h
【容量规划完整示例】
业务需求:
· 用户会话缓存,预计 2000 万活跃会话
· 每个会话是一个 hash,8 个字段,平均总长度 300 字节
· 都设置 30 分钟 TTL
估算过程:
1) 单 key 数据部分:300 字节
2) hash 字段数 8 < 128,长度 < 64 → 使用 listpack 紧凑编码,额外开销小
3) 单 key 固定开销:redisObject 16 + dictEntry 32 + SDS 头 8
+ 过期字典条目 32 ≈ 88 字节
4) 单 key 合计 ≈ 300 + 88 ≈ 390 字节
5) 数据总量 = 2000万 × 390 ≈ 7.8 GB
6) 加 20% 碎片冗余 → 9.4 GB
7) 加客户端缓冲 + backlog ≈ 0.5 GB → 9.9 GB
结论:
· 单实例接近 10GB 上限,【建议拆分为 2~4 个实例】或上集群
· 若单实例:物理内存至少 20GB(maxmemory 设 10gb,占 50%),
因为要给 fork 的 COW 留空间
· 若拆 4 个实例:每实例 maxmemory 2.5gb,
单机部署时物理内存需 4 × 2.5 ÷ 0.5 = 20GB(多实例可能同时 fork)
· 更优方案:4 个实例分散到 2 台 16GB 机器,每台 2 个实例(各 2.5gb),
占用 5GB / 16GB ≈ 31%,非常安全
十二、排错清单
本章按现象 → 原因 → 排查命令 → 解决的结构组织。排障通用流程:先看报错原文 → 再看 INFO 对应段 → 再看 Redis 日志 → 最后看系统层面(磁盘、内存、网络)。
# 【排障三板斧】任何 Redis 问题先跑这三条
tail -100 /var/log/redis/redis.log # ① Redis 自己的日志最直接
redis-cli INFO | grep -E 'redis_version|role|uptime_in_seconds|used_memory_human|maxmemory_human|mem_fragmentation_ratio|rdb_last_bgsave_status|aof_last_write_status|connected_clients|rejected_connections|master_link_status'
df -h; free -h # ③ 磁盘与内存
12.1 连接类故障
12.1.1 Connection refused
| 项目 | 内容 |
|---|---|
| 现象 | Could not connect to Redis at 127.0.0.1:6379: Connection refused |
| 原因 | ① Redis 进程没启动或已崩溃;② 端口不对(改过 port 却没同步客户端);③ bind 没有包含你连接的地址(如只 bind 192.168.1.10 而你用 redis-cli 连 127.0.0.1);④ 防火墙 REJECT(DROP 表现为超时而非 refused);⑤ 用了 port 0 只开 Unix Socket;⑥ 进程被 OOM Killer 杀掉 |
# 【排查】
systemctl status redis # CentOS 7
# service redis status # CentOS 6
ps -ef | grep [r]edis-server # 进程是否存在
ss -tlnp | grep redis # 监听在哪个地址和端口
redis-cli CONFIG GET port # 若能本地连上,确认端口
grep -E '^bind|^port|^unixsocket' /etc/redis/redis.conf
tail -50 /var/log/redis/redis.log # 启动失败的原因都在这里
dmesg | grep -i 'killed process' # 是否被 OOM Killer 杀了
grep -i 'out of memory' /var/log/messages
firewall-cmd --list-all # CentOS 7
iptables -L -n --line-numbers # CentOS 6
# 【解决】
# 1) 进程没起来 → 启动并看日志
systemctl start redis && journalctl -u redis -n 50 --no-pager
# 2) bind 问题 → 加上 127.0.0.1
sed -i 's/^bind .*/bind 127.0.0.1 192.168.1.10/' /etc/redis/redis.conf
systemctl restart redis
# 3) 端口不对 → 用正确端口
redis-cli -p 26380 PING
# 4) 只开了 Unix Socket
redis-cli -s /var/run/redis/redis.sock PING
# 5) 被 OOM Killer 杀掉 → 设置 maxmemory 并调整 oom_score_adj
redis-cli CONFIG SET maxmemory 4gb
echo -1000 > /proc/$(pgrep -x redis-server)/oom_score_adj # 降低被杀概率
# systemd 里持久化:OOMScoreAdjust=-900
12.1.2 DENIED Redis is running in protected mode
| 项目 | 内容 |
|---|---|
| 现象 | (error) DENIED Redis is running in protected mode because protected mode is enabled, no bind address was specified, no authentication password is requested to clients. In this mode connections are only accepted from the loopback interface... |
| 完整成因 | protected-mode yes(3.2+ 默认值)且配置文件中没有任何 bind 指令(被注释掉了)且没有设置 `requirepass``。三个条件同时成立时,Redis 只接受来自 127.0.0.1/::1/Unix Socket 的连接,外部连接虽能完成 TCP 握手,但执行任何命令都被拒绝。这是 Redis 3.2 为遏制未授权访问漏洞加的安全网 |
# 【排查】确认三个条件
redis-cli CONFIG GET protected-mode # yes
grep -c '^bind' /etc/redis/redis.conf # 0 说明没配 bind
redis-cli CONFIG GET requirepass # 空说明没设密码
三种解决方式(按推荐程度排序):
# ===== 方式一(最推荐):设置密码 =====
# 设了 requirepass 后,protected-mode 自动让路,且真正提升了安全性
requirepass K7xQ2mR9vLpZ3nT8wY4bC6dF1gH5jS0a
# ===== 方式二(推荐):显式配置 bind =====
# 明确指定监听地址,protected-mode 认为管理员已知晓风险
bind 127.0.0.1 192.168.1.10
# ===== 方式三(【不推荐,仅测试环境】):关闭保护模式 =====
protected-mode no
# ⚠️ 单独用这一条等于把 Redis 完全裸露,必须同时有防火墙严格白名单
# 生效(三种方式都需要重启或热改)
redis-cli CONFIG SET requirepass 'K7xQ2mR9vLpZ3nT8wY4bC6dF1gH5jS0a'
redis-cli -a 'K7xQ...' --no-auth-warning CONFIG REWRITE
# bind 与 protected-mode 也可热改,但 bind 热改需 Redis 7.0+
systemctl restart redis
正确做法是方式一 + 方式二一起用(设密码 + 绑内网地址 + 防火墙白名单),而不是简单地
protected-mode no了事。Redis 3.0 及更早没有这个参数,遇到类似问题只能靠bind+requirepass+ 防火墙。
12.1.3 NOAUTH Authentication required
| 项目 | 内容 |
|---|---|
| 现象 | (error) NOAUTH Authentication required. |
| 原因 | 服务端设置了 requirepass(或启用了 ACL),客户端连接后没有认证就直接发命令 |
# 【排查】
redis-cli CONFIG GET requirepass # 需要先用正确密码连上才能查
grep '^requirepass' /etc/redis/redis.conf
redis-cli ACL LIST # 6.0+ 看是否配了 ACL
# 【解决】
# 1) 命令行带密码
redis-cli -a 'YourPassword' --no-auth-warning PING
# 2) 用环境变量(推荐,不会出现在 ps 与 history 中)
export REDISCLI_AUTH='YourPassword'
redis-cli PING
# 3) 交互式登录后 AUTH
redis-cli
127.0.0.1:6379> AUTH YourPassword
OK
# Redis 6.0+ 的 ACL 用户形式
127.0.0.1:6379> AUTH app-order 0rd3r_Pass
# 4) 应用侧:检查连接配置里的 password 是否为空、是否被环境变量覆盖
# 5) 主从场景:从库日志出现 NOAUTH → 从库没配 masterauth
redis-cli -h 从库 CONFIG SET masterauth 'MasterPassword'
redis-cli -h 从库 CONFIG REWRITE
# 6) 哨兵场景 → sentinel auth-pass mymaster <password>
12.1.4 WRONGPASS invalid username-password pair
| 项目 | 内容 |
|---|---|
| 现象 | (error) WRONGPASS invalid username-password pair or user is disabled.(Redis 6.0+)老版本报 (error) ERR invalid password |
| 原因 | ① 密码错误(含特殊字符没转义、复制时带上了空格或换行);② ACL 用户名不存在或拼错;③ 该 ACL 用户被设为 off;④ 用了 default 用户但 default 已被禁用;⑤ 配置文件里 requirepass 与 ACL 的 user default 冲突,后加载的覆盖了前者;⑥ 主从/集群各节点密码不一致 |
# 【排查】
redis-cli -a 'YourPassword' --no-auth-warning ACL WHOAMI # 确认认证成功后的身份
redis-cli ACL LIST # 看用户是否存在、是否 on
redis-cli ACL GETUSER app-order | head -6
redis-cli ACL LOG 5 # reason=auth 的记录含 username
grep -E '^requirepass|^user |^aclfile' /etc/redis/redis.conf
cat /etc/redis/users.acl 2>/dev/null
# 【解决】
# 1) 密码含特殊字符 → 用单引号包裹,避免 shell 解释 $ ! ` 等
redis-cli -a 'P@ss!w0rd$123' --no-auth-warning PING
# 2) 密码里有换行/空格 → 用 od 检查是否有隐藏字符
echo -n 'YourPassword' | od -c | head -3
# 3) 用户被禁用 → 启用
redis-cli ACL SETUSER app-order on
# 4) default 被禁用 → 显式带用户名
redis-cli --user app-order --pass '0rd3r_Pass' --no-auth-warning PING
# 5) 重置密码
redis-cli CONFIG SET requirepass 'NewPassword'
redis-cli -a 'NewPassword' --no-auth-warning CONFIG REWRITE
# 记得同步修改:应用配置、masterauth、sentinel auth-pass、监控采集配置
# 6) 主从/集群密码不一致 → 逐节点核对
for h in 192.168.1.10 192.168.1.11 192.168.1.12; do
echo -n "$h: "; redis-cli -h $h -a 'YourPassword' --no-auth-warning PING
done
12.2 写入被拒类故障
12.2.1 MISCONF Redis is configured to save RDB snapshots(最经典的写入故障)
| 项目 | 内容 |
|---|---|
| 现象 | (error) MISCONF Redis is configured to save RDB snapshots, but it is currently not able to persist on disk. Commands that may modify the data set are disabled, because this instance is configured to report errors during writes if RDB snapshotting fails (stop-writes-on-bgsave-error option). Please check the Redis logs for details about the RDB error.业务表现:读全部正常,所有写命令报错,且重启后依然如故 |
完整成因链:
┌─────────────────────────────────────────────┐
│ 根因(任一) │
├─────────────────────────────────────────────┤
│ ① 磁盘空间满(df 100%) │
│ ② 数据目录 dir 不存在 / 属主不是 redis │
│ ③ 数据目录权限不足(如 chown 后漏了目录) │
│ ④ 文件系统被挂载为只读(磁盘故障后内核自保) │
│ ⑤ fork 失败(内存不足 + overcommit_memory=0)│
│ ⑥ inode 耗尽(df -i 100%) │
│ ⑦ CONFIG SET dir 改到了不可写的路径 │
│ ⑧ SELinux 阻止写入(CentOS 7 enforcing) │
└────────────────────┬────────────────────────┘
▼
BGSAVE 或 AOF 写入执行失败
▼
rdb_last_bgsave_status:err / aof_last_write_status:err
▼
Redis 检测到"配置了持久化但持久化不了",为防止数据只在内存里
(宕机即全丢),依据 stop-writes-on-bgsave-error yes(默认值)
【主动拒绝所有可能修改数据集的命令】
▼
客户端收到 MISCONF 错误
# 【排查】按成因链逐项确认
# 1) 确认哪个持久化失败了
redis-cli INFO persistence | grep -E 'rdb_last_bgsave_status|aof_last_write_status|aof_last_bgrewrite_status'
# rdb_last_bgsave_status:err ← 就是它
# 2) 【最直接】看日志,Redis 会明确写出失败原因
tail -50 /var/log/redis/redis.log
# Write error saving DB on disk: No space left on device
# Failed opening the RDB file dump.rdb (in server root dir /var/lib/redis) for saving: Permission denied
# Can't save in background: fork: Cannot allocate memory
# 3) 磁盘空间与 inode
df -h /var/lib/redis
df -i /var/lib/redis
# 4) 目录与权限
redis-cli CONFIG GET dir
ls -ld /var/lib/redis
ls -l /var/lib/redis/
ps -ef | grep [r]edis-server | awk '{print $1}' # 确认运行用户
# 5) 文件系统是否只读
mount | grep -E "$(df /var/lib/redis | awk 'NR==2{print $1}')"
# 出现 "ro," 就是只读挂载
touch /var/lib/redis/.writetest && rm -f /var/lib/redis/.writetest && echo "可写" || echo "不可写"
# 6) fork 相关
cat /proc/sys/vm/overcommit_memory
free -h
redis-cli INFO memory | grep used_memory_human
# 7) SELinux(CentOS 7)
getenforce
ausearch -m avc -ts recent | grep -i redis
# 【解决】对症处理
# ===== 磁盘满 =====
du -sh /var/lib/redis/* # 看是谁占的
ls -lh /var/lib/redis/*.rdb /var/lib/redis/*.aof
# 清理旧备份、日志、无用文件
find /var/log -name '*.log.*' -mtime +7 -delete
# AOF 过大 → 手动重写压缩
redis-cli BGREWRITEAOF
# 长期方案:扩容磁盘,或把 dir 移到大分区
systemctl stop redis
mv /var/lib/redis /data/redis
chown -R redis:redis /data/redis
sed -i 's#^dir .*#dir /data/redis#' /etc/redis/redis.conf
systemctl start redis
# ===== 权限问题(最常见)=====
chown -R redis:redis /var/lib/redis
chmod 750 /var/lib/redis
# 若 dir 的【父目录】也不可进入,同样会失败
namei -l /var/lib/redis # 逐级检查路径上每一层的权限
# ===== 文件系统只读 =====
dmesg | tail -50 # 查看磁盘错误
mount -o remount,rw /data # 尝试重新挂载为读写
# 若是磁盘硬件故障,需先修复文件系统(fsck)甚至更换磁盘
# ===== fork 失败 =====
sysctl -w vm.overcommit_memory=1
echo 'vm.overcommit_memory = 1' >> /etc/sysctl.conf
sysctl -p
# ===== SELinux =====
restorecon -Rv /var/lib/redis
semanage fcontext -a -t redis_var_lib_t '/data/redis(/.*)?'
restorecon -Rv /data/redis
# 临时验证是否是 SELinux 导致
setenforce 0
# ===== 修复后:手动触发一次保存确认恢复 =====
redis-cli BGSAVE
sleep 3
redis-cli INFO persistence | grep rdb_last_bgsave_status
# rdb_last_bgsave_status:ok ← 恢复后写命令立刻可用,无需重启
redis-cli SET test:write ok
# OK
# ===== 应急旁路(【慎用】)=====
# 允许 BGSAVE 失败时仍接受写入 —— 代价是【数据只在内存中,宕机即全部丢失】
stop-writes-on-bgsave-error no
redis-cli CONFIG SET stop-writes-on-bgsave-error no
# ⚠️ 这只是让业务先恢复的【临时措施】,绝不能当作最终解决方案。
# 必须尽快修复磁盘/权限问题,然后改回 yes:
redis-cli CONFIG SET stop-writes-on-bgsave-error yes
MISCONF的本质是 Redis 的自我保护:它宁愿拒绝写入让你立刻发现问题,也不愿让你误以为数据安全落盘。看到 MISCONF 的第一反应应该是df -h和ls -ld $(redis-cli CONFIG GET dir | tail -1),九成以上的情况就是磁盘满或权限不对。
12.2.2 OOM command not allowed when used memory > 'maxmemory'
| 项目 | 内容 |
|---|---|
| 现象 | (error) OOM command not allowed when used memory > 'maxmemory'.读命令正常,写命令全部被拒 |
| 原因 | ① used_memory 已达到 maxmemory 且 maxmemory-policy 是 noeviction(默认值);② 策略是 volatile-* 但没有任何 key 设置了 TTL,导致找不到可淘汰的 key,行为退化为 noeviction;③ 大 key 突然写入导致瞬间超限;④ 客户端缓冲区吃掉了大量内存(maxmemory 的判断包含部分开销) |
# 【排查】
redis-cli INFO memory | grep -E 'used_memory_human|maxmemory_human|maxmemory_policy'
redis-cli INFO stats | grep -E 'evicted_keys|total_eviction_exceeded_time'
redis-cli INFO keyspace # 比对 keys 与 expires,看有没有 TTL
# db0:keys=1523400,expires=0,... ← expires=0 且策略是 volatile-* 就是原因②
redis-cli INFO errorstats | grep OOM # 6.2+
redis-cli --bigkeys # 找大 key
redis-cli --memkeys # 按内存找大 key
redis-cli MEMORY DOCTOR
redis-cli MEMORY STATS | head -30 # 看开销构成
# 【解决】按优先级
# 1) 【应急】调大 maxmemory(前提:物理内存有余量)
redis-cli CONFIG SET maxmemory 6gb
redis-cli CONFIG REWRITE
# 2) 【应急】改为淘汰策略(纯缓存场景,可接受丢数据)
redis-cli CONFIG SET maxmemory-policy allkeys-lru
redis-cli CONFIG REWRITE
# 3) 清理无用数据
redis-cli --scan --pattern 'tmp:*' | xargs -L 100 redis-cli UNLINK
redis-cli --scan --pattern 'debug:*' | xargs -L 100 redis-cli UNLINK
# 4) 给忘记设 TTL 的 key 补上过期时间
redis-cli --scan --pattern 'session:*' | while read k; do
[ "$(redis-cli TTL "$k")" = "-1" ] && redis-cli EXPIRE "$k" 1800
done
# 5) 治理大 key(长期方案)
# hash 拆分:user:profile:1001 → user:profile:1001:{0..9}(按字段 hash 取模分片)
# 大 list 改用 Stream 并设 MAXLEN
# 大 string 压缩后再存(gzip/snappy),或改存对象存储只在 Redis 放引用
# 6) 降低内存开销
redis-cli CONFIG SET hash-max-listpack-entries 512 # 让更多 hash 走紧凑编码
redis-cli CONFIG SET activedefrag yes # 回收碎片
# 7) 【根治】扩容或水平拆分
# 单实例 → 多实例按业务拆分 → Redis Cluster 分片
OOM与MISCONF的区别:OOM是内存不足导致写被拒(改maxmemory或淘汰策略即可恢复);MISCONF是持久化失败导致写被拒(必须修磁盘/权限)。两者都表现为"读正常写全挂",看报错关键字就能区分。
12.2.3 fork 失败与 Cannot allocate memory
| 项目 | 内容 |
|---|---|
| 现象 | 日志中出现 Can't save in background: fork: Cannot allocate memory或 Can't rewrite append only file in background: fork: Cannot allocate memory随后 rdb_last_bgsave_status:err → 触发 MISCONF → 写入全挂 |
| 原因 | fork() 系统调用失败。核心原因是 Linux 的内存过量分配策略(vm.overcommit_memory):fork 时内核(悲观模式下)会检查"如果子进程把所有共享页都写一遍,是否有足够内存",即需要 Redis内存 × 2 的可用量。而 Redis 的 fork 依赖 copy-on-write,实际上根本不需要那么多内存(只有被修改的页才会真正复制),所以悲观检查会误判失败 |
# 【排查】
cat /proc/sys/vm/overcommit_memory # 【关键】必须是 1
free -h # 可用内存
redis-cli INFO memory | grep -E 'used_memory_human|used_memory_rss_human'
redis-cli INFO stats | grep latest_fork_usec
redis-cli INFO persistence | grep -E 'rdb_last_cow_size|rdb_last_bgsave_status'
grep -i 'fork' /var/log/redis/redis.log | tail -20
dmesg | tail -30
cat /proc/meminfo | grep -E 'MemFree|MemAvailable|Committed_AS|CommitLimit'
vm.overcommit_memory 的三个取值:
| 值 | 名称 | 行为 | 对 Redis 的影响 |
|---|---|---|---|
0(默认) |
启发式 | 内核用启发式算法判断,"明显过量"的请求会被拒绝 | Redis 官方明确要求改掉。内存使用率高时 fork 会失败 |
1(Redis 要求的值) |
总是允许 | 总是允许过量分配,不做检查 | fork 不会因为内存检查而失败,正好匹配 COW 的实际行为 |
2 |
严格 | 提交总量不得超过 swap + 物理内存 × overcommit_ratio |
更严格,Redis 上会频繁 fork 失败 |
# 【解决】
# 1) 【必做】设置 overcommit_memory = 1
sysctl -w vm.overcommit_memory=1
# 持久化
echo 'vm.overcommit_memory = 1' >> /etc/sysctl.conf
sysctl -p
cat /proc/sys/vm/overcommit_memory # 确认为 1
# 【CentOS 7 也可用 sysctl.d 目录(更规范)】
cat > /etc/sysctl.d/99-redis.conf <<'EOF'
vm.overcommit_memory = 1
vm.swappiness = 1
net.core.somaxconn = 2048
net.ipv4.tcp_max_syn_backlog = 2048
EOF
sysctl --system
# 【CentOS 6】只能写 /etc/sysctl.conf,然后 sysctl -p
# 2) 降低 maxmemory,给 fork 留出空间(见 11.14.8)
redis-cli CONFIG SET maxmemory 4gb # 8GB 机器设 50%
redis-cli CONFIG REWRITE
# 3) 关闭透明大页(THP)—— 会让 fork 后的 COW 复制单位从 4KB 变成 2MB,
# 大幅增加内存开销与延迟。Redis 启动时会警告
cat /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
# 持久化(CentOS 7)
cat >> /etc/rc.local <<'EOF'
echo never > /sys/kernel/mm/transparent_hugepage/enabled
echo never > /sys/kernel/mm/transparent_hugepage/defrag
EOF
chmod +x /etc/rc.d/rc.local
# 或在 grub 中加内核参数:transparent_hugepage=never
# 【CentOS 6】写入 /etc/rc.local 即可(默认可执行)
# 4) 减少 fork 频率
# 放宽 RDB 触发条件
redis-cli CONFIG SET save "900 1 300 100"
# 放宽 AOF 重写阈值
redis-cli CONFIG SET auto-aof-rewrite-percentage 200
redis-cli CONFIG SET auto-aof-rewrite-min-size 512mb
# 把持久化放到从库做,主库完全关闭持久化(需谨慎评估)
# 5) 保留少量 swap 作为 fork 时的缓冲(配合 swappiness=1)
free -h | grep -i swap
latest_fork_usec与实例大小的关系:fork 耗时约为每 GB 内存 20~50 毫秒(物理机),虚拟机上可能翻数倍。这就是"单实例不要超过 10GB"的主要理由之一——10GB 实例一次 fork 可能阻塞 200~500 毫秒,期间所有命令排队等待,表现为周期性的延迟尖刺。
12.2.4 RDB 后台保存失败
| 项目 | 内容 |
|---|---|
| 现象 | rdb_last_bgsave_status:err;日志 Background saving error / Write error saving DB on disk / Failed opening the RDB file |
| 原因 | 与 12.2.1 同源:磁盘满、inode 满、权限、只读文件系统、fork 失败、SELinux、dir 路径不存在 |
# 【排查与解决】
tail -50 /var/log/redis/redis.log | grep -iE 'saving|rdb|fork'
df -h /var/lib/redis; df -i /var/lib/redis
ls -ld /var/lib/redis; namei -l /var/lib/redis
redis-cli CONFIG GET dir
cat /proc/sys/vm/overcommit_memory
# 手动测试同步保存(会阻塞,但报错信息最直接)
redis-cli SAVE
# (error) ERR ← 报错则看日志详情
# OK ← 成功说明问题已解决
# 修复后确认
redis-cli BGSAVE && sleep 3 && redis-cli INFO persistence | grep rdb_last_bgsave_status
ls -lh /var/lib/redis/dump.rdb
# 校验 RDB 文件完整性
redis-check-rdb /var/lib/redis/dump.rdb
12.2.5 AOF 文件损坏
| 项目 | 内容 |
|---|---|
| 现象 | 启动失败,日志:Bad file format reading the append only file appendonly.aof: make a backup of your AOF file, then use ./redis-check-aof --fix <filename>或 Unexpected end of file reading the append only file或 AOF loading aborted |
| 原因 | ① 服务器突然断电/强制关机,最后一条命令只写了一半(最常见);② 磁盘写满时 AOF 被截断;③ 磁盘硬件故障或文件系统损坏;④ 手工编辑过 AOF 文件;⑤ 拷贝 AOF 文件时传输不完整 |
# 【排查】
tail -50 /var/log/redis/redis.log
ls -lh /var/lib/redis/appendonly.aof
# Redis 7.0+ 是 multi-part AOF,文件在 appendonlydir 目录下
ls -lh /var/lib/redis/appendonlydir/
cat /var/lib/redis/appendonlydir/appendonly.aof.manifest
# 先检查损坏情况(不加 --fix 是只检查)
redis-check-aof /var/lib/redis/appendonly.aof
0x 4a1b2c: Expected prefix '*', got: '\x00'
AOF analyzed: filename=appendonly.aof, size=157286400, ok_up_to=4857900, ok_up_to_line=98234, diff=152428500
AOF is not valid. Use the --fix option to try fixing it.
# ↑ 前 4857900 字节是好的,之后是损坏部分
# 【解决】
# ===== 步骤 1:【务必先备份】=====
cp /var/lib/redis/appendonly.aof /backup/appendonly.aof.$(date +%F-%H%M).bak
# Redis 7.0+
cp -r /var/lib/redis/appendonlydir /backup/appendonlydir.$(date +%F-%H%M).bak
# ===== 步骤 2:修复 =====
redis-check-aof --fix /var/lib/redis/appendonly.aof
# This will shrink the AOF from 157286400 bytes, with 152428500 bytes, to 4857900 bytes
# Continue? [y/N]: y
# Successfully truncated AOF
# 【原理】--fix 就是【把第一个损坏点之后的所有内容截断丢弃】,
# 所以【会丢失损坏点之后的数据】,这是不可避免的代价
# Redis 7.0+ 的 multi-part AOF 需指定 manifest
redis-check-aof --fix /var/lib/redis/appendonlydir/appendonly.aof.manifest
# 也可以只截断到最后一个完整命令
redis-check-aof --truncate-to-timestamp <unix时间戳> /var/lib/redis/appendonlydir/appendonly.aof.manifest
# ===== 步骤 3:启动并验证 =====
systemctl start redis
redis-cli DBSIZE
redis-cli INFO keyspace
tail -30 /var/log/redis/redis.log
# ===== 步骤 4:立即重写 AOF,生成干净文件 =====
redis-cli BGREWRITEAOF
redis-cli INFO persistence | grep aof_last_bgrewrite_status
# ===== 预防配置 =====
# 允许 Redis 在发现 AOF 尾部不完整时【自动截断并继续启动】(默认 yes)
# 这能让"断电导致最后一条命令写一半"的情况自动恢复,强烈建议保持 yes
aof-load-truncated yes
# 混合持久化(4.0+):AOF 重写时前半部分用 RDB 格式,后半部分是增量命令
# 优点:文件更小、加载更快;缺点:4.0 之前的版本无法解析该 AOF
aof-use-rdb-preamble yes
# AOF 重写期间是否暂停 fsync(避免与子进程争抢磁盘 I/O)
# yes 可减少阻塞,代价是重写期间宕机可能丢更多数据(最多丢重写这段时间的)
no-appendfsync-on-rewrite no
# ===== 相关工具:redis-check-rdb =====
# 校验 RDB 文件完整性(【注意:RDB 只能检查,不能修复】)
redis-check-rdb /var/lib/redis/dump.rdb
[offset 0] Checking RDB file dump.rdb
[offset 26] AUX FIELD redis-ver = '7.2.4'
[offset 40] AUX FIELD redis-bits = '64'
[offset 92] Selecting DB ID 0
[offset 157286400] Checksum OK
[offset 157286400] \o/ RDB looks OK! \o/
[info] 1523400 keys read
[info] 1420000 expires
[info] 0 already expired
# RDB 损坏时的报错
# --- RDB ERROR DETECTED ---
# [offset 4857900] Internal error in RDB reading offset 4857900, function at rdb.c:1234 -> Unknown RDB string encoding type
# 【RDB 无法修复】,只能:
# 1) 从备份恢复(这就是为什么必须有定期备份)
# 2) 从从库的 RDB 恢复
# 3) 如果同时开了 AOF,删掉损坏的 RDB,靠 AOF 恢复(AOF 优先级更高)
# 4) 尝试用 rdbtools 部分解析出可读数据
rdb --command json /var/lib/redis/dump.rdb > /tmp/partial.json 2>/dev/null
持久化文件的加载优先级:同时存在 AOF 与 RDB 时,Redis 启动【只加载 AOF】(因为 AOF 通常更完整)。这个规则有两个重要推论:① 冷迁移导入 RDB 时必须先关 AOF(见 11.14.4);② RDB 损坏但 AOF 完好时,可以直接删掉 RDB 让 AOF 接管。
12.3 主从与哨兵类故障
12.3.1 master_link_status:down(从库连不上主库)
| 项目 | 内容 |
|---|---|
| 现象 | 从库 INFO replication 显示 master_link_status:down,role:slave 但数据不同步;日志反复出现 MASTER <-> REPLICA sync started 后失败 |
| 原因 | ① 主库地址/端口写错;② 主库设了 requirepass 但从库没配 masterauth(最常见);③ 防火墙/安全组拦住了从库到主库 6379 的连接;④ 主库 bind 127.0.0.1 导致外部连不上;⑤ 主库 protected-mode yes 且无密码;⑥ 主库 maxclients 已满;⑦ 主库 RDB 生成失败(无法产生全量快照);⑧ 网络不通或延迟过大导致 repl-timeout 超时;⑨ 主库输出缓冲区超限被断开;⑩ 主从版本差异过大(RDB 版本不兼容) |
# 【排查】按顺序执行,逐层排除
# ===== 第 1 层:从库自身状态 =====
redis-cli -p 6380 INFO replication
role:slave
master_host:192.168.1.10
master_port:6379
master_link_status:down ← 问题所在
master_last_io_seconds_ago:-1 ← -1 表示从未成功通信
master_sync_in_progress:0
slave_read_only:1
master_link_down_since_seconds:1823 ← 断开了多久
# ===== 第 2 层:看从库日志(最关键,报错信息都在这里)=====
tail -50 /var/log/redis/redis-6380.log | grep -iE 'master|sync|auth|err'
# 典型报错及含义:
# "Error condition on socket for SYNC: Connection refused" → 主库没起/端口错/防火墙
# "Unable to connect to MASTER: Connection timed out" → 网络不通/防火墙 DROP
# "MASTER aborted replication with an error: NOAUTH Authentication required" → 缺 masterauth
# "MASTER aborted replication with an error: ERR invalid password" → masterauth 密码错
# "-DENIED Redis is running in protected mode" → 主库 protected-mode
# "Timeout receiving bulk data from MASTER" → repl-timeout 太小/网络慢
# "Master is currently unable to PSYNC but should be in the future" → 主库正在 fork/加载
# ===== 第 3 层:网络连通性 =====
ping -c 3 192.168.1.10
telnet 192.168.1.10 6379 # 或 nc -zv 192.168.1.10 6379
redis-cli -h 192.168.1.10 -p 6379 -a 'MasterPass' --no-auth-warning PING
# ===== 第 4 层:认证配置 =====
redis-cli -p 6380 CONFIG GET masterauth # 从库是否配了主库密码
redis-cli -h 192.168.1.10 -p 6379 -a 'MasterPass' --no-auth-warning CONFIG GET requirepass
# 【注意】从库自己的 requirepass 与 masterauth 是两回事:
# requirepass = 客户端连【本从库】要用的密码
# masterauth = 本从库连【主库】要用的密码
# 发生故障切换后从库会变主库,所以【所有节点的 requirepass 应该一致,
# 且每个节点都要配 masterauth】
# ===== 第 5 层:主库侧 =====
redis-cli -h 192.168.1.10 -p 6379 -a 'MasterPass' --no-auth-warning INFO replication
redis-cli -h 192.168.1.10 -p 6379 -a 'MasterPass' --no-auth-warning CONFIG GET bind
redis-cli -h 192.168.1.10 -p 6379 -a 'MasterPass' --no-auth-warning CONFIG GET protected-mode
redis-cli -h 192.168.1.10 -p 6379 -a 'MasterPass' --no-auth-warning INFO clients
redis-cli -h 192.168.1.10 -p 6379 -a 'MasterPass' --no-auth-warning INFO persistence | grep rdb_last_bgsave_status
tail -50 /var/log/redis/redis.log # 在主库机器上执行
# ===== 第 6 层:防火墙 =====
# CentOS 7
firewall-cmd --list-all
iptables -L -n | grep 6379
# CentOS 6
service iptables status
# 【解决】
# 1) 补配 masterauth(最常见的原因)
redis-cli -p 6380 CONFIG SET masterauth 'MasterPass'
redis-cli -p 6380 CONFIG REWRITE
# 永久写入配置文件
# /etc/redis/redis-6380.conf
replicaof 192.168.1.10 6379 # 5.0+;4.x 及之前用 slaveof
masterauth MasterPass # 【必须】主库有密码时
requirepass MasterPass # 建议与主库一致,便于故障切换
replica-read-only yes # 5.0+;之前叫 slave-read-only
replica-serve-stale-data no # 5.0+;之前叫 slave-serve-stale-data
# no = 同步断开时拒绝读请求(返回 SYNC with master in progress)
# yes = 继续返回可能过期的旧数据
repl-timeout 120 # 大实例全量同步慢时需调大(默认 60)
# 2) 主库放开监听与防火墙
redis-cli -h 192.168.1.10 -p 6379 -a 'MasterPass' --no-auth-warning CONFIG SET bind "192.168.1.10 127.0.0.1"
# CentOS 7
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.11/32" port protocol="tcp" port="6379" accept'
firewall-cmd --reload
# CentOS 6
iptables -I INPUT -s 192.168.1.11 -p tcp --dport 6379 -j ACCEPT
service iptables save
# 3) 调大复制相关缓冲区(全量同步中途断开时)
redis-cli -h 192.168.1.10 -p 6379 -a 'MasterPass' --no-auth-warning CONFIG SET repl-backlog-size 128mb
redis-cli -h 192.168.1.10 -p 6379 -a 'MasterPass' --no-auth-warning CONFIG SET client-output-buffer-limit "replica 512mb 128mb 60"
redis-cli -h 192.168.1.10 -p 6379 -a 'MasterPass' --no-auth-warning CONFIG SET repl-timeout 120
# 4) 强制重新发起同步(改完配置后)
redis-cli -p 6380 REPLICAOF NO ONE # 先断开
redis-cli -p 6380 REPLICAOF 192.168.1.10 6379 # 再重连
# 4.x 及之前:SLAVEOF NO ONE / SLAVEOF host port
# 5) 验证
redis-cli -p 6380 INFO replication | grep -E 'master_link_status|master_last_io_seconds_ago'
# master_link_status:up
# master_last_io_seconds_ago:0 ← 正常
redis-cli -h 192.168.1.10 -p 6379 -a 'MasterPass' --no-auth-warning INFO replication | grep -E 'connected_slaves|slave0'
# connected_slaves:1
# slave0:ip=192.168.1.11,port=6380,state=online,offset=8823401,lag=0
state字段的三个阶段:wait_bgsave(主库正在生成 RDB)→send_bulk(正在传输 RDB)→online(进入增量复制)。卡在wait_bgsave说明主库 fork/RDB 有问题;卡在send_bulk说明网络慢或缓冲区不足。
12.3.2 从库数据与主库不一致
| 项目 | 内容 |
|---|---|
| 现象 | master_link_status:up 但从库 DBSIZE 与主库不同;某些 key 在主库有、从库没有;或从库数据明显滞后 |
| 原因 | ① 有人往从库直接写入(replica-read-only no 时);② 主从过期 key 处理机制导致的"逻辑不一致"(正常现象);③ 复制延迟(异步复制的固有特性);④ repl-backlog 被覆盖导致部分命令丢失后又没触发全量同步(老版本 bug);⑤ 主库执行了随机性命令(SPOP、SRANDMEMBER)而版本过老(Redis 3.2 前部分命令非确定性);⑥ 从库 maxmemory 小于主库导致从库自己淘汰了 key(严重陷阱);⑦ 主从 maxmemory-policy 不一致 |
# 【排查】
# 1) 对比 key 数量(【注意】DBSIZE 包含已逻辑过期但未删除的 key,有正常偏差)
redis-cli -h 192.168.1.10 -p 6379 DBSIZE
redis-cli -h 192.168.1.11 -p 6380 DBSIZE
# 2) 看复制偏移量差(真正判断是否同步的指标)
redis-cli -h 192.168.1.10 -p 6379 INFO replication
# master_repl_offset:8823401
# slave0:ip=192.168.1.11,port=6380,state=online,offset=8823401,lag=0
# ↑ 与 master_repl_offset 相等 = 完全同步
redis-cli -h 192.168.1.11 -p 6380 INFO replication | grep slave_repl_offset
# 3) 检查从库是否可写(【最常见的人为原因】)
redis-cli -h 192.168.1.11 -p 6380 CONFIG GET replica-read-only
# 应为 yes。若为 no,则任何人都能往从库写脏数据,
# 而这些数据在下次全量同步后会【凭空消失】,极难排查
# 4) 检查从库内存上限(【严重陷阱】)
redis-cli -h 192.168.1.10 -p 6379 CONFIG GET maxmemory
redis-cli -h 192.168.1.11 -p 6380 CONFIG GET maxmemory
redis-cli -h 192.168.1.11 -p 6380 CONFIG GET maxmemory-policy
redis-cli -h 192.168.1.11 -p 6380 INFO stats | grep evicted_keys
# 从库 evicted_keys > 0 说明【从库在自己淘汰数据】,必然导致不一致
# 5) 抽样比对具体 key
for k in user:1001 user:1002 order:5001; do
echo "=== $k ==="
echo "master: $(redis-cli -h 192.168.1.10 -p 6379 GET $k) ttl=$(redis-cli -h 192.168.1.10 -p 6379 TTL $k)"
echo "slave : $(redis-cli -h 192.168.1.11 -p 6380 GET $k) ttl=$(redis-cli -h 192.168.1.11 -p 6380 TTL $k)"
done
# 6) 全量比对(数据量大时慎用,会遍历所有 key)
redis-cli -h 192.168.1.10 -p 6379 --scan | sort > /tmp/master.keys
redis-cli -h 192.168.1.11 -p 6380 --scan | sort > /tmp/slave.keys
diff /tmp/master.keys /tmp/slave.keys | head -50
wc -l /tmp/master.keys /tmp/slave.keys
主从过期 key 的处理机制(理解这一点才能区分"真不一致"和"假不一致"):
| 环节 | 行为 |
|---|---|
| 删除权归主库 | 从库【不会】自己删除过期 key,只有主库删除后同步 DEL/UNLINK 给从库 |
| 从库读取过期 key | 从库虽然物理上还存着该 key,但读取时会判断 TTL,逻辑上返回 nil(Redis 3.2+ 的行为) |
| 导致的现象 | DBSIZE 主从不等是正常的(从库残留了尚未被主库清理的过期 key) |
| 老版本的坑 | Redis 3.2 之前,从库读过期 key 会返回旧值,这是真正的数据不一致 bug,也是必须升级的理由之一 |
# 【解决】
# 1) 强制从库只读(【必做】)
redis-cli -h 192.168.1.11 -p 6380 CONFIG SET replica-read-only yes
redis-cli -h 192.168.1.11 -p 6380 CONFIG REWRITE
# 4.x 及之前:slave-read-only yes
# 2) 从库 maxmemory 必须 >= 主库(【必做】)
# 原理:从库要额外存主库同步过来的复制缓冲区,理论上需要比主库更多内存
redis-cli -h 192.168.1.11 -p 6380 CONFIG SET maxmemory 4gb
redis-cli -h 192.168.1.11 -p 6380 CONFIG SET maxmemory-policy noeviction
redis-cli -h 192.168.1.11 -p 6380 CONFIG REWRITE
# 3) 【终极修复】强制从库全量重同步
redis-cli -h 192.168.1.11 -p 6380 REPLICAOF NO ONE
redis-cli -h 192.168.1.11 -p 6380 FLUSHALL # 【危险】清空脏数据;生产环境需评估
redis-cli -h 192.168.1.11 -p 6380 REPLICAOF 192.168.1.10 6379
# 观察同步过程
watch -n 2 "redis-cli -h 192.168.1.11 -p 6380 INFO replication | grep -E 'link_status|sync_in_progress'"
# 4) 用 min-replicas 提高一致性(牺牲可用性换一致性)
redis-cli -h 192.168.1.10 -p 6379 CONFIG SET min-replicas-to-write 1
redis-cli -h 192.168.1.10 -p 6379 CONFIG SET min-replicas-max-lag 10
# 含义:至少有 1 个从库的 lag <= 10 秒时主库才接受写入,否则返回
# "NOREPLICAS Not enough good replicas to write"
# 【版本差异】5.0+ 叫 min-replicas-*,4.x 及之前叫 min-slaves-*
# 5) 关键写入后用 WAIT 命令等待同步(3.0+)
redis-cli -h 192.168.1.10 -p 6379 SET order:9001 paid
redis-cli -h 192.168.1.10 -p 6379 WAIT 1 1000
# (integer) 1 ← 1 秒内有 1 个从库确认收到;返回 0 说明超时未同步
异步复制的本质:Redis 主从是异步的,主库执行完写命令立刻返回客户端,之后才把命令发给从库。这意味着"主库宕机丢失最后几毫秒的写入"是设计使然,不是 bug。要求强一致的业务(余额、库存扣减)不能只依赖 Redis 主从,必须配合数据库事务或
WAIT+ 幂等设计。
12.3.3 哨兵不切换(故障转移失败)
| 项目 | 内容 |
|---|---|
| 现象 | 主库已宕机,但客户端仍指向旧主库;SENTINEL masters 显示 s_down 却不发起 failover;或选出新主后从库不跟随 |
| 原因 | ① 哨兵数量不足或 quorum 设置不合理(如只部署 1 个哨兵,或 quorum 大于存活哨兵数);② 哨兵之间互相不通(哨兵端口 26379 被防火墙拦住);③ 未配 sentinel auth-pass,哨兵连不上有密码的实例;④ 未配 masterauth,切换后新主库的从库连不上;⑤ 所有从库都不可用(s_down/o_down),无候选主库;⑥ 从库 replica-priority 0(被排除在选举外);⑦ down-after-milliseconds 设得过大,还没判定下线;⑧ 上一次 failover 未超过 failover-timeout,处于冷却期;⑨ 哨兵配置文件不可写(哨兵需要重写配置文件保存状态);⑩ 哨兵与 Redis 实例部署在同一台机器且整机宕机 |
# 【排查】
# 1) 哨兵视角的主库状态
redis-cli -p 26379 SENTINEL masters
redis-cli -p 26379 SENTINEL master mymaster
name mymaster
ip 192.168.1.10
port 6379
flags s_down,o_down,master ← s_down=主观下线 o_down=客观下线
【只有 o_down 才会触发 failover】
num-slaves 2
num-other-sentinels 2 ← 【关键】其他哨兵数量,0 说明哨兵间不通
quorum 2 ← 达成客观下线所需的哨兵票数
down-after-milliseconds 5000
failover-timeout 60000
# 2) 哨兵之间是否互相发现(【最常见的失败原因】)
redis-cli -p 26379 SENTINEL sentinels mymaster
# 返回为空 = 哨兵之间完全不通,此时永远无法达成客观下线
# 3) 从库候选情况
redis-cli -p 26379 SENTINEL replicas mymaster # 5.0+
redis-cli -p 26379 SENTINEL slaves mymaster # 4.x 及之前
# 关注每个从库的 flags 是否为 slave(而非 s_down,slave)
# 4) 哨兵日志(决策过程全在这里)
tail -100 /var/log/redis/sentinel.log
# 关键日志及含义:
# "+sdown master mymaster" → 本哨兵判定主观下线
# "+odown master mymaster #quorum 2/2" → 达成客观下线,即将开始 failover
# "+try-failover master mymaster" → 尝试发起故障转移
# "+vote-for-leader <runid> 1" → 投票选举 leader 哨兵
# "+failover-state-select-slave" → 正在挑选新主库
# "-failover-abort-no-good-slave" → 【没有合适的从库,转移中止】
# "+switch-master mymaster 192.168.1.10 6379 192.168.1.11 6380" → 切换成功
# "-failover-abort-not-elected" → 本哨兵没被选为 leader(正常,由别的哨兵执行)
# "NOAUTH Authentication required" → 缺 sentinel auth-pass
# 5) 哨兵间网络与端口
for h in 192.168.1.10 192.168.1.11 192.168.1.12; do
echo -n "$h:26379 -> "; nc -zv -w 2 $h 26379 && echo OK || echo FAIL
done
# CentOS 7 放开哨兵端口
firewall-cmd --permanent --add-port=26379/tcp; firewall-cmd --reload
# CentOS 6
iptables -I INPUT -p tcp --dport 26379 -j ACCEPT; service iptables save
# 6) 哨兵配置文件是否可写(哨兵必须能重写自己的配置)
ls -l /etc/redis/sentinel.conf
# 应属主为 redis 且可写;只读会导致哨兵无法持久化 leader/epoch 状态
chown redis:redis /etc/redis/sentinel.conf
chmod 640 /etc/redis/sentinel.conf
# 【解决】
# 1) 哨兵必须【至少 3 个且部署在 3 台不同机器上】,quorum 建议 2
# 原因:failover 需要【超过半数哨兵】投票选出 leader,
# 1 个哨兵无法容错,2 个哨兵挂 1 个就无法过半
# quorum=1 的隐患:网络抖动就会误切换(脑裂风险)
# quorum > 存活哨兵数:永远无法达成客观下线(不切换)
# 2) 补齐认证配置
redis-cli -p 26379 SENTINEL SET mymaster auth-pass 'MasterPass'
# 6.0+ 还可指定用户名(配合 ACL)
redis-cli -p 26379 SENTINEL SET mymaster auth-user 'sentinel-user'
# 3) 调整判定与超时参数
redis-cli -p 26379 SENTINEL SET mymaster down-after-milliseconds 5000
redis-cli -p 26379 SENTINEL SET mymaster failover-timeout 60000
redis-cli -p 26379 SENTINEL SET mymaster parallel-syncs 1
redis-cli -p 26379 SENTINEL SET mymaster quorum 2
# 4) 检查从库优先级(0 表示永不被选为主库)
redis-cli -h 192.168.1.11 -p 6380 CONFIG GET replica-priority
redis-cli -h 192.168.1.11 -p 6380 CONFIG SET replica-priority 100
# 4.x 及之前:slave-priority
# 5) 重置哨兵状态(清除对该 master 的全部记忆并重新发现拓扑)
redis-cli -p 26379 SENTINEL RESET mymaster
# 6) 【应急】手动强制切换(不等自动判定)
redis-cli -p 26379 SENTINEL FAILOVER mymaster
# 【注意】该命令不需要其他哨兵同意,会立即执行,仅用于计划内维护或应急
# 7) 验证切换结果
redis-cli -p 26379 SENTINEL get-master-addr-by-name mymaster
# 1) "192.168.1.11"
# 2) "6380"
redis-cli -h 192.168.1.11 -p 6380 INFO replication | grep role # role:master
哨兵完整配置(三节点部署,每台一份):
# /etc/redis/sentinel.conf
port 26379
daemonize yes
pidfile /var/run/redis-sentinel.pid
logfile /var/log/redis/sentinel.log
dir /var/lib/redis-sentinel
# 【必须】绑定内网地址,不要用 0.0.0.0
bind 192.168.1.10 127.0.0.1
protected-mode no # 3.2+;哨兵间需互访,配合防火墙白名单使用
# 监控的主库:<主库别名> <IP> <端口> <quorum>
# quorum = 判定客观下线所需的哨兵票数,3 哨兵建议 2
sentinel monitor mymaster 192.168.1.10 6379 2
# 连接 Redis 实例的密码(主从密码必须一致)
sentinel auth-pass mymaster MasterPass
# sentinel auth-user mymaster sentinel-user # 6.0+ 配合 ACL
# 多久收不到 PING 判定主观下线(毫秒)
sentinel down-after-milliseconds mymaster 5000
# 故障转移后同时向新主库发起同步的从库数量
# 1 = 逐个同步(对新主库压力小,但恢复慢)
sentinel parallel-syncs mymaster 1
# 单次故障转移的超时时间(也是同一 master 两次 failover 的冷却时间)
sentinel failover-timeout mymaster 60000
# 哨兵自身的访问密码(5.0+)
# requirepass SentinelPass
# 拒绝危险命令(哨兵也应加固)
sentinel deny-scripts-reconfig yes # 6.0+,禁止运行时改脚本路径
# 【启动哨兵的两种等价方式】
redis-sentinel /etc/redis/sentinel.conf
redis-server /etc/redis/sentinel.conf --sentinel
# 【CentOS 7】systemd 托管
systemctl start redis-sentinel
systemctl enable redis-sentinel
systemctl status redis-sentinel
# 【CentOS 6】SysV
service redis-sentinel start
chkconfig redis-sentinel on
# 停止哨兵
redis-cli -p 26379 SHUTDOWN # 推荐(优雅退出)
# 若哨兵设了 requirepass
redis-cli -p 26379 -a 'SentinelPass' --no-auth-warning SHUTDOWN
# 【不推荐】pkill -9 redis-sentinel —— 会丢失哨兵状态文件的最新写入
哨兵会自动改写配置文件:failover 后哨兵会把
sentinel monitor行里的 IP 改成新主库,并追加sentinel known-replica、sentinel known-sentinel、sentinel current-epoch等条目。因此:① 配置文件必须可写;② 不要用配置管理工具(Ansible/Puppet)反复覆盖哨兵配置文件,否则会把哨兵的状态记忆冲掉;③ 同理,Redis 实例的replicaof行也会被哨兵改写。
12.4 集群类故障
12.4.1 CLUSTERDOWN Hash slot not served
| 项目 | 内容 |
|---|---|
| 现象 | (error) CLUSTERDOWN Hash slot not served或 (error) CLUSTERDOWN The cluster is down |
| 原因 | ① 有槽位没有被任何节点负责(新建集群忘记分配槽、reshard 中断、节点被误删);② 某主节点宕机且没有从节点可以接管;③ cluster-require-full-coverage yes(默认)时,只要有一个槽不可用,整个集群就拒绝所有请求;④ 集群节点间通信中断(集群总线端口 端口+10000 被防火墙拦截);⑤ 超过半数主节点失联,集群进入 fail 状态 |
# 【排查】
# 1) 集群整体状态
redis-cli -c -p 7000 CLUSTER INFO
cluster_enabled:1
cluster_state:fail ← ok 才正常
cluster_slots_assigned:14562 ← 【必须是 16384】
cluster_slots_ok:14562
cluster_slots_pfail:0
cluster_slots_fail:1822 ← 有 1822 个槽处于 fail 状态
cluster_known_nodes:6
cluster_size:3 ← 负责槽的主节点数
cluster_current_epoch:12
cluster_my_epoch:6
# 2) 节点与槽位分布
redis-cli -c -p 7000 CLUSTER NODES
redis-cli -c -p 7000 CLUSTER SLOTS
redis-cli --cluster check 127.0.0.1:7000
redis-cli --cluster info 127.0.0.1:7000
# 3) 找出缺失的槽(--cluster check 会直接报告)
redis-cli --cluster check 127.0.0.1:7000 | grep -iE 'slot|cover|error|warn'
[ERR] Not all 16384 slots are covered by nodes.
[WARNING] Node 127.0.0.1:7002 has slots in importing state (5461-6999).
[ERR] Nodes don't agree about configuration!
# 4) 集群总线端口连通性(【常被忽略】)
# Cluster 需要【两个端口】:数据端口 7000 + 总线端口 17000(端口+10000)
for p in 7000 7001 7002 17000 17001 17002; do
echo -n "port $p: "; ss -lntp | grep -q ":$p " && echo LISTEN || echo DOWN
done
# CentOS 6 用 netstat
netstat -lntp | grep -E '7000|17000'
# 5) 查看节点标记
redis-cli -c -p 7000 CLUSTER NODES | grep -E 'fail|pfail|noaddr|handshake'
# fail? = 疑似下线(pfail)
# fail = 已确认下线
# noaddr = 节点地址未知(需要 CLUSTER FORGET 清理)
# handshake = 正在握手中(长期停留说明总线不通)
# 【解决】
# 1) 修复槽位覆盖(自动,推荐)
redis-cli --cluster fix 127.0.0.1:7000
# 会自动处理 migrating/importing 状态残留、把未分配槽指派给合适的节点
# 2) 手动补分配缺失的槽
redis-cli -c -p 7002 CLUSTER ADDSLOTS {5461..6999}
# Redis 7.0+ 支持范围写法,更高效
redis-cli -c -p 7002 CLUSTER ADDSLOTSRANGE 5461 6999
# 3) 清理 importing/migrating 残留状态
redis-cli -c -p 7002 CLUSTER SETSLOT 5461 STABLE
# 4) 主节点宕机且无从节点时:手动提升一个从节点
redis-cli -c -p 7003 CLUSTER FAILOVER TAKEOVER
# FORCE = 不与原主库协商,但仍需过半主节点同意
# TAKEOVER = 【最强制】单方面提升自己,会自增 epoch,有数据丢失风险,仅用于灾难恢复
# 5) 【应急】允许部分槽不可用时继续提供服务
redis-cli -c -p 7000 CONFIG SET cluster-require-full-coverage no
# 效果:只有落在缺失槽上的 key 报错,其他槽正常服务(可用性优先)
# 【必须给所有节点都设置】
for p in 7000 7001 7002 7003 7004 7005; do
redis-cli -p $p CONFIG SET cluster-require-full-coverage no
redis-cli -p $p CONFIG REWRITE
done
# 6) 清理已彻底移除的节点
redis-cli -c -p 7000 CLUSTER FORGET <node-id>
# 【注意】必须在【所有其他节点】上都执行 FORGET,否则会被 gossip 重新发现
# 7) 放开总线端口
# CentOS 7
firewall-cmd --permanent --add-port=7000-7005/tcp
firewall-cmd --permanent --add-port=17000-17005/tcp
firewall-cmd --reload
# CentOS 6
iptables -I INPUT -p tcp -m multiport --dports 7000:7005,17000:17005 -j ACCEPT
service iptables save
cluster-require-full-coverage的取舍:默认yes是一致性优先(宁可整体不可用,也不提供不完整的数据视图);改成no是可用性优先。缓存场景建议改成no,避免一个分片故障拖垮全站;数据存储场景保持yes,让故障立刻暴露。
12.4.2 CROSSSLOT Keys in request don't hash to the same slot
| 项目 | 内容 |
|---|---|
| 现象 | (error) CROSSSLOT Keys in request don't hash to the same slot执行 MGET、MSET、DEL 多 key、SUNION、ZUNIONSTORE、Lua 脚本、事务时报错 |
| 原因 | Redis Cluster 要求单条命令涉及的所有 key 必须落在同一个槽。因为槽分布在不同节点上,跨节点的多 key 原子操作无法实现。CRC16(key) mod 16384 决定槽位,不同 key 通常落在不同槽 |
# 【排查】
# 查看每个 key 的槽位
redis-cli -c -p 7000 CLUSTER KEYSLOT user:1001:name
# (integer) 12043
redis-cli -c -p 7000 CLUSTER KEYSLOT user:1001:age
# (integer) 741 ← 不同槽,所以 MGET 报错
# 复现
redis-cli -c -p 7000 MGET user:1001:name user:1001:age
# (error) CROSSSLOT Keys in request don't hash to the same slot
# 查槽位归属哪个节点
redis-cli -c -p 7000 CLUSTER SLOTS | head -20
# 【解决】
# ===== 方案 1:使用 hash tag(推荐)=====
# 【原理】key 中用 {} 包裹的部分【单独参与 CRC16 计算】,
# {} 内容相同的 key 必然落在同一个槽
redis-cli -c -p 7000 CLUSTER KEYSLOT 'user:{1001}:name'
# (integer) 9842
redis-cli -c -p 7000 CLUSTER KEYSLOT 'user:{1001}:age'
# (integer) 9842 ← 相同槽
redis-cli -c -p 7000 MSET 'user:{1001}:name' zhangsan 'user:{1001}:age' 28
redis-cli -c -p 7000 MGET 'user:{1001}:name' 'user:{1001}:age'
# 1) "zhangsan"
# 2) "28"
# hash tag 规则细节:
# 只取【第一个 {】到【它之后第一个 }】之间的内容
# user:{1001}:{2002} → 只用 "1001"
# user:{}:1001 → {} 为空,退化为整个 key 参与计算
# user:{1001 → 没有闭合 },退化为整个 key
# {1001} → 用 "1001"
# 【hash tag 的代价】过度使用会造成【数据倾斜】:
# 把大量 key 都标成 {global} 会让它们全挤到一个槽/节点上,
# 丧失分片的意义。tag 应该用业务实体 ID(用户 ID、订单 ID),保证分散性
# ===== 方案 2:拆成多次单 key 操作 =====
for k in user:1001:name user:1001:age user:1001:city; do
echo "$k = $(redis-cli -c -p 7000 GET $k)"
done
# 客户端库(Jedis/Lettuce/redis-py-cluster)通常会自动做这件事:
# 把 MGET 拆成按节点分组的多个请求,再合并结果(性能不如单槽 MGET,但可用)
# ===== 方案 3:改用 hash 结构把相关字段收进一个 key =====
redis-cli -c -p 7000 HSET user:1001 name zhangsan age 28 city Beijing
redis-cli -c -p 7000 HMGET user:1001 name age city
# 单个 key = 单个槽,天然没有 CROSSSLOT 问题,且内存更省(见容量规划)
# ===== 方案 4:Lua 脚本必须声明 KEYS 且同槽 =====
redis-cli -c -p 7000 EVAL "return {redis.call('GET',KEYS[1]),redis.call('GET',KEYS[2])}" \
2 'user:{1001}:name' 'user:{1001}:age'
# 【集群下的 Lua 硬性要求】:
# ① 所有 key 必须通过 KEYS 数组传入(不能在脚本里硬编码 key 名)
# ② 所有 KEYS 必须属于同一个槽,否则报 CROSSSLOT
# ③ 7.0+ 若脚本访问了未声明的 key,会报
# "Script attempted to access a non local key in a cluster node"
受 CROSSSLOT 限制的常见命令:
| 类别 | 命令 | 集群下的处理方式 |
|---|---|---|
| 多 key 读 | MGET、EXISTS k1 k2、TOUCH |
hash tag 或客户端拆分 |
| 多 key 写 | MSET、MSETNX、DEL k1 k2、UNLINK |
hash tag 或逐个执行 |
| 集合运算 | SINTER、SUNION、SDIFF、*STORE |
必须 hash tag |
| 有序集合 | ZUNIONSTORE、ZINTERSTORE、ZDIFFSTORE |
必须 hash tag |
| 列表搬移 | RPOPLPUSH、LMOVE、BLMOVE |
必须 hash tag |
| 复制/重命名 | RENAME、RENAMENX、COPY |
必须 hash tag |
| 位运算 | BITOP |
必须 hash tag |
| 事务 | MULTI/EXEC 内的所有命令 |
所有 key 必须同槽 |
| 脚本 | EVAL/EVALSHA/FCALL |
所有 KEYS 必须同槽 |
12.4.3 MOVED 与 ASK 报错
| 项目 | 内容 |
|---|---|
| 现象 | (error) MOVED 12043 192.168.1.11:7001或 (error) ASK 5461 192.168.1.12:7002 |
| 原因 | 这不是集群故障,而是客户端没有以集群模式访问。① redis-cli 忘了加 -c 参数(最常见);② 应用用了单机客户端(Jedis 而非 JedisCluster、StrictRedis 而非 RedisCluster)连集群;③ 客户端槽位缓存过期(reshard 后未刷新拓扑) |
# 【排查与解决】
# 1) 不带 -c 的表现
redis-cli -p 7000 GET user:1001
# (error) MOVED 12043 192.168.1.11:7001 ← 报错并给出正确节点地址
# 2) 加 -c 后 redis-cli 会【自动重定向】
redis-cli -c -p 7000 GET user:1001
# -> Redirected to slot [12043] located at 192.168.1.11:7001
# "zhangsan"
# 3) 集群批量操作也要加 -c
redis-cli -c -p 7000 --scan --pattern 'user:*'
MOVED 与 ASK 的本质区别(面试高频):
| 对比项 | MOVED |
ASK |
|---|---|---|
| 含义 | 该槽已经永久属于目标节点 | 该槽正在迁移中,这个 key 已经搬过去了 |
| 发生时机 | 客户端槽位映射过期,或本来就没建立映射 | reshard 进行中,槽处于 migrating/importing 状态 |
| 客户端动作 | 更新本地槽位缓存,之后该槽的请求都直发新节点 | 只对本次请求重定向,不更新缓存 |
| 后续协议 | 直接向新节点发命令 | 必须先发 ASKING 命令,再发真正的命令 |
| 是否需处理 | 客户端库自动处理 | 客户端库自动处理 |
# 观察 ASK 的产生(reshard 过程中)
redis-cli -p 7002 CLUSTER SETSLOT 5461 IMPORTING <源节点ID>
redis-cli -p 7001 CLUSTER SETSLOT 5461 MIGRATING <目标节点ID>
# 此时访问已迁移的 key 会返回 ASK
# 手动模拟客户端行为
redis-cli -p 7002 ASKING
redis-cli -p 7002 GET somekey
# 各语言客户端的正确用法
# Python: from redis.cluster import RedisCluster
# rc = RedisCluster(host='192.168.1.10', port=7000, password='xxx')
# Java: JedisCluster jc = new JedisCluster(nodes, ...);
# 或 Lettuce RedisClusterClient
# Go: redis.NewClusterClient(&redis.ClusterOptions{Addrs: []string{...}})
# PHP: new RedisCluster(NULL, ['192.168.1.10:7000', ...])
看到
MOVED先检查客户端类型,而不是去改集群配置。集群模式下单机客户端只能访问恰好落在它所连节点上的 key,大约只有 1/N 的成功率,表现为"时好时坏",极易误判为集群不稳定。
12.5 内存类故障
12.5.1 内存持续增长的三种原因
| 项目 | 内容 |
|---|---|
| 现象 | used_memory 只涨不跌,最终触发 OOM 或被系统 OOM Killer 杀掉 |
| 原因 | ① 数据本身在增长(业务量上涨、key 未设 TTL、大 key 膨胀);② 客户端缓冲区占用(输出缓冲区、输入缓冲区、复制缓冲区);③ 内存碎片(used_memory_rss 远大于 used_memory) |
# 【排查】三步定位到底是哪一类
# ===== 第 1 步:看内存构成 =====
redis-cli INFO memory
used_memory_human:5.20G ← Redis 自己统计的逻辑内存
used_memory_rss_human:8.10G ← 操作系统看到的物理内存(含碎片)
used_memory_peak_human:8.50G ← 历史峰值
used_memory_dataset:4.10G ← 【纯数据占用】
used_memory_overhead:1.10G ← 【管理开销:客户端缓冲、AOF 缓冲、复制缓冲等】
used_memory_lua_human:0.05M
used_memory_scripts_human:2.00K
mem_fragmentation_ratio:1.56 ← rss/used,> 1.5 说明碎片明显
mem_allocator:jemalloc-5.3.0
maxmemory_human:6.00G
maxmemory_policy:noeviction
mem_clients_normal:820.00M ← 【6.0+】普通客户端占用
mem_clients_slaves:210.00M ← 【6.0+】从库连接占用
mem_replication_backlog:64.00M
mem_aof_buffer:12.00M
# ===== 第 2 步:判断是数据还是开销 =====
# 若 used_memory_dataset 占比高 → 原因① 数据增长
# 若 used_memory_overhead 占比高 → 原因② 缓冲区
redis-cli MEMORY STATS | head -40
redis-cli MEMORY DOCTOR
redis-cli INFO keyspace
redis-cli DBSIZE
redis-cli --bigkeys
redis-cli --memkeys # 6.0+
# 统计无 TTL 的 key 比例
redis-cli INFO keyspace
# db0:keys=15234000,expires=120,avg_ttl=0 ← expires 远小于 keys = 大量 key 永不过期
# ===== 第 3 步:查客户端缓冲区 =====
redis-cli INFO clients
# connected_clients:3200
# client_recent_max_input_buffer:20480
# client_recent_max_output_buffer:335544320 ← 【320MB 的输出缓冲区,异常】
# blocked_clients:12
redis-cli CLIENT LIST | awk '{print $1,$2,$14,$15,$16,$17,$18}' | head -20
# 关注 qbuf(输入缓冲)、qbuf-free、obl(输出缓冲固定部分)、oll(输出列表长度)、omem(输出内存)
redis-cli CLIENT LIST | awk -F'omem=' '{print $2}' | awk '{print $1}' | sort -rn | head -5
# 【解决】按原因分类
# ===== 原因①:数据增长 =====
# 给忘记设 TTL 的 key 补上
redis-cli --scan --pattern 'cache:*' | while read k; do
[ "$(redis-cli TTL "$k")" = "-1" ] && redis-cli EXPIRE "$k" 3600
done
# 清理无用前缀
redis-cli --scan --pattern 'temp:*' | xargs -L 100 redis-cli UNLINK
# 治理大 key(拆分/压缩/改用 Stream + MAXLEN)
# 开启淘汰策略兜底
redis-cli CONFIG SET maxmemory-policy allkeys-lru
# ===== 原因②:客户端缓冲区 =====
# 限制普通客户端输出缓冲区(默认 0 = 不限制,【这是个隐患】)
redis-cli CONFIG SET client-output-buffer-limit "normal 256mb 64mb 60"
redis-cli CONFIG SET client-output-buffer-limit "replica 512mb 128mb 60"
redis-cli CONFIG SET client-output-buffer-limit "pubsub 32mb 8mb 60"
redis-cli CONFIG REWRITE
# 格式:<class> <hard limit> <soft limit> <soft seconds>
# hard limit = 一旦超过立即断开连接
# soft limit = 持续超过 soft seconds 秒才断开
# 0 0 0 = 不限制
# 限制单条请求大小(防止超大 value 撑爆输入缓冲区)
redis-cli CONFIG SET proto-max-bulk-len 256mb # 默认 512mb
# 杀掉异常客户端
redis-cli CLIENT KILL ADDR 192.168.1.99:52341
redis-cli CLIENT KILL LADDR 192.168.1.10:6379 # 6.2+ 按本地地址杀
redis-cli CLIENT KILL MAXAGE 3600 # 7.4+ 杀掉存活超过 1 小时的连接
# 找出并杀掉执行 MONITOR 的连接(MONITOR 会让输出缓冲区疯长)
redis-cli CLIENT LIST | grep 'cmd=monitor'
# 控制复制积压缓冲区
redis-cli CONFIG SET repl-backlog-size 64mb
redis-cli CONFIG SET repl-backlog-ttl 3600 # 无从库连接时多久释放 backlog
# ===== 原因③:内存碎片 → 见 12.5.2 =====
三种原因的判定速查:
| 判断依据 | 结论 | 处理方向 |
|---|---|---|
used_memory_dataset 占 used_memory 90%+ 且 DBSIZE 在涨 |
数据增长 | TTL、淘汰策略、大 key 治理、扩容 |
used_memory_overhead 占比高、mem_clients_normal 大 |
客户端缓冲区 | client-output-buffer-limit、杀连接、查 MONITOR/KEYS |
used_memory 平稳但 used_memory_rss 涨、mem_fragmentation_ratio > 1.5 |
内存碎片 | activedefrag、MEMORY PURGE、重启 |
mem_fragmentation_ratio < 1 |
已在用 swap(危险) | 降 maxmemory、vm.swappiness=1、扩内存 |
12.5.2 内存碎片率过高
| 项目 | 内容 |
|---|---|
| 现象 | mem_fragmentation_ratio 大于 1.5(严重时超过 2);used_memory 只有 5GB 但 used_memory_rss 达到 10GB;机器内存告警但 Redis 逻辑内存不高 |
| 原因 | ① 大量写入后又大量删除,jemalloc 释放的内存块无法归还给操作系统(最主要原因);② value 大小变化剧烈(如 hash 频繁增删字段),内存分配器的固定规格块(size class)造成内部碎片;③ 使用了 libc 分配器而非 jemalloc;④ Redis 长期运行未重启 |
# 【排查】
redis-cli INFO memory | grep -E 'used_memory_human|used_memory_rss_human|mem_fragmentation_ratio|mem_allocator|allocator_frag'
used_memory_human:5.02G
used_memory_rss_human:10.30G
mem_fragmentation_ratio:2.05 ← 【严重碎片】浪费了 5GB
mem_allocator:jemalloc-5.3.0 ← 必须是 jemalloc 才能主动整理
allocator_frag_ratio:1.82 ← 4.0+,【分配器层面】的碎片率,更准确
allocator_frag_bytes:4123456789
allocator_resident:9.80G
allocator_active:8.90G
allocator_allocated:4.89G
redis-cli MEMORY DOCTOR
# Sam, I detected a few issues in this Redis instance memory implants:
# * High allocator fragmentation: This instance has an allocator external
# fragmentation greater than 1.1. This problem is usually due to a large peak
# memory (...). Try the MEMORY PURGE command...
redis-cli CONFIG GET activedefrag
redis-cli INFO memory | grep -E 'active_defrag_running'
redis-cli INFO stats | grep -E 'active_defrag_hits|active_defrag_misses|active_defrag_key_hits'
ps -o rss= -p $(pgrep -f 'redis-server.*6379') | awk '{printf "RSS: %.2f GB\n", $1/1024/1024}'
mem_fragmentation_ratio 的完整解读:
| 取值 | 含义 | 处理 |
|---|---|---|
< 1 |
RSS 小于逻辑内存 = 部分数据被换到了 swap | 最危险的情况。Redis 单线程访问 swap 页会导致毫秒级甚至秒级延迟。立即降 maxmemory、设 vm.swappiness=1、扩内存 |
1 ~ 1.5 |
正常范围 | 无需处理 |
> 1.5 |
碎片明显,浪费超过 50% 内存 | 开 activedefrag 或 MEMORY PURGE |
> 2 |
严重碎片 | 主动整理;无效则从库切换后重启 |
| 异常大(如 10+) | 通常是实例刚启动且数据极少(分母太小),不是真碎片 | 忽略,等数据量上来再看 |
# 【解决】
# ===== 方案 1:主动碎片整理(4.0+,推荐)=====
# 【前提】必须使用 jemalloc 分配器(源码编译默认就是;用 make MALLOC=libc 编译的不支持)
redis-cli INFO memory | grep mem_allocator # 确认含 jemalloc
redis-cli CONFIG SET activedefrag yes
redis-cli CONFIG REWRITE
# 观察整理进度
watch -n 2 "redis-cli INFO memory | grep -E 'frag|active_defrag_running'"
# ===== activedefrag 完整参数(4.0+)=====
activedefrag yes # 总开关,默认 no
# 【触发条件】必须【同时满足】以下两个阈值才开始整理
active-defrag-ignore-bytes 100mb # 碎片字节数下限(低于此值不整理)
active-defrag-threshold-lower 10 # 碎片率下限百分比(10%)
# 【全力整理阈值】碎片率超过此值时使用 cycle-max 的 CPU
active-defrag-threshold-upper 100 # 碎片率 100% 时全力整理
# 【CPU 占用控制】—— 【关键调优项】
active-defrag-cycle-min 1 # 最小 CPU 占用百分比(老版本默认 25,太高)
active-defrag-cycle-max 25 # 最大 CPU 占用百分比(老版本默认 75,太高)
# 单次扫描 hash/set/zset/list 的最大字段数(避免长时间阻塞)
active-defrag-max-scan-fields 1000
# 【生产环境建议的保守配置】
redis-cli CONFIG SET active-defrag-ignore-bytes 500mb
redis-cli CONFIG SET active-defrag-threshold-lower 20
redis-cli CONFIG SET active-defrag-threshold-upper 80
redis-cli CONFIG SET active-defrag-cycle-min 1
redis-cli CONFIG SET active-defrag-cycle-max 20
redis-cli CONFIG SET activedefrag yes
redis-cli CONFIG REWRITE
# 【重要】碎片整理是【在主线程中进行的】,会挤占命令处理时间,
# cycle-max 设太大会导致延迟上升。建议在业务低峰期开启并观察 LATENCY
# ===== 方案 2:MEMORY PURGE(4.0+)=====
# 让 jemalloc 把空闲页归还给操作系统。【只清空闲内存,不移动数据,不阻塞】
redis-cli MEMORY PURGE
redis-cli INFO memory | grep -E 'used_memory_rss_human|mem_fragmentation_ratio'
# 【注意】对【外部碎片】(空闲页)有效,对【内部碎片】(块内浪费)无效
# ===== 方案 3:重启实例(最彻底,但需要停机)=====
# 【正确的零感知重启流程】:利用主从切换
# 1) 确认从库数据已同步
redis-cli -p 6380 INFO replication | grep -E 'master_link_status|slave_repl_offset'
# 2) 手动切换(哨兵环境)
redis-cli -p 26379 SENTINEL FAILOVER mymaster
# 3) 等切换完成后重启原主库(此时它已是从库,无业务流量)
systemctl restart redis
# 4) 观察碎片率恢复
redis-cli INFO memory | grep mem_fragmentation_ratio
# ===== 方案 4:预防性措施 =====
# 避免同一实例内 value 大小差异过大 → 按大小分实例存放
# 用 UNLINK 代替 DEL 删除大 key(异步释放,减少主线程阻塞)
redis-cli UNLINK bigkey1 bigkey2
# 开启惰性释放(4.0+)
redis-cli CONFIG SET lazyfree-lazy-eviction yes
redis-cli CONFIG SET lazyfree-lazy-expire yes
redis-cli CONFIG SET lazyfree-lazy-server-del yes
redis-cli CONFIG SET replica-lazy-flush yes
redis-cli CONFIG SET lazyfree-lazy-user-del yes # 6.0+,让 DEL 行为等同 UNLINK
redis-cli CONFIG SET lazyfree-lazy-user-flush yes # 6.2+,FLUSHALL/FLUSHDB 异步
redis-cli CONFIG REWRITE
used_memory_rss与maxmemory的关系陷阱:maxmemory限制的是used_memory(逻辑内存),不是used_memory_rss(物理内存)。碎片率 2.0 时,maxmemory 6gb实际会占用约 12GB 物理内存,极易触发系统 OOM Killer。这正是"maxmemory只设物理内存 50%~60%"的原因之一。用dmesg | grep -i 'killed process'可确认是否被 OOM Killer 杀过。
12.5.3 swap 导致的性能雪崩
| 项目 | 内容 |
|---|---|
| 现象 | QPS 骤降,平均延迟从 0.1ms 涨到几十甚至几百毫秒;mem_fragmentation_ratio < 1;free -h 显示 swap 被大量使用;CPU 的 %wa(iowait)升高 |
| 原因 | Redis 是单线程内存数据库,任何一次内存访问命中 swap,都会变成一次磁盘 I/O(微秒级 → 毫秒级,慢 1000 倍以上),且会阻塞后面所有命令。触发原因:① maxmemory 设置过大接近物理内存;② 同机跑了其他吃内存的进程;③ vm.swappiness 默认值偏高(CentOS 6 默认 60,CentOS 7 默认 30);④ fork 时 COW 造成内存瞬时翻倍 |
# 【排查】
# 1) 系统层面
free -h
vmstat 1 5
# 关注 si(swap in)/ so(swap out)列,非 0 就说明正在换页
sar -B 1 5 # 需 sysstat 包
top -bn1 | head -5 # 看 %wa
# 2) 【精确确认 Redis 进程用了多少 swap】(关键排查命令)
PID=$(pgrep -f 'redis-server.*6379')
grep -E '^(VmSwap|VmRSS)' /proc/$PID/status
# VmRSS: 5242880 kB
# VmSwap: 819200 kB ← 【非 0 就是问题】800MB 被换出
# 更详细:按内存区域看 swap 分布
grep -i swap /proc/$PID/smaps | awk '{sum+=$2} END {printf "Total Swap: %.2f MB\n", sum/1024}'
# 3) Redis 层面
redis-cli INFO memory | grep -E 'used_memory_human|used_memory_rss_human|mem_fragmentation_ratio'
# mem_fragmentation_ratio:0.65 ← 【小于 1 = 正在用 swap】
redis-cli --latency -h 127.0.0.1 -p 6379
redis-cli --intrinsic-latency 5 # 测系统固有延迟,swap 时会明显偏高
redis-cli LATENCY LATEST
redis-cli LATENCY DOCTOR
redis-cli SLOWLOG GET 10
# 4) 系统参数
cat /proc/sys/vm/swappiness
swapon -s # 或 swapon --show
# 【解决】
# ===== 1) 【立即】降低 swappiness =====
sysctl -w vm.swappiness=1
echo 'vm.swappiness = 1' >> /etc/sysctl.conf
sysctl -p
# 【为什么是 1 而不是 0】:
# swappiness=0 在【3.5 内核之后】语义变为"绝不使用 swap",
# 内存不足时会【直接触发 OOM Killer 杀进程】,风险更大。
# swappiness=1 表示"尽量不用 swap,但极端情况下允许",是 Redis 官方推荐值
# ===== 2) 【应急】把已换出的数据换回内存 =====
# 【方式 A】临时关闭再开启 swap(【要求当前空闲内存 > swap 已用量,否则会 OOM】)
free -h # 先确认 available 足够
swapoff -a && swapon -a
# 验证
grep VmSwap /proc/$PID/status
# 【方式 B】重启 Redis(借助主从切换实现零感知,见 12.5.2 方案 3)
# ===== 3) 【根治】降低 maxmemory =====
redis-cli CONFIG SET maxmemory 4gb # 8GB 机器:开持久化设 50%,纯缓存设 70%
redis-cli CONFIG REWRITE
# ===== 4) 清理同机的其他内存消耗者 =====
ps aux --sort=-%mem | head -10
# Redis 生产实例应【独占机器】,不要与 MySQL/Java 应用/Elasticsearch 混部
# ===== 5) 保护 Redis 不被 OOM Killer 优先杀掉 =====
echo -1000 > /proc/$PID/oom_score_adj
cat /proc/$PID/oom_score
# 【CentOS 7】写进 systemd unit(永久生效)
mkdir -p /etc/systemd/system/redis.service.d
cat > /etc/systemd/system/redis.service.d/oom.conf <<'EOF'
[Service]
OOMScoreAdjust=-900
EOF
systemctl daemon-reload
systemctl restart redis
# 【CentOS 6】在 /etc/init.d/redis 启动后追加:
# echo -900 > /proc/$(cat /var/run/redis.pid)/oom_score_adj
# ===== 6) 完整的内核参数基线 =====
cat > /etc/sysctl.d/99-redis.conf <<'EOF'
vm.overcommit_memory = 1 # 允许过量分配,保证 fork 成功
vm.swappiness = 1 # 尽量不用 swap
net.core.somaxconn = 2048 # 提高 accept 队列,配合 redis.conf 的 tcp-backlog
net.ipv4.tcp_max_syn_backlog = 2048
net.ipv4.tcp_tw_reuse = 1
EOF
sysctl --system # CentOS 7
# 【CentOS 6】只支持 /etc/sysctl.conf + sysctl -p
能不能干脆关掉 swap? 生产上不建议完全关闭。保留少量 swap(如 2~4GB)配合
swappiness=1,可以在fork造成内存瞬时尖峰时提供缓冲,避免直接被 OOM Killer 杀死。完全关闭 swap + 内存吃紧 = 进程被直接杀掉,可用性更差。正确做法是:保留 swap,但通过maxmemory和swappiness保证它平时不被使用。
12.6 延迟与阻塞类故障
12.6.1 延迟突增的定位流程
| 项目 | 内容 |
|---|---|
| 现象 | 应用端出现 Redis 超时告警,平均响应时间从亚毫秒跳到几十/几百毫秒;表现为间歇性尖刺或持续变慢 |
| 原因 | 十类:慢命令、大 key、fork(持久化)、swap、AOF fsync、内存淘汰、网络、集中过期、碎片整理、CPU 竞争 |
# 【标准定位流程】按顺序执行,逐步收敛
# ===== 第 1 步:确认延迟是否真的在 Redis 侧 =====
redis-cli --latency -h 127.0.0.1 -p 6379
# min: 0, max: 1, avg: 0.11 (1438 samples) ← Redis 侧正常,问题在网络或应用
redis-cli --latency-history -i 5 # 每 5 秒一个区间,观察波动趋势
redis-cli --latency-dist # 6.0+,延迟分布彩色直方图
# ===== 第 2 步:测系统固有延迟(排除机器本身的问题)=====
redis-cli --intrinsic-latency 10
# Max latency so far: 1 microseconds. ← 正常应在几十微秒内
# 若这里就有几毫秒,说明是【宿主机/虚拟化/CPU 争抢】问题,与 Redis 无关
# ===== 第 3 步:看 Redis 自身的延迟监控(最有价值)=====
redis-cli CONFIG SET latency-monitor-threshold 100 # 记录耗时 >= 100ms 的事件
# 等待一段时间后
redis-cli LATENCY LATEST
1) 1) "command" ← 事件名:普通命令执行慢
2) (integer) 1724300000 ← 最近发生的时间戳
3) (integer) 312 ← 最近一次耗时(毫秒)
4) (integer) 890 ← 历史最大耗时
2) 1) "fork" ← fork 阻塞
2) (integer) 1724299800
3) (integer) 205
4) (integer) 480
3) 1) "expire-cycle" ← 过期清理周期阻塞
2) (integer) 1724299700
3) (integer) 118
4) (integer) 118
LATENCY LATEST 常见事件名及对应根因:
| 事件名 | 含义 | 排查方向 |
|---|---|---|
command |
命令执行本身慢 | SLOWLOG、大 key、KEYS/SMEMBERS 等 O(N) 命令 |
fast-command |
O(1)/O(logN) 命令也变慢 | 系统级问题:swap、CPU 争抢、THP |
fork |
fork 子进程耗时长 | 实例过大、THP 未关、虚拟化开销 |
rdb-unlink-temp-file |
删除临时 RDB 文件慢 | 磁盘 I/O 慢、文件过大 |
aof-write / aof-fsync-always |
AOF 写盘慢 | appendfsync always + 慢盘 |
aof-write-pending-fsync |
等待 fsync 完成 | 磁盘 I/O 打满 |
aof-rewrite-diff-write |
AOF 重写期间写差异数据慢 | 重写期间 I/O 争抢 |
expire-cycle |
过期 key 清理慢 | 大量 key 同时过期(见 12.6.3) |
eviction-del / eviction-cycle |
淘汰 key 慢 | 内存打满 + 大 key,开 lazyfree-lazy-eviction |
active-defrag-cycle |
碎片整理占用主线程 | 调低 active-defrag-cycle-max |
# ===== 第 4 步:慢日志(找出具体是哪条命令)=====
redis-cli CONFIG SET slowlog-log-slower-than 10000 # 10ms(单位微秒)
redis-cli CONFIG SET slowlog-max-len 1024
redis-cli SLOWLOG GET 20
redis-cli SLOWLOG LEN
1) 1) (integer) 42 ← 唯一 ID
2) (integer) 1724300012 ← 发生时间戳
3) (integer) 385291 ← 【耗时 385 毫秒】
4) 1) "KEYS" ← 【罪魁祸首】
2) "user:*"
5) "192.168.1.99:52341" ← 客户端地址(4.0+)
6) "" ← 客户端名(4.0+)
# ===== 第 5 步:命令维度统计(找出整体拖慢的命令)=====
redis-cli CONFIG RESETSTAT
sleep 60
redis-cli INFO commandstats | sort -t= -k3 -rn | head -15
# cmdstat_hgetall:calls=8231,usec=41155000,usec_per_call=5000.00
# ↑ 平均 5 毫秒/次,明显偏慢
redis-cli INFO latencystats # 7.0+,各命令延迟百分位(p50/p99/p99.9)
# ===== 第 6 步:排除系统与持久化因素 =====
grep VmSwap /proc/$(pgrep -f 'redis-server.*6379')/status # swap
cat /sys/kernel/mm/transparent_hugepage/enabled # THP 必须 never
redis-cli INFO stats | grep latest_fork_usec # fork 耗时
redis-cli INFO persistence | grep -E 'aof_enabled|rdb_bgsave_in_progress|aof_rewrite_in_progress'
redis-cli CONFIG GET appendfsync # always 会显著增加延迟
iostat -x 1 5 # 磁盘 %util / await
top -bn1 | grep -i redis # CPU 使用率
redis-cli INFO cpu
# ===== 第 7 步:网络与连接 =====
redis-cli INFO clients | grep -E 'connected_clients|blocked_clients'
redis-cli INFO stats | grep -E 'rejected_connections|total_net_output_bytes'
ss -s
ping -c 20 <redis主机> # 看网络 RTT 与丢包
redis-cli --stat -i 1 # 实时总览
延迟排查口诀:先分内外(
--latency与--intrinsic-latency判断是 Redis 还是机器)→ 再看事件(LATENCY LATEST定位类别)→ 最后抓命令(SLOWLOG+commandstats找具体语句)。绝大多数生产延迟问题在这三步内就能定位。
12.6.2 大 key 阻塞单线程
| 项目 | 内容 |
|---|---|
| 现象 | 特定业务操作时整个实例卡住,所有客户端同时超时;SLOWLOG 中出现 HGETALL、LRANGE、SMEMBERS、DEL、ZRANGE 等命令耗时数百毫秒 |
| 原因 | Redis 用单线程处理命令,一条 O(N) 命令在大 key 上执行时会独占主线程,期间所有其他客户端的请求全部排队等待。典型大 key:百万元素的 hash/set/zset、几十 MB 的 string、几十万元素的 list |
# 【排查】
# 1) 扫描大 key(【生产环境务必加 -i 限速】)
redis-cli --bigkeys -i 0.01
# -i 0.01 = 每扫 100 个 key 休眠 0.01 秒,降低对线上的影响
# 【原理】底层用 SCAN 遍历 + TYPE + 对应的长度命令(STRLEN/HLEN/LLEN/SCARD/ZCARD/XLEN)
# 【只统计元素个数/字符串长度,不统计实际内存占用】
[00.00%] Biggest string found so far '"config:template"' with 20971520 bytes
[15.32%] Biggest hash found so far '"user:profile:all"' with 1250000 fields
[42.18%] Biggest list found so far '"queue:orders"' with 830000 items
[78.90%] Biggest zset found so far '"rank:global"' with 2100000 members
-------- summary -------
Sampled 1523400 keys in the keyspace!
Biggest string found '"config:template"' has 20971520 bytes
Biggest hash found '"user:profile:all"' has 1250000 fields
Biggest list found '"queue:orders"' has 830000 items
Biggest set found '"online:users"' has 450000 members
Biggest zset found '"rank:global"' has 2100000 members
# 2) 按【实际内存】扫描(6.0+,比 --bigkeys 更准确)
redis-cli --memkeys -i 0.01
redis-cli --memkeys-samples 0 # 精确模式(不采样,更慢更准)
# 3) 查单个 key 的内存占用
redis-cli MEMORY USAGE user:profile:all
# (integer) 104857600 ← 100MB,严重大 key
redis-cli MEMORY USAGE user:profile:all SAMPLES 0 # 精确统计(大 key 上较慢)
# 4) 查 key 的类型、长度与编码
redis-cli TYPE user:profile:all
redis-cli HLEN user:profile:all
redis-cli OBJECT ENCODING user:profile:all # hashtable = 已超出紧凑编码阈值
redis-cli OBJECT FREQ user:profile:all # LFU 策略下的访问频率
redis-cli OBJECT IDLETIME user:profile:all # LRU 下的空闲秒数
redis-cli DEBUG OBJECT user:profile:all # serializedlength 等内部信息
# 5) 离线分析 RDB(【零线上影响,推荐用于定期巡检】)
redis-cli --rdb /tmp/dump-analyze.rdb
pip install rdbtools python-lzf
rdb -c memory /tmp/dump-analyze.rdb --bytes 10240 -f /tmp/bigkeys.csv
sort -t, -k4 -rn /tmp/bigkeys.csv | head -20
# 输出列:database,type,key,size_in_bytes,encoding,num_elements,len_largest_element
大 key 的判定标准(生产经验值):
| 类型 | 警戒线 | 危险线 | 说明 |
|---|---|---|---|
| String | 10KB | 1MB | 单值过大会撑爆网络带宽与输出缓冲区 |
| Hash / Set / Zset | 1000 元素 | 10000 元素 | HGETALL/SMEMBERS 全量取出时阻塞 |
| List | 1000 元素 | 10000 元素 | LRANGE 0 -1 是主要风险点 |
| 单 key 内存 | 1MB | 10MB | MEMORY USAGE 判断 |
# 【解决】
# ===== 1) 禁用/避免全量读取命令 =====
# 【错误】一次取出百万字段
redis-cli HGETALL user:profile:all
# 【正确】用 HSCAN 分批取
redis-cli HSCAN user:profile:all 0 COUNT 100
# 循环取完
cursor=0
while :; do
out=$(redis-cli HSCAN user:profile:all $cursor COUNT 100)
cursor=$(echo "$out" | head -1)
[ "$cursor" = "0" ] && break
done
# 同理:SSCAN(set)、ZSCAN(zset)、SCAN(key 空间)
# list 没有 LSCAN,用 LRANGE 分页:LRANGE key 0 99 / LRANGE key 100 199
# ===== 2) 拆分大 key(根本解法)=====
# hash 分片:user:profile:all → user:profile:{0..99}(按字段名 hash 取模)
# 伪代码:shard = crc32(field) % 100; HSET "user:profile:$shard" field value
# 大 list 改用 Stream 并限长
redis-cli XADD stream:orders MAXLEN '~' 100000 '*' order_id 5001 amount 99
# 大 zset 按时间/业务维度切分:rank:global → rank:2026-08 / rank:2026-07
# 大 string 压缩后存储,或只存对象存储的引用(OSS/S3 URL)
# ===== 3) 用 UNLINK 代替 DEL 删除大 key =====
# 【DEL 大 key 的危害】释放百万元素的内存是同步操作,可能阻塞【数百毫秒】
redis-cli UNLINK user:profile:all # 4.0+,后台线程异步释放,主线程只解引用
# 【4.0 之前】只能渐进式删除:用 HSCAN + HDEL 分批删字段,最后再 DEL
# 让 DEL 自动变成异步(6.0+)
redis-cli CONFIG SET lazyfree-lazy-user-del yes
# ===== 4) 开启全部惰性释放开关 =====
lazyfree-lazy-eviction yes # 4.0+,淘汰 key 时异步释放
lazyfree-lazy-expire yes # 4.0+,过期删除时异步释放
lazyfree-lazy-server-del yes # 4.0+,隐式删除(如 SET 覆盖旧值)时异步
replica-lazy-flush yes # 4.0+,从库全量同步前 flush 旧数据时异步
lazyfree-lazy-user-del yes # 6.0+,DEL 行为等同 UNLINK
lazyfree-lazy-user-flush yes # 6.2+,FLUSHALL/FLUSHDB 默认异步
# ===== 5) 屏蔽危险命令 =====
# redis.conf —— 生产环境建议
rename-command KEYS "" # 直接禁用
rename-command FLUSHALL ""
rename-command FLUSHDB ""
# ===== 6) 建立巡检机制 =====
# 每天凌晨用从库做大 key 扫描,避免影响主库
cat > /usr/local/bin/redis-bigkey-check.sh <<'EOF'
#!/bin/bash
LOG=/var/log/redis/bigkeys-$(date +%F).log
redis-cli -h 127.0.0.1 -p 6380 -a "$REDIS_PASS" --no-auth-warning \
--bigkeys -i 0.01 > "$LOG" 2>&1
grep -E 'Biggest' "$LOG" | mail -s "Redis 大 key 巡检 $(date +%F)" ops@example.com
EOF
chmod +x /usr/local/bin/redis-bigkey-check.sh
echo '30 3 * * * /usr/local/bin/redis-bigkey-check.sh' >> /var/spool/cron/root
大 key 的三重危害:① 阻塞——O(N) 命令独占单线程;② 网络——一次传输几十 MB 会打满带宽并撑大输出缓冲区;③ 倾斜——集群模式下大 key 让某个分片的内存与流量远高于其他分片,失去水平扩展的意义。大 key 治理是 Redis 运维中收益最高的优化项。
12.6.3 大量 key 同时过期导致的抖动
| 项目 | 内容 |
|---|---|
| 现象 | 每隔一段时间(常见于整点、整分)出现规律性延迟尖刺;LATENCY LATEST 中出现 expire-cycle 事件;INFO stats 的 expired_keys 在某个瞬间暴增 |
| 原因 | Redis 的惰性删除 + 定期删除双机制中,定期删除(activeExpireCycle)运行在主线程的 serverCron 中。当同一时刻有海量 key 过期时,定期删除任务会连续多轮扫描并删除,占用主线程时间。若过期的还是大 key,配合同步释放内存,阻塞会成倍放大 |
# 【排查】
# 1) 确认过期事件
redis-cli CONFIG SET latency-monitor-threshold 50
redis-cli LATENCY LATEST | grep -A3 expire
redis-cli LATENCY HISTORY expire-cycle
# 2) 观察过期数量的时间分布
watch -n 1 "redis-cli INFO stats | grep -E 'expired_keys|expired_stale_perc'"
# expired_keys 是【累计值】,要看【每秒增量】
prev=0
for i in $(seq 60); do
cur=$(redis-cli INFO stats | awk -F: '/^expired_keys:/{print $2+0}')
echo "$(date +%T) delta=$((cur - prev))"
prev=$cur
sleep 1
done
# 3) 查看 TTL 的分布(找出扎堆的过期时间点)
redis-cli --scan --pattern 'cache:*' | head -10000 | while read k; do
redis-cli TTL "$k"
done | sort -n | uniq -c | sort -rn | head -20
# 若某个 TTL 值对应上万个 key,就是典型的"过期扎堆"
# 4) 换算成绝对过期时刻,看是否集中在整点
redis-cli --scan --pattern 'cache:*' | head -5000 | while read k; do
t=$(redis-cli TTL "$k")
[ "$t" -gt 0 ] && date -d "+${t} seconds" '+%H:%M'
done | sort | uniq -c | sort -rn | head -10
# 5) 相关配置
redis-cli CONFIG GET hz
redis-cli CONFIG GET dynamic-hz
redis-cli CONFIG GET lazyfree-lazy-expire
redis-cli INFO stats | grep expired_subkeys_exposed
过期删除的两种机制:
| 机制 | 触发时机 | 特点 |
|---|---|---|
| 惰性删除(lazy) | 客户端访问该 key 时才检查 TTL 并删除 | 零额外开销,但永不被访问的过期 key 会一直占内存 |
| 定期删除(active) | serverCron 每秒执行 hz 次,每次随机采样 20 个带 TTL 的 key,删除其中已过期的;若过期比例超过 25%,立即重复本轮,直到低于 25% 或超时(默认单轮上限为 CPU 时间的 25%) |
保证内存能被回收,但海量 key 同时过期时会连续多轮循环,占用主线程 |
# 【解决】
# ===== 1) 【核心方案】给 TTL 加随机抖动(打散过期时间)=====
# 【错误】所有缓存都设固定 3600 秒 → 一小时后同时过期
redis-cli SET cache:user:1001 data EX 3600
# 【正确】基础时间 + 随机偏移
base=3600; jitter=$((RANDOM % 600)) # 0~600 秒随机
redis-cli SET cache:user:1001 data EX $((base + jitter))
# 应用侧同理:expire = base + random(0, base * 0.2)
# 【这同时也是缓存雪崩的标准解法】(见第九章 9.2)
# ===== 2) 开启过期时的异步释放(4.0+)=====
redis-cli CONFIG SET lazyfree-lazy-expire yes
redis-cli CONFIG REWRITE
# 效果:过期 key 的【内存释放】交给后台线程,主线程只做解引用
# ===== 3) 调整 hz(谨慎)=====
redis-cli CONFIG GET hz # 默认 10(每秒 10 次 serverCron)
# 调高 hz:过期清理更及时、单次工作量更小,但 CPU 开销上升
redis-cli CONFIG SET hz 20 # 建议范围 10~100,不要超过 500
# 开启动态 hz(5.0+,推荐):客户端多时自动提高频率
redis-cli CONFIG SET dynamic-hz yes
redis-cli CONFIG REWRITE
# ===== 4) 避免"大 key + TTL"组合 =====
# 一个百万字段的 hash 过期时,释放内存的开销远大于普通 key
# 应拆分成多个小 key,各自带随机 TTL
# ===== 5) 主动分批清理,替代依赖 TTL 集中过期 =====
# 低峰期用 SCAN + UNLINK 渐进式清理
redis-cli --scan --pattern 'cache:20260801:*' | xargs -L 200 redis-cli UNLINK
从库不会主动删除过期 key(见 12.3.2),所以过期抖动只发生在主库。这也带来一个优化思路:把只读流量放到从库,主库的过期清理抖动就不会直接影响读请求。
12.6.4 KEYS 命令拖死生产库
| 项目 | 内容 |
|---|---|
| 现象 | 某个时刻整个实例完全无响应,持续数秒到数十秒;期间所有客户端超时;恢复后 SLOWLOG 中出现一条 KEYS *,耗时数千毫秒 |
| 原因 | KEYS pattern 是 O(N) 命令,会遍历整个 key 空间且不可中断。100 万 key 约需 100~300 毫秒,1000 万 key 可达数秒。加之它一次性把所有匹配结果放进输出缓冲区,还会造成内存尖峰与网络拥塞。这是新手最容易犯、后果最严重的操作 |
# 【排查】
redis-cli SLOWLOG GET 20 | grep -A2 -i keys
redis-cli INFO commandstats | grep -E 'cmdstat_keys|cmdstat_smembers|cmdstat_hgetall|cmdstat_flushall'
# cmdstat_keys:calls=3,usec=9821000,usec_per_call=3273666.67
# ↑ 平均 3.27 秒/次
redis-cli DBSIZE # key 总量,判断 KEYS 的危险程度
redis-cli LATENCY HISTORY command
# 找出是哪个客户端执行的(SLOWLOG 第 5 个字段,4.0+)
redis-cli SLOWLOG GET 20
# 【解决】
# ===== 1) 【必做】禁用或重命名 KEYS =====
# /etc/redis/redis.conf
rename-command KEYS "" # 空字符串 = 完全禁用
# 或重命名为只有运维知道的名字(保留应急能力)
# rename-command KEYS KEYS_ONLY_FOR_OPS_9f3a2b
# 【注意】rename-command 必须重启才生效,无法通过 CONFIG SET 动态修改
systemctl restart redis # CentOS 7
service redis restart # CentOS 6
# 验证
redis-cli KEYS '*'
# (error) ERR unknown command 'KEYS'
# ===== 2) 用 SCAN 替代 KEYS(【标准做法】)=====
# SCAN 的三大特性:
# ① 【游标式分批遍历】,每次只返回少量结果,单次时间复杂度接近 O(1)
# ② 【不阻塞】,多次调用之间其他命令可以正常执行
# ③ 【保证最终遍历到所有始终存在的 key】,但【可能重复返回】,
# 遍历过程中新增/删除的 key 不保证是否出现(弱一致性,业务需容忍)
redis-cli SCAN 0 MATCH 'user:*' COUNT 100
# 1) "17408" ← 【返回的游标】,下次传入它继续;返回 0 表示遍历结束
# 2) 1) "user:1001"
# 2) "user:1002"
# 完整遍历脚本
cursor=0
count=0
while :; do
result=$(redis-cli SCAN $cursor MATCH 'user:*' COUNT 500)
cursor=$(echo "$result" | head -1)
keys=$(echo "$result" | tail -n +2)
[ -n "$keys" ] && count=$((count + $(echo "$keys" | wc -l)))
[ "$cursor" = "0" ] && break
done
echo "匹配到 $count 个 key"
# 【最简写法】redis-cli 内置的 --scan(自动完成游标循环)
redis-cli --scan --pattern 'user:*'
redis-cli --scan --pattern 'user:*' | wc -l
redis-cli --scan --pattern 'session:*' | xargs -L 200 redis-cli UNLINK
# 【集群模式】要加 -c,且 --scan 只扫当前连接的节点,
# 需对【每个主节点】分别执行
for p in 7000 7001 7002; do
redis-cli -p $p --scan --pattern 'user:*'
done
# 【COUNT 参数的含义】是【每次迭代的工作量提示】,不是精确返回条数。
# COUNT 太小 → 往返次数多,总耗时长
# COUNT 太大 → 单次阻塞变长,失去 SCAN 的意义
# 生产建议 100~1000
# ===== 3) 相关的 SCAN 家族命令 =====
redis-cli SCAN 0 TYPE string COUNT 100 # 6.0+,按类型过滤
redis-cli HSCAN user:profile 0 COUNT 50 # 遍历 hash 字段
redis-cli SSCAN online:users 0 COUNT 50 # 遍历 set 成员
redis-cli ZSCAN rank:global 0 COUNT 50 # 遍历 zset 成员
redis-cli HSCAN user:profile 0 NOVALUES # 7.4+,只返回字段名不返回值
# ===== 4) 从设计上避免"需要遍历 key" =====
# 用集合维护索引,把"找出所有 user:*"变成"读一个 set"
redis-cli SADD idx:users user:1001 user:1002
redis-cli SMEMBERS idx:users # 【注意】这个 set 大了也是大 key,需分片
# 用 Hash 按业务分组,用 Stream/List 做队列,都比遍历 key 空间更好
# ===== 5) 若必须使用,在从库上执行 =====
redis-cli -h 192.168.1.11 -p 6380 --scan --pattern 'user:*'
必须警惕的 O(N) / 阻塞类命令清单:
| 命令 | 复杂度 | 危险点 | 替代方案 |
|---|---|---|---|
KEYS pattern |
O(N) | 遍历全库,最危险 | SCAN |
FLUSHALL / FLUSHDB |
O(N) | 清空数据 + 同步释放内存 | 加 ASYNC(4.0+),或 rename-command 禁用 |
SMEMBERS |
O(N) | 大 set 全量返回 | SSCAN |
HGETALL / HKEYS / HVALS |
O(N) | 大 hash 全量返回 | HSCAN、HMGET 指定字段 |
LRANGE key 0 -1 |
O(N) | 大 list 全量返回 | LRANGE 分页 |
ZRANGE key 0 -1 |
O(N) | 大 zset 全量返回 | ZSCAN、ZRANGE 分页 |
SORT |
O(N log N) | CPU 密集 | 应用侧排序,或用 zset |
SINTERSTORE / SUNIONSTORE |
O(N*M) | 大集合运算 | 拆小、离线计算 |
DEL 大 key |
O(N) | 同步释放内存 | UNLINK(4.0+) |
MONITOR |
持续 | 吞吐下降 50%+ | SLOWLOG、--stat,用完立刻退出 |
DEBUG SLEEP |
阻塞 | 直接阻塞主线程 | 仅测试环境使用 |
SAVE |
O(N) | 同步落盘,全程阻塞 | BGSAVE |
SCRIPT/EVAL 长脚本 |
取决于脚本 | 脚本执行期间独占主线程 | 拆分脚本,设 busy-reply-threshold |
*`KEYS
的教学价值**:它是理解"Redis 单线程"这一核心特性的最佳反面案例。**任何一条命令慢,就是整个实例慢**——这与 MySQL 中"一条慢查询只影响一个连接"完全不同。**生产环境的 Redis 必须在配置层面禁用KEYS、FLUSHALL、FLUSHDB`**,不能只依赖人的自觉。
12.7 连接数与客户端类故障
12.7.1 连接数打满
| 项目 | 内容 |
|---|---|
| 现象 | 客户端报 ERR max number of clients reached(老版本文案为 max number of clients reached);新连接全部失败但已有连接正常;INFO stats 的 rejected_connections 持续增长 |
| 原因 | ① maxclients 设置过小(Redis 2.x 默认仅 128,3.0+ 默认 10000);② 系统文件描述符限制过低,导致 Redis 实际能用的 maxclients 被自动下调;③ 应用连接池配置不当或连接泄漏(用完不归还);④ 短连接模式下 TIME_WAIT 堆积;⑤ 慢命令导致连接堆积;⑥ 客户端未设 timeout,空闲连接长期不释放 |
# 【排查】
# 1) 当前连接情况
redis-cli INFO clients
connected_clients:9998 ← 接近上限
cluster_connections:0 ← 7.0+
maxclients:10000
client_recent_max_input_buffer:20480
client_recent_max_output_buffer:0
blocked_clients:3 ← 阻塞在 BLPOP/BRPOP/XREAD 等命令的连接
tracking_clients:0
pubsub_clients:12 ← 7.4+
total_watched_keys:0
total_blocking_keys:0
redis-cli INFO stats | grep -E 'total_connections_received|rejected_connections'
# rejected_connections:8231 ← 【被拒绝的连接数,非 0 就是打满过】
redis-cli CONFIG GET maxclients
# 2) 【关键】Redis 启动时的实际生效值可能被系统限制下调,看日志
grep -iE 'maxclients|max open files|file descriptor' /var/log/redis/redis.log
# WARNING: You requested maxclients of 10000 requiring at least 10032 max file descriptors.
# Server can't set maximum open files to 10032 because of OS error: Operation not permitted.
# Current maximum open files is 4096. maxclients has been reduced to 4064 to compensate
# for low ulimit. If you need higher maxclients increase 'ulimit -n'.
# ↑ 【maxclients 被自动降到 4064】
# 3) 系统 fd 限制
ulimit -n # 当前 shell 的限制
PID=$(pgrep -f 'redis-server.*6379')
cat /proc/$PID/limits | grep -i 'open files' # 【Redis 进程实际的限制】
ls /proc/$PID/fd | wc -l # 当前已用 fd 数
cat /proc/sys/fs/file-max # 系统全局上限
cat /proc/sys/fs/file-nr # 已分配/空闲/上限
# 4) 分析连接来源,找出泄漏方
redis-cli CLIENT LIST | wc -l
redis-cli CLIENT LIST | awk '{print $2}' | awk -F'[=:]' '{print $2}' | sort | uniq -c | sort -rn | head -10
# 4820 192.168.1.55 ← 某台应用服务器占了近一半,可疑
# 按空闲时间排序,找出长期不活动的连接
redis-cli CLIENT LIST | awk -F'age=' '{print $2}' | awk '{print $1}' | sort -rn | head -5
redis-cli CLIENT LIST | grep -E 'idle=[0-9]{4,}' | head -10
# 按命令统计连接在干什么
redis-cli CLIENT LIST | awk -F'cmd=' '{print $2}' | awk '{print $1}' | sort | uniq -c | sort -rn
# 5) 系统侧连接状态
ss -s
ss -tan | awk '{print $1}' | sort | uniq -c
ss -tan 'sport = :6379' | awk '{print $1}' | sort | uniq -c
# CentOS 6 用 netstat
netstat -an | grep 6379 | awk '{print $6}' | sort | uniq -c
# 【解决】
# ===== 1) 【应急】杀掉空闲/异常连接 =====
# 杀掉指定地址的连接
redis-cli CLIENT KILL ADDR 192.168.1.55:52341
# 按 ID 杀(可批量,6.2+ 支持多个 ID)
redis-cli CLIENT LIST | awk -F'[= ]' '/idle=[0-9]{4,}/{print $2}' | while read id; do
redis-cli CLIENT KILL ID $id
done
# 杀掉存活超过 1 小时的连接(7.4+)
redis-cli CLIENT KILL MAXAGE 3600
# 按类型杀(normal/master/replica/pubsub)
redis-cli CLIENT KILL TYPE normal
# 杀掉除自己以外的所有普通连接(6.2+ 的 SKIPME)
redis-cli CLIENT KILL TYPE normal SKIPME yes
# ===== 2) 【必做】调大系统 fd 限制 =====
# 【CentOS 7 —— systemd 托管,ulimit/limits.conf 对服务【无效】,必须写 unit】
mkdir -p /etc/systemd/system/redis.service.d
cat > /etc/systemd/system/redis.service.d/limits.conf <<'EOF'
[Service]
LimitNOFILE=65535
LimitNPROC=65535
EOF
systemctl daemon-reload
systemctl restart redis
# 验证
cat /proc/$(pgrep -f 'redis-server.*6379')/limits | grep -i 'open files'
# Max open files 65535 65535 files
# 【CentOS 6 —— SysV init,用 limits.conf + 启动脚本内 ulimit】
cat >> /etc/security/limits.conf <<'EOF'
redis soft nofile 65535
redis hard nofile 65535
* soft nofile 65535
* hard nofile 65535
EOF
# 【关键】SysV 启动脚本中显式设置(因为 limits.conf 只对登录会话生效)
# 在 /etc/init.d/redis 的 start() 函数开头加入:
# ulimit -n 65535
# 【CentOS 6 还需检查】/etc/security/limits.d/90-nproc.conf 是否覆盖了设置
# 系统全局上限
sysctl -w fs.file-max=1000000
echo 'fs.file-max = 1000000' >> /etc/sysctl.conf
sysctl -p
# ===== 3) 调大 maxclients =====
redis-cli CONFIG SET maxclients 20000
redis-cli CONFIG REWRITE
# 【前提】fd 限制必须 >= maxclients + 32(Redis 自留 32 个 fd 用于
# 监听套接字、AOF/RDB 文件、集群总线、日志等)
# 若 fd 不够,Redis 会自动把 maxclients 降回来(见排查第 2 步的日志)
# ===== 4) 设置空闲连接超时(回收泄漏的连接)=====
redis-cli CONFIG SET timeout 300 # 300 秒无交互则断开;0 = 永不超时
redis-cli CONFIG REWRITE
# 【注意】timeout 【不会影响】阻塞命令(BLPOP 等)和主从复制连接
# ===== 5) 开启 TCP keepalive(清理半开连接)=====
redis-cli CONFIG SET tcp-keepalive 300 # 默认 300 秒;0 = 关闭
# 作用:网络异常(如客户端机器直接断电)导致的"僵死连接"能被内核探测并回收
# ===== 6) 【根治】修正应用连接池 =====
# 连接池配置原则:
# maxTotal(最大连接数)≈ 单实例 QPS 需求 / 单连接 QPS 能力,通常 20~50 足够
# maxIdle ≈ maxTotal 的 50%~80%
# minIdle ≈ 预热连接数,避免突发流量时才建连
# maxWaitMillis 必须设置(不设会无限等待,掩盖问题)
# testOnBorrow=false(每次借出都 PING 会增加一次 RTT),
# 改用 testWhileIdle=true 定期检测
# 【常见泄漏原因】
# ① 没在 finally 中归还连接(Jedis 需 jedis.close())
# ② 异常路径上直接 return,跳过了归还
# ③ 每次请求都 new 一个连接池对象
# ④ 用了短连接模式(每次操作建连再断开)
# ===== 7) 提高连接建立能力(应对突发大量建连)=====
redis-cli CONFIG GET tcp-backlog # 默认 511
# redis.conf
tcp-backlog 2048 # accept 队列长度
# 【必须同时调大系统参数】否则 redis.conf 的值会被截断到系统上限
sysctl -w net.core.somaxconn=2048
sysctl -w net.ipv4.tcp_max_syn_backlog=2048
cat >> /etc/sysctl.conf <<'EOF'
net.core.somaxconn = 2048
net.ipv4.tcp_max_syn_backlog = 2048
EOF
sysctl -p
# Redis 启动时若发现 somaxconn 过小会打印警告:
# WARNING: The TCP backlog setting of 511 cannot be enforced because
# /proc/sys/net/core/somaxconn is set to the lower value of 128.
# ===== 8) 缓解 TIME_WAIT 堆积(短连接场景)=====
sysctl -w net.ipv4.tcp_tw_reuse=1
sysctl -w net.ipv4.tcp_fin_timeout=30
# 【不要开启 tcp_tw_recycle】:在 NAT 环境下会导致连接被莫名拒绝,
# 且 Linux 4.12 之后已被移除
# ===== 9) 验证 =====
redis-cli INFO clients | grep -E 'connected_clients|maxclients'
redis-cli INFO stats | grep rejected_connections # 应停止增长
cat /proc/$(pgrep -f 'redis-server.*6379')/limits | grep -i 'open files'
maxclients的历史演变:Redis 2.x 时代默认值只有 128(见配置参数章节),超限时报max number of clients reached;3.0 之后默认提升到 10000。从老版本升级或沿用老配置模板时,必须检查这一项——128 个连接对现代应用的连接池而言远远不够。
12.7.2 中文显示为转义码
| 项目 | 内容 |
|---|---|
| 现象 | redis-cli 中查询含中文的值,显示为 "\xe5\xbc\xa0\xe4\xb8\x89" 而不是 张三 |
| 原因 | 这不是乱码,也不是数据存错了。redis-cli 在交互模式下默认使用"带引号的转义输出格式",会把所有非 ASCII 字节以 \xHH 形式展示,目的是让二进制安全的数据能被无歧义地显示 |
# 【现象复现】
redis-cli
127.0.0.1:6379> SET user:name 张三
OK
127.0.0.1:6379> GET user:name
"\xe5\xbc\xa0\xe4\xb8\x89" ← 转义输出
# 【解决方案 1:启动时加 --raw】
redis-cli --raw
127.0.0.1:6379> GET user:name
张三
# 【解决方案 2:非交互模式默认就是 raw】
redis-cli GET user:name
# 张三 ← 直接输出中文
# 【原理】redis-cli 检测到标准输出不是 tty(如管道/重定向)时自动启用 raw 模式
# 【解决方案 3:交互模式下临时切换】
redis-cli
127.0.0.1:6379> GET user:name
"\xe5\xbc\xa0\xe4\xb8\x89"
# 【7.0+ 支持运行时切换】
127.0.0.1:6379> raw
127.0.0.1:6379> GET user:name
张三
# 【反向:强制使用转义输出】
redis-cli --no-raw GET user:name
# "\xe5\xbc\xa0\xe4\xb8\x89"
# 用途:查看二进制数据(如 BITMAP、序列化对象)、确认末尾有无隐藏字符
# 【确认终端与环境编码】
echo $LANG # 应为 zh_CN.UTF-8 或 en_US.UTF-8
locale
# 若 LANG 是 POSIX/C,即使加 --raw 也可能显示为 ?
export LANG=en_US.UTF-8
# 持久化(CentOS 7)
localectl set-locale LANG=en_US.UTF-8
# 【CentOS 6】写 /etc/sysconfig/i18n:LANG="en_US.UTF-8"
# 【验证数据本身没错:比对字节长度】
redis-cli STRLEN user:name
# (integer) 6 ← UTF-8 下"张三"是 6 字节,说明存储正确
redis-cli --no-raw GET user:name | od -c | head -3
--raw 对其他输出的影响:
| 输出类型 | 默认(交互模式) | --raw |
|---|---|---|
| 字符串 | "value"(带引号,非 ASCII 转义) |
value(原样输出) |
| 整数 | (integer) 42 |
42 |
| 数组 | 1) "a" / 2) "b"(带序号) |
每行一个元素,无序号 |
| nil | (nil) |
空行 |
| 错误 | (error) ERR ... |
ERR ... |
写脚本时的实用推论:
--raw输出没有序号和引号,非常适合直接用管道处理,例如redis-cli --raw KEYS 'x:*' | xargs ...。但要注意 nil 会变成空行,脚本中判断"是否存在"应改用EXISTS的返回值,而不是判断输出是否为空。
12.8 故障速查总表
| 报错 / 现象 | 最可能的原因 | 第一条排查命令 | 处理方向 |
|---|---|---|---|
Connection refused |
服务未启动 / 端口错 / 只绑 127.0.0.1 | ss -lntp \| grep 6379 |
起服务、改 bind、放防火墙 |
Connection timed out |
防火墙 DROP / 安全组 / 网络不通 | nc -zv <ip> 6379 |
放行端口,检查云安全组 |
DENIED Redis is running in protected mode |
3.2+ 保护模式:无密码 + 默认 bind | CONFIG GET protected-mode |
设 requirepass(首选) |
NOAUTH Authentication required |
有密码但未认证 | CONFIG GET requirepass |
-a 或 AUTH |
WRONGPASS / invalid password |
密码错、特殊字符未转义、ACL 用户名错 | od -c 检查隐藏字符 |
用单引号包裹密码 |
MISCONF Redis is configured to save RDB snapshots... |
持久化失败(磁盘满/权限/fork) | df -h + ls -ld <dir> |
清盘、改权限、overcommit_memory=1 |
OOM command not allowed |
内存达 maxmemory + noeviction |
INFO memory |
调 maxmemory 或改淘汰策略 |
fork: Cannot allocate memory |
vm.overcommit_memory 不是 1 |
cat /proc/sys/vm/overcommit_memory |
设为 1,关 THP,降 maxmemory |
Bad file format reading the append only file |
AOF 被截断(断电) | redis-check-aof <file> |
备份后 --fix |
| RDB 校验失败 | RDB 损坏(不可修复) | redis-check-rdb dump.rdb |
从备份或从库恢复 |
master_link_status:down |
缺 masterauth / 防火墙 / bind |
从库日志 grep -i master |
补 masterauth,放端口 |
| 主从数据不一致 | 从库可写 / 从库 maxmemory 偏小 |
CONFIG GET replica-read-only |
设只读,从库内存 ≥ 主库 |
| 哨兵不切换 | 哨兵 <3 个 / 哨兵间不通 / 缺 auth-pass |
SENTINEL sentinels mymaster |
3 台哨兵、放 26379、补密码 |
CLUSTERDOWN Hash slot not served |
槽未全覆盖 / 主节点无从节点接管 | CLUSTER INFO |
--cluster fix、ADDSLOTS |
CROSSSLOT Keys in request... |
多 key 不在同一槽 | CLUSTER KEYSLOT <key> |
hash tag {} 或拆分 |
MOVED / ASK |
客户端没用集群模式 | 检查是否加了 -c |
redis-cli -c、用 Cluster 客户端 |
| 内存只涨不跌 | 无 TTL / 缓冲区 / 碎片 | INFO memory 看 dataset vs overhead |
补 TTL、限缓冲区、开 defrag |
mem_fragmentation_ratio > 1.5 |
内存碎片 | INFO memory \| grep frag |
activedefrag yes、MEMORY PURGE |
mem_fragmentation_ratio < 1 |
正在使用 swap(最危险) | grep VmSwap /proc/<pid>/status |
swappiness=1、降 maxmemory |
| 周期性延迟尖刺 | fork / 集中过期 / AOF 重写 | LATENCY LATEST |
看事件名对症处理 |
| 整个实例卡死数秒 | *大 key 上的 O(N) 命令 / `KEYS `** | SLOWLOG GET 20 |
禁 KEYS、拆大 key、用 SCAN |
| 规律性整点抖动 | 大量 key 同时过期 | LATENCY HISTORY expire-cycle |
TTL 加随机抖动 |
max number of clients reached |
maxclients 小 / fd 限制低 / 连接泄漏 |
INFO clients + /proc/<pid>/limits |
调 LimitNOFILE、修连接池 |
中文显示 \xe5\xbc\xa0 |
redis-cli 交互模式的转义输出 |
STRLEN key 验证字节数 |
加 --raw |
| QPS 骤降 + iowait 高 | swap / 磁盘打满 / appendfsync always |
vmstat 1 5 |
见 12.5.3、换 everysec |
练习
本节练习分三类共九节:第一节概念简答检验原理理解;第二至第八节是命令、配置、架构、安全与设计类题目,绝大多数在一台单机实例上即可完成,少量涉及主从/集群的题目只需读懂配置与报错即可作答;第九节综合实验需要多台虚拟机(或单机多端口多实例)。所有题目均附参考答案。
建议环境:3 台 CentOS 7 / Rocky 8 虚拟机,IP 规划
192.168.1.10、192.168.1.11、192.168.1.12,内存 ≥ 2GB;实验期间关闭 SELinux,防火墙按各实验要求放行端口。所有实验都在隔离的内网测试环境中进行,不要对生产实例执行破坏性操作。
一、概念简答
1. Redis 为什么快?请从数据存储位置、线程模型、数据结构、通信协议四个角度回答。
答:
- 存储位置:数据全部驻留内存,读写不经过磁盘寻址,内存随机访问延迟约 100ns 级,比机械磁盘快 5 个数量级;
- 线程模型:核心命令处理是单线程 + I/O 多路复用(epoll),没有锁竞争、没有线程上下文切换,命令天然串行执行因而具备原子性;6.0+ 把网络读写解析拆给 I/O 线程,但命令执行仍是单线程;
- 数据结构:针对每种类型做了编码优化(SDS、ziplist/listpack、intset、quicklist、skiplist、hashtable),小数据量用紧凑编码省内存并提升 CPU cache 命中率;
- 通信协议:RESP 是纯文本+长度前缀的协议,解析开销极小,且支持 pipeline 批量发送,把 N 次 RTT 压缩为 1 次。
2. 五种基本数据类型各自的底层编码与典型适用场景是什么?
答:
| 类型 | 底层编码 | 典型场景 |
|---|---|---|
| String | int / embstr(≤44 字节)/ raw | 缓存对象 JSON、计数器(INCR)、分布式锁(SET NX PX)、限流 |
| Hash | listpack(小)/ hashtable | 存储对象的字段,可只读写单个字段,比整体 JSON 省网络与 CPU |
| List | quicklist(listpack 链表) | 消息队列(LPUSH+BRPOP)、最新 N 条时间线(配合 LTRIM) |
| Set | intset(全整数且少)/ listpack / hashtable | 去重、标签、共同好友(SINTER)、抽奖(SPOP) |
| ZSet | listpack(小)/ skiplist + hashtable | 排行榜、延时队列(score 存时间戳)、范围查询、滑动窗口限流 |
3. RDB 与 AOF 的本质区别是什么?什么是混合持久化,它解决了什么问题?
答:RDB 是某一时刻全量数据的二进制快照(结果),AOF 是写命令的追加日志(过程)。RDB 体积小、恢复快,但两次快照之间宕机会丢失全部增量;AOF 数据安全性高(everysec 最多丢 1 秒),但文件大、恢复慢。
混合持久化(4.0+ aof-use-rdb-preamble yes)在 AOF 重写时,把重写基线以 RDB 格式写入 AOF 文件头部,其后继续追加增量命令。这样既有 RDB 的体积小、加载快,又保留 AOF 的丢数据窗口小,是目前生产的推荐配置。
4. 什么是 fork 与 COW?为什么执行 BGSAVE 时要预留内存?
答:BGSAVE/AOF 重写通过 fork() 创建子进程,子进程与父进程共享同一份物理内存页,只在页被写入时才复制(Copy-On-Write)。因此子进程看到的是 fork 瞬间的一致性数据视图。
风险在于:fork 之后父进程若持续写入,被修改的页会不断被复制,最坏情况下额外占用与数据集等量的内存。所以 maxmemory 建议不超过物理内存的 50%~70%,且 vm.overcommit_memory 必须设为 1,否则 fork 可能因内存不足直接失败。
5. 主从复制中,全量重同步与部分重同步分别在什么情况下发生?各自流程是什么?
答:
- 全量重同步(full resync):首次建立复制关系、
replid不匹配、或从库请求的 offset 已不在主库积压缓冲区中时发生。流程:从库发PSYNC ? -1→ 主库fork生成 RDB → 传输 RDB → 从库清空旧数据并加载 → 主库把期间的写命令从复制缓冲区补发。 - 部分重同步(partial resync):短暂网络闪断后重连,且从库的
offset仍落在主库repl-backlog范围内。流程:从库发PSYNC <replid> <offset>→ 主库回+CONTINUE→ 只补发缺失的那段命令流。 - 判断依据:
INFO stats中的sync_full与sync_partial_ok/sync_partial_err。sync_full频繁增长通常说明repl-backlog-size太小或从库输出缓冲区被打爆。
6. 哨兵的三大职责是什么?主观下线、客观下线与选主过程分别如何进行?
答:三大职责是监控(周期 PING 主从与其他哨兵)、提醒/通知(异常时告警或发布事件)、自动故障转移。
- 主观下线(SDOWN):单个哨兵在
down-after-milliseconds内没收到有效回复,自己认定主库下线; - 客观下线(ODOWN):该哨兵向其他哨兵询问,达到
quorum个哨兵都判定 SDOWN 后升级为客观下线; - 选举领导者哨兵:哨兵之间基于 Raft 风格投票,需获得超过半数哨兵(majority)的授权才能执行故障转移,因此哨兵总数应为奇数且 ≥ 3;
- 选主(挑新主库):依次比较 ① 过滤掉下线与失联过久的从库 → ②
replica-priority小者优先(0 表示永不当主)→ ③ 复制偏移量offset最大者(数据最新)→ ④run_id字典序最小者。 - 注意
quorum(判定客观下线所需票数)与majority(执行故障转移所需授权数)是两个不同的门槛,只有前者可配置。
7. Redis Cluster 为什么是 16384 个槽?MOVED 与 ASK 有什么区别?为什么跨槽的多 key 命令会报错?
答:槽数取 16384(2^14)是带宽与精度的折中:节点间心跳包要携带槽位图 bitmap,16384 位仅 2KB,若用 65536 则为 8KB,心跳开销过大;同时 16384 远大于常见节点数,足以保证分片均匀。
MOVED 表示槽已经永久属于另一节点,客户端应更新本地槽位缓存并重定向;ASK 表示槽正在迁移中且该 key 已迁到目标节点,是一次性重定向,客户端需先发 ASKING 再发命令,且不应更新槽位缓存。
跨槽多 key 命令(如 MSET a 1 b 2、SINTER s1 s2)会返回 CROSSSLOT 错误,因为这些命令要求所有 key 在同一节点上才能保证原子性。解决办法是用 hash tag:把 key 写成 {user:1001}:profile、{user:1001}:cart,只对花括号内的内容计算 CRC16,从而强制落到同一槽。
8. maxmemory-policy 的八种策略分别是什么?线上应如何选择?
答:
| 策略 | 行为 |
|---|---|
noeviction |
不淘汰,内存满后写命令返回 OOM 错误,读命令仍可用(默认值) |
volatile-lru |
仅在设了过期时间的 key 中,淘汰最近最少使用的 |
volatile-lfu |
仅在设了过期时间的 key 中,淘汰访问频率最低的(4.0+) |
volatile-random |
仅在设了过期时间的 key 中随机淘汰 |
volatile-ttl |
仅在设了过期时间的 key 中,淘汰剩余 TTL 最短的 |
allkeys-lru |
在所有 key 中淘汰最近最少使用的 |
allkeys-lfu |
在所有 key 中淘汰访问频率最低的(4.0+) |
allkeys-random |
在所有 key 中随机淘汰 |
选择原则:纯缓存用 allkeys-lru(有明显热点时用 allkeys-lfu 更抗偶发扫描污染);缓存与持久数据混存用 volatile-lru,并确保缓存类 key 都带 TTL;当数据库用、数据不可丢用 noeviction,同时必须做内存容量告警。volatile-* 策略下若没有任何 key 带过期时间,行为等同 noeviction。
9. Redis 如何删除过期键?主从架构下从库会自己删除过期键吗?
答:两种机制配合:惰性删除(访问 key 时发现已过期才删)+ 定期删除(serverCron 每秒若干次随机抽样 hz 个带 TTL 的 key,过期比例高则继续循环)。这是"允许少量过期 key 暂留内存"换取"不阻塞主线程"的折中。
主从架构下从库不会自行删除过期键,只等主库把 DEL(4.0+ 为 UNLINK)同步过来,以保证主从数据一致。但从库在读取时会判断 TTL,对逻辑上已过期的 key 返回空,因此客户端读不到脏数据。
10. 缓存穿透、缓存击穿、缓存雪崩的区别与对策是什么?
答:
| 问题 | 成因 | 对策 |
|---|---|---|
| 穿透 | 查询根本不存在的数据,缓存永远不命中,请求全部打到数据库 | 缓存空值(短 TTL)、布隆过滤器前置拦截、接口参数校验与限流 |
| 击穿 | 单个热点 key 过期瞬间,大量并发同时回源 | 互斥锁/单飞(SET NX 抢锁重建)、逻辑过期 + 异步重建、热点 key 永不过期 |
| 雪崩 | 大批 key 同时过期或 Redis 整体宕机,流量集体压向数据库 | TTL 加随机抖动、多级缓存、熔断降级、集群化 + 哨兵保证高可用、预热 |
11. 大 key 与热 key 各有什么危害?分别如何定位?
答:大 key(如单个 String 上百 MB、单个 Hash/List 上百万元素)会导致网络传输阻塞、DEL 时长时间阻塞主线程、内存分配不均、集群下无法迁移槽位。定位手段:redis-cli --bigkeys、redis-cli --memkeys、MEMORY USAGE key、离线 rdb 分析工具。删除时用 UNLINK 异步回收,或对 Hash/ZSet 分批 HSCAN + HDEL。
热 key 是访问极度集中的 key,会把单个分片的 CPU 与网卡打满,集群下造成倾斜。定位手段:redis-cli --hotkeys(需 maxmemory-policy 为 *-lfu)、MONITOR 短时抽样、代理层或客户端埋点统计。处理手段:本地多级缓存、key 加随机后缀打散到多个分片、读多写少时增加从库。
*12. 为什么线上禁止使用 `KEYS ?SCAN` 提供了什么保证,又有什么局限?**
答:KEYS * 会一次性遍历全部 key 空间并在单线程中完成,百万级 key 时可造成数百毫秒到数秒的阻塞,期间所有请求排队,等同于一次事故。同类危险命令还有 FLUSHALL、HGETALL 大 hash、SMEMBERS 大集合。
SCAN 是基于游标的渐进式迭代,每次只返回少量结果(COUNT 控制),不阻塞。它保证:迭代开始前就存在且直到结束都未被删除的元素一定会被返回至少一次。局限是:可能重复返回同一元素(需客户端去重),迭代期间新增或删除的元素不保证被返回,且 COUNT 只是提示而非精确数量。
13. 所谓 Redis"未授权访问"风险是怎么形成的?如何自查?
答:风险成因是多个不安全默认值同时叠加:① bind 0.0.0.0 或注释掉 bind 使实例监听所有网卡;② 未设置 requirepass,任何人连上即可执行全部命令;③ 把 protected-mode 改成 no 关掉了兜底保护;④ 用 root 身份运行 Redis 进程;⑤ 数据目录指向 Web 站点目录等敏感路径;⑥ 防火墙未做源地址白名单甚至直接映射到公网。以上条件凑齐后,攻击者可任意读写数据、篡改配置项与持久化路径,进而危害宿主机。
自查方法:ss -tlnp | grep 6379 确认监听地址是否为内网/回环;CONFIG GET requirepass protected-mode dir 确认认证与目录;ps -ef | grep redis-server 确认运行用户不是 root;从外网机器尝试 redis-cli -h <公网IP> PING 确认不可达;ls -l /var/lib/redis 确认 RDB/AOF 权限为 600。加固方向见本章第十节与下方安全加固清单。
14. MULTI/EXEC 事务与 Lua 脚本的原子性有什么区别?WATCH 的作用是什么?
答:MULTI/EXEC 把多条命令打包排队后连续执行,中间不会插入其他客户端命令,因此具备"隔离性";但它没有回滚——若某条命令运行时出错(如对 String 执行 LPUSH),其余命令仍会执行,EXEC 只返回各命令结果。语法错误的命令会在入队阶段就被拒绝,导致整个 EXEC 失败。
Lua 脚本同样整体原子执行,但可以在服务端做条件判断与分支,把"读—判断—写"合成一次原子操作,因此实现分布式锁释放、库存扣减等逻辑时优于事务。代价是脚本必须短小,长脚本会长时间阻塞单线程。
WATCH key 提供乐观锁:在 MULTI 之前监视若干 key,若在 EXEC 之前这些 key 被任何客户端修改,EXEC 直接返回 nil 表示放弃,由客户端重试。UNWATCH 或 EXEC/DISCARD 会清除监视。
二、基础命令
1. 用一条命令写入键 name(值为 zhangsan)并设置 60 秒过期,再查询它的剩余生存时间;随后去掉过期时间,确认它已变成永久 key。
参考答案
```bash redis-cli SET name zhangsan EX 60 redis-cli TTL name # (integer) 60 redis-cli PERSIST name redis-cli TTL name # (integer) -1 ← -1 表示存在但永久,-2 表示 key 不存在 ``` `SET` 的 `EX`/`PX`/`EXAT`/`PXAT`/`NX`/`XX`/`KEEPTTL`(6.0+)/`GET`(6.2+)选项可以把"写入 + 设过期 + 条件判断"合并成一条原子命令,优于 `SET` 后再 `EXPIRE`(后者非原子,中间宕机会留下永不过期的 key)。`TTL` 返回 `-1` 表示存在但无过期时间,`-2` 表示 key 不存在。2. 分别用五种基本数据类型存储一条数据并读取出来。
参考答案
```bash # string redis-cli SET k1 v1 redis-cli GET k1 # hash redis-cli HMSET user:1 name zhangsan age 28 redis-cli HGET user:1 name redis-cli HGETALL user:1 # 【大 hash 上禁用】改用 HSCAN # 【版本说明】HMSET 在 4.0+ 已标记为废弃,推荐用 HSET(HSET 从 4.0 起支持多字段) redis-cli HSET user:1 city Beijing email a@b.com # list redis-cli LPUSH mylist a b c redis-cli LRANGE mylist 0 -1 # 1) "c" # 2) "b" # 3) "a" ← LPUSH 从左侧插入,顺序反转 # set(无序、去重) redis-cli SADD myset x y z x redis-cli SMEMBERS myset # 【大 set 上禁用】改用 SSCAN redis-cli SCARD myset # (integer) 3,重复的 x 只存一份 # zset(按 score 排序) redis-cli ZADD rank 100 tom 200 jerry redis-cli ZRANGE rank 0 -1 WITHSCORES redis-cli ZREVRANGE rank 0 0 WITHSCORES # 取分数最高的 ```3. 说明 DEL、EXISTS、EXPIRE、PEXPIRE、PERSIST、RENAME、RENAMENX、TYPE 各自的作用。
参考答案
| 命令 | 作用 | 返回值 | | --- | --- | --- | | `DEL key [key...]` | 删除一个或多个 key(**同步释放内存**,大 key 上改用 `UNLINK`) | 实际删除的数量 | | `EXISTS key [key...]` | 判断 key 是否存在(3.0.3+ 支持多个 key,返回存在的个数) | 1/0 或计数 | | `EXPIRE key seconds` | 设置过期时间,**单位秒** | 1 成功,0 key 不存在 | | `PEXPIRE key milliseconds` | 设置过期时间,**单位毫秒** | 1 成功,0 key 不存在 | | `PERSIST key` | **移除过期时间**,让 key 变为永久 | 1 成功,0 无 TTL 或不存在 | | `RENAME key newkey` | 重命名,**若 newkey 已存在则直接覆盖** | OK | | `RENAMENX key newkey` | 重命名,**仅当 newkey 不存在时才执行** | 1 成功,0 目标已存在 | | `TYPE key` | 返回 key 的数据类型 | string/hash/list/set/zset/stream/none | 补充:`EXPIREAT`/`PEXPIREAT` 用绝对时间戳;`EXPIRETIME`/`PEXPIRETIME`(7.0+)查询绝对过期时刻;7.0+ 的 `EXPIRE` 支持 `NX`/`XX`/`GT`/`LT` 选项。**集群模式下 `RENAME` 要求两个 key 在同一槽**(见 12.4.2)。*4. 为什么生产环境禁止执行 `KEYS `?应该用什么替代?**
参考答案
`KEYS` 是 **O(N)** 命令,会一次性遍历整个 key 空间且**不可中断**。由于 Redis 用**单线程**处理命令,这条命令执行期间**所有其他客户端的请求全部排队等待**——百万 key 约阻塞 100~300 毫秒,千万 key 可达数秒,足以引发全站超时。此外它把全部结果一次性写入输出缓冲区,还会造成内存尖峰与网络拥塞。 替代方案是 **`SCAN`**:游标式分批遍历,单次接近 O(1),多次调用之间其他命令可正常执行。 ```bash redis-cli --scan --pattern 'user:*' redis-cli SCAN 0 MATCH 'user:*' COUNT 500 ``` `SCAN` 的代价是**弱一致性**:可能重复返回同一个 key,遍历期间新增/删除的 key 不保证是否出现,但**保证始终存在的 key 一定会被遍历到**。生产环境应在配置中用 `rename-command KEYS ""` 从机制上禁用它。5. redis-cli 查询中文显示为 "\xe5\xbc\xa0\xe4\xb8\x89",数据存错了吗?如何正常显示?
参考答案
**没有存错**。这是 `redis-cli` **交互模式**的默认转义输出格式,为了让二进制安全的数据能无歧义显示,所有非 ASCII 字节都以 `\xHH` 展示。 ```bash redis-cli --raw # 启动时加 --raw redis-cli GET user:name # 非交互模式自动 raw(输出非 tty) # 交互模式下 7.0+ 可直接输入 raw 切换 redis-cli STRLEN user:name # 6 字节,验证 UTF-8 存储正确 ``` 反向确认二进制内容或排查末尾隐藏字符时,用 `redis-cli --no-raw` 强制转义输出。同时确认 `echo $LANG` 为 UTF-8 编码。三、数据类型与场景选择题
1. 需要保存"用户 1001 的昵称、头像、积分"三个字段,并且业务经常只更新积分一个字段,最合适的类型是( ) A. String 存 JSON B. Hash C. List D. ZSet
答:B。Hash 支持 HSET user:1001 score 120 只改单字段,HINCRBY 还能原子加减;用 String 存 JSON 必须整体读出、反序列化、改完再整体写回,既费网络又有并发覆盖问题。
2. 实现一个"游戏实时排行榜,要求能取前 100 名、能查某玩家排名、能按分数区间统计人数",应选( ) A. Set B. List C. ZSet D. Hash
答:C。ZREVRANGE key 0 99 WITHSCORES 取榜首、ZREVRANK 查排名、ZCOUNT 按分数区间计数,都是 O(logN) 级操作,ZSet 是排行榜的标准解法。
3. 实现一个"先进先出的简单任务队列,消费者需要在无任务时阻塞等待",应选( )
A. List + LPUSH / BRPOP B. Set + SPOP C. String + INCR D. Hash + HSCAN
答:A。LPUSH 从头入队、BRPOP 从尾阻塞出队即构成 FIFO,且 BRPOP 免去客户端轮询。注意 List 队列没有 ack 机制,消费者崩溃会丢消息,要求可靠投递时应改用 Stream(XADD/XREADGROUP)。
4. 统计"今天有哪些用户登录过"并支持"求今天和昨天都登录的用户",应选( ) A. List B. Set C. ZSet D. String
答:B。Set 天然去重,SADD login:20260822 <uid> 记录,SINTER login:20260822 login:20260821 直接求交集。若用户量极大且只需判断"某人是否登录"和"登录总数",可进一步用 Bitmap(SETBIT/BITCOUNT)或 HyperLogLog(PFADD/PFCOUNT)以极小内存换取近似结果。
5. 实现"短信验证码,5 分钟内有效,且同一手机号 60 秒内只能发一次",最简洁的组合是( )
A. SET code:<phone> 1234 EX 300 + SET limit:<phone> 1 EX 60 NX B. LPUSH + EXPIRE C. HSET + TTL D. ZADD + ZREMRANGEBYSCORE
答:A。EX 一步完成"写值 + 设过期",NX 保证只有第一次发送能成功写入限流 key,返回 nil 即表示 60 秒内重复请求。避免用"先 SET 再 EXPIRE"两条命令,中间宕机会留下永不过期的 key。
四、安装、配置与持久化
1. 源码编译 Redis 时报错 zmalloc.h:50:31: fatal error: jemalloc/jemalloc.h: No such file or directory,原因和解决办法是什么?
参考答案
Redis 默认使用 **jemalloc** 作为内存分配器(比 glibc 的 malloc 碎片率更低),编译时会去 `deps/jemalloc` 目录找头文件。报这个错通常是因为**曾经执行过 `make` 失败后残留了中间文件**,或 `deps` 目录未正确初始化。 ```bash # 【方案 1:推荐】清理后重新编译,保留 jemalloc cd /usr/local/redis-4.0.11 make distclean make # 【方案 2】改用 libc 分配器(源码文档中给出的办法) make MALLOC=libc ``` **注意方案 2 的代价**:用 `libc` 编译出的 Redis **不支持 `activedefrag` 主动碎片整理**(该功能依赖 jemalloc),且长期运行碎片率通常更高。可用 `redis-cli INFO memory | grep mem_allocator` 确认实际使用的分配器。生产环境**应优先保证使用 jemalloc**。2. 完整写出 Redis 4.0.11 的源码安装步骤,并配置 init 启动脚本。
参考答案
```bash # ===== 1) 下载并解压 ===== cd /usr/local/src wget http://download.redis.io/releases/redis-4.0.11.tar.gz tar xf redis-4.0.11.tar.gz -C /usr/local/ # ===== 2) 编译 ===== cd /usr/local/redis-4.0.11 make # 若报 jemalloc.h 错误:make distclean && make,或 make MALLOC=libc # ===== 3) 改名,便于后续升级时只改软链 ===== mv /usr/local/redis-4.0.11/ /usr/local/redis # ===== 4) 加入 PATH ===== export PATH=$PATH:/usr/local/redis/src echo 'export PATH=$PATH:/usr/local/redis/src' >> /etc/profile source /etc/profile # ===== 5) 复制官方 init 脚本 ===== cp /usr/local/redis/utils/redis_init_script /etc/init.d/redis chmod +x /etc/init.d/redis ``` ```bash # ===== 6) 修改 /etc/init.d/redis 中的路径变量 ===== REDISPORT=6379 REDIS_DIR=/usr/local/redis # 【自定义变量,便于统一管理】 EXEC=$REDIS_DIR/src/redis-server CLIEXEC=$REDIS_DIR/src/redis-cli PIDFILE=/var/run/redis_${REDISPORT}.pid CONF="$REDIS_DIR/redis.conf" # 【指向实际的配置文件位置】 ``` ```bash # ===== 7) 【关键修改】设置了密码后 shutdown 会失败 ===== # 原脚本的停止逻辑:$CLIEXEC -p $REDISPORT shutdown # 有密码时会返回 NOAUTH,导致服务停不掉,必须加 -a: # $CLIEXEC -a "password" -p $REDISPORT shutdown # ===== 8) 修改 redis.conf 让其以守护进程运行 ===== # daemonize yes # pidfile /var/run/redis_6379.pid # logfile /var/log/redis/redis.log # ===== 9) 启动与开机自启 ===== # 【CentOS 6】 service redis start chkconfig --add redis chkconfig redis on # 【CentOS 7】SysV 脚本仍可用,但推荐改用 systemd unit systemctl daemon-reload systemctl start redis systemctl enable redis # ===== 10) 验证 ===== redis-cli PING # PONG redis-cli INFO server | grep redis_version ```3. 列出 redis.conf 中最核心的通用配置项及其默认值。
参考答案
| 参数 | 默认值 | 说明 | | --- | --- | --- | | `daemonize` | `no` | 是否以守护进程后台运行。改为 `yes` 后 Redis 会自行 fork 并脱离终端 | | `pidfile` | `/var/run/redis.pid` | PID 文件路径。**多实例部署时必须区分**(如 `redis_6379.pid`) | | `port` | `6379` | 监听端口。这个数字来自意大利女演员 **Alessia Merz** 的名字——在手机九宫格键盘上 `MERZ` 对应 6379 | | `bind` | `127.0.0.1` | 监听地址。**只填 `127.0.0.1` 则外部无法访问**;生产应填内网 IP,**绝不要填 `0.0.0.0`** | | `timeout` | `300` | 客户端空闲多少秒后断开连接。`0` = 永不超时 | | `loglevel` | `verbose` | 日志级别,四档:`debug`(最详细,仅开发用)、`verbose`、`notice`(生产推荐)、`warning`(只记严重信息) | | `logfile` | `stdout` | 日志文件路径。**若 `daemonize yes` 且 `logfile stdout`,日志会被发送到 `/dev/null`(全部丢失)**,因此后台运行时必须指定真实路径 | | `databases` | `16` | 数据库数量,编号 0~15,用 `SELECT n` 切换。**集群模式下只能用 db0** | ```ini daemonize yes pidfile /var/run/redis_6379.pid port 6379 bind 192.168.1.10 127.0.0.1 timeout 300 loglevel notice logfile /var/log/redis/redis.log databases 16 # 包含其他配置文件(便于把公共配置与实例差异配置分离) # 【注意】include 的位置很重要:后加载的会覆盖先加载的同名参数 include /path/to/local.conf ```4. 解释 save 900 1、save 300 10、save 60 10000 三行配置的含义,以及 RDB 相关的其他参数。
参考答案
三行是**并列的触发条件,满足任意一条即触发 `BGSAVE`**,格式为 `save <秒数> <变更次数>`: | 配置 | 含义 | | --- | --- | | `save 900 1` | 900 秒(15 分钟)内至少有 **1** 个 key 发生变更 → 保存 | | `save 300 10` | 300 秒(5 分钟)内至少有 **10** 个 key 变更 → 保存 | | `save 60 10000` | 60 秒内至少有 **10000** 个 key 变更 → 保存 | 设计意图是**变更越频繁,保存越及时**:数据变化剧烈时 1 分钟就落盘,变化很少时才等 15 分钟,在数据安全与 I/O 开销之间取平衡。 ```ini # 其他 RDB 参数 rdbcompression yes # 对字符串使用 LZF 算法压缩。省磁盘省带宽,代价是少量 CPU # 设为 no 可省 CPU,但文件明显变大 rdbchecksum yes # 文件末尾写 CRC64 校验和(5.0+ 默认 yes) dbfilename dump.rdb # RDB 文件名 dir ./ # 【工作目录】RDB 与 AOF 都存在这里 # 【重要】生产必须改成绝对路径(如 /var/lib/redis), # 否则文件会落在启动 Redis 时的当前目录,极易丢失 stop-writes-on-bgsave-error yes # BGSAVE 失败后拒绝所有写入(产生 MISCONF 错误) # 这是保护机制,不要轻易关闭 # 关闭 RDB 的写法:注释掉所有 save 行,或写 # save "" ```5. appendfsync 的三个取值有什么区别?各自的数据安全性与性能如何?
参考答案
| 取值 | 行为 | 数据安全性 | 性能 | | --- | --- | --- | --- | | `no` | Redis **只写入 OS 的页缓存,不主动调用 `fsync`**,由操作系统决定何时刷盘(Linux 通常约 **30 秒**一次) | **最差**,宕机可能丢 30 秒数据 | **最快**(几乎没有磁盘同步开销) | | **`everysec`(默认,推荐)** | **每秒调用一次 `fsync`**。由后台线程执行,官方称为"**组提交**"(group commit) | 折中,宕机**最多丢 1 秒**数据 | 接近 `no`,生产首选 | | `always` | **每条写命令都调用 `fsync`** 后才返回客户端 | **最好**,理论上不丢数据 | **最慢**,QPS 可能下降数倍到十几倍,且延迟受磁盘性能直接影响 | ```ini appendonly no # AOF 总开关,默认关闭 appendfilename "appendonly.aof" # AOF 文件名(7.0+ 变为 appendonlydir 目录下的多文件) appendfsync everysec # 刷盘策略 # 相关参数 no-appendfsync-on-rewrite no # 重写期间是否暂停 fsync(yes 可减少 I/O 争抢, # 但重写这段时间宕机会丢更多数据) auto-aof-rewrite-percentage 100 # AOF 增长到上次重写后大小的 100% 时触发重写 auto-aof-rewrite-min-size 64mb # 且文件至少 64MB 才触发(避免小文件频繁重写) aof-load-truncated yes # 启动时若 AOF 尾部不完整,自动截断并继续(【建议 yes】) aof-use-rdb-preamble yes # 4.0+ 混合持久化:重写后的 AOF 前半部分是 RDB 格式 ``` `always` 也不能保证绝对不丢数据:磁盘控制器有自己的写缓存,且主从复制是异步的。手动触发重写用 `BGREWRITEAOF`(后台执行,不阻塞主线程)。6. AOF 文件在服务器断电后损坏,Redis 启动失败,如何恢复?RDB 损坏能修复吗?
参考答案
**AOF 可以修复**(代价是丢失损坏点之后的数据): ```bash # 1) 【务必先备份】 cp /var/lib/redis/appendonly.aof /backup/appendonly.aof.$(date +%F-%H%M).bak # 2) 先检查(不加 --fix 只报告) redis-check-aof /var/lib/redis/appendonly.aof # AOF analyzed: size=157286400, ok_up_to=4857900, diff=152428500 # 3) 修复:原理是【把第一个损坏点之后的内容全部截断丢弃】 redis-check-aof --fix /var/lib/redis/appendonly.aof # Continue? [y/N]: y # 4) 启动并验证,然后立即重写生成干净文件 systemctl start redis redis-cli DBSIZE redis-cli BGREWRITEAOF # Redis 7.0+ 的 multi-part AOF 需指定 manifest redis-check-aof --fix /var/lib/redis/appendonlydir/appendonly.aof.manifest ``` **RDB 无法修复**,`redis-check-rdb` **只能校验不能修复**: ```bash redis-check-rdb /var/lib/redis/dump.rdb # [offset 157286400] Checksum OK # \o/ RDB looks OK! \o/ ``` RDB 损坏时的出路只有:① 从**备份**恢复;② 从**从库**的 RDB 恢复;③ 若同时开了 AOF,删掉损坏的 RDB 让 AOF 接管(**启动时 AOF 优先级更高**);④ 用 `rdbtools` 尝试部分解析。 **预防**:保持 `aof-load-truncated yes`,可让"断电导致最后一条命令写一半"的常见情况在启动时自动截断恢复,无需人工介入。7. 说明 maxclients、maxmemory、requirepass 三个参数的作用与注意事项。
参考答案
```ini maxclients 128 # 【Redis 2.x 的默认值仅 128】,3.0+ 提升到 10000 # 超限时新连接被拒绝并返回错误: # "max number of clients reached" # 【注意】实际生效值受系统 fd 限制约束, # 若 ulimit -n 不足,Redis 启动时会自动下调并在日志中警告 maxmemory8. vm-enabled、vm-swap-file、vm-max-memory、vm-max-threads 这组参数是做什么的?现在还能用吗?
参考答案
这是 Redis **早期版本(2.0~2.4)的虚拟内存(VM)功能**,设计目的是在内存不足时把冷数据的 value 交换到磁盘文件,只在内存中保留 key。 ```ini vm-enabled no # VM 总开关,默认关闭 vm-swap-file /tmp/redis.swap # 交换文件路径 vm-max-memory 0 # 最大使用内存;0 表示【所有 value 都可以被换出】, # 只把 key 留在内存中 vm-max-threads 4 # 处理 VM I/O 的线程数;设为 0 表示所有操作在主线程完成 # (会导致严重阻塞) ``` **这组参数从 Redis 2.6 起已被彻底移除,现代版本(4.0 及以后)中配置它们会导致启动失败并报未知配置项错误。** 被废弃的原因:VM 机制与 Redis"内存数据库"的定位相悖,实际表现远不如直接让操作系统做 swap,且引入了大量复杂性和不可预测的延迟。 现在解决内存不足的正确做法是:**设置 `maxmemory` + 合适的淘汰策略**、**治理大 key**、**水平拆分或使用 Redis Cluster 分片扩容**,而**不是**试图把数据放到磁盘上。若确实需要"内存 + 磁盘"混合存储,应选用其他产品(如 SSDB、Pika、KeyDB Flash 或云厂商的混合存储版)。五、主从、哨兵与集群
1. 用命令临时把一台 Redis 配成从库,再写出永久生效的配置。如何验证主从关系?
参考答案
```bash # ===== 临时生效(重启后失效)===== redis-cli -p 6380 SLAVEOF 192.168.1.10 6379 # 4.x 及之前 redis-cli -p 6380 REPLICAOF 192.168.1.10 6379 # 【5.0+ 的新名称,推荐】 # 取消从库身份,提升为主库 redis-cli -p 6380 REPLICAOF NO ONE # ===== 永久生效:写入配置文件 ===== ``` ```ini # /etc/redis/redis-6380.conf slaveof 192.168.1.10 6379 # 4.x 及之前的写法 # replicaof 192.168.1.10 6379 # 【5.0+ 推荐写法】 masterauth 123 # 【主库有密码时必须配】,否则同步失败 requirepass 123 # 建议与主库一致,便于故障切换后角色互换 slave-read-only yes # 从库只读(5.0+ 叫 replica-read-only) slave-serve-stale-data no # 【重要】同步断开时【拒绝】读请求, # 返回 "SYNC with master in progress" # 设 yes 则继续返回可能已过期的旧数据 # (5.0+ 叫 replica-serve-stale-data) appendfsync always # 从库可用更严格的刷盘策略保证数据安全 ``` ```bash # ===== 验证 ===== redis-cli -p 6379 INFO replication # role:master # connected_slaves:1 # slave0:ip=192.168.1.11,port=6380,state=online,offset=8823401,lag=0 # ↑ online 表示已进入增量复制 redis-cli -p 6380 INFO replication # role:slave # master_link_status:up ← 【最关键的字段】 # master_last_io_seconds_ago:0 # 实测数据同步 redis-cli -p 6379 SET testkey hello redis-cli -p 6380 GET testkey # "hello" redis-cli -p 6380 SET testkey world # (error) READONLY You can't write against a read only replica. ← 只读生效 ``` `state` 字段的三个阶段:`wait_bgsave`(主库生成 RDB)→ `send_bulk`(传输 RDB)→ `online`(增量复制)。**卡在 `wait_bgsave` 说明主库 fork/RDB 有问题;卡在 `send_bulk` 说明网络慢或复制缓冲区不足**。2. 从库日志反复出现 MASTER aborted replication with an error: NOAUTH Authentication required,原因是什么?
参考答案
**主库设置了 `requirepass`,但从库没有配置 `masterauth`**,因此从库连上主库后无法通过认证,全量同步被主库中止。这是主从故障中**最常见的原因**。 ```bash # 排查 redis-cli -p 6380 CONFIG GET masterauth # 为空即是原因 redis-cli -p 6380 INFO replication | grep master_link_status # down # 解决 redis-cli -p 6380 CONFIG SET masterauth 'MasterPass' redis-cli -p 6380 CONFIG REWRITE # 触发重新同步 redis-cli -p 6380 REPLICAOF NO ONE redis-cli -p 6380 REPLICAOF 192.168.1.10 6379 # 验证 redis-cli -p 6380 INFO replication | grep -E 'master_link_status|master_last_io_seconds_ago' ``` **必须区分两个参数**: | 参数 | 含义 | | --- | --- | | `requirepass` | 客户端连接**本节点**时要用的密码 | | `masterauth` | 本节点作为从库去连**主库**时要用的密码 | 由于故障切换后从库会升级为主库、原主库会降级为从库,**最佳实践是:集群内所有节点的 `requirepass` 保持一致,且每个节点都配上相同的 `masterauth`**。这样无论怎么切换都不会因认证问题中断复制。3. 哨兵的三大职责是什么?写出完整的 sentinel.conf 配置。
参考答案
**哨兵(Sentinel)的三大职责**: | 职责 | 说明 | | --- | --- | | **监控**(Monitoring) | 不断检查主库和从库是否正常运行 | | **提醒**(Notification) | 被监控的实例出问题时,通过 API 或脚本向管理员/其他程序发送通知 | | **自动故障迁移**(Automatic failover) | 主库失效时,自动将一个从库升级为新主库,让其他从库改为复制新主库,并**通知客户端新的主库地址** | ```ini # /etc/redis/sentinel.conf port 26379 daemonize yes logfile '/var/log/redis/sentinel.log' pidfile '/var/run/sentinel.pid' dir /usr/local/redis/sentinel/ # 监控的主库:<别名> <主库IP> <主库端口>4. 编写一个哨兵启停管理脚本,支持 start / stop / restart / status 四个动作。
参考答案
脚本从配置文件中解析端口与 pidfile,用 `lsof -i:5. 集群中执行 MGET user:1001:name user:1001:age 报 CROSSSLOT 错误,为什么?给出三种解决方案。
参考答案
**原因**:Redis Cluster 通过 `CRC16(key) mod 16384` 把 key 映射到 16384 个槽,槽分布在不同节点上。**单条命令涉及的所有 key 必须落在同一个槽**,否则无法保证原子性。两个 key 名不同,计算出的槽通常不同。 ```bash redis-cli -c -p 7000 CLUSTER KEYSLOT user:1001:name # (integer) 12043 redis-cli -c -p 7000 CLUSTER KEYSLOT user:1001:age # (integer) 741 ``` **方案 1:hash tag(推荐)**——key 中 `{}` 包裹的部分**单独参与 CRC16 计算**: ```bash redis-cli -c -p 7000 CLUSTER KEYSLOT 'user:{1001}:name' # 9842 redis-cli -c -p 7000 CLUSTER KEYSLOT 'user:{1001}:age' # 9842 ← 同槽 redis-cli -c -p 7000 MGET 'user:{1001}:name' 'user:{1001}:age' ``` 规则:只取**第一个 `{`** 到其后**第一个 `}`** 之间的内容;`{}` 为空或没有闭合时退化为整个 key 参与计算。**代价**:过度使用会造成**数据倾斜**(如都标成 `{global}` 会挤到同一节点),tag 应用业务实体 ID 保证分散性。 **方案 2:拆成多次单 key 操作**——成熟的集群客户端(JedisCluster、Lettuce、redis-py 的 `RedisCluster`)会自动把 `MGET` 按节点分组后并发请求再合并结果。 **方案 3:改用 hash 结构**——把相关字段收进一个 key,单 key 必然单槽,且内存开销更小: ```bash redis-cli -c -p 7000 HSET user:1001 name zhangsan age 28 redis-cli -c -p 7000 HMGET user:1001 name age ``` 同样受此限制的还有 `MSET`、`SINTER`/`SUNION`、`ZUNIONSTORE`、`RENAME`、`BITOP`、`RPOPLPUSH`、`MULTI` 事务、`EVAL` 脚本等。6. redis-cli -p 7000 GET user:1001 返回 (error) MOVED 12043 192.168.1.11:7001,集群坏了吗?MOVED 与 ASK 有什么区别?
参考答案
**集群没坏**。这是因为 `redis-cli` **没有加 `-c` 参数**,以单机模式访问集群节点,而请求的 key 不属于当前节点负责的槽,节点便返回 `MOVED` 重定向提示。 ```bash redis-cli -c -p 7000 GET user:1001 # -> Redirected to slot [12043] located at 192.168.1.11:7001 # "zhangsan" ``` 应用侧同理:必须使用**集群客户端**(`JedisCluster`/`RedisClusterClient`/`redis.cluster.RedisCluster`/`redis.NewClusterClient`),而不是单机客户端。 | 对比项 | `MOVED` | `ASK` | | --- | --- | --- | | 含义 | 该槽**已永久属于**目标节点 | 该槽**正在迁移中**,这个 key 已搬过去 | | 发生时机 | 客户端槽位映射缺失或过期 | reshard 进行中(`migrating`/`importing` 状态) | | 客户端动作 | **更新本地槽位缓存**,后续直发新节点 | **仅本次重定向,不更新缓存** | | 后续协议 | 直接发命令 | 必须先发 `ASKING`,再发真正的命令 | **排错提示**:用单机客户端连集群时,只有恰好落在所连节点上的 key 能访问成功(约 1/N 概率),表现为"时好时坏",极易被误判为集群不稳定。**看到 `MOVED` 先检查客户端类型,而不是去改集群配置**。7. 集群报 CLUSTERDOWN Hash slot not served,如何排查和恢复?
参考答案
```bash # 【排查】 redis-cli -c -p 7000 CLUSTER INFO # cluster_state:fail # cluster_slots_assigned:14562 ← 【必须是 16384】 # cluster_slots_fail:1822 redis-cli -c -p 7000 CLUSTER NODES redis-cli --cluster check 127.0.0.1:7000 # [ERR] Not all 16384 slots are covered by nodes. # 【常被忽略】集群需要两个端口:数据端口 7000 + 总线端口 17000(端口+10000) ss -lntp | grep -E '7000|17000' ``` ```bash # 【解决】 # 1) 自动修复(首选) redis-cli --cluster fix 127.0.0.1:7000 # 2) 手动补分配缺失的槽 redis-cli -c -p 7002 CLUSTER ADDSLOTS {5461..6999} redis-cli -c -p 7002 CLUSTER ADDSLOTSRANGE 5461 6999 # 7.0+ 范围写法 # 3) 清理 migrating/importing 残留状态 redis-cli -c -p 7002 CLUSTER SETSLOT 5461 STABLE # 4) 主节点宕机且无从节点接管时,手动提升从节点 redis-cli -c -p 7003 CLUSTER FAILOVER TAKEOVER # 【有数据丢失风险,仅灾难恢复用】 # 5) 【应急】允许部分槽不可用时继续服务(需在所有节点执行) for p in 7000 7001 7002 7003 7004 7005; do redis-cli -p $p CONFIG SET cluster-require-full-coverage no redis-cli -p $p CONFIG REWRITE done # 6) 放开总线端口 firewall-cmd --permanent --add-port=17000-17005/tcp && firewall-cmd --reload # CentOS 7 iptables -I INPUT -p tcp -m multiport --dports 17000:17005 -j ACCEPT # CentOS 6 ``` `cluster-require-full-coverage` 默认 `yes` 是**一致性优先**(一个槽缺失则整个集群拒绝服务);改为 `no` 是**可用性优先**(只有落在缺失槽的 key 报错)。**缓存场景建议 `no`**,**数据存储场景保持 `yes`**。六、安全加固与监控排错
1. 什么是 Redis 未授权访问风险?请给出一份完整的加固清单。
参考答案
**风险成因**:Redis 默认**无密码**、早期版本默认监听所有网卡,一旦端口暴露到公网,任何人都能以 Redis 进程的身份连接并执行任意命令。危害的放大点在于:**若 Redis 以 root 运行**,攻击者可通过修改 `dir` 与 `dbfilename` 把持久化文件写到敏感路径,从而威胁服务器控制权。因此防守重点是"网络不可达 + 有强认证 + 低权限运行 + 目录受控"。 **加固清单(按优先级)**: | 优先级 | 措施 | 关键配置 / 命令 | | --- | --- | --- | | **P0** | **不要用 root 运行** | `useradd -r -s /sbin/nologin -M redis`;systemd `User=redis` | | **P0** | **绝不暴露到公网** | 云安全组只放行应用网段,Redis 置于 VPC 私有子网 | | **P0** | **设置强密码** | `requirepass <32位以上随机串>`,`openssl rand -base64 32` | | **P0** | **绑定内网地址** | `bind 192.168.1.10 127.0.0.1`(**不要 `0.0.0.0`**) | | P1 | 开启保护模式(3.2+) | `protected-mode yes` | | P1 | 防火墙白名单 | `firewall-cmd --add-rich-rule`(CentOS 7)/ `iptables`(CentOS 6) | | P1 | 禁用危险命令 | `rename-command CONFIG ""`、`FLUSHALL`、`FLUSHDB`、`KEYS`、`DEBUG`、`SHUTDOWN`、`EVAL` | | P1 | 配置文件与数据目录权限 | 配置文件 `600`,数据目录 `750`,属主 `redis:redis`,RDB/AOF 文件 `600` | | P2 | ACL 细粒度授权(6.0+) | `ACL SETUSER app on >pass ~cache:* +@read +@write -@dangerous` | | P2 | 启用 TLS 传输加密(6.0+) | `port 0` + `tls-port 6379` + `tls-auth-clients yes` | | P2 | 及时升级版本 | 关注 CVE 公告,淘汰有已知漏洞的旧版本 | | P2 | 日志留存与审计 | `logfile` 指向真实路径,`loglevel notice`,接入日志平台 | ```bash # 验证加固效果(自查,不涉及任何攻击利用步骤) ss -lntp | grep 6379 # 确认只监听内网地址 ps -o user= -p $(pgrep -f redis-server) # 【必须是 redis,不是 root】 redis-cli -h <内网IP> PING # 应返回 NOAUTH redis-cli -h <内网IP> -a 'pass' --no-auth-warning CONFIG GET dir # 应返回 unknown command ``` **加固口诀:密码要长、地址要窄、身份要低、命令要少、网络要关、版本要新、日志要看。** > 防火墙与 SELinux 的详细配置见 `07-03-防火墙与SELinux.md`。2. 一台 Redis 突然所有写命令都失败,读命令正常。可能是哪两种情况?如何区分和处理?
参考答案
**两种情况:`MISCONF`(持久化失败)与 `OOM`(内存不足)**。**看报错关键字即可区分**: | 报错 | 根本原因 | 第一条排查命令 | 处理 | | --- | --- | --- | --- | | `MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk.` | **RDB/AOF 落盘失败** + `stop-writes-on-bgsave-error yes` 的保护机制 | `df -h` 和 `ls -ld $(redis-cli CONFIG GET dir \| tail -1)` | 清磁盘 / 改权限 / `vm.overcommit_memory=1` | | `OOM command not allowed when used memory > 'maxmemory'.` | 内存达到 `maxmemory` 且策略是 `noeviction` | `redis-cli INFO memory` | 调大 `maxmemory` 或改用 `allkeys-lru` | ```bash # ===== MISCONF 的完整因果链 ===== # 磁盘满/inode满/权限错/只读FS/fork失败/SELinux/dir不存在 # → BGSAVE 或 AOF 写入失败 # → rdb_last_bgsave_status:err 或 aof_last_write_status:err # → stop-writes-on-bgsave-error yes(默认) # → 【拒绝所有写命令】 redis-cli INFO persistence | grep -E 'rdb_last_bgsave_status|aof_last_write_status' df -h; df -i ls -ld /var/lib/redis; namei -l /var/lib/redis cat /proc/sys/vm/overcommit_memory # 必须是 1 # 修复后验证 redis-cli BGSAVE && sleep 3 && redis-cli INFO persistence | grep rdb_last_bgsave_status # 【应急绕过,务必同时排查根因】 redis-cli CONFIG SET stop-writes-on-bgsave-error no # ===== OOM 的处理 ===== redis-cli INFO memory | grep -E 'used_memory_human|maxmemory_human|maxmemory_policy' redis-cli INFO keyspace # expires 远小于 keys = 大量 key 没设 TTL redis-cli CONFIG SET maxmemory 6gb redis-cli CONFIG SET maxmemory-policy allkeys-lru redis-cli --bigkeys -i 0.01 # 治理大 key ``` **特别注意**:若策略是 `volatile-*` 但**没有任何 key 设置 TTL**,找不到可淘汰的 key,行为会**退化为 `noeviction`**,同样报 OOM。用 `INFO keyspace` 观察 `expires` 是否为 0 即可确认。另外若报的是 `READONLY You can't write against a read only replica`,说明连到了**从库**,与内存和磁盘无关。3. mem_fragmentation_ratio 的值为 0.65、1.2、2.1 分别代表什么?如何处理?
参考答案
该指标 = `used_memory_rss / used_memory`,即**操作系统看到的物理内存 ÷ Redis 统计的逻辑内存**。 | 值 | 含义 | 处理 | | --- | --- | --- | | **`0.65`(< 1)** | **RSS 小于逻辑内存 = 部分数据被换到了 swap。这是最危险的情况**:Redis 单线程访问 swap 页会把微秒级操作变成毫秒级磁盘 I/O,慢 1000 倍以上,并阻塞后续所有命令 | 立即 `vm.swappiness=1`、降低 `maxmemory`、清理同机其他进程、必要时 `swapoff -a && swapon -a` 换回内存 | | `1.2`(1~1.5) | 正常范围 | 无需处理 | | **`2.1`(> 2)** | **严重内存碎片**,浪费超过一半物理内存。常见于大量写入后又大量删除 | 开启 `activedefrag yes`(需 jemalloc)、`MEMORY PURGE`、必要时借主从切换重启实例 | ```bash # 确认是否在用 swap(最直接的判断) PID=$(pgrep -f 'redis-server.*6379') grep -E '^(VmRSS|VmSwap)' /proc/$PID/status # VmSwap: 819200 kB ← 非 0 即为问题 # 碎片整理(4.0+,必须是 jemalloc 分配器) redis-cli INFO memory | grep mem_allocator redis-cli CONFIG SET active-defrag-ignore-bytes 500mb redis-cli CONFIG SET active-defrag-threshold-lower 20 redis-cli CONFIG SET active-defrag-cycle-min 1 redis-cli CONFIG SET active-defrag-cycle-max 20 # 【整理在主线程进行,不要设太大】 redis-cli CONFIG SET activedefrag yes redis-cli MEMORY PURGE # 归还空闲页给操作系统 redis-cli INFO memory | grep -E 'allocator_frag_ratio|mem_fragmentation_ratio' ``` **特例**:实例刚启动、数据量极少时该值可能异常大(如 10+),这是分母太小导致的,不是真碎片,可忽略。 **重要陷阱**:`maxmemory` 限制的是 `used_memory`(逻辑内存),**不是** `used_memory_rss`。碎片率 2.0 时 `maxmemory 6gb` 实际会占用约 12GB 物理内存,极易触发系统 OOM Killer。这正是"`maxmemory` 只设物理内存 50%~60%"的原因之一。4. 监控 Redis 应重点关注哪些指标?给出告警阈值建议。
参考答案
| 分类 | 指标 | 来源 | 告警阈值 | | --- | --- | --- | --- | | 可用性 | 实例存活 | `PING` | 无响应即告警 | | 内存 | `used_memory / maxmemory` | `INFO memory` | > 80% 警告,> 90% 严重 | | 内存 | `mem_fragmentation_ratio` | `INFO memory` | **< 1 立即严重告警**(swap);> 1.5 警告 | | 命中率 | `keyspace_hits/(hits+misses)` | `INFO stats` | 缓存场景 **< 90% 警告**(用增量计算) | | 持久化 | `rdb_last_bgsave_status` | `INFO persistence` | `err` 立即严重告警 | | 持久化 | `aof_last_write_status` / `aof_last_bgrewrite_status` | `INFO persistence` | `err` 立即严重告警 | | 持久化 | `rdb_changes_since_last_save` | `INFO persistence` | 长期不下降说明 BGSAVE 未成功 | | 复制 | `master_link_status` | `INFO replication`(从库) | `down` 立即告警 | | 复制 | 主从 offset 差 | `master_repl_offset` - `slave offset` | > 100MB 或持续增大 | | 复制 | `sync_full` | `INFO stats` | 增量 > 0 即告警(频繁全量同步) | | 连接 | `connected_clients / maxclients` | `INFO clients` | > 80% 警告 | | 连接 | `rejected_connections` | `INFO stats` | 增量 > 0 即告警 | | 连接 | `blocked_clients` | `INFO clients` | 异常升高需关注 | | 淘汰 | `evicted_keys` | `INFO stats` | 非缓存场景增量 > 0 即告警 | | 过期 | `expired_keys` 增量 | `INFO stats` | 瞬时暴增 = 过期扎堆 | | 延迟 | `latest_fork_usec` | `INFO stats` | > 200000(200ms)警告 | | 延迟 | 慢日志条数 | `SLOWLOG LEN` | 增量异常升高 | | CPU | `used_cpu_sys` + `used_cpu_user` | `INFO cpu` | 单核接近 100% | | 安全 | `ACL LOG` 记录数、认证失败次数 | `ACL LOG`、日志 | 突增即告警(可能存在爆破或配置错误) | | 集群 | `cluster_state` | `CLUSTER INFO` | 非 `ok` 立即严重告警 | | 集群 | `cluster_slots_assigned` | `CLUSTER INFO` | **≠ 16384 即告警** | **采集方式**: ```bash # Zabbix:Agent 2 内置 redis 插件,或自定义 UserParameter # /etc/zabbix/zabbix_agent2.d/redis.conf # Plugins.Redis.Sessions.local.Uri=tcp://127.0.0.1:6379 # Plugins.Redis.Sessions.local.Password=5. 应用报 Redis 超时,如何一步步定位延迟来源?
参考答案
**三步法:先分内外 → 再看事件 → 最后抓命令。** ```bash # ===== 第 1 步:分内外,判断是 Redis 还是机器/网络 ===== redis-cli --latency -h 127.0.0.1 -p 6379 # min: 0, max: 1, avg: 0.11 ← Redis 侧正常,问题在网络或应用端 redis-cli --intrinsic-latency 10 # 若这里就有几毫秒 → 【宿主机/虚拟化/CPU 争抢】问题,与 Redis 无关 # ===== 第 2 步:看事件,定位延迟类别 ===== redis-cli CONFIG SET latency-monitor-threshold 100 redis-cli LATENCY LATEST redis-cli LATENCY DOCTOR redis-cli LATENCY HISTORY6. 客户端报 max number of clients reached,如何排查处理?CentOS 6 与 7 的 fd 限制配置有什么不同?
参考答案
```bash # 【排查】 redis-cli INFO clients # connected_clients / maxclients redis-cli INFO stats | grep rejected_connections # 非 0 说明已打满过 redis-cli CONFIG GET maxclients # 【关键】Redis 启动时可能因 fd 不足自动下调 maxclients,看日志 grep -iE 'maxclients|max open files' /var/log/redis/redis.log # maxclients has been reduced to 4064 to compensate for low ulimit. PID=$(pgrep -f 'redis-server.*6379') cat /proc/$PID/limits | grep -i 'open files' # 【进程实际的限制】 ls /proc/$PID/fd | wc -l # 已用 fd # 找出泄漏来源 redis-cli CLIENT LIST | awk '{print $2}' | awk -F'[=:]' '{print $2}' | sort | uniq -c | sort -rn | head redis-cli CLIENT LIST | grep -E 'idle=[0-9]{4,}' | head ``` ```bash # 【应急】杀掉空闲连接 redis-cli CLIENT KILL MAXAGE 3600 # 7.4+ redis-cli CLIENT KILL ADDR 192.168.1.55:52341 redis-cli CLIENT KILL TYPE normal SKIPME yes # 6.2+ ``` **CentOS 6 与 7 的 fd 限制配置差异(重要)**: ```bash # ===== CentOS 7:systemd 托管,limits.conf 对服务【无效】 ===== mkdir -p /etc/systemd/system/redis.service.d cat > /etc/systemd/system/redis.service.d/limits.conf <<'EOF' [Service] LimitNOFILE=65535 LimitNPROC=65535 EOF systemctl daemon-reload && systemctl restart redis # ===== CentOS 6:SysV init,用 limits.conf + 启动脚本内显式 ulimit ===== cat >> /etc/security/limits.conf <<'EOF' redis soft nofile 65535 redis hard nofile 65535 EOF # 【关键】limits.conf 只对登录会话生效,SysV 脚本必须自己设置: # 在 /etc/init.d/redis 的 start() 开头加入:ulimit -n 65535 # 还需检查 /etc/security/limits.d/90-nproc.conf 是否覆盖了配置 # ===== 通用:系统全局上限 ===== sysctl -w fs.file-max=1000000 echo 'fs.file-max = 1000000' >> /etc/sysctl.conf && sysctl -p ``` ```bash # 【调整 Redis 参数】 redis-cli CONFIG SET maxclients 20000 # 前提:fd 限制 >= maxclients + 32 redis-cli CONFIG SET timeout 300 # 回收空闲连接 redis-cli CONFIG SET tcp-keepalive 300 # 清理半开连接 redis-cli CONFIG REWRITE ``` ```ini tcp-backlog 2048 # 需同时调大 net.core.somaxconn ``` **根治**是修正应用**连接池**:设置合理的 `maxTotal`(通常 20~50 足够)、必须设 `maxWaitMillis`、确保在 `finally` 中归还连接、不要每次请求都新建连接池。 **历史注意**:Redis 2.x 的 `maxclients` 默认仅 **128**,从老配置模板迁移时必须检查这一项。七、设计与综合题
1. 为一台 16GB 内存、开启 AOF 持久化的服务器规划 Redis 容量,写出关键参数。
参考答案
```text 物理内存 16 GB ├─ 操作系统与其他进程 约 2 GB ├─ fork 时的 COW 冗余 需预留(写入越频繁,冗余越大) ├─ 内存碎片 按 1.3 倍系数估算 └─ Redis 可用 → maxmemory 设为物理内存的 50%~60% maxmemory = 16GB × 55% ≈ 8.8GB → 取整设为 8gb ``` | 场景 | `maxmemory` 建议 | 理由 | | --- | --- | --- | | **开启持久化**(RDB/AOF) | 物理内存 **50%~60%** | fork 时 COW 最坏情况需要接近一倍内存 | | 纯缓存、**关闭持久化** | 物理内存 **70%~80%** | 无 fork,但仍需为碎片留余量 | ```ini maxmemory 8gb maxmemory-policy allkeys-lru # 缓存场景;数据存储场景用 noeviction 并做好告警 appendonly yes appendfsync everysec auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 512mb # 大实例应调大,避免频繁重写 aof-use-rdb-preamble yes # 4.0+ 混合持久化,文件更小加载更快 lazyfree-lazy-eviction yes lazyfree-lazy-expire yes activedefrag yes active-defrag-cycle-max 20 ``` ```bash # 内核参数(必做) cat > /etc/sysctl.d/99-redis.conf <<'EOF' vm.overcommit_memory = 1 vm.swappiness = 1 net.core.somaxconn = 2048 EOF sysctl --system # CentOS 7;CentOS 6 用 /etc/sysctl.conf + sysctl -p echo never > /sys/kernel/mm/transparent_hugepage/enabled ``` **内存估算方法**:单 key 固定开销约 **50~100 字节**(dictEntry、robj、SDS 头、过期字典条目等)。例如 500 万个 `user:2. 需要把一套单机 Redis 的数据迁移到新机器,要求业务尽量不中断,如何操作?
参考答案
**推荐方案:主从复制在线迁移(业务中断仅切换的那一瞬间)**。 ```bash # ===== 1) 新机器上部署同版本 Redis,配置为旧实例的从库 ===== redis-cli -h 新机器 -p 6379 CONFIG SET masterauth 'OldPass' redis-cli -h 新机器 -p 6379 REPLICAOF 旧机器 6379 # ===== 2) 等待全量同步完成 ===== watch -n 2 "redis-cli -h 新机器 -p 6379 INFO replication | \ grep -E 'master_link_status|master_sync_in_progress|slave_repl_offset'" # master_link_status:up 且 master_sync_in_progress:0 表示已进入增量复制 # ===== 3) 校验数据一致性 ===== redis-cli -h 旧机器 DBSIZE; redis-cli -h 新机器 DBSIZE redis-cli -h 旧机器 INFO replication | grep master_repl_offset redis-cli -h 新机器 INFO replication | grep slave_repl_offset # 两者应相等 # ===== 4) 切换(业务侧短暂停写,或直接切 DNS/VIP/配置中心)===== redis-cli -h 旧机器 CLIENT PAUSE 5000 WRITE # 6.2+ 只暂停写命令 redis-cli -h 新机器 -p 6379 REPLICAOF NO ONE # 新实例提升为主库 # 修改应用配置/DNS/VIP 指向新机器 # ===== 5) 观察与收尾 ===== redis-cli -h 新机器 INFO clients redis-cli -h 新机器 INFO stats | grep instantaneous_ops_per_sec # 确认业务正常后再下线旧实例(建议保留一段时间作为回滚点) ``` **其他方案对比**: | 方案 | 命令 | 适用场景 | 中断时间 | | --- | --- | --- | --- | | **主从在线迁移** | `REPLICAOF` | **生产首选**,数据量大 | 秒级 | | RDB 冷迁移 | `BGSAVE` + `scp dump.rdb` | 可停机维护窗口 | 分钟级 | | 单 key 迁移 | `DUMP` + `RESTORE` | 少量 key 补数 | 无 | | 服务端搬移 | `MIGRATE` | 少量 key,跨实例 | 无(但**阻塞**两端) | | 工具同步 | `redis-shake` / `redis-port` / `redis-migrate-tool` | 跨版本、单机↔集群、云上云下 | 秒级 | | 云厂商 DTS | 控制台操作 | 迁移到云托管 Redis | 秒级 | ```bash # RDB 冷迁移的关键:【导入前必须先关 AOF】 # 因为启动时 AOF 优先级高于 RDB,否则导入的 RDB 不会被加载 redis-cli -h 旧机器 BGSAVE scp /var/lib/redis/dump.rdb 新机器:/var/lib/redis/ chown redis:redis /var/lib/redis/dump.rdb # 新机器 redis.conf 中 appendonly no,启动后再 redis-cli CONFIG SET appendonly yes # 会自动触发一次 AOF 重写生成新 AOF # 单 key 迁移 redis-cli -h 旧机器 --no-raw DUMP mykey redis-cli -h 新机器 RESTORE mykey 0 "<序列化数据>" REPLACE # 【限制】RDB 版本只能【向前兼容】:低版本导出的可以导入高版本,反之报 # "DUMP payload version or checksum are wrong" # 服务端直接搬移(【阻塞两端实例】,只适合少量 key) redis-cli -h 旧机器 MIGRATE 新机器IP 6379 "" 0 5000 AUTH2 default NewPass KEYS k1 k2 k3 # COPY = 保留源 key,REPLACE = 覆盖目标已存在的 key # 批量导入(比逐条执行快数十倍) cat commands.txt | redis-cli --pipe ``` 集群内部的槽迁移(reshard)见第七章,用 `redis-cli --cluster reshard` / `--cluster rebalance`。3. 设计一套 Redis 缓存方案,说明如何应对缓存穿透、击穿、雪崩。
参考答案
| 问题 | 定义 | 解决方案 | | --- | --- | --- | | **缓存穿透** | 查询**根本不存在**的数据,每次都打到数据库 | ① **缓存空值**并设短 TTL(如 60 秒);② **布隆过滤器**前置拦截;③ 接口层参数校验(ID 格式、范围);④ 对恶意请求限流 | | **缓存击穿** | **单个热点 key** 过期瞬间,大量并发同时打到数据库 | ① **互斥锁/分布式锁**(`SET key val NX EX 10`),只让一个线程回源;② **逻辑过期 + 异步刷新**(value 内嵌过期时间,过期后返回旧值同时后台更新);③ 热点 key **永不过期**,靠后台任务定时刷新 | | **缓存雪崩** | **大量 key 同时过期**或 **Redis 整体宕机**,流量瞬间压垮数据库 | ① **TTL 加随机抖动**打散过期时间;② 缓存**高可用**(主从 + 哨兵 / Cluster);③ **多级缓存**(本地 Caffeine + Redis);④ 数据库侧**限流、熔断、降级**;⑤ 缓存预热 | ```bash # ===== TTL 加随机抖动(同时解决雪崩与过期抖动)===== base=3600; jitter=$((RANDOM % 600)) redis-cli SET cache:user:1001 "$data" EX $((base + jitter)) # ===== 分布式锁防击穿 ===== # 加锁:NX 保证只有一个客户端成功,EX 防止死锁 redis-cli SET lock:user:1001 "$(uuidgen)" NX EX 10 # 解锁必须用 Lua 保证【校验持有者 + 删除】的原子性, # 否则可能误删别人的锁(自己的锁已超时释放,被他人获取) redis-cli EVAL "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 lock:user:1001 "$uuid" # ===== 缓存空值防穿透 ===== redis-cli SET cache:user:99999 "__NULL__" EX 60 # ===== 布隆过滤器(RedisBloom 模块)===== redis-cli BF.RESERVE bf:users 0.001 10000000 redis-cli BF.ADD bf:users 1001 redis-cli BF.EXISTS bf:users 99999 # 返回 0 = 一定不存在,直接拒绝 ``` **配套的运维配置**: ```ini maxmemory 8gb maxmemory-policy allkeys-lru # 缓存场景,内存满时淘汰最久未用 lazyfree-lazy-eviction yes # 淘汰时异步释放,减少阻塞 lazyfree-lazy-expire yes rename-command KEYS "" # 防止误操作拖死实例 rename-command FLUSHALL "" rename-command FLUSHDB "" ``` **设计要点**:布隆过滤器**有误判但无漏判**(判断"不存在"一定准确,"存在"可能误判),正好契合穿透防护;分布式锁的**解锁必须用 Lua 保证原子性**;集群模式下缓存 key 与其锁 key 若需在同一节点,可用 hash tag(如 `cache:{1001}` 与 `lock:{1001}`)。缓存穿透、击穿、雪崩的详细原理见第九章。4. 生产环境上线一套 Redis 主从 + 哨兵,列出上线前的检查清单。
参考答案
| 类别 | 检查项 | 验证命令 | | --- | --- | --- | | **安全** | 以 `redis` 用户运行,**不是 root** | `ps -o user= -p $(pgrep -f redis-server)` | | **安全** | 所有节点 `requirepass` 一致且足够强 | `CONFIG GET requirepass` | | **安全** | 每个节点都配了 `masterauth` | `CONFIG GET masterauth` | | **安全** | `bind` 为内网 IP,非 `0.0.0.0` | `ss -lntp \| grep 6379` | | **安全** | `protected-mode yes`(3.2+) | `CONFIG GET protected-mode` | | **安全** | 危险命令已禁用 | `redis-cli CONFIG GET dir` 应报未知命令 | | **安全** | 配置文件 600,数据目录 750,RDB/AOF 文件 600 | `ls -l /etc/redis/ /var/lib/redis` | | **安全** | 防火墙只放行应用网段 + 哨兵端口 26379 | `firewall-cmd --list-all` | | **内存** | `maxmemory` 已设且为物理内存 50%~60% | `CONFIG GET maxmemory` | | **内存** | `maxmemory-policy` 符合业务定位 | `CONFIG GET maxmemory-policy` | | **内核** | **`vm.overcommit_memory = 1`** | `cat /proc/sys/vm/overcommit_memory` | | **内核** | `vm.swappiness = 1` | `cat /proc/sys/vm/swappiness` | | **内核** | **THP 已关闭** | `cat /sys/kernel/mm/transparent_hugepage/enabled` | | **内核** | `net.core.somaxconn >= tcp-backlog` | `sysctl net.core.somaxconn` | | **限制** | `LimitNOFILE`(CentOS 7)/ `ulimit -n`(CentOS 6)≥ 65535 | `cat /proc/八、命令实操题
在单机实例上依次完成,写出你使用的命令(第二节已练过的"写入并设置过期时间"不再重复):
1. 用一条命令原子地实现"计数器加 5",再把它改成浮点加 0.5。
答:INCRBY visits 5;INCRBYFLOAT visits 0.5。注意 INCRBYFLOAT 之后该 key 不能再用 INCR(会报 not an integer)。
2. 把一个 Hash 中的所有字段名、所有值、字段总数分别列出来,并把 score 字段加 10。
答:HKEYS user:1001 / HVALS user:1001 / HLEN user:1001 / HINCRBY user:1001 score 10。
3. 向 List 依次压入 a b c,只保留最新的 2 个元素,并查看结果。
答:LPUSH tl a b c → LTRIM tl 0 1 → LRANGE tl 0 -1(结果为 c、b;注意 LPUSH 多值时是逐个左插,最后一个参数在最左端)。
4. 求两个集合的交集、并集、差集,并把交集结果直接存到新 key。
答:SINTER s1 s2 / SUNION s1 s2 / SDIFF s1 s2 / SINTERSTORE s_result s1 s2。
5. 在 ZSet 中给成员加分,并查出它的排名(分数从高到低)与分数。
答:ZINCRBY rank 30 tom → ZREVRANK rank tom(排名从 0 开始)→ ZSCORE rank tom。
6. 不阻塞地找出所有以 session: 开头的 key。
答:SCAN 0 MATCH 'session:*' COUNT 100,然后用返回的游标继续迭代直到游标回到 0;命令行可用 redis-cli --scan --pattern 'session:*' 自动完成整轮迭代。禁止使用 KEYS session:*。
7. 用事务把两个 key 一起写入,并演示 WATCH 冲突导致事务放弃。
答:MULTI → SET k1 v1 → SET k2 v2 → EXEC。冲突演示:客户端 A 执行 WATCH k1 后 MULTI、INCR k1(暂不 EXEC);客户端 B 执行 SET k1 999;此时 A 执行 EXEC 返回 (nil),事务被放弃。
8. 查出实例当前内存使用量、连接数、命中率,并计算命中率。
答:INFO memory | grep used_memory_human、INFO clients | grep connected_clients、INFO stats | grep keyspace;命中率 = keyspace_hits / (keyspace_hits + keyspace_misses),纯缓存场景低于 0.9 就要排查 key 设计或容量。
9. 安全地删除一个 500 万元素的大 Hash,不阻塞主线程。
答:先用 HSCAN bigkey 0 COUNT 500 分批取出字段,配合 HDEL bigkey field... 分批删除;最后 UNLINK bigkey 异步回收(4.0+)。直接 DEL bigkey 会长时间阻塞单线程。
九、综合实验(10 个)
实验 1:编译安装 Redis 并配置为 systemd 服务
目标:源码编译安装 Redis,创建专用运行用户与目录,编写 systemd 单元实现开机自启与 systemctl 管理。
要求与验收:
- 安装编译依赖(
gcc、make、systemd-devel),下载稳定版源码,make USE_SYSTEMD=yes编译后make install PREFIX=/usr/local/redis;若报jemalloc/jemalloc.h缺失,改用make MALLOC=libc; - 创建系统用户
redis(useradd -r -s /sbin/nologin redis)与目录/etc/redis、/var/lib/redis、/var/log/redis、/var/run/redis,属主全部改为redis; - 配置文件设置
supervised systemd、daemonize no、dir /var/lib/redis、logfile /var/log/redis/redis.log、pidfile /var/run/redis/redis_6379.pid; - 编写
/etc/systemd/system/redis.service,Type=notify、User=redis、ExecStart=/usr/local/redis/bin/redis-server /etc/redis/redis.conf、LimitNOFILE=65535; - 验收:
systemctl enable --now redis后systemctl status redis为active (running),redis-cli PING返回PONG,重启主机后服务自动恢复,ps -ef | grep redis-server显示运行用户为redis而非root。
思考:为什么推荐 daemonize no + Type=notify 而不是 daemonize yes?
答:交给 systemd 托管进程时,若 Redis 自己 fork 到后台,systemd 会丢失真实主进程,导致状态显示异常、Restart= 失效。Type=notify 配合 supervised systemd 由 Redis 主动上报就绪状态,最准确。
实验 2:五种数据类型综合建模
目标:用五种类型分别为同一个电商场景建模,体会类型选择差异。
要求:用 String 做商品详情缓存与库存计数器;用 Hash 存用户资料;用 List 做下单消息队列;用 Set 做商品标签与"猜你喜欢"交集计算;用 ZSet 做销量排行榜。
验收:对每个 key 执行 TYPE、MEMORY USAGE、OBJECT ENCODING,记录小数据量与大数据量下编码的变化(如 Hash 超过 hash-max-listpack-entries 128 后由 listpack 变为 hashtable,内存明显上升),并解释为什么紧凑编码更省内存。
实验 3:RDB 与 AOF 故障恢复演练与数据丢失量对比
目标:分别在只开 RDB、只开 AOF 两种配置下模拟进程崩溃,量化对比数据丢失量。
要求与验收:
- RDB 组:配置
save 900 1、appendonly no,用redis-benchmark -t set -n 100000写入数据,记录INFO persistence中rdb_last_save_time与rdb_changes_since_last_save,然后kill -9掉进程(不要SHUTDOWN,那会触发保存),重启后DBSIZE对比写入量,丢失量 ≈ 上次快照之后的全部写入; - AOF 组:配置
appendonly yes、appendfsync everysec,同样压测后kill -9,重启后DBSIZE对比,丢失量应 ≤ 最近 1 秒的写入; - 再把
appendfsync改为always重复一次,对比 QPS 下降幅度与丢失量(理论上为 0); - 验收:整理一张三行表格,列出配置、崩溃前写入量、恢复后数据量、丢失条数、压测 QPS,并给出你的生产建议(通常是 RDB + AOF 同时开启 +
aof-use-rdb-preamble yes)。
补充演练:故意破坏 AOF 文件尾部若干字节,观察 Redis 启动失败,然后用 redis-check-aof --fix appendonly.aof 修复;同理用 redis-check-rdb dump.rdb 校验 RDB。修复前务必先备份原文件。
实验 4:一主两从与读写分离验证
目标:搭建 1 主 2 从,验证数据同步、从库只读与读写分离。
要求与验收:
- 三个实例(
192.168.1.10:6379为主,.11:6379、.12:6379为从),从库配置replicaof 192.168.1.10 6379与masterauth,主库配置requirepass(两端密码保持一致,便于将来角色互换); - 主库
INFO replication应显示role:master、connected_slaves:2,且每个slaveN的state=online、offset与主库master_repl_offset接近; - 主库写入
SET k1 v1,两个从库立即GET k1应能读到; - 在从库执行
SET k2 v2,应报(error) READONLY You can't write against a read only replica,说明replica-read-only yes生效; - 验收:把从库的
replica-read-only临时改为no,在从库写入一个 key,再让主库同步一次数据,观察该 key 被覆盖或造成主从不一致,从而理解为什么生产必须保持从库只读。
思考:replica-serve-stale-data 设为 no 会带来什么行为变化?
答:当从库与主库断开或正在全量同步时,除 INFO、REPLICAOF 等少数命令外,其余请求会返回 SYNC with master in progress 错误,避免读到过期数据;代价是可用性下降,需业务侧能容忍。
实验 5:三节点哨兵与手工故障转移
目标:为实验 4 的主从架构加上 3 个哨兵,手工触发故障转移并完整观察选主过程。
要求与验收:
- 三台机器各起一个哨兵(端口 26379),配置
sentinel monitor mymaster 192.168.1.10 6379 2、sentinel auth-pass mymaster <密码>、down-after-milliseconds 5000、parallel-syncs 1、failover-timeout 60000; redis-cli -p 26379 SENTINEL masters与SENTINEL replicas mymaster、SENTINEL sentinels mymaster确认三个哨兵互相发现、监控到 2 个从库;- 触发方式一(优雅):
redis-cli -p 26379 SENTINEL FAILOVER mymaster;触发方式二(模拟宕机):在主库执行DEBUG SLEEP 30或直接kill -9主库进程; - 同时开一个终端订阅事件:
redis-cli -p 26379 PSUBSCRIBE '*',观察+sdown、+odown、+try-failover、+vote-for-leader、+elected-leader、+switch-master、+slave-reconf-sent的完整时序; - 验收:故障转移完成后,新主库
INFO replication为role:master且带 1 个从库;原主库恢复启动后自动变为新主库的从库;检查三份sentinel.conf与redis.conf是否被哨兵自动改写(replicaof行被重写)。
思考:为什么哨兵数量必须 ≥ 3 且为奇数?quorum=2 时挂掉一个哨兵还能故障转移吗?
答:执行故障转移需要超过半数哨兵授权(3 个哨兵需 2 票),偶数个哨兵在网络分区时容易双方都拿不到多数。3 哨兵 + quorum=2 时,挂 1 个哨兵仍有 2 个可用,满足 quorum 与 majority,可以正常转移;挂 2 个则无法转移。
实验 6:三主三从 Cluster、MOVED 重定向与在线扩缩容
目标:搭建三主三从集群,验证槽位分配、重定向机制与 reshard 扩缩容。
要求与验收:
- 三台机器各起 2 个实例(6379、6380),配置
cluster-enabled yes、cluster-config-file nodes-<port>.conf、cluster-node-timeout 15000,requirepass与masterauth必须相同; - 用
redis-cli --cluster create <6 个 ip:port> --cluster-replicas 1 -a <密码>创建(Redis 5.0+ 内置,无需 Ruby 的redis-trib.rb),确认输出[OK] All 16384 slots covered.;确保主从交叉分布在不同物理机上; CLUSTER INFO中cluster_state:ok、cluster_slots_assigned:16384;CLUSTER NODES记录每个主节点的槽区间(通常 0-5460、5461-10922、10923-16383);- 重定向验证:用不带
-c的客户端连接节点 A 写一个不属于它的 key,观察(error) MOVED 12182 192.168.1.12:6379;再用-c连接,客户端自动跟随重定向。用CLUSTER KEYSLOT <key>手工验算槽号,并用 hash tag 让{user:1}:a与{user:1}:b落到同一槽,验证跨槽MSET报CROSSSLOT而带 tag 的可以成功; - 扩容:新增一个节点
--cluster add-node,再redis-cli --cluster reshard从三个主节点各迁出约 1365 个槽给它;迁移过程中持续GET/SET观察是否出现ASK重定向与短暂错误; - 缩容:先用
reshard把该节点的槽全部迁回,再--cluster del-node移除,最后--cluster check确认槽位完整; - 验收:整理扩缩容前后的槽位分布表,并说明
MOVED与ASK出现的时机差异。
故障演练:kill -9 掉一个主节点,观察其从节点在 cluster-node-timeout 后被提升为主;再停掉某台机器上的两个实例,用 cluster-require-full-coverage yes/no 两种配置对比集群是否整体拒绝服务。
实验 7:redis-benchmark 压测与指标解读
目标:用官方压测工具建立性能基线,并能解释每项指标含义。
要求与验收:
- 基线:
redis-benchmark -h 127.0.0.1 -p 6379 -a <密码> -c 50 -n 100000 -t set,get,incr,lpush,sadd,zadd -q; - 变量对比:分别调整并发数(
-c 10/50/200/1000)、数据大小(-d 64/1024/10240)、pipeline 深度(-P 1/16/64),记录 QPS 与--latency输出的变化; - 用
redis-cli --latency与--latency-history观察基础往返延迟,用--intrinsic-latency 60测量本机内核/调度带来的固有延迟; - 验收:写出一份结论,至少解释以下四点:
-P提高 QPS 的原因是减少 RTT 而非提升 Redis 处理能力;并发过高时 QPS 不再增长反而延迟上升;-d增大后 QPS 下降主要受网络带宽与内存拷贝限制;avg latency与p99 latency不能互相替代,容量规划必须看 p99/p999。
注意:redis-benchmark 默认会写入形如 key:__rand_int__ 的测试数据,绝不可在生产实例上运行;测试实例压测后用 FLUSHALL 清理(生产环境该命令应已被 rename-command 屏蔽)。
实验 8:慢查询日志与 SCAN 排查大 key、热 key
目标:主动制造慢查询与大 key,走完一次完整的排查闭环。
要求与验收:
- 配置
slowlog-log-slower-than 10000(10ms)、slowlog-max-len 1024,用CONFIG SET即时生效; - 制造慢查询:写入一个 100 万元素的 List 与一个 100 万字段的 Hash,然后执行
LRANGE biglist 0 -1、HGETALL bighash、KEYS *; SLOWLOG GET 10查看记录(含耗时、命令、客户端地址),SLOWLOG LEN、SLOWLOG RESET;- 大 key 定位:
redis-cli --bigkeys、redis-cli --memkeys、MEMORY USAGE bighash、STRLEN/LLEN/HLEN/SCARD/ZCARD逐项确认; - 热 key 定位:把
maxmemory-policy设为allkeys-lfu后用redis-cli --hotkeys;再用redis-cli MONITOR | head -n 2000短时抽样统计(注意MONITOR本身有性能开销,生产只能秒级抽样); - 安全删除:用
SCAN/HSCAN分批 +UNLINK清理,全过程用redis-cli --latency监控延迟,对比直接DEL造成的阻塞; - 验收:交付一份排查报告,包含慢查询 Top5、大 key 清单(key 名、类型、元素数、内存占用)、热 key 清单,以及针对每一项的整改方案(拆分大 key、改用
HSCAN分页、热 key 加本地缓存或打散)。
实验 9:缓存穿透与雪崩的模拟与防护
目标:在测试环境复现两类缓存事故,并实现并验证防护措施。
要求与验收:
- 穿透模拟:写一段脚本用随机不存在的 ID 高频查询(
GET product:<随机大数>),统计缓存命中率骤降与后端"数据库"(可用日志计数模拟)请求量暴涨; - 穿透防护:① 查询结果为空时
SET product:<id> "" EX 60缓存空值;② 前置布隆过滤器(可用SETBIT/GETBIT手写简易版或 RedisBloom 模块的BF.ADD/BF.EXISTS);③ 对比防护前后的后端请求量下降比例; - 雪崩模拟:批量写入 10000 个 key 且统一设置相同 TTL,等待集体过期,观察后端请求量出现尖峰;
- 雪崩防护:TTL 改为
基础值 + rand(0, 300)秒随机抖动,重复实验对比后端请求曲线是否被削平; - 击穿防护(加分项):对单个热点 key,用
SET lock:<key> <uuid> NX EX 10抢重建锁,未抢到锁的请求短暂 sleep 后重读缓存;释放锁必须用 Lua 脚本校验 value 一致后再删,避免误删他人的锁; - 验收:给出四张对比数据(后端请求量、缓存命中率、p99 延迟、错误率),并总结"空值缓存 + 布隆过滤器 + TTL 抖动 + 单飞重建 + 熔断降级"这一组合各自解决什么问题。
实验 10:安全加固自查
目标:对一个已运行实例按清单做一次完整自查与整改,并验证每项生效。
要求与验收:
- 逐项检查:
bind是否限定内网/回环;protected-mode是否为yes;requirepass是否为 16 位以上高强度随机口令;运行用户是否为非 root 的redis;dir是否为/var/lib/redis专用目录且 RDB/AOF 权限为600;端口是否改为非默认并只对应用服务器放行;FLUSHALL/FLUSHDB/KEYS/CONFIG/DEBUG/SHUTDOWN等是否已rename-command或用 ACL 收敛;Redis 6.0+ 是否已建立按业务划分的 ACL 用户(如~order:* +@read +@write -@dangerous)并禁用default;版本是否在维护期内; - 逐项验证:
ss -tlnp | grep 6379看监听地址;从非白名单主机redis-cli -h <IP> PING应超时或被拒;无密码连接执行命令应返回NOAUTH;错误密码应返回WRONGPASS;用受限 ACL 用户访问越权 key 应返回NOPERM,并能在ACL LOG中看到记录;ps -ef | grep redis-server确认非 root; - 验收:产出一份自查表,每项标注"符合 / 不符合 / 整改后符合",附上整改前后的命令输出对照。
本实验只做防守方的自查与加固验证,不涉及任何攻击利用步骤。请始终在自己拥有的、与生产隔离的测试环境中操作;对外部或未授权系统进行探测与攻击均属违法行为。
本章小结
一、常用命令速查表
通用 KEY 操作
| 命令 | 用途 |
|---|---|
EXISTS key [key...] |
判断 key 是否存在(3.0.3+ 支持多个,返回存在个数) |
TYPE key |
查看 key 的值类型 |
DEL key / UNLINK key |
删除 key;UNLINK 异步回收,删大 key 必用 |
EXPIRE key n / PEXPIRE key n |
设置过期时间,单位秒 / 毫秒(7.0+ 支持 NX/XX/GT/LT) |
EXPIREAT / PEXPIREAT key ts |
用绝对时间戳设置过期时刻 |
TTL key / PTTL key |
查看剩余存活时间(-1 永久,-2 不存在) |
EXPIRETIME / PEXPIRETIME key |
7.0+,查询绝对过期时刻 |
PERSIST key |
移除过期时间,变为永久 key |
RENAME / RENAMENX key newkey |
改名;NX 版本仅当新名不存在时才改(集群下要求同槽) |
SCAN cursor MATCH p COUNT n |
渐进式遍历 key,线上替代 KEYS |
KEYS pattern |
一次性匹配所有 key,线上禁用 |
RANDOMKEY / MOVE key db |
随机取一个 key / 把 key 移到另一个库 |
SELECT n / SWAPDB a b |
切换数据库 / 交换两个库(集群只能用 db0) |
COPY src dst [REPLACE] |
6.2+,复制一个 key |
DBSIZE |
当前库的 key 总数 |
String
| 命令 | 用途 |
|---|---|
SET key value [EX s] [NX\|XX] [KEEPTTL] [GET] |
写入,可同时设过期与存在性条件(分布式锁基础) |
GET key / MGET k1 k2 |
取单个 / 批量取值 |
SETNX / SETEX / GETSET |
不存在才写 / 写入并设过期 / 写新值并返回旧值 |
GETDEL / GETEX key |
6.2+,取值并删除 / 取值并(重)设过期时间 |
MSET / MSETNX |
批量写入 / 全部不存在时才批量写入 |
INCR / DECR / INCRBY / DECRBY / INCRBYFLOAT |
原子加减,计数器与限流的核心 |
APPEND key value |
在原值末尾追加字符串 |
STRLEN key / GETRANGE key s e / SETRANGE key off v |
取长度 / 取子串 / 按偏移覆写 |
SETBIT / GETBIT / BITCOUNT / BITPOS |
位操作,签到、活跃用户统计(Bitmap) |
BITOP / BITFIELD |
多个 Bitmap 做 AND/OR/XOR / 按位域读写整数 |
PFADD / PFCOUNT / PFMERGE |
HyperLogLog 基数近似统计与合并,极省内存 |
List
| 命令 | 用途 |
|---|---|
LPUSH / RPUSH key v... |
从头 / 从尾入队(LPUSHX/RPUSHX 仅在 key 存在时) |
LPOP / RPOP key [n] |
从头 / 从尾出队 |
BLPOP / BRPOP key timeout |
阻塞出队,实现无轮询消费者 |
LRANGE key start stop |
取区间元素,0 -1 表示全部(大 List 慎用) |
LLEN key / LINDEX key i / LPOS key v |
取长度 / 按下标取值 / 6.0.6+ 查元素位置 |
LTRIM key start stop |
只保留区间内元素,做"最新 N 条" |
LSET / LINSERT / LREM |
按下标改值 / 在指定元素前后插入 / 删除指定值 |
RPOPLPUSH / BRPOPLPUSH src dst |
出队并转移到另一个 List,可实现可靠队列 |
LMOVE / BLMOVE / BLMPOP |
6.2+ 任意方向转移 / 阻塞版 / 7.0+ 多 key 阻塞弹出 |
Hash
| 命令 | 用途 |
|---|---|
HSET / HSETNX key field value |
写字段 / 字段不存在时才写(HMSET 4.0+ 已废弃) |
HGET / HMGET key field... |
取单字段 / 批量取字段 |
HGETALL key |
取全部字段与值(大 Hash 禁用,改用 HSCAN) |
HDEL key field... |
删除一个或多个字段 |
HEXISTS key field / HLEN key |
判断字段是否存在 / 字段总数 |
HKEYS / HVALS key |
取所有字段名 / 所有值 |
HINCRBY / HINCRBYFLOAT |
对字段原子加减整数 / 浮点数 |
HRANDFIELD key [n] |
6.2+,随机取字段 |
HSCAN key cursor |
渐进式遍历字段,排查与清理大 Hash |
Set
| 命令 | 用途 |
|---|---|
SADD key member... |
添加成员,自动去重 |
SREM key member... |
移除成员 |
SMEMBERS key |
取全部成员(大集合禁用,改用 SSCAN) |
SISMEMBER / SMISMEMBER key member... |
判断是否为成员,O(1);SMISMEMBER 为 6.2+ 批量版 |
SCARD key |
成员总数 |
SINTER / SUNION / SDIFF |
交集 / 并集 / 差集运算 |
SINTERSTORE / SUNIONSTORE / SDIFFSTORE |
运算结果直接存入新 key,避免大结果集回传 |
SINTERCARD key... [LIMIT n] |
7.0+,只返回交集基数,不返回成员 |
SPOP / SRANDMEMBER key [n] |
随机弹出(抽奖)/ 随机取但不删除 |
SMOVE src dst member |
在两个集合间原子移动成员 |
SSCAN key cursor |
渐进式遍历成员,安全替代 SMEMBERS |
ZSet
| 命令 | 用途 |
|---|---|
ZADD key [NX\|XX] [GT\|LT] [CH] [INCR] score member... |
添加成员或更新分数 |
ZINCRBY key n member |
对成员分数原子加减 |
ZSCORE / ZMSCORE key member... |
查询成员分数(ZMSCORE 为 6.2+ 批量版) |
ZRANK / ZREVRANK key member |
升序 / 降序排名(从 0 开始) |
ZRANGE / ZREVRANGE key s e [WITHSCORES] |
按排名区间取成员,排行榜取 TopN |
ZRANGEBYSCORE / ZREVRANGEBYSCORE |
按分数区间取成员,延时队列取到期任务 |
ZRANGEBYLEX / ZREVRANGEBYLEX |
分数相同时按字典序取区间 |
ZCARD key / ZCOUNT key min max |
成员总数 / 分数区间内成员数 |
ZREM / ZREMRANGEBYSCORE / ZREMRANGEBYRANK |
删成员 / 按分数区间删 / 按排名区间删 |
ZINTERSTORE / ZUNIONSTORE / ZDIFF |
交集 / 并集并存入新 key(可带权重)/ 6.2+ 差集 |
ZRANDMEMBER / ZMPOP |
6.2+ 随机取成员 / 7.0+ 多 key 弹出 |
ZSCAN key cursor |
渐进式遍历成员与分数 |
扩展类型(GEO / Stream / 发布订阅)
| 命令 | 用途 |
|---|---|
GEOADD / GEOPOS / GEODIST |
写入经纬度 / 查坐标 / 算两点距离 |
GEOSEARCH(6.2+)/ GEORADIUS(废弃) |
按半径或矩形范围检索附近的成员 |
XADD / XLEN / XRANGE / XTRIM |
Stream 追加消息 / 长度 / 范围读 / 裁剪 |
XREAD / XGROUP / XREADGROUP |
独立消费 / 建消费组 / 按组消费(有 ack,可靠队列首选) |
XACK / XPENDING / XCLAIM / XAUTOCLAIM(6.2+) |
确认 / 查未确认 / 转移消息所有权 |
XINFO STREAM/GROUPS/CONSUMERS |
查看 Stream、消费组与消费者状态 |
PUBLISH / SUBSCRIBE / PSUBSCRIBE / UNSUBSCRIBE |
发布订阅(消息不持久化,客户端离线即丢) |
PUBSUB CHANNELS/NUMSUB / SSUBSCRIBE(7.0+) |
查看频道与订阅数 / 集群分片订阅 |
事务与脚本
| 命令 | 用途 |
|---|---|
MULTI |
开启事务,后续命令进入队列 |
EXEC |
执行队列中所有命令(无回滚) |
DISCARD |
放弃事务,清空命令队列 |
WATCH key... / UNWATCH |
乐观锁:被监视 key 若被改则 EXEC 返回 nil / 取消监视 |
EVAL script numkeys key... arg... |
执行 Lua 脚本,实现"读—判断—写"整体原子 |
SCRIPT LOAD / EVALSHA sha1 |
预加载脚本 / 按 SHA 执行,省带宽 |
SCRIPT EXISTS/FLUSH/KILL |
查脚本是否已缓存 / 清空脚本缓存 / 终止运行中的脚本 |
FUNCTION / FCALL(7.0+) |
函数库方式管理服务端逻辑,替代散装 EVAL |
持久化与运维
| 命令 | 用途 |
|---|---|
SAVE / BGSAVE |
同步(阻塞,禁用)/ 后台 fork 生成 RDB 快照 |
BGREWRITEAOF |
后台重写 AOF,压缩体积 |
LASTSAVE |
最后一次成功保存的时间戳 |
INFO [section] |
查看运行状态,最常用 memory/stats/persistence/replication;6.2+ 支持 INFO everything |
CONFIG GET/SET param |
在线查看 / 修改配置(CONFIG REWRITE 落盘,CONFIG RESETSTAT 清统计) |
DBSIZE / FLUSHDB / FLUSHALL |
key 总数 / 清空当前库 / 清空所有库(后两者线上必须屏蔽) |
SLOWLOG GET/LEN/RESET |
慢查询日志的读取、计数与清空 |
ROLE / TIME / COMMAND COUNT/DOCS/INFO |
查角色 / 服务端时间 / 命令元信息 |
DUMP / RESTORE / MIGRATE |
序列化导出 / 导入 / 服务端间直接搬移(阻塞两端) |
CLIENT LIST / CLIENT KILL |
查看连接明细 / 踢掉异常连接 |
CLIENT PAUSE/UNPAUSE(6.2+)/INFO/SETNAME/GETNAME/ID |
暂停写入、连接标识与管理 |
CLIENT NO-EVICT(7.0+) / NO-TOUCH(7.2+) |
连接级免淘汰 / 不更新 LRU 空转时间 |
内存、延迟与调试诊断
| 命令 | 用途 |
|---|---|
MEMORY USAGE key / MEMORY DOCTOR |
单 key 内存占用 / 内存问题体检建议 |
MEMORY STATS / MEMORY PURGE |
内存分项明细 / 把空闲页归还操作系统 |
OBJECT ENCODING/FREQ/IDLETIME/REFCOUNT |
查看底层编码、LFU 频率、空闲时间、引用计数 |
LATENCY LATEST/HISTORY/RESET/DOCTOR/GRAPH |
延迟事件的最新值、历史、重置与诊断建议 |
DEBUG OBJECT / DEBUG SLEEP / DEBUG JMAP |
对象内部信息 / 阻塞主线程(仅演练用)/ 调试辅助 |
MONITOR |
实时打印所有命令(吞吐下降 50%+,生产仅秒级抽样) |
ACL SETUSER/GETUSER/DELUSER/LIST/USERS/WHOAMI(6.0+) |
建/查/删 ACL 用户,查看当前身份 |
ACL CAT/GENPASS/LOG/SAVE/LOAD/DRYRUN(7.0+) |
命令类别 / 生成强口令 / 权限拒绝审计日志 / 规则落盘与试算 |
主从与哨兵
| 命令 | 用途 |
|---|---|
REPLICAOF host port(旧 SLAVEOF) |
运行时建立复制关系,重启失效 |
REPLICAOF NO ONE |
断开复制,晋升为独立主库 |
INFO replication |
查看角色、从库列表、复制偏移量 |
WAIT numreplicas timeout |
阻塞等待指定数量从库确认写入,提升写安全 |
FAILOVER(6.2+)/ PSYNC |
主库主动发起协调式切换 / 复制协议命令(内部使用) |
SENTINEL masters / master <name> / replicas / sentinels |
查看被监控主库、单个主库详情、从库与其他哨兵 |
SENTINEL get-master-addr-by-name mymaster |
查询当前主库地址,客户端服务发现的入口 |
SENTINEL FAILOVER mymaster |
手工触发故障转移(演练与计划内切换) |
SENTINEL reset/set/ckquorum |
重置监控状态 / 动态改参数 / 检查票数是否足够 |
Cluster
| 命令 | 用途 |
|---|---|
CLUSTER INFO |
查看集群状态,关注 cluster_state 与 slots_assigned |
CLUSTER NODES / CLUSTER SLOTS / CLUSTER SHARDS(7.0+) |
查看节点、角色、主从关系与槽区间 |
CLUSTER KEYSLOT key |
计算 key 所属槽号,验证 hash tag 效果 |
CLUSTER COUNTKEYSINSLOT / GETKEYSINSLOT |
槽内 key 数量 / 列出槽内 key |
CLUSTER ADDSLOTS/DELSLOTS/ADDSLOTSRANGE(7.0+)/SETSLOT |
手工分配、回收、按范围分配与迁移槽位 |
CLUSTER MEET/FORGET/REPLICATE/SLAVES <id> |
加入节点 / 移除节点 / 指定复制目标 / 查从节点 |
CLUSTER MYID |
查看本节点 ID |
CLUSTER FAILOVER [TAKEOVER] |
在从节点上执行,手工提升为主(TAKEOVER 有丢数据风险) |
redis-cli --cluster create ... --cluster-replicas 1 |
创建集群(5.0+ 取代 redis-trib.rb) |
redis-cli --cluster add-node / del-node |
增加 / 移除节点 |
redis-cli --cluster reshard / rebalance / check / fix |
迁移槽位 / 自动均衡 / 校验 / 修复槽位覆盖 |
生产必须禁用或重命名的危险命令:FLUSHALL、FLUSHDB、KEYS、CONFIG、SHUTDOWN、DEBUG、MONITOR、SAVE、EVAL(视业务而定)。
redis-cli 常用参数速查:
| 参数 | 用途 |
|---|---|
-h / -p / -a / -n |
主机 / 端口 / 密码 / 数据库编号 |
--user / --pass |
6.0+ ACL 用户名密码 |
--no-auth-warning |
抑制命令行传密码的警告 |
-c |
集群模式(自动跟随 MOVED/ASK 重定向) |
--raw / --no-raw |
原样输出(中文正常显示)/ 强制转义输出 |
--scan --pattern |
安全替代 KEYS |
--bigkeys |
扫描大 key(按元素个数),生产加 -i 0.01 限速 |
--memkeys |
6.0+,扫描大 key(按实际内存) |
--hotkeys |
4.0+,扫描热 key(需 LFU 淘汰策略) |
--stat |
实时总览(key 数、内存、连接、QPS) |
--latency / --latency-history / --latency-dist |
延迟采样 / 分区间历史 / 分布直方图 |
--intrinsic-latency |
测系统固有延迟(排除机器因素) |
--rdb |
从实例拉取 RDB 快照(离线分析、备份) |
--pipe |
批量导入(比逐条快数十倍) |
--cluster |
集群管理(create/check/fix/info/reshard/rebalance/add-node/del-node) |
-i <秒> / -r <次> |
命令重复执行的间隔与次数 |
--eval |
执行 Lua 脚本文件 |
二、配置参数速查表
| 分类 | 参数 | 默认值 | 生产建议 |
|---|---|---|---|
| 网络 | bind |
127.0.0.1 |
内网 IP + 127.0.0.1,绝不用 0.0.0.0 |
| 网络 | port |
6379 |
改为非默认端口 |
| 网络 | protected-mode |
yes(3.2+) |
保持 yes(未设密码且未显式 bind 时只允许本机访问) |
| 网络 | timeout |
300(老版本)/ 0 |
300,回收空闲连接(0 = 永不断开) |
| 网络 | tcp-keepalive |
300 |
300,回收半死连接 |
| 网络 | tcp-backlog |
511 |
2048,同时调 net.core.somaxconn |
| 通用 | daemonize |
no |
yes(或交由 systemd 管理时用 no + supervised systemd) |
| 通用 | pidfile |
/var/run/redis.pid |
按端口区分:redis_6379.pid |
| 通用 | logfile |
stdout |
必须为真实路径(daemonize yes + stdout 会丢日志) |
| 通用 | loglevel |
verbose |
notice |
| 通用 | databases |
16 |
默认即可(集群只能用 db0) |
| 通用 | include |
— | 分离公共配置与实例差异(后加载覆盖先加载) |
| 安全 | requirepass |
无(注释) | 32 位以上随机强密码 |
| 安全 | rename-command |
无 | 禁用 CONFIG/FLUSHALL/FLUSHDB/KEYS/DEBUG/SHUTDOWN/EVAL |
| 安全 | aclfile |
无(6.0+) | 用户多时启用 |
| 安全 | tls-port / tls-cert-file |
无(6.0+) | 跨网传输时启用 |
| 内存 | maxmemory |
0(不限) |
持久化 50%~60%,纯缓存 70%~80%,必须显式设置 |
| 内存 | maxmemory-policy |
noeviction |
缓存 allkeys-lru;存储保持 noeviction + 告警(八种策略见下表) |
| 内存 | maxclients |
128(2.x) / 10000 |
≥ 10000,且 fd 限制 ≥ 其值 + 32 |
| 内存 | activedefrag |
no(4.0+) |
碎片率 > 1.5 时开启(需 jemalloc) |
| 内存 | lazyfree-lazy-* |
no(4.0+) |
全部开启 |
| RDB | save <秒> <改动数> |
900 1 / 300 10 / 60 10000 |
按业务调整;多行并存,关闭写 save "" |
| RDB | dbfilename |
dump.rdb |
默认即可 |
| RDB | dir |
./ |
必须改为绝对路径(如 /var/lib/redis),禁用 Web 目录、/tmp、家目录 |
| RDB | rdbcompression |
yes |
yes(LZF 压缩) |
| RDB | rdbchecksum |
yes |
yes |
| RDB | stop-writes-on-bgsave-error |
yes |
保持 yes(产生 MISCONF 是保护而非故障) |
| AOF | appendonly |
no |
需要数据安全时 yes |
| AOF | appendfilename |
appendonly.aof |
默认即可(7.0+ 为 appendonlydir 目录) |
| AOF | appendfsync |
everysec |
everysec(always 慢数倍,no 可丢 30 秒) |
| AOF | auto-aof-rewrite-percentage |
100 |
大实例调至 200 |
| AOF | auto-aof-rewrite-min-size |
64mb |
大实例调至 512mb |
| AOF | aof-load-truncated |
yes |
保持 yes(断电后可自动恢复) |
| AOF | aof-use-rdb-preamble |
yes(4.0+) |
yes(混合持久化,兼顾体积与安全) |
| AOF | no-appendfsync-on-rewrite |
no |
磁盘慢时可设 yes |
| 主从 | replicaof / slaveof |
无 | 从库配置(5.0+ 用 replicaof),写在配置文件中重启仍生效 |
| 主从 | masterauth / masteruser(6.0+) |
无 | 主库有密码时必须配;用 ACL 时配 masteruser 指定用户名 |
| 主从 | replica-read-only |
yes |
保持 yes |
| 主从 | replica-serve-stale-data |
yes |
强一致场景设 no |
| 主从 | repl-backlog-size |
1mb |
64mb~128mb(减少全量同步) |
| 主从 | repl-timeout |
60 |
大实例调至 120 |
| 主从 | min-replicas-to-write |
0 |
需要一致性时设 1(5.0+ 更名,旧名 min-slaves-*) |
| 主从 | client-output-buffer-limit replica |
256mb 64mb 60 |
大实例 512mb 128mb 60 |
| 集群 | cluster-enabled |
no |
集群节点设 yes |
| 集群 | cluster-config-file |
nodes.conf |
按端口区分 |
| 集群 | cluster-node-timeout |
15000 |
15000(跨机房可调大) |
| 集群 | cluster-require-full-coverage |
yes |
缓存 no,存储 yes |
| 性能 | hz |
10 |
10~20,配合 dynamic-hz yes(5.0+) |
| 性能 | slowlog-log-slower-than |
10000 |
10000(10ms) |
| 性能 | latency-monitor-threshold |
0(关闭) |
100(开启延迟监控) |
| 性能 | io-threads |
1(6.0+) |
CPU 核多、网络密集时设 4~8(只加速网络 I/O) |
maxmemory-policy 八种策略
| 策略 | 一句话说明 |
|---|---|
noeviction |
不淘汰,内存满后写命令直接报 OOM 错误,读命令仍可用(默认) |
volatile-lru |
只在带 TTL 的 key 中淘汰最久未使用的 |
volatile-lfu |
只在带 TTL 的 key 中淘汰访问频率最低的(4.0+) |
volatile-random |
只在带 TTL 的 key 中随机淘汰 |
volatile-ttl |
只在带 TTL 的 key 中淘汰剩余存活时间最短的 |
allkeys-lru |
在全部 key 中淘汰最久未使用的,纯缓存首选 |
allkeys-lfu |
在全部 key 中淘汰访问频率最低的,热点集中时更优 |
allkeys-random |
在全部 key 中随机淘汰,仅在访问完全均匀时考虑 |
注意:
volatile-*策略下若没有任何 key 带 TTL,找不到淘汰对象,行为退化为noeviction,同样报 OOM。
必须调整的系统层参数:
| 参数 | 位置 | 建议值 | 不改的后果 |
|---|---|---|---|
vm.overcommit_memory |
/etc/sysctl.conf |
1 |
fork 失败 → BGSAVE 失败 → MISCONF → 写入全挂 |
vm.swappiness |
/etc/sysctl.conf |
1 |
数据被换到 swap,延迟涨 1000 倍 |
| THP | /sys/kernel/mm/transparent_hugepage/enabled |
never |
fork 后 COW 单位变 2MB,内存暴涨、延迟尖刺 |
net.core.somaxconn |
/etc/sysctl.conf |
2048 |
tcp-backlog 被截断,突发建连失败 |
fs.file-max |
/etc/sysctl.conf |
1000000 |
系统级 fd 耗尽 |
| fd 限制 | CentOS 7:LimitNOFILE=65535CentOS 6: limits.conf + 脚本内 ulimit -n |
65535 |
maxclients 被自动下调,连接数打满 |
| OOM 保护 | CentOS 7:OOMScoreAdjust=-900CentOS 6:写 oom_score_adj |
-900 |
Redis 被系统 OOM Killer 优先杀掉 |
三、架构选型对照表
| 架构 | 可用性 | 扩展性 | 复杂度 | 适用场景 |
|---|---|---|---|---|
| 单机 | 无冗余,宕机即不可用 | 只能垂直扩容(加内存) | 最低,一个进程一份配置 | 开发测试、内部工具、可容忍中断的边缘缓存 |
| 主从 | 主库宕机需人工切换 | 读可横向扩展,写仍单点 | 低,只多一行 replicaof |
读多写少、需要数据副本与备份分离(在从库做 BGSAVE) |
| 哨兵 | 高,主库故障自动转移(秒级) | 读可扩展,写与容量仍受单机内存限制 | 中,需额外 3 个哨兵进程与客户端支持服务发现 | 单实例内存能装下全部数据,但要求高可用的绝大多数业务 |
| Cluster | 高,分片内自动故障转移 | 高,数据分片可横向扩容至数十节点 | 高,运维、扩缩容、跨槽限制都更复杂 | 数据量超单机内存、写入量大、需要在线水平扩容 |
选型口诀:能单实例装下就别上 Cluster。先用主从+哨兵满足高可用,只有当"内存装不下"或"单节点写入打满"时才分片。Cluster 的代价是不支持多数据库切换、跨槽多 key 操作受限、事务与 Lua 只能作用于同槽 key。
四、RDB 与 AOF 对照表
| 维度 | RDB | AOF |
|---|---|---|
| 生成/写入速度 | fork 后一次性落盘,主线程几乎无感;但 fork 本身随数据量增大而变慢 | 每条写命令追加,everysec 开销小,always 明显影响 QPS |
| 文件体积 | 小,二进制压缩存储 | 大,通常为 RDB 的数倍;需靠重写压缩(开混合持久化后接近 RDB) |
| 数据丢失窗口 | 大,最坏丢失自上次快照以来的全部写入 | 小,everysec 最多丢 1 秒,always 理论上不丢 |
| 恢复速度 | 快,直接加载内存镜像 | 慢,需重放命令;混合持久化后前半段按 RDB 加载,明显加快 |
| 损坏可修复性 | 不可修复,redis-check-rdb 只能校验 |
可修复,redis-check-aof --fix 截断损坏点之后的内容 |
| 适用场景 | 定时全量备份、异地容灾、主从全量同步的载体 | 对丢数据敏感的业务、需要细粒度恢复的场景 |
生产标准做法:两者同时开启 +
aof-use-rdb-preamble yes。日常靠 AOF 保证低丢失,靠定期归档的 RDB 做冷备与跨机房拷贝;恢复时优先用 AOF(Redis 启动时若appendonly yes会只加载 AOF 而忽略 RDB,这一点常被误解)。
五、安全加固清单
| 优先级 | 要点 | 自查方法 | 一句话理由 |
|---|---|---|---|
| P0 | 以低权限 redis 用户运行,绝不用 root |
ps -o user= -p $(pgrep -f redis-server);systemd 单元显式 User=redis |
这是未授权访问风险能升级为服务器失陷的关键前提 |
| P0 | 不暴露到公网(云安全组 + VPC 私有子网) | 从外网机器连接应不可达;检查安全组与端口映射 | 从网络层根除风险,比任何应用层措施都可靠 |
| P0 | 设置 32 位以上随机强密码 requirepass |
CONFIG GET requirepass;openssl rand -base64 32 生成 |
Redis 每秒可承受数十万次密码尝试,弱密码等于无密码 |
| P0 | bind 内网 IP + 回环,不用 0.0.0.0 |
ss -tlnp \| grep 6379 确认监听地址 |
从监听层收窄暴露面 |
| P1 | protected-mode yes(3.2+)保持开启 |
CONFIG GET protected-mode |
官方为无密码 + 默认 bind 的场景加的兜底保护,不要因"连不上"就关掉 |
| P1 | 防火墙白名单只放行应用网段(集群另放 +10000 总线端口、哨兵 26379) | firewall-cmd --list-all / iptables -L -n |
纵深防御的第二道闸门 |
| P1 | rename-command 或 ACL 屏蔽 CONFIG/FLUSHALL/FLUSHDB/KEYS/DEBUG/SHUTDOWN/MONITOR/SAVE/EVAL |
执行 CONFIG GET dir 应报未知命令 |
同时防攻击与防误操作(运维自己也会失去这些命令,需保留应急方案) |
| P1 | 配置文件 600、数据目录 750、RDB/AOF 文件 600,属主 redis:redis |
ls -l /etc/redis/ /var/lib/redis |
防止本地其他用户读到密码或篡改数据 |
| P1 | dir 指向 /var/lib/redis 专用目录,禁止 Web 站点目录、/tmp、家目录 |
CONFIG GET dir |
持久化路径可被写入,指向敏感目录会被利用 |
| P1 | 修改默认端口 | CONFIG GET port |
规避针对 6379 的批量自动化扫描 |
| P1 | 主从/集群的 masterauth 与 requirepass 全节点一致;口令不落入 shell 历史 |
CONFIG GET masterauth;命令行加 --no-auth-warning 或改用配置文件 |
切换角色后不会因认证失败中断复制,也避免口令被 history 记录 |
| P2 | ACL 细粒度授权(6.0+):按业务建用户并限定命令类别与 key 前缀(如 ~order:* +@read +@write -@dangerous),收紧或禁用 default |
ACL LIST、ACL GETUSER <user>、ACL DRYRUN(7.0+) |
最小权限原则,越权访问会被拒绝并记入 ACL LOG |
| P2 | TLS 传输加密(6.0+ tls-port),或走 stunnel / VPN 隧道 |
CONFIG GET tls-port |
跨机房、跨云、经不可信网络传输时必需 |
| P2 | 及时升级版本、跟踪 CVE,淘汰 EOL 版本 | INFO server \| grep redis_version |
老版本存在已修复的已知漏洞 |
| P2 | 日志落文件并集中采集,监控 ACL LOG、认证失败、连接数突增、rdb_last_bgsave_status |
CONFIG GET logfile;ACL LOG;监控告警规则 |
事后可追溯,也是发现异常访问的手段 |
| P2 | maxmemory + 淘汰策略 + 容量告警 |
CONFIG GET maxmemory maxmemory-policy |
防止内存耗尽被 OOM Killer 杀掉 |
加固口诀:密码要长、地址要窄、身份要低、命令要少、网络要关、版本要新、日志要看。
若确认实例已被入侵,改密码和加防火墙都不足以清除后门:应隔离主机 → 取证留存 → 备份业务数据 → 重装系统后从干净备份恢复 → 轮换所有可能泄露的凭据。
六、排错口诀
排障三板斧(任何 Redis 故障的第一组命令):
redis-cli PING # 1. 活着吗
redis-cli INFO # 2. 状态如何
tail -100 /var/log/redis/redis.log # 3. 日志说了什么
五句总纲:
- 先看四个 INFO,再猜原因——
INFO memory看内存与碎片率,INFO stats看命中率与sync_full,INFO persistence看三个*_status是否为ok,INFO replication看角色与偏移量差;四段都正常再去查网络与客户端。 - 慢就查慢查询,卡就查大 key——延迟毛刺先看
SLOWLOG GET与--latency,再用--bigkeys/--hotkeys定位;单线程模型下,一条HGETALL大 Hash 就能拖垮整个实例。 - 写不进去先看内存与磁盘——报
OOM查maxmemory与淘汰策略;报MISCONF查磁盘空间与上次BGSAVE是否失败;报READONLY说明连到了从库。 - 主从不同步先看
sync_full——sync_full频繁增长基本是repl-backlog-size太小或client-output-buffer-limit replica被打爆,其次查repl-timeout、masterauth与网络抖动。 - 集群异常先看槽位覆盖——
CLUSTER INFO的cluster_state与slots_assigned:16384、redis-cli --cluster check一把过;报CROSSSLOT是 key 设计问题(缺 hash tag),报MOVED是客户端没开集群模式或槽位缓存过期。
按现象速查:
| 现象 | 口诀 | 关键命令 |
|---|---|---|
| 连不上 | 先看进程再看端口,然后看绑定再看防火墙 | ps aux \| grep redis → ss -lntp \| grep 6379 → CONFIG GET bind → firewall-cmd --list-all |
| 写不进去 | MISCONF 看磁盘,OOM 看内存,READONLY 看角色 |
df -h + ls -ld <dir> / INFO memory / INFO replication |
| fork 失败 | overcommit_memory 必须是 1 |
cat /proc/sys/vm/overcommit_memory |
| 主从不同步 | 十有八九缺 masterauth,其次是防火墙,再看 sync_full |
从库日志 grep -i master → CONFIG GET masterauth → INFO stats \| grep sync_ |
| 主从数据不一致 | 先查从库是不是可写,再查从库内存是不是更小 | CONFIG GET replica-read-only / 对比 maxmemory |
| 哨兵不切换 | 哨兵要三台、要互通、要密码、配置要可写 | SENTINEL sentinels mymaster |
| 集群不可用 | 槽要满 16384,端口要开两个(+10000) | CLUSTER INFO → --cluster check → --cluster fix |
CROSSSLOT |
多 key 要同槽,同槽靠 {} |
CLUSTER KEYSLOT <key> |
MOVED |
不是集群坏了,是客户端没用集群模式 | 加 -c,换 Cluster 客户端 |
| 内存涨 | 数据、缓冲、碎片,三者对号入座 | INFO memory 比 dataset 与 overhead |
| 碎片率高 | 大于 1.5 开 defrag,小于 1 赶紧治 swap | INFO memory \| grep frag / grep VmSwap /proc/<pid>/status |
| 延迟高 | 先分内外,再看事件,最后抓命令 | --latency + --intrinsic-latency → LATENCY LATEST → SLOWLOG GET |
| 整点抖动 | 过期扎堆,TTL 要加随机数 | LATENCY HISTORY expire-cycle |
| 实例卡死 | *一律先怀疑大 key 和 `KEYS `** | SLOWLOG GET 20 → --bigkeys -i 0.01 |
| 连接打满 | 先看 fd 限制,再看连接池泄漏 | cat /proc/<pid>/limits → CLIENT LIST 按来源统计 |
| 中文乱码 | 不是乱码,加 --raw 就好 |
redis-cli --raw |
Redis 运维的三条铁律:
- 单线程铁律——任何一条命令慢,整个实例都慢。这与 MySQL"一条慢查询只影响一个连接"根本不同,因此
KEYS、FLUSHALL、大 key 上的HGETALL/SMEMBERS/LRANGE 0 -1必须从配置层面禁止,不能只靠人的自觉。 - 单实例 10GB 铁律——fork 耗时约每 GB 20~50 毫秒,实例越大,
BGSAVE/AOF 重写/全量同步/故障恢复的阻塞与耗时越不可控。超过 10GB 就水平拆分,不要纵向堆大。 - 演练铁律——没演练过的高可用等于没有高可用。主从切换、哨兵 failover、备份恢复、集群 reshard,都必须在上线前实测一遍并记录耗时。
本章相关文档:防火墙与 SELinux 配置见
07-03-防火墙与SELinux.md;Zabbix 监控部署见08-05-Zabbix监控.md。
此处评论已关闭