在 2 vCPU 测试机上,容器中的 Redis 通过 Docker 发布的回环端口完成每秒 21,400 次 SET;rootless Podman 的发布端口达到 25,200,容器内基准测试达到 44,800。经过发布端口会损失约一半吞吐,但它很少是初次 docker run redis 或 podman run redis 失败的原因。真正常见的问题有三个:使用 -a 传入密码导致其进入 shell 历史、Rocky Linux 上的 SELinux 阻止 Podman 卷,以及首次重启前从未验证持久化。
本文在 Docker 和 rootless Podman 中运行 Redis(镜像 redis:8.10.3,于 2026 年 11 月在 Ubuntu 26.04 与 Rocky Linux 10 上测试),随后介绍日常使用的 redis-cli 命令,包括用 SCAN 替代 KEYS、TTL、哈希、列表、集合、慢日志、大键扫描、持久化验证和最小权限 ACL 用户。速查表在前,后面的编号步骤展示每条命令在测试机上的实际表现。
redis-cli 速查表(快速参考)
下表中的每一行都在后文构建的 redis:8.10.3 容器中运行过。表格假设已在第 6 步完成两行 shell 设置:REDISCLI_AUTH 保存密码,redis-cli 是 docker exec -i -e REDISCLI_AUTH redis redis-cli 的别名(Rocky 上将 docker 换成 podman)。由于客户端在容器内没有 TTY,返回采用 raw 模式,因此看到的是 3,而不是 (integer) 3。
连接与认证
| 命令 | 作用 |
|---|---|
export REDISCLI_AUTH="$(cat ~/redis-lab/default.pass)" |
向 redis-cli 提供密码,且不放进 argv |
redis-cli PING |
健康检查;返回 PONG 表示已连接并通过认证 |
redis-cli -h 127.0.0.1 -p 6379 PING |
连接指定主机和端口 |
REDISCLI_AUTH="$(cat ~/redis-lab/app.pass)" redis-cli --user app GET app:visits |
以 ACL 用户身份执行一条命令 |
redis-cli --user app --askpass GET app:visits < ~/redis-lab/app.pass |
从 stdin 而非环境变量读取密码 |
redis-cli -n 1 DBSIZE |
在 1 号数据库中执行命令 |
服务器信息与运行时配置
| 命令 | 作用 |
|---|---|
redis-cli INFO server |
版本、模式、操作系统与运行时间 |
redis-cli INFO memory |
已用内存、RSS、碎片率与淘汰策略 |
redis-cli INFO persistence |
RDB 与 AOF 状态、上次保存结果 |
redis-cli CONFIG GET 'maxmemory*' |
按 glob 模式读取设置 |
redis-cli CONFIG SET maxmemory 256mb |
在线修改设置(重启后丢失) |
redis-cli CONFIG SET maxmemory-policy allkeys-lru |
达到内存上限时淘汰最近最少使用的键 |
redis-cli DBSIZE |
当前数据库的键数量 |
redis-cli CLIENT LIST |
列出所有客户端及标志 |
查找和检查键
| 命令 | 作用 |
|---|---|
redis-cli --scan --pattern 'user:9999*' |
以非阻塞方式遍历匹配键 |
redis-cli SCAN 0 MATCH 'user:*' COUNT 1000 |
执行一页 SCAN;第一行是下一游标 |
redis-cli KEYS 'user:*' |
在单次阻塞调用中匹配所有键(生产环境避免) |
redis-cli TYPE profile:1001 |
键的数据类型 |
redis-cli OBJECT ENCODING profile:1001 |
内部编码(listpack、hashtable 等) |
redis-cli MEMORY USAGE profile:1001 |
单个键占用的字节数,含开销 |
redis-cli EXISTS profile:1001 |
存在返回 1,不存在返回 0 |
redis-cli UNLINK big:list |
删除键并在后台释放内存 |
字符串、计数器、过期与锁
| 命令 | 作用 |
|---|---|
redis-cli SET session:42 token-abc EX 300 |
设置 300 秒后过期的值 |
redis-cli GET lock:job1 |
读取字符串值(不存在时为空行) |
redis-cli TTL session:42 |
剩余秒数;-1 无过期,-2 键不存在 |
redis-cli EXPIRE session:42 60 |
设置或替换键的过期时间 |
redis-cli PERSIST session:42 |
移除过期时间 |
redis-cli SET lock:job1 worker-a NX EX 30 |
仅在无人持有时获取锁 |
redis-cli INCR counter:hits |
原子计数器,不存在时从 0 创建 |
哈希、列表和集合
| 命令 | 作用 |
|---|---|
redis-cli HSET profile:1001 name ada plan pro logins 0 |
设置哈希字段 |
redis-cli HGET profile:1001 plan |
读取一个字段 |
redis-cli HGETALL profile:1001 |
读取全部字段和值,逐行交替 |
redis-cli HINCRBY profile:1001 logins 1 |
递增数值字段 |
redis-cli RPUSH queue:emails job1 job2 |
向列表尾部追加(队列尾) |
redis-cli LPOP queue:emails |
从列表头部取出 |
redis-cli LRANGE queue:emails 0 -1 |
打印整个列表 |
redis-cli BLPOP queue:emails 2 |
等待列表元素,最多 2 秒 |
redis-cli SADD tags:post1 redis docker podman |
添加集合成员 |
延迟、慢命令和大键排障
| 命令 | 作用 |
|---|---|
redis-cli SLOWLOG GET 2 |
最近 2 条超过阈值的命令 |
redis-cli CONFIG SET slowlog-log-slower-than 1000 |
记录超过 1 ms 的命令(默认 10000 µs) |
redis-cli SLOWLOG RESET |
清空慢日志 |
redis-cli --bigkeys |
按元素数量列出每种类型的最大键 |
redis-cli --memkeys |
按字节数列出每种类型的最大键 |
redis-cli --keystats |
键大小百分位和各类型总量 |
redis-cli --intrinsic-latency 5 |
5 秒内的主机调度延迟 |
redis-cli --latency |
往返延迟;无 TTY 时采样 1 秒 |
docker exec -e REDISCLI_AUTH redis timeout 3 redis-cli MONITOR |
监视所有命令 3 秒后退出 |
持久化与 ACL 用户
| 命令 | 作用 |
|---|---|
redis-cli BGSAVE |
后台写出 RDB 快照 |
redis-cli LASTSAVE |
上次成功保存的 Unix 时间 |
redis-cli ACL USERS |
列出 ACL 用户 |
redis-cli ACL DRYRUN app GET user:1 |
按用户规则测试命令 |
redis-cli ACL LOG 1 |
最近被拒绝的命令及客户端 |
redis-cli ACL SETUSER app '+ping' '+echo' |
逐条授予命令(比整类更安全) |
redis-cli ACL GETUSER app |
显示用户标志、命令规则和键模式 |
seq 1 100000 | awk '{print "SET user:"$1" value-"$1}' | redis-cli --pipe |
通过单个流批量加载 100,000 个键 |
本页其余部分展示各组命令在测试机上的运行结果,并从容器本身开始。输出均来自真实记录,可与自己的结果对照。
步骤 1:安装 Docker 或 Podman
测试使用两台 e2-standard-2 云虚拟机(2 vCPU、8 GB 内存):Ubuntu 26.04 使用发行版仓库中的 Docker 与 Podman,Rocky Linux 10.2 使用 AppStream 中的 Podman。请选择对应主机的选项卡。只需一个引擎;除第 4 步的 SELinux 处理外,Redis 步骤完全相同。若希望把 Redis 作为发行版软件包的主机服务运行,可参考在 Ubuntu 26.04 或 Rocky Linux/AlmaLinux 上安装 Redis 的指南。本页 redis-cli 表格对这些安装方式同样适用。
Ubuntu 26.04
Rocky Linux 10
sudo apt-get update && sudo apt-get install -y docker.io docker-compose-v2 podman
sudo usermod -aG docker $USER
sudo dnf -y install podman openssl vim-enhanced
执行 usermod 后请注销再登录,使 docker 组权限应用到当前会话。该组成员在主机的权限等价于 root,因此只应授予管理员账户;Docker 与 Podman 速查表涵盖了本文未涉及的引擎命令。随后确认安装版本,因为两台主机的引擎版本不同,后续所有输出都与之相关。
Ubuntu 26.04
Rocky Linux 10
docker --version; docker compose version; podman --version
Ubuntu 软件仓库提供 Docker 29.1、Compose 2.40 和 Podman 5.7,因此该主机无需第三方仓库。
Docker version 29.1.3, build 29.1.3-0ubuntu4.1
Docker Compose version 2.40.3+ds1-0ubuntu1
podman version 5.7.0
podman --version; getenforce; cat /etc/rocky-release
Rocky 显示 Podman 5.8,且 SELinux 为 Enforcing;下文所有 Rocky 步骤都假设这一状态,因此第 4 步的卷标签非常重要。
podman version 5.8.2
Enforcing
Rocky Linux release 10.2 (Red Quartz)
测试过程中有两点需要注意。在 Ubuntu 上,除非启用 lingering,否则 SSH 会话结束时 rootless Podman 容器会停止;该主机上的基准容器在退出登录后停止,直到执行 sudo loginctl enable-linger $USER,与第 5 步 Rocky 使用的命令相同。在 Rocky 上,来自 download.docker.com 的 Docker CE 也能安装(测试版本 29.9.3),但其守护进程关闭了 SELinux 支持;未加标签挂载的主机目录会报 Can't open or create append-only dir appendonlydir: Permission denied。同一 Rocky 主机上,第 3 步的命名卷方式可在 Docker CE 中正常启动,因此在这里应使用命名卷或继续使用 Podman。
步骤 2:将 Redis 密码保存在文件中
官方镜像默认没有密码;一旦有人把端口发布到 0.0.0.0,这就不再安全。这里从生成文件读取 requirepass,而不是手工输入密码,因为使用 -a 输入的密码会进入 shell 历史和主机进程列表。以下三条命令在 Ubuntu 与 Rocky 上相同,用来创建私有目录和 48 位随机密码。目录权限比看起来更重要,因为它们保护的是本步骤稍后写入的配置文件。
mkdir -p ~/redis-lab && chmod 700 ~/redis-lab
openssl rand -hex 24 > ~/redis-lab/default.pass && chmod 600 ~/redis-lab/default.pass
创建一个最小 redis.conf,从该文件读取密码,并启用每秒 fsync 的 AOF。该设置崩溃时最多损失约一秒写入,适合既要作为缓存、又保存希望保留数据的 Redis。
printf 'requirepass %s\nappendonly yes\nappendfsync everysec\n' "$(cat ~/redis-lab/default.pass)" > ~/redis-lab/redis.conf
挂载前检查权限,因为容器会以不同于文件创建者的用户读取文件。密码文件应只允许所有者访问,下面的列表将验证这一点。
ls -l ~/redis-lab
密码文件仅所有者可读,而 redis.conf 保留 umask 产生的 664 权限。这个差异是有意为之,下一段会解释。
total 8
-rw------- 1 klinsmann klinsmann 49 Nov 11 07:48 default.pass
-rw-rw-r-- 1 klinsmann klinsmann 97 Nov 11 07:48 redis.conf
redis.conf 的 664 看似宽松,因为文件已经包含密码。这里之所以可接受,是因为父目录为 700,其他主机用户无法进入。容器内的 Redis 以 redis 用户(UID 999)运行,通过“其他用户”权限位读取绑定挂载的文件,因此更严格的模式会让服务器无法读取。我们测试过 600 权限的副本,Docker 容器立即退出,日志只说 Redis 无法读取配置,文件明明存在时这条信息很容易误导。
1:C 11 Nov 2026 08:03:59.578 # Fatal error, can't open config file '/usr/local/etc/redis/redis.conf': Permission denied
因此应收紧目录权限,而不是文件权限,并让 redis.conf 保持 664。700 的目录可以阻止其他主机用户进入,文件权限只需保证 UID 999 能读取。
步骤 3:在 Docker 容器中运行 Redis
将镜像固定到完整版本,而不是 redis:latest,这样下次 docker pull 或重建容器时不会在数据下方突然更换主版本。下面的标签是测试时 Redis 发布页上的最新稳定版,包含 cluster-bus 堆溢出修复,较旧的 8.x 标签值得替换。
time docker pull redis:8.10.3 # https://github.com/redis/redis/releases
测试机拉取耗时 4.2 秒。检查磁盘上的实际内容:
docker image ls redis
镜像下载大小为 57.6 MB,解包后为 213 MB,因为 Redis 8 内置了原先 Redis Stack 的 Search、JSON、时间序列和概率模块。这些模块会在启动时加载,也是空闲服务器后续显示较高碎片率的原因。
WARNING: This output is designed for human readability. For machine-readable output, please use --format.
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
redis:8.10.3 1f88291b4c57 213MB 57.6MB
启动容器,为 /data 使用命名卷,以只读方式挂载配置,并只将端口发布到回环地址。绑定 127.0.0.1 可以防止 Docker 通过自身绕过 UFW 的 iptables 规则把 6379 暴露给全世界,因此这是安全配置而非装饰性参数。
docker run -d --name redis --restart unless-stopped -p 127.0.0.1:6379:6379 -v redis-data:/data -v ~/redis-lab/redis.conf:/usr/local/etc/redis/redis.conf:ro redis:8.10.3 redis-server /usr/local/etc/redis/redis.conf
确认容器已启动并绑定到预期地址,因为使用错误地址启动的容器看起来仍健康,直到有人扫描主机。因此先检查 PORTS 列,它能最快显示绑定地址。
docker ps --filter name=redis
PORTS 列必须显示 127.0.0.1:6379->6379/tcp。若只显示 0.0.0.0:6379,说明 -p 参数丢失了地址,应停止容器并修正命令后再继续;否则后续写入的数据会暴露在网络上所有主机都能访问的端口后面。
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4c27982ae0af redis:8.10.3 "docker-entrypoint.s…" 6 seconds ago Up 4 seconds 127.0.0.1:6379->6379/tcp redis
最后几行日志表明配置已被读取,因为只有在 appendonly yes 生效时,启动阶段才会创建 AOF 基础文件。如果缺少这些行,说明文件没有挂载到 Redis 期望的位置。应先修复挂载,因为后续所有持久化检查都依赖该设置。
docker logs redis 2>&1 | tail -n 6
寻找两条 “Creating AOF” 和 “Ready to accept connections”。这组信息共同说明 Redis 已创建只追加文件并开始监听;我们将其作为就绪检查,因为它同时证明配置、卷和服务器都已正常启动。
1:M 11 Nov 2026 07:48:42.574 * <search> Acquired RedisJSON_V8 API
1:M 11 Nov 2026 07:48:42.575 * Server initialized
1:M 11 Nov 2026 07:48:42.575 * BGSAVE done, 0 keys saved, 0 keys skipped, 89 bytes written.
1:M 11 Nov 2026 07:48:42.578 * Creating AOF base file appendonly.aof.1.base.rdb on server start
1:M 11 Nov 2026 07:48:42.582 * Creating AOF incr file appendonly.aof.1.incr.aof on server start
1:M 11 Nov 2026 07:48:42.582 * Ready to accept connections tcp
现在无凭据命令会被拒绝,这是期望结果。直接发送不带密码的 PING 来验证。每个新容器都应做此检查,因为无需凭据即可响应的服务器会让后续安全措施失去意义。
docker exec redis redis-cli PING
Redis 返回认证错误而不是 PONG,说明第 2 步设置的密码已生效,端口没有敞开。这个失败正是预期结果,因此本次检查应视为通过。
NOAUTH Authentication required.
下面的终端展示 Ubuntu 26.04 测试机从固定版本拉取到未认证 PING 被拒绝的同一过程。

从第 6 步开始,所有操作都连接名为 redis 的容器,因此在完成本页其余内容前请保持其运行。
使用 Docker Compose 运行同一容器
当 Redis 与应用程序容器相邻部署时,Compose 更合适,因为服务定义可保存在 Git 中。创建项目目录并复制自己的配置文件。
mkdir -p ~/redis-compose && chmod 700 ~/redis-compose && cp ~/redis-lab/redis.conf ~/redis-compose/redis.conf
vim ~/redis-compose/compose.yaml
粘贴以下服务定义。这里发布到 6381,只是为了能与第一个容器并行运行;若只有一个容器则使用 6379。回环地址必须保留,Compose 文件不能撤销第 3 步的保护。
services:
redis:
image: redis:8.10.3
container_name: redis-compose
restart: unless-stopped
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
ports:
- "127.0.0.1:6381:6379"
volumes:
- redis-compose-data:/data
- ./redis.conf:/usr/local/etc/redis/redis.conf:ro
volumes:
redis-compose-data:
在项目目录中启动:
cd ~/redis-compose && docker compose up -d
Compose 会创建项目网络、命名卷和容器。独立网络与卷意味着这套栈无法接触第一个容器的数据。
Network redis-compose_default Creating
Network redis-compose_default Created
Volume redis-compose_redis-compose-data Creating
Volume redis-compose_redis-compose-data Created
Container redis-compose Creating
Container redis-compose Created
Container redis-compose Starting
Container redis-compose Started
先导出密码到 REDISCLI_AUTH(原因见第 6 步),再统计键数量。该计数同时证明复制后的配置密码在此容器中有效,因为未认证计数会被拒绝。这里统计键而不是发送 PING,还能显示卷是否为空。
export REDISCLI_AUTH="$(cat ~/redis-lab/default.pass)"
docker exec -e REDISCLI_AUTH redis-compose redis-cli DBSIZE
新卷为空,因此结果为 0。用 docker compose down 拆除项目;命名卷会保留,只有 docker compose down -v 才会删除,真正清理时应保留 -v。
步骤 4:在启用 SELinux 的 rootless Podman 中运行 Redis
这一 Podman 配置以普通用户在 Rocky Linux 10 上运行同一容器,并保持 SELinux Enforcing。使用完全限定镜像名,避免 Podman 为短名称选择仓库。拉取耗时 5.3 秒。
podman pull docker.io/library/redis:8.10.3 # https://github.com/redis/redis/releases
在这台主机上重新执行第 2 步命令,并为数据添加普通目录,因为绑定挂载才会让 SELinux 标签产生影响。命名卷会掩盖接下来要演示的问题。
mkdir -p ~/redis-lab/data && chmod 700 ~/redis-lab
先使用大量 Docker 教程中常见的卷参数,不加标签选项,以观察失败。见过一次后,之后更容易识别该症状。我们完全按常见写法执行,因为静默退出的容器最容易浪费读者一小时。
podman run -d --name redis -p 127.0.0.1:6379:6379 -v ~/redis-lab/data:/data -v ~/redis-lab/redis.conf:/usr/local/etc/redis/redis.conf:ro docker.io/library/redis:8.10.3 redis-server /usr/local/etc/redis/redis.conf
Podman 打印容器 ID 并返回 0,因此运行看似成功。四秒后容器消失,说明故障发生在容器内部,而不是 podman run 调用本身。
podman ps -a --filter name=redis --format '{{.Names}} {{.Status}}'
状态 1 是退出码。如此早的非零退出通常指向入口脚本,下一步应查看容器日志。
redis Exited (1) 4 seconds ago
日志只有一行,而且完全没有提到 SELinux。它来自镜像入口脚本,该脚本在启动 Redis 前遍历 /data 修正所有权,因此读取被拒绝会让容器在 Redis 启动前退出。
podman logs redis 2>&1 | tail -n 5
下面是那条唯一日志。它报告 . 的权限错误,却没有说明原因是 SELinux。
find: '.': Permission denied
在我们的主机上,sudo ausearch -m avc -ts recent 对该拒绝没有任何输出,因此审计日志不是最快的诊断方式,文件标签才是。两个路径仍带有主目录类型 user_home_t,容器进程无权读取;用 ls -dZ 查看标签即可确认。
ls -dZ ~/redis-lab/data ~/redis-lab/redis.conf
两者都显示主目录上下文。这就是失败原因:SELinux 策略不允许容器进程读取 user_home_t 文件。第 3 步的 Docker 在 Ubuntu 上不需要标签,因此相同参数在那里可用,在这里却失败。
unconfined_u:object_r:user_home_t:s0 /home/klinsmann/redis-lab/data
unconfined_u:object_r:user_home_t:s0 /home/klinsmann/redis-lab/redis.conf
用 podman rm -f redis 删除已退出的容器,并在两个挂载上加入 :Z 重新运行。两种标签选项的区别在于之后谁可以访问文件。
-
大写
Z: 为这个容器重新标记私有标签,其他容器无法读取。 -
小写
:z: 应用共享标签,任何容器都可使用,只应在多个容器挂载相同数据时选择。
Redis 数据只属于一个容器,因此这里应使用私有标签。
podman run -d --name redis -p 127.0.0.1:6379:6379 -v ~/redis-lab/data:/data:Z -v ~/redis-lab/redis.conf:/usr/local/etc/redis/redis.conf:ro,Z docker.io/library/redis:8.10.3 redis-server /usr/local/etc/redis/redis.conf
现在容器保持运行。能通过入口脚本检查,说明重新标记后的路径可以从容器内读取。
podman ps --filter name=redis --format '{{.Names}} {{.Status}} {{.Ports}}'
状态为 Up,且端口绑定在回环地址,说明容器既在运行,也只绑定本机,与 Docker 下相同。我们有意保留该绑定,因为标签修复与网络暴露无关。
redis Up 4 seconds 127.0.0.1:6379->6379/tcp
再次列出标签,可看到 :Z 对主机文件所做的改变;重标记作用于文件本身,而不是容器内部。并排比较两次列表,类型列的变化就是全部修复。
ls -dZ ~/redis-lab/data ~/redis-lab/redis.conf
类型现在为 container_file_t,MCS 对 c332,c647 将两个路径仅绑定到此容器。不同 MCS 对的另一个容器会被拒绝,这就是私有标签提供的隔离。
system_u:object_r:container_file_t:s0:c332,c647 /home/klinsmann/redis-lab/data
system_u:object_r:container_file_t:s0:c332,c647 /home/klinsmann/redis-lab/redis.conf
下面的截图将失败运行、修改前后的标签和成功运行的容器放在一起,来自 SELinux Enforcing 的 Rocky Linux 10 测试机。

切勿将 :Z 指向 /etc 等系统路径或整个主目录。Podman 会重新标记你提供的所有对象,其他受限服务随后会失去访问。
rootless 模式下数据文件属于谁
写入一个键并执行 BGSAVE 后(第 6 步会设置客户端),在主机上列出数据目录。对习惯 Docker 的人来说,所有者列会带来意外。
ls -ln ~/redis-lab/data
这些文件属于 UID 1180646,而不是你本人,因此普通主机工具会把它们视为其他用户的数据。这是查看数据目录时首先注意到的现象,也解释了下面的备份建议。
total 4
drwx------. 2 1180646 1180646 103 Nov 11 08:02 appendonlydir
-rw-------. 1 1180646 1180646 112 Nov 11 08:02 dump.rdb
该数字是容器内 redis 用户(UID 999)映射到从属 UID 范围后的结果。在用户命名空间内读取同一目录,可以看到容器视角。
podman unshare ls -ln ~/redis-lab/data
在命名空间内所有者又显示为 999,确认文件本身没有问题,只是主机视角不同。备份时要利用该映射,因为命名空间视角才与 Redis 看到的相同。
total 4
drwx------. 2 999 999 103 Nov 11 08:02 appendonlydir
-rw-------. 1 999 999 112 Nov 11 08:02 dump.rdb
这对备份有直接影响。以普通用户在主机侧对 dump.rdb 执行 cp 或 tar 会因 0600 权限失败,因此应使用 podman unshare 包一层,或用 podman cp redis:/data/dump.rdb . 复制出来。
步骤 5:使用 Quadlet 单元在开机时启动 Redis
Docker 的 --restart unless-stopped 依赖 root 守护进程在开机时启动。rootless Podman 没有守护进程,因此由 systemd 通过 Quadlet 文件接管,替代旧的 podman generate systemd 输出。先用 podman rm -f redis 删除手工容器,否则单元会与名称和端口冲突;然后创建单元目录和文件。
mkdir -p ~/.config/containers/systemd
vim ~/.config/containers/systemd/redis.container
粘贴该单元。说明符 %h 会展开为主目录,两个卷都保留 :Z,从而让第 4 步的标签延续到服务。若省略,单元会像首次运行一样因 SELinux 阻止绑定挂载而失败。
[Unit]
Description=Redis 8 in rootless Podman
[Container]
ContainerName=redis
Image=docker.io/library/redis:8.10.3
Exec=redis-server /usr/local/etc/redis/redis.conf
PublishPort=127.0.0.1:6379:6379
Volume=%h/redis-lab/data:/data:Z
Volume=%h/redis-lab/redis.conf:/usr/local/etc/redis/redis.conf:ro,Z
[Service]
Restart=always
[Install]
WantedBy=default.target
启动前先让生成器展示将产生的内容。键名拼写错误会在此出现,而不是变成一个静默缺失的服务,因此 dry run 几乎无成本,可省去一次排障。启动单元前阅读生成文件中的挂载和端口。
/usr/libexec/podman/quadlet -dryrun -user 2>&1 | head -n 25
输出为生成的 redis.service,其中自动加入 network-online 依赖和 podman rm 停止钩子。这些都不是手工编写的,也是使用 Quadlet 而不是手写单元的原因。
quadlet-generator[8752]: Loading source unit file /home/klinsmann/.config/containers/systemd/redis.container
---redis.service---
[Unit]
Wants=podman-user-wait-network-online.service
After=podman-user-wait-network-online.service
Description=Redis 8 in rootless Podman
SourcePath=/home/klinsmann/.config/containers/systemd/redis.container
RequiresMountsFor=%t/containers
[X-Container]
ContainerName=redis
Image=docker.io/library/redis:8.10.3
Exec=redis-server /usr/local/etc/redis/redis.conf
PublishPort=127.0.0.1:6379:6379
Volume=%h/redis-lab/data:/data:Z
Volume=%h/redis-lab/redis.conf:/usr/local/etc/redis/redis.conf:ro,Z
[Service]
Restart=always
Environment=PODMAN_SYSTEMD_UNIT=%n
KillMode=mixed
ExecStop=/usr/bin/podman rm -v -f -i redis
ExecStopPost=-/usr/bin/podman rm -v -f -i redis
Delegate=yes
Type=notify
启用 lingering,使用户的 systemd 实例无需登录即可随系统启动,然后加载并启动服务。跳过 linger 时,Redis 只会在你 SSH 登录后出现,单元在测试中看似正常,但首次无人值守重启后会失败。
sudo loginctl enable-linger $USER
systemctl --user daemon-reload && systemctl --user start redis.service
生成的单元不能通过 systemctl --user enable 启用,因为 .container 文件中的 [Install] 节才负责挂接到 default.target。检查状态确认生效;若显示已加载但未激活,通常说明安装步骤被跳过。
systemctl --user status redis.service --no-pager | head -n 12
单元处于 Active (running),主 PID 为 conmon,Redis 位于负载 cgroup 中,占用 39.7 MB 内存。此时数据集几乎为空,因此这个数字只是空闲下限,不能用于容量规划。
● redis.service - Redis 8 in rootless Podman
Loaded: loaded (/home/klinsmann/.config/containers/systemd/redis.container; generated)
Active: active (running) since Wed 2026-11-11 08:02:53 UTC; 3s ago
Invocation: 1a0dc9ca1c3341f5b466c88cdc37c85e
Main PID: 8862 (conmon)
Tasks: 8 (limit: 48395)
Memory: 39.7M (peak: 46.9M)
CPU: 283ms
CGroup: /user.slice/user-1010.slice/user@1010.service/app.slice/redis.service
├─libpod-payload-cae8a5a48306f156eb9f5fea6ffff51cbbad20b19cce8e264ae349499a1d251d
│ └─8864 "redis-server *:6379"
└─runtime
真正的测试是重启。执行 sudo systemctl reboot 并重新 SSH 登录后,单元已自动激活,重启前写入的键仍存在,因此该检查同时覆盖开机启动和磁盘数据。
systemctl --user is-active redis.service
redis-cli GET probe:selinux
两个结果都符合预期,证明 lingering、生成单元和重新标记的数据目录协同工作。任何一个缺失,服务都不会激活,或者键会丢失。
active
ok
下面的截图展示第一次启动时的单元状态,以及同一 Rocky 主机重启后的两项检查。

第二条命令中的 redis-cli 是第 6 步的两行 shell 设置,别名中使用 podman exec。重启后的新登录不会自动具备该别名,因此要先重新执行两行,或将其加入 ~/.bashrc。
步骤 6:使用 redis-cli 连接且不暴露密码
redis-cli 已包含在镜像内,主机无需安装。两行 shell 设置让它像本地命令一样工作。第一行把密码导出到 REDISCLI_AUTH,这是 redis-cli 未收到 -a 时读取的变量(显式 -a 或 --askpass 优先)。第二行让 redis-cli 在容器内运行,并按名称传递该变量,因此值不会出现在任何进程参数中。在共享主机上这很重要,否则任何能运行 ps 的人都能读到 -a 后的密码;同时也要让第 2 步的设置对后续所有命令保持一致。
export REDISCLI_AUTH="$(cat ~/redis-lab/default.pass)"
alias redis-cli='docker exec -i -e REDISCLI_AUTH redis redis-cli'
在 Podman 主机上,别名相同,只需把 docker exec 换成 podman exec。用 PING 测试连接,该命令需要密码,因此能证明变量已传达到服务器。
redis-cli PING
返回 PONG 说明密码已到达并被接受。若出现认证错误,应检查导出的变量,而不是容器。
PONG
该别名有一个陷阱:shell 只在命令的第一个词展开别名。因此 timeout 3 redis-cli MONITOR 会在主机上寻找真实的 redis-cli 二进制,Ubuntu 26.04 的 Rust coreutils 会报告找不到。
timeout: failed to execute process: No such file or directory (os error 2)
凡是用 timeout 包装的命令,都应把超时放进容器内,如第 9 步所示。若想完全避免环境变量,可用 --askpass 从 stdin 读取密码,使密码既不进入 argv,也不进入 shell 环境。
docker exec -i redis redis-cli --user app --askpass GET app:visits < ~/redis-lab/app.pass
它返回了第 11 步创建的 app 用户保存的值 1。从其他机器或客户端容器连接时,按常规使用 -h 和 -p,同样传递变量,密码也不会进入参数列表。
docker run --rm --network host -e REDISCLI_AUTH redis:8.10.3 redis-cli -h 127.0.0.1 -p 6379 PING
主机网络上的临时客户端容器返回 PONG,说明远程客户端只需地址、端口和相同的密码处理方式。图形客户端同理;Redis GUI 指南涵盖了这一部分。
读取服务器信息并在线修改设置
INFO 按区段划分,只请求一个区段可保持输出可读。新实例的内存区段是有用基线,之后每次读数都可与它比较。加载数据前先保存输出,以便为增长量提供起点。
redis-cli INFO memory | grep -E 'used_memory_human|used_memory_rss_human|maxmemory_human|maxmemory_policy|mem_fragmentation_ratio'
镜像默认没有内存上限,也没有淘汰策略。应先检查输出中的 maxmemory 和策略行,因为它们决定内存不足时发生什么。按镜像默认值,答案是没有任何键被淘汰,服务器会持续增长。
used_memory_human:1.40M
used_memory_rss_human:24.27M
maxmemory_human:0B
maxmemory_policy:noeviction
mem_fragmentation_ratio:17.52
17.52 的碎片率看起来惊人,但它是 RSS 除以 1.4 MB 数据集的结果,空服务器上加载的模块占据了大部分 RSS。只有数据集达到数百 MB 后再判断该比例才有意义。第一天上线的关键设置是 maxmemory:在 noeviction 且无上限时,Redis 会持续增长,直到内核 OOM killer 选择进程。可用 glob 读取当前值。
redis-cli CONFIG GET 'maxmemory*'
raw 模式把每个名称和值分别放在一行,因此输出在设置名和当前值之间交替。应按对读取,因为 glob 匹配了五个 maxmemory 设置,每个名称都紧挨其值上方。
maxmemory
0
maxmemory-eviction-tenacity
10
maxmemory-clients
0
maxmemory-policy
noeviction
maxmemory-samples
5
为缓存负载设置上限和淘汰策略。两者都会返回 OK 并立即生效,无需重启,因此可在运行中的服务器上尝试。
redis-cli CONFIG SET maxmemory 256mb
redis-cli CONFIG SET maxmemory-policy allkeys-lru
重新读取会得到字节数 268435456。这些更改只存在内存中。CONFIG REWRITE 通常会保存到配置文件,但本文文件以只读方式挂载,因此会失败。
redis-cli CONFIG REWRITE
这个错误是有意保留的,因为第 3 步的只读挂载阻止任何人通过运行中的服务器修改文件。这样 Git 中的文件始终是配置的唯一来源。
ERR Rewriting config file: Permission denied
我们更希望如此。容器配置应来自 Git 中保存的文件;若要让设置持久化,请把 maxmemory 256mb 和 maxmemory-policy allkeys-lru 加入 ~/redis-lab/redis.conf,再重启容器。
步骤 7:加载测试键并用 SCAN 替代 KEYS
空服务器无法体现 KEYS 与 SCAN 的差异,因此先加载 100,000 个字符串键。--pipe 把 stdin 中的全部内容一次性传输给服务器,只等待最后一个回复,比 100,000 次单独调用快得多。这里的 awk 生成简单内联命令,适合基本键值;二进制安全加载应按 Redis 批量插入指南生成 raw RESP 协议。
time (seq 1 100000 | awk '{print "SET user:"$1" value-"$1}' | redis-cli --pipe)
100,000 次写入在半秒内完成,且零错误。错误数比耗时更重要,因为静默失败会让后续扫描测试拿到的键少于预期。干净加载也能保证后续计时每次可重复。
All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 100000
real 0m0.490s
user 0m0.077s
sys 0m0.037s
现在分别计时两种查找以 user:9999 开头的 11 个键的方法。如下方结果所示,实际耗时只是比较的一半。
time redis-cli KEYS 'user:9999*' | wc -l
time redis-cli --scan --pattern 'user:9999*' | wc -l
两者都返回 11 个键,而 KEYS 以 0.097 秒快于 --scan 的 0.629 秒。这正是陷阱。KEYS 在客户端墙钟时间上更快,因为它一次完成完整遍历;但遍历期间,单线程服务器无法服务其他客户端。--scan 通过许多小调用分页读取键空间,因此更慢,但客户端之间能得到服务。要观察阻塞成本,可将慢日志阈值降到 1 ms,再执行不带过滤条件的 KEYS。
redis-cli CONFIG SET slowlog-log-slower-than 1000
redis-cli SLOWLOG RESET
redis-cli KEYS 'user:*' | wc -l
redis-cli SLOWLOG GET 2
raw 模式下每条日志被展平为每行一个值,因此需按顺序阅读。字段依次为条目 ID 1、Unix 时间戳、微秒耗时、命令及参数、客户端地址、空客户端名和参数总数。最后一个值是 Redis 8.10 给 SLOWLOG GET 新增的第七个字段。
1
1794383388
19368
KEYS
user:*
127.0.0.1:37980
2
服务器为返回 100,000 个键名阻塞了 19.4 ms。按线性比例推算到一百万键约为 200 ms(这是估算而非实测),足以触发应用超时,因此不带过滤的 KEYS 不应出现在在线服务器上。慢日志捕捉到了它,因为其记录所有超过阈值的命令;建议在任何批量操作后检查慢日志,确认单次调用是否长时间占住服务器。完成后用 redis-cli CONFIG SET slowlog-log-slower-than 10000 恢复默认阈值。

raw SCAN 还有一个意外:某一页可能为空,但游标仍非零,因此空回复不代表扫描结束。我们在前面加载的 100,000 个键上遇到了这一点,因此下面两张截图分别展示游标和空页。
redis-cli SCAN 0 MATCH 'user:9999*' COUNT 1000
第一行是下一游标,第二行空内容是本页匹配结果。非零游标且无匹配,正是会让简单脚本出错的场景。
49472
MATCH 在读取分页后进行过滤,因此遇到第一个空页就停止的脚本会漏键。应循环到游标回到 0,或让 --scan 代为循环。内置循环更安全,因为它不会因空页提前停止。
步骤 8:处理字符串、TTL、哈希、列表和集合
缓存缺陷大多与过期有关,因此值得弄清 TTL 的返回码。设置一个 300 秒后过期的会话键,并依次观察各状态。
redis-cli SET session:42 token-abc EX 300
redis-cli TTL session:42
redis-cli EXPIRE session:42 60
redis-cli PERSIST session:42
redis-cli TTL session:42
redis-cli TTL nosuchkey
每条命令返回一个值,各值含义不同。
-
299: 键剩余秒数,比初始 300 少 1 秒。
-
1 和 1:
EXPIRE与PERSIST均成功。 -
-1: 键存在但没有过期时间。 -
-2: 键不存在。
下面的 raw 回复按照该顺序排列,请逐行对应。
OK
299
1
1
-1
-2
对本应过期的键出现 -1 是典型泄漏,因为不带 EX 的普通 SET 会静默移除已有 TTL。带 NX 和 EX 的 SET 也可实现简单锁,只在无人持有时设置,因此持有者崩溃也不会永久阻塞其他人。过期时间是安全网;没有过期时间的锁会一直保持,直到有人手工删除键,因此我们绝不在没有 EX 时设置锁。
redis-cli SET lock:job1 worker-a NX EX 30
redis-cli SET lock:job1 worker-b NX EX 30
redis-cli GET lock:job1
第一次调用输出 OK,第二次输出空行(raw 模式下为 nil,表示锁已被占用),GET 返回 worker-a。哈希在一个键下保存对象字段。这里使用 profile:1001,因为 user:1001 已在批量加载中作为字符串存在,而在字符串键上执行 HSET 会报 WRONGTYPE Operation against a key holding the wrong kind of value。
redis-cli HSET profile:1001 name ada plan pro logins 0
redis-cli HINCRBY profile:1001 logins 1
redis-cli HGETALL profile:1001
HSET 报告新增 3 个字段,HINCRBY 返回新计数,HGETALL 交替输出字段和值。由于 raw 回复是扁平结构,脚本必须自行配对行。
3
1
name
ada
plan
pro
logins
1
列表可作为队列:RPUSH 追加,LPOP 从头部取出,BLPOP 带超时等待元素,因此工作进程可以睡眠到任务到来,而不是轮询。超时参数很重要;没有它时,调用会在列表持续为空期间一直等待。
redis-cli RPUSH queue:emails job1 job2 job3
redis-cli LPOP queue:emails
redis-cli LLEN queue:emails
time redis-cli BLPOP queue:empty 2
列表长度增至 3,LPOP 返回 job1,剩余 2 项;对空列表执行 BLPOP 在 2.139 秒后返回空,即 2 秒等待加上约 0.14 秒的 docker exec 启动时间。集合保存唯一成员,并回答成员关系与交集问题。
redis-cli SADD tags:post1 redis docker podman
redis-cli SADD tags:post2 redis valkey
redis-cli SISMEMBER tags:post1 docker
redis-cli SINTER tags:post1 tags:post2
返回结果表示新增 3 个和 2 个成员,1 表示“docker 是成员”,共享标签只有 redis。键出现异常时,有三条命令可告诉你它是什么、消耗多少,先运行它们再猜测。每条命令只读取单个键并立即返回,因此可在在线服务器上安全试查。
redis-cli TYPE profile:1001
redis-cli OBJECT ENCODING profile:1001
redis-cli MEMORY USAGE profile:1001
结果分别是 hash、listpack 和 70 字节。小哈希使用紧凑的 listpack 编码,超过 hash-max-listpack-entries 或包含超过 hash-max-listpack-value 的值后切换为真正哈希表(默认 512 个字段和 64 字节;redis-cli CONFIG GET 'hash-max-listpack-*' 在我们的服务器上打印了两者)。切换后每个字段的内存开销会跳升,因此 70 字节的小哈希不能代表大哈希成本。
步骤 9:查找慢命令、大键与延迟
MONITOR 打印服务器收到的每条命令,是必须始终限制运行时间的 redis-cli 模式。在主机侧用 timeout 包裹 docker exec 看似合理,但我们的机器在主机侧进程被杀后,监视器仍在容器内运行。下面的检查列出已连接客户端以证明这一点。
timeout 3 docker exec -i -e REDISCLI_AUTH redis redis-cli MONITOR
redis-cli CLIENT LIST | grep 'flags=O' | cut -d' ' -f1-3
O 标志表示 monitor 客户端,在 timeout 结束主机侧命令后仍有一个保持连接。因此杀死主机侧进程不会结束服务器端会话。
id=26 addr=127.0.0.1:38274 laddr=127.0.0.1:6379
孤立的 monitor 会持续损耗吞吐,因为服务器要把每条命令复制给它。先用 redis-cli CLIENT KILL ID 26 按 ID 杀死,再在容器内执行 timeout,让超时结束客户端本身。后台任务会在一秒后写入键,以便流中能看到内容。
(sleep 1; redis-cli SET probe 2 >/dev/null) & docker exec -e REDISCLI_AUTH redis timeout 3 redis-cli MONITOR
流中的密码会被遮蔽,因此 monitor 截图可以安全粘贴到工单。之后 redis-cli CLIENT LIST | grep -c 'flags=O' 输出 0,说明容器内 timeout 已自行清理。每次监视会话后都应检查该计数,因为孤立 monitor 在被清除前会一直损耗吞吐。
OK
1794384072.931575 [0 127.0.0.1:50954] "AUTH" "(redacted)"
1794384072.931815 [0 127.0.0.1:50954] "SET" "probe" "2"
通过别名执行延迟模式不需要 timeout。没有 TTY 时,redis-cli 只采样一秒、打印一行摘要并退出,适合写入日志(真实终端中会持续重绘,直到按 Ctrl+C)。
redis-cli --latency
四列分别是往返延迟的最小值、最大值、平均值(毫秒)和样本数。应重点看最大值,因为平均值会掩盖偶发慢回复。这里最大值为 0.584 ms,98 个样本的平均值为 0.250 ms,本次没有明显离群值。
0.114 0.584 0.250 98
这类亚毫秒数字对云虚拟机回环连接很正常。要判断尖峰来自 Redis 还是 hypervisor,应测量主机本身。--intrinsic-latency 不执行任何 Redis 命令,而是对 CPU 忙循环计时。
redis-cli --intrinsic-latency 5
最后两行给出结论,因为它们报告繁忙循环期间调度器造成的最坏暂停。
Max latency so far: 209 microseconds.
96814192 total runs (avg latency: 0.0516 microseconds / 51.65 nanoseconds per run).
Worst run took 4047x longer than the average latency.
仅调度器就产生 209 µs 最坏延迟,说明该虚拟机上任何 Redis 命令的尾部延迟都不可能稳定低于约 0.2 ms。若 --latency 远高于此,问题在 Redis 或网络路径,而不是主机。
查找最大的键
刻意加入一个大列表,让扫描有对象可发现,因为干净数据集没有可供大键扫描排序的内容。
seq 1 50000 | awk '{print "RPUSH big:list item-"$1}' | redis-cli --pipe
三种扫描模式回答不同问题,应根据正在排查的问题选择。
-
--bigkeys: 按元素数量排序,找出元素最多的列表。 -
--memkeys: 按字节数排序,找出真正占用内存的对象。 -
--keystats: 增加大小百分位,展示整个键空间的分布。
在同一服务器上运行三者,因为它们从不同角度读取数据。
redis-cli --bigkeys | tail -n 13
redis-cli --memkeys | tail -n 11
--memkeys 汇总显示 50,000 项列表占 592,400 字节,而平均字符串仅占 47.92 字节。一个键的重量相当于数千个普通键,这正是大键扫描要揭示的问题。
Biggest list found "big:list" has 592400 bytes
Biggest hash found "profile:1001" has 70 bytes
Biggest string found "session:43" has 56 bytes
Biggest set found "tags:post1" has 59 bytes
1 lists with 592400 bytes (00.00% of keys, avg size 592400.00)
1 hashs with 70 bytes (00.00% of keys, avg size 70.00)
0 streams with 0 bytes (00.00% of keys, avg size 0.00)
100004 strings with 4792180 bytes (100.00% of keys, avg size 47.92)
2 sets with 110 bytes (00.00% of keys, avg size 55.00)
0 zsets with 0 bytes (00.00% of keys, avg size 0.00)
两种模式选出的最大字符串不同。--bigkeys 按 12 字节值选择 user:100000,而 --memkeys 选择 56 字节的 session: 键,因为它还把键名和分配器开销计入。截图中写的是侧测产生的 session:43;你的运行中应为 session:42,第一轮同样占 56 字节。问题在内存时以 --memkeys 为准;容量规划时 --keystats 更适合:
redis-cli --keystats | tail -n 20
其百分位表显示 99.994% 的键不超过 48 字节,而单个列表键占 578.53K。
Key size Percentile Total keys
-------- ---------- -----------
29B 0.0010% 1
48B 99.9940% 100002
56B 99.9970% 100005
70B 99.9990% 100007
578.53K 100.0000% 100008
Note: 0.01% size precision, Mean: 53B, StdDeviation: 1.83K
Key name length Percentile Total keys
--------------- ---------- -----------
12B 100.0000% 100008
Total key length is 965.80K (9B avg)
Type Total keys Keys % Tot size Avg size Total length/card Avg ln/card
--------- ------------ ------- -------- -------- ------------------ -----------
list 1 0.00% 578.52K 578.52K 50000 items 50000.00
hash 1 0.00% 70B 70B 3 fields 3.00
string 100004 100.00% 4.57M 47B 1.04M 11B
set 2 0.00% 110B 55B 5 members 2.50
三种模式内部都使用 SCAN,因此可安全用于在线服务器。删除大键时使用 redis-cli UNLINK big:list 而非 DEL;UNLINK 在后台线程释放内存,不会因整个列表而阻塞。对于 50,000 项的列表,这决定其他客户端是否会感知清理。

50,000 项列表会在每份报告中突出显示,这正是希望在事故前而不是事故中扫描得到的结果。
步骤 10:验证持久化能经受重启
重启后丢键的容器通常缺少 /data 卷或 AOF 设置,因此在把重要数据交给它前先测试持久化。触发快照并读取持久化计数器,以确认上次保存是否成功、AOF 是否启用。
redis-cli BGSAVE
redis-cli INFO persistence | grep -E 'rdb_last_bgsave_status|rdb_last_bgsave_time_sec|rdb_changes_since_last_save|aof_enabled|aof_last_write_status'
出现 Background saving started 后,计数器显示快照在一秒内成功且 AOF 已启用。两点都重要,因为第 2 步配置要求每秒 fsync 的 AOF,崩溃时最多损失约一秒写入。
rdb_changes_since_last_save:0
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:0
aof_enabled:1
aof_last_write_status:ok
查看 Redis 写入卷中的文件,大小可以显示每种持久化方式的磁盘成本,以及写入积累时哪种增长更快。快照和只追加文件彼此独立,因此都会出现在列表中。
docker exec redis ls -la /data /data/appendonlydir
RDB 快照为 2.9 MB,而 AOF 增量文件已增长到 7.0 MB。差距来自各自存储内容,下一段会计算。
/data:
total 2868
drwxr-xr-x 3 redis redis 4096 Nov 11 07:53 .
drwxr-xr-x 1 root root 4096 Nov 11 07:48 ..
drwx------ 2 redis redis 4096 Nov 11 07:48 appendonlydir
-rw------- 1 redis redis 2924463 Nov 11 07:53 dump.rdb
/data/appendonlydir:
total 6900
drwx------ 2 redis redis 4096 Nov 11 07:48 .
drwxr-xr-x 3 redis redis 4096 Nov 11 07:53 ..
-rw------- 1 redis redis 89 Nov 11 07:48 appendonly.aof.1.base.rdb
-rw------- 1 redis redis 7046471 Nov 11 07:50 appendonly.aof.1.incr.aof
-rw------- 1 redis redis 102 Nov 11 07:48 appendonly.aof.manifest
AOF 是快照的 2.4 倍,因为它记录每次写入,包括 50,000 次 RPUSH,直到重写将其压缩为新的基础文件。磁盘规划应关注这一比例,按 AOF 而非快照规划卷容量。上面的数字只是某一时刻的值,不是上限。现在重启并统计键数,总数一致即证明数据从卷中读回。
redis-cli DBSIZE; docker restart redis >/dev/null && sleep 2 && redis-cli DBSIZE
重启前后的计数完全相同:
100007
100007
100,007 个键全部恢复。只有带 TTL 的键可能合法丢失,因为它们在容器停止期间会过期。LASTSAVE 返回上次成功保存的 Unix 时间;若该值停止变化应触发告警,因为冻结的时间戳意味着保存正在静默失败。
步骤 11:创建最小权限 ACL 用户
应用程序不应共享默认用户密码。创建一个仅能访问 app:* 键的 app 用户,并通过 -x 从 stdin 传入密码,使其不出现在 argv 中。> 前缀是 ACL 中“添加密码”的语法,因此从 stdin 传入的值应包含该前缀。
openssl rand -hex 24 > ~/redis-lab/app.pass && chmod 600 ~/redis-lab/app.pass
printf '>%s' "$(cat ~/redis-lab/app.pass)" | redis-cli -x ACL SETUSER app on resetkeys '~app:*' resetchannels '+@read' '+@write' '-@dangerous'
无需以该用户登录即可测试规则。ACL DRYRUN 只执行权限检查,不写入数据、不建立会话,因此可在调整模式时反复运行。应分别测试用户应访问和不应访问的键,因为要信任规则必须同时验证两侧。
redis-cli ACL DRYRUN app SET app:visits 1
redis-cli ACL DRYRUN app GET user:1
Redis 允许第一条命令并拒绝第二条,因为该键不在 app:* 模式内。对每个键模式做 dry run,是在应用正式使用前低成本验证规则的方法。
OK
User app has no permissions to access the 'user:1' key
然后真正登录,仅覆盖当前命令的 REDISCLI_AUTH。写入成功,但用户甚至无法查询自己是谁,因为账户只授予了数据命令。
REDISCLI_AUTH="$(cat ~/redis-lab/app.pass)" redis-cli --user app SET app:visits 1
REDISCLI_AUTH="$(cat ~/redis-lab/app.pass)" redis-cli --user app ACL WHOAMI
写入返回 OK,身份查询被拒绝。这一对结果说明规则双向有效:同一用户既被允许执行一条命令,也被拒绝另一条。
OK
NOPERM User app has no permissions to run the 'acl|whoami' command
下一个会出问题的是健康检查,因为连接池会发送 PING,而 app 用户尚无权限。授予任何权限前先尝试,可知道连接池会记录哪种错误。
REDISCLI_AUTH="$(cat ~/redis-lab/app.pass)" redis-cli --user app PING
拒绝信息会指出命令,从而明确应授予什么。
NOPERM User app has no permissions to run the 'ping' command
我们的第一次修复是添加 +@connection,该类别包含 PING 与 ECHO,但也包含 CLIENT KILL 和 CLIENT PAUSE。ACL 规则从左到右应用,因此在 -@dangerous 后写 +@connection 会把危险命令重新授予。按这种方式创建的临时用户在我们的服务器上通过了 ACL DRYRUN probe CLIENT KILL ID 1 并返回 OK,应用因此可以断开所有其他客户端。应改为授予单独命令:
redis-cli ACL SETUSER app '+ping' '+echo' '+client|setname'
REDISCLI_AUTH="$(cat ~/redis-lab/app.pass)" redis-cli --user app PING
redis-cli ACL DRYRUN app CLIENT KILL ID 1
SETUSER 返回 OK,app 用户现在能得到 PONG,dry run 也证明危险的 client 命令仍不可用。CLIENT SETNAME 用于会给连接命名的客户端库;若不需要可去掉。
OK
PONG
User app has no permissions to run the 'client|kill' command
用默认用户执行 redis-cli ACL LOG 1 可看到先前拒绝记录,包括客户端地址、原因和命令,这对定位配置错误的服务很有用。检查最终规则集:
redis-cli ACL GETUSER app | grep -A1 -E '^(commands|keys)$'
命令规则从 -@all 开始,仅添加我们授予的内容,未列出的全部拒绝。仅授予的类别是 @read 与 @write,并用 -@dangerous 裁剪;未来 Redis 若把某命令归入这两类,该用户会自动获得权限,因此其他权限都应逐命令授予。这个默认拒绝结构使账户保持最小权限,前提是后续每次授权都指定命令而非整个类别。
commands
-@all +@read +@write -@dangerous +ping +echo +client|setname
keys
~app:*
下面是 Ubuntu 测试机上从 dry run 到拒绝 CLIENT KILL 的完整 ACL 过程:

运行时用户会在重启后消失。ACL SAVE 需要 aclfile 指令(我们的配置没有,因此会报错),而 CONFIG REWRITE 无法写入只读配置。我们执行 docker restart redis 验证了丢失,重启后 ACL USERS 只列出 default。适合只读配置的修复方式是在 redis.conf 中声明用户:
printf 'user app on >%s ~app:* resetchannels +@read +@write -@dangerous +ping +echo +client|setname\n' "$(cat ~/redis-lab/app.pass)" >> ~/redis-lab/redis.conf
docker restart redis >/dev/null && sleep 2 && redis-cli ACL USERS
这次两个用户在重启后都保留,因为定义已进入 Redis 每次启动都会读取的配置文件。
app
default
该命令中的两秒暂停很重要,因为容器需要片刻才能重新接受连接。没有暂停时,我们有一次运行与重启竞速,得到 Could not connect to Redis at 127.0.0.1:6379: Connection refused。
步骤 12:运行协议兼容的 Valkey 替代方案
Valkey 是 Redis 7.2.4 的 Linux Foundation 分支,使用相同协议,因此客户端和 redis-cli 可直接工作。数据文件则不同:Valkey 迁移指南说明 Redis 7.4 及之后写出的 RDB 文件无法加载,包括本文的 redis-data 卷。应在新卷上启动 Valkey,并逐键迁移数据,不能复制文件。将它运行在另一个回环端口旁;标签为 Valkey 发布页上的当前稳定版:
docker run -d --name valkey -p 127.0.0.1:6380:6379 valkey/valkey:9.1.3 # https://github.com/valkey-io/valkey/releases
Redis 8 镜像中的 redis-cli 可直接连接,因为 Valkey 保持相同协议。让新端口上的服务器表明身份:
docker run --rm --network host redis:8.10.3 redis-cli -p 6380 INFO server | grep -E 'server_name|valkey_version'
Valkey 会返回自己的名称和版本,因此同一主机上同时运行时可以区分。把应用指向错误端口前应检查,因为两者响应相同命令,这种错误很难察觉。
server_name:valkey
valkey_version:9.1.3
在 Valkey 自己的容器中客户端名为 valkey-cli,其 INFO 仍报告 redis_version:7.2.4,以兼容据此分支的客户端库。我们仅用 Redis 8 客户端对 Valkey 执行 PING 和 INFO,两者均正常工作;本速查表其余部分只在 Redis 上测试。若要在两者之间选择,Valkey 与 Redis 对比涵盖许可证和 RDB 安全迁移路径。
Docker 与 rootless Podman:Redis 实测吞吐
开头吞吐数据来自 redis-benchmark:每个路径 200,000 请求、50 客户端、运行三次,目标为各引擎上的一次性无认证容器。发布端口行使用主机网络中的客户端,回环行使用服务器容器内的客户端,因此两者差距隔离了端口路径成本。为每个引擎启动一个无密码、无卷的一次性服务器:
docker run -d --name bench-docker -p 127.0.0.1:6390:6379 redis:8.10.3
podman run -d --name bench-podman -p 127.0.0.1:6391:6379 docker.io/library/redis:8.10.3
然后让基准测试指向各发布端口(Podman 为 6391);回环行则在容器内执行 docker exec bench-docker redis-benchmark -q -n 200000 -c 50 -t set,get:
docker run --rm --network host redis:8.10.3 redis-benchmark -p 6390 -q -n 200000 -c 50 -t set,get | tr '\r' '\n' | grep 'requests per second'
tr 步骤很重要,因为 -q 用回车重绘进度行;不处理会令日志文件充满约 70 KB 的不完整行。每次运行输出 SET 与 GET 两行,三次运行即每条路径六个数字。
SET: 21281.12 requests per second, p50=1.831 msec
GET: 21588.95 requests per second, p50=1.791 msec
下表全部为每秒 SET 请求数。每个 Ubuntu 运行中 GET 与 SET 差距在约 2.2% 内;Rocky 的一次容器内运行差距达到 12%,这也是 Rocky 行波动较大的原因之一。每格是一轮运行,可自行判断离散度。
| 主机与路径 | 第 1 轮 | 第 2 轮 | 第 3 轮 | p50 延迟 |
|---|---|---|---|---|
| Ubuntu 26.04,Docker 发布端口 127.0.0.1 | 21,281 | 21,452 | 21,333 | 1.80–1.83 ms |
| Ubuntu 26.04,rootless Podman 发布端口 | 25,179 | 25,278 | 25,000 | 1.63–1.71 ms |
| Ubuntu 26.04,Docker 容器内 | 44,633 | 45,147 | 44,663 | 0.57 ms |
| Rocky Linux 10,rootless Podman 发布端口 | 20,947 | 20,931 | 19,737 | 1.45–1.57 ms |
| Rocky Linux 10,Podman 容器内 | 40,104 | 37,030 | 34,941 | 0.62–0.64 ms |
表格有三种读法,因为每组比较回答不同问题,只有同一主机内的行才能直接公平比较。
-
发布端口 vs 容器内: 两个引擎中端口路径都把吞吐减半,Docker 从约 44,800 降至 21,400。2 vCPU 下,基准客户端、端口转发器和 Redis 竞争同样核心,因此这是最差情况;另一台机器上的客户端不会消耗 Redis 主机的转发器 CPU。
-
Ubuntu 上 Docker vs rootless Podman: Podman 发布端口每次都较 Docker 的
docker-proxy路径快 18%,波动低于 1.2%。rootless 网络已不再是旧版 Podman 的性能惩罚。 -
Ubuntu vs Rocky: Rocky 主机更慢、波动更大,容器内运行从最好到最差相差 13%。共享云核心上的跨虚拟机比较只能作为参考;同主机内的行才可信。
一个有趣现象:Rocky 发布端口虽吞吐更低,但 p50 延迟为 1.45–1.57 ms,低于 Ubuntu Docker 端口的 1.80–1.83 ms,因此其慢速来自尾延迟而不是典型请求。内存不是差异因素:空闲基准容器在 Docker 下使用 5.7 MiB、Podman 下 5.8 MB。
若缓存只供本机容器访问,应完全跳过发布端口,把应用放入同一 Docker 网络或 Podman pod,从而保持在更快的容器内路径。当主机上的应用必须连接时,rootless Podman 在 Ubuntu 上是更快、更安全的默认选择;在 Rocky 上,它也是无需更改守护进程就能正确处理 SELinux 标签的唯一引擎。若要在这些数字之上构建仪表板,Redis Prometheus 与 Grafana 配置可导出本速查表手工读取的相同 INFO 计数器。










