Redis入门
- Redis(Remote Dictionary Server,远程字典服务)是一个开源的、基于内存的数据结构存储系统,可以同时当数据库、缓存和消息中间件用。它按 BSD 三条款许可证发布,支持主从复制、持久化和 Lua 脚本,最近还加入了模块化机制。
- 它和「内存数据库」这个说法常被混用,但准确讲:Redis 把数据放在内存里以保证速度,同时用 RDB/AOF 把数据落盘以保证不丢,所以它是「内存优先 + 可持久化」的存储,而不是纯粹的易失缓存。
- 常见用法:
- 缓存:把数据库查询结果放进来,配
EXPIRE做 TTL 失效。 - 会话与计数器:
INCR是原子的,天然适合限流、PV/UV、库存扣减。 - 队列:用 List(
LPUSH+BRPOP)或 Stream(消费组、ACK、可回溯)。 - 排行与去重:ZSET 做排行榜,Set 做标签/共同好友,HyperLogLog 做基数统计。
- 分布式锁:
SET key val NX PX 30000是最小可用的写法。
- 缓存:把数据库查询结果放进来,配
- 本文所有结论都在本机实测:WSL2 的 Ubuntu 26.04.1,Redis 8.2.8(源码编译,
/usr/local/bin/redis-server,配置/etc/redis/redis.conf,数据目录/var/lib/redis),客户端redis-cli 8.2.8,Python 侧redis-py 8.1.0。 - 为什么是 8.2 而不是最新的 8.10:Redis 开源版没有 LTS 这个标签(官方只给 Redis Software 企业版发生命周期表),但各条 8.x 线的支持期差别很大——第三方 EOL 表里 8.0 支持到 2026-12、8.2 到 2030-09、7.4/7.2 到 2029-12,而 8.4/8.6/8.8 这几个中间线发布后几个月就被下一线取代。所以「长期维护的最新版」在 Redis 这里对应的就是 8.2 线,本机按这个口径把 8.10.3 卸掉重装了 8.2.8。
- 换线不是「换个号继续用」,有两处硬差别本文都实测到了:一是 RDB 版本号(8.10.3 写出来的
dump.rdb头是REDIS0015,8.2.8 写的是REDIS0012,低版本读不了高版本快照,所以降级必须清库重来);二是构建系统与命令集合(make build redis、VRANGE、STRALGO、ACL COUNT、rdb-compression-type、# Hotkeys这些在 8.2 上都不存在)。
Redis 特点
- 基于内存操作:数据常驻内存,读写是微秒级;也正因如此,容量受内存限制,数据集要能装进内存。
- 支持多种数据结构:字符串(string)、列表(list)、集合(set)、有序集合(zset)、哈希(hash)是五种基本类型;在此之上还有位图(bitmap,其实是 string)、HyperLogLog(也是 string)、地理空间(GEO,是 zset 的封装)、流(Stream);Redis 8 又把 JSON、搜索、时序、概率型结构等能力逐步并入。
- 数据持久化:RDB(某一时刻的二进制快照)与 AOF(写命令追加日志)两种机制,可同时开启。本机实测两者都开着:
dump.rdb加appendonlydir/下的三个文件(appendonly.aof.1.base.rdb、appendonly.aof.1.incr.aof、appendonly.aof.manifest)。- RDB 文件头里写着格式版本:
head -c 9 dump.rdb实测 8.2.8 写出来的是REDIS0012,8.10.3 写的是REDIS0015。这条决定「能不能直接换二进制降级」——低版本读不了高版本的 RDB/AOF,会启动失败,所以降级要么先redis-cli --rdb out.rdb逻辑导出,要么清空数据目录重来(本机 8.10.3 → 8.2.8 时 16 个库全空,直接清了dump.rdb与appendonlydir/)。
- RDB 文件头里写着格式版本:
- 单线程命令执行:命令是一个一个串行执行的,所以单条命令天然原子,不需要加锁。这也是「Redis 快」的原因——省掉了锁竞争和上下文切换,但代价是一条慢命令会卡住所有人。
- 事件驱动 + IO 多线程:命令执行仍是单线程,但网络读写可以交给多个 IO 线程。实测本机
io-threads默认是1(等于不启用),要提速大流量场景才把它调到 2~8 之间;Redis 8 的INFO里还多了一个独立的# Threads统计段。 - 高度可配置:
redis.conf里有几百个参数,可以在持久性、内存上限、淘汰策略之间做权衡。 - 发布/订阅:
SUBSCRIBE/PUBLISH走的是「尽力而为、不存历史」的频道消息;要可靠消息用 Stream。 - 高可用与自动分片:Sentinel(哨兵)负责故障转移,Cluster 负责把 key 分到 16384 个哈希槽上。注意 Cluster 模式下只有
db0,SELECT会被拒。 - 广泛的语言支持:官方与各社区驱动覆盖 Java、Python、Go、Node.js、C#、PHP 等。
- 事务:
MULTI/EXEC/WATCH把多条命令打包后顺序执行,中途不会被别的客户端插入;但它不是关系数据库那种事务——命令入队时的语法错误会让整个EXEC失败,而运行期错误(如对字符串执行INCR)只会让那一条失败,其余照常执行。 - Lua 脚本:
EVAL把一段脚本作为整体原子执行,适合「读-判断-写」这种需要一致性的复合操作。 - 安全性:
requirepass做简单密码认证;6.0 起有 ACL,能按用户配「能跑哪些命令、能碰哪些 key」。原笔记写的「基于角色的访问控制(RBAC)」不够准确——Redis 的 ACL 是「用户 + 命令/键模式」授权模型,不是角色体系。TLS 需要编译时开启(本机这份没开,实测tls-port为0)。 - 简单实用:装起来就一个二进制,跑起来就一个端口,命令直觉化,适合快速上手。
- 顺带一个能体现「单线程」的实验:
SETBIT/GETBIT/BITCOUNT这类操作在 100 万条INCR压测下不会互相踩踏,因为所有写命令都在同一个线程里排队。实测PFADD pfc a b c d→1、PFCOUNT pfc→4,HyperLogLog 的基数统计也是原子更新的。
-
Ubuntu / Debian(发行版自己的仓库)
1
2
3
4
5sudo apt-get update
apt-cache policy redis-server | head -3
sudo apt-get install -y redis-server
sudo systemctl enable --now redis-server
systemctl is-active redis-server1
2
3
4redis-server:
Installed: (none)
Candidate: 5:8.0.5-1ubuntu0.1
active- 实测 Ubuntu 26.04 仓库里的是 8.0.5(完整版本串
5:8.0.5-1ubuntu0.1,开头那个5:是 deb 的 epoch,不是版本号),想要更新的版本只能走官方源码或 GitHub 编译。 - 别把「发行版仓库里的版本」当成上游的 LTS:Redis 上游没有 LTS 概念,Ubuntu 只是按自己的维护周期给 8.0.5 打安全补丁。
- 单元名是
redis-server.service(不是redis.service),这点在写部署脚本时容易踩。
- 实测 Ubuntu 26.04 仓库里的是 8.0.5(完整版本串
-
RHEL / CentOS / Rocky(
yum或dnf)1
sudo dnf install redis
- 老系统上
yum是dnf的兼容软链,两者等价;RHEL 7 只有yum。想装新版本要启用 Redis 官方仓库(redis包的仓库按版本分 7/8/9 三条线)。
- 老系统上
-
源码安装(本机实测路径,按「支持期最长的 8.2 线」装)
1
2
3
4wget https://download.redis.io/releases/redis-8.2.8.tar.gz
sha256sum redis-8.2.8.tar.gz
tar -xzf redis-8.2.8.tar.gz
cd redis-8.2.81
2-rw-r--r-- 1 root root 3915682 Jul 24 03:21 redis-8.2.8.tar.gz
01354ee4449e758e6e45d056fe5802abd9ccde4d669b79c9d5e9f6d730a80759 redis-8.2.8.tar.gz- 编译:
1
make -j8
- 8.2 这一版直接
make就行:实测make -j8真实耗时 34.6 秒(user 2m14s,8 核),产物src/redis-server19,215,112 字节、src/redis-cli7,811,640 字节,make install之后/usr/local/bin里是这六个名字(redis-check-aof/redis-check-rdb/redis-sentinel只是指向redis-server的软链)。 make build redis在 8.2 上根本不存在,照抄会报:1
2make[1]: *** No rule to make target 'build'. Stop.
make: *** [Makefile:11: build] Error 2build是 8.4 之后新构建脚本才有的目标,所以网上「Redis 8 编译必须用 make build」的文章只适用于 8.4+,写文章的人多半在 8.10 上试的。- 为什么默认不编模块,顶层
Makefile全文就十几行,实测原文:模块是开关式的:要连 JSON/时序/搜索/布隆一起编得写1
2
3
4
5
6
7
8
9
10
11
12
13
14# Top level makefile, the real stuff is at ./src/Makefile and in ./modules/Makefile
SUBDIRS = src
ifeq ($(BUILD_WITH_MODULES), yes)
SUBDIRS += modules
endif
default: all
.DEFAULT:
for dir in $(SUBDIRS); do $(MAKE) -C $$dir $@; done
install:
for dir in $(SUBDIRS); do $(MAKE) -C $$dir $@; donemake BUILD_WITH_MODULES=yes;而modules/Makefile的子项目是redisjson redistimeseries redisbloom redisearch,里面还有install-rust这一步——它们是 Rust 工程,没装 Rust 工具链就编不过。这正是 8.10 上直接全量构建会报ERROR: make build finished with module failure(s): redisbloom redisearch redisjson redistimeseries的根因。 - 编译前先确认有
build-essential(本机gcc 15.2.0、make 4.4.1)。
- 8.2 这一版直接
- 装到系统里:
1
2sudo make install
which redis-server redis-cli1
2/usr/local/bin/redis-server
/usr/local/bin/redis-cli - 关于
make test:8.2 的顶层Makefile只显式定义了default、install两个目标,其它名字都靠.DEFAULT规则转发给src;test、test-modules、test-sentinel、test-cluster全部定义在src/Makefile(实测第 496/499/502/505 行)。所以顶层make test能跑是转发的功劳,想精确跑某一块最好写make -C src test-sentinel。本机没有跑完整测试套件——它要几分钟且会起很多临时实例。 - 源码装完不会有 systemd 单元,要自己写一个。这里有个 8.x 的坑:源码编译出来的
redis-server没有链接 libsystemd(本机编译时pkg-config都不存在),所以单元文件里写Type=notify会永远卡在activating,必须用Type=simple并给--supervised no:1
2
3
4[Service]
Type=simple
ExecStart=/usr/local/bin/redis-server /etc/redis/redis.conf --supervised no --daemonize no
User=redis- 顺带:控制调试命令的参数真名是
enable-debug-command(不是enable-debug),实测CONFIG GET enable-debug-command→no,此时DEBUG OBJECT k报:同一族还有1
ERR DEBUG command not allowed. If the enable-debug-command option is set to "local", you can run it from a local connection, otherwise you need to set this option in the configuration file, and then restart the server.
enable-protected-configs(no)与enable-module-command(no),生产环境保持默认。 - 启动日志里还有一条 WSL/容器里常见的告警,值得提前知道它不是错误:官方给的修法就是它说的那样:
1
WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition.
vm.overcommit_memory = 1写进/etc/sysctl.conf后重启,或sysctl vm.overcommit_memory=1。不处理的话BGSAVE/REPLICAOF在内存紧张时会失败。
- 顺带:控制调试命令的参数真名是
- 编译:
1 | brew install redis |
1 | PONG |
- Homebrew 装的是它自己编译的版本,配置在
/opt/homebrew/etc/redis.conf(Intel 机器是/usr/local/etc/redis.conf)。 - 想停并用前台方式调试:
brew services stop redis,再redis-server /opt/homebrew/etc/redis.conf。
- 官方不提供 Windows 原生版本。原笔记说「Microsoft 维护了一个 Windows 版本的 Redis」已经过时:微软的 MSOpenTech 端口早就不维护了,社区里流传的
tporadowski/redis是第三方移植,只到 Redis 5.x,且不含 8.x 的新类型。 - 推荐两条路:
- WSL(本文用的就是这条):在 WSL2 的 Ubuntu 里按上面 Linux 的方式装,Windows 侧通过
127.0.0.1:6379直连(WSL2 会自动做 localhost 端口转发,实测 Windows 上redis-cli -h 127.0.0.1 -p 6379能通)。 - Docker:
1
2docker run -d --name redis -p 6379:6379 redis:8
docker exec -it redis redis-cli ping
- WSL(本文用的就是这条):在 WSL2 的 Ubuntu 里按上面 Linux 的方式装,Windows 侧通过
- 只在本地学习用
.exe的话,注意它和 Linux 版在持久化路径、权限模型上都不一样,别拿它验证生产行为。
-
默认监听
127.0.0.1:6379,本机实测:1
ss -ltnp | grep 6379
1
2LISTEN 0 511 127.0.0.1:6379 0.0.0.0:* users:(("redis-server",pid=11605,fd=8))
LISTEN 0 511 [::1]:6379 [::]:* users:(("redis-server",pid=11605,fd=13))- 配置项
bind 127.0.0.1 -::1里的-::1表示「IPv6 回环」,前面的-是「有则绑、没有也不报错」的意思。
- 配置项
-
本机连(同一台机器、默认端口):
1
redis-cli
1
2127.0.0.1:6379> PING
PONG -
跨主机或改过端口时:
1
redis-cli -h <hostname> -p <port>
- 也可以用 URI 一次给全:
redis-cli -u redis://127.0.0.1:6379/0,/0是库号,用户名密码可写成redis://user:pass@host:port/db。
- 也可以用 URI 一次给全:
-
非交互地跑一条命令(脚本里最常用):
1
2
3redis-cli -h 127.0.0.1 -p 6379 PING
redis-cli SET mykey "Hello"
redis-cli GET mykey1
2
3PONG
OK
Hello- 注意
redis-cli SET mykey "Hello"这种「一条命令一个进程」的方式每次都会新建连接,批量操作用redis-cli -x、--pipe或直接进交互模式。
- 注意
-
检查连通性的三个层次,实测都可用:
1
2
3redis-cli PING # 服务活着
redis-cli INFO server | head -3 # 能拿到运行时信息
redis-cli --latency # 按住 Ctrl-C 结束,看往返延迟1
2
3
4PONG
# Server
redis_version:8.2.8
redis_mode:standalone -
退出交互模式:
exit(或QUIT、Ctrl-C、Ctrl-D)。 -
连不上时的排查顺序:服务是否
active→ss里端口在不在监听 →bind是否只绑了回环 →protected-mode yes且没设密码时,来自非回环地址的连接会被直接拒 → 防火墙/安全组 → 有没有requirepass。 -
一个真实差异:
SELECT与多数据库Redis 单机默认提供 16 个逻辑库(
databases 16,实测CONFIG GET databases返回16),用SELECT切换:1
2
3
4127.0.0.1:6379> SELECT 15
OK
127.0.0.1:6379[(15)]> DBSIZE
(integer) 0- 提示符会变成
[(15)],这是 redis-cli 在告诉你当前在哪个库。 - 库之间不共享 key,
MOVE key db可以把键搬到另一个库(实测MOVE ra 14返回1)。 - 但 Cluster 模式只有
db0,SELECT 1会被拒绝(本机是 standalone,redis_mode:standalone,没能实测这条报错)。所以「用 16 个库来隔离业务」是单机时代的习惯,现代做法是用 key 前缀隔离(如order:1001),或者干脆多实例。
- 提示符会变成
Redis 密码
-
设置密码
- 找到配置文件。本机是
/etc/redis/redis.conf(源码安装时也可以把配置放在安装目录里)。 - 找到
# requirepass foobared这一行,去掉注释并换成强密码:1
requirepass <你的强密码>
- 密码强度由你自己保证,Redis 不会校验(
requirepass ""等于关闭认证)。 - 注意:
requirepass在配置文件里是明文,CONFIG GET requirepass也能读到它。所以配置文件权限要收紧(本机/etc/redis/redis.conf属主redis),也别把它提交进仓库。
- 密码强度由你自己保证,Redis 不会校验(
- 让改动生效——两种办法:或者不重启、运行时热改(只影响当前进程,重启后回到配置文件):
1
sudo systemctl restart redis-server
1
2redis-cli -a '旧密码' CONFIG SET requirepass '新密码'
redis-cli -a '新密码' CONFIG REWRITECONFIG REWRITE会把当前运行时配置写回配置文件,是「热改之后固化」的标准动作。
- 用 systemd 管理时,服务名按安装方式不同:发行版包是
redis-server,本机自建单元叫redis。实测两者都可用:1
2sudo systemctl restart redis-server
sudo service redis-server restartservice xxx restart在没有 systemd 的老系统上才走/etc/init.d/;有 systemd 时它只是转发给systemctl。
- 找到配置文件。本机是
-
通过密码连接
- 命令行带
-a:1
2redis-cli -a mySecurePassword
redis-cli -h <hostname> -p <port> -a mySecurePassword- 每次都会提示一句安全告警,这是正常的:因为密码会出现在 shell 历史和
1
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
ps的进程列表里。脚本里加--no-auth-warning只是闭嘴,不解决泄露问题。 - 更好的做法是用环境变量,
ps里看不到:1
REDISCLI_AUTH='mySecurePassword' redis-cli PING
- 每次都会提示一句安全告警,这是正常的:
- 不设
-a,先连再认证(原笔记推荐的方式,实测可行):1
2127.0.0.1:6379> AUTH mySecurePassword
OK- 没认证之前,除了
AUTH、PING等极少数命令,其他一律被拒:1
NOAUTH Authentication required.
- 密码错时的报错,实测原文:
1
WRONGPASS invalid username-password pair or user is disabled.
- 没认证之前,除了
- 认证之后想退回未认证状态(调试用):
AUTH传空串不行,只能断开重连。 - 想临时改密码:
CONFIG SET requirepass ''(空串即关闭认证),改完记得再设回来。
- 命令行带
-
更细的权限:ACL
requirepass只能「全有或全无」,Redis 6 起的 ACL 才能做真正的最小权限。实测建一个受限用户:1
redis-cli ACL SETUSER noteuser on '>NotePass123' '~note:*' '+get' '+set'
1
OK
然后用它连接,能做什么、不能做什么一目了然:
1
2
3redis-cli -u redis://noteuser:NotePass123@127.0.0.1:6379 SET note:k 1
redis-cli -u redis://noteuser:NotePass123@127.0.0.1:6379 GET note:k
redis-cli -u redis://noteuser:NotePass123@127.0.0.1:6379 KEYS '*'1
2
3OK
1
(error) NOPERM User noteuser has no permissions to run the 'keys' command~note:*是 key 模式,+get/+set是命令白名单;也可以用+@read这种按分类授权(实测ACL CAT返回 21 个分类)。- 默认用户长这样(实测
ACL LIST):1
user default on sanitize-payload #61d215e6... ~* &* +@all
#61d2...是密码的 SHA256,不是明文;sanitize-payload是 8.x 的日志脱敏开关;&*是 pub/sub 频道模式。
- 用完删掉:
ACL DELUSER noteuser。 - 想把 ACL 用户持久化,必须先配
aclfile,否则ACL SAVE直接报错,实测原文:1
2
3ERR This Redis instance is not configured to use an ACL file. You may want to specify users
via the ACL SETUSER command and then issue a CONFIG REWRITE (assuming you have a Redis
configuration file set) in order to store users in the Redis configuration.- 本机
CONFIG GET aclfile就是空串。所以两条路:在redis.conf里加aclfile /etc/redis/users.acl再用ACL SAVE;或者ACL SETUSER之后执行CONFIG REWRITE,把用户写进主配置文件。 - 排查权限问题时
ACL LOG很有用,每条记录带count、reason、context、object、username、age-seconds、client-info等字段(实测ACL LOG 1就这个结构),它记录「哪个用户因为什么被拒」。注意这份日志在实例重启后清零,别拿它当长期审计。 ACL DRYRUN 用户名 命令可以预先试一下有没有权限,实测ACL DRYRUN noteuser KEYS *直接回User noteuser has no permissions to run the 'keys' command,而ACL DRYRUN noteuser GET note:1回OK。- 8.2 没有
ACL COUNT这个子命令(实测ERR unknown subcommand 'COUNT'),ACL HELP列出的全集是CAT DELUSER DRYRUN GETUSER GENPASS LIST LOAD LOG SAVE SETUSER USERS WHOAMI HELP;要数用户请用ACL USERS | wc -l。
- 本机
- 安全提醒:不要把 Redis 直接暴露到公网。历史上大量 Redis 被入侵都是因为「公网 + 无密码 + 可写目录」,攻击者用
CONFIG SET dir+CONFIG SET dbfilename把 RDB 写成 cron 文件或 SSHauthorized_keys。至少要做到:只绑内网/回环、设密码或 ACL、关闭CONFIG命令(rename-command CONFIG "")、用受保护的用户跑服务。
-
下面每一条都在本机 Redis 8.2.8 上跑过,输出照抄。写命令时记住三条通用规律:
- 命令不区分大小写(
set和SET一样),但 key 区分大小写(MyKey和mykey是两个键)。 - 类型不对就报
WRONGTYPE Operation against a key holding the wrong kind of value,实测多次撞过。 redis-cli里返回值分三类:简单字符串(OK)、整数((integer) 3)、批量(列表)。nil表示「不存在」,显示为空。
- 命令不区分大小写(
-
通用命令
命令 作用 本机实测 PING探活 PONGSELECT n切到第 n 个逻辑库 OK,提示符变[(15)]DBSIZE当前库的键数量 (integer) 0INFO [section]运行时统计 13 个段落: Server、Clients、Memory、Persistence、Threads、Stats、Replication、CPU、Modules、Errorstats、Cluster、Keyspace、Keysizes(实测INFO | grep -E '^#')。8.2 没有# Hotkeys段,那是 8.10 才加的;# Threads、# Errorstats、# Keysizes三段 8.2 已经有了TYPE key看值的类型 stringEXISTS key [key...]存在个数(不是布尔) 单个存在 1,EXISTS k1 k2→2,不存在0DEL key [key...]删除,返回删掉的个数 删掉 1,删不存在的键返回0(不报错)UNLINK key异步删除(后台释放内存) 1EXPIRE key seconds设过期 1(成功)/0(键不存在)PEXPIRE key ms毫秒版过期 1TTL key剩余秒数 有 TTL → 60;无 TTL →-1;键不存在 →-2PERSIST key取消过期 1,之后TTL变-1RENAME old new改名 OKMOVE key db搬到另一个库 1COPY src dst复制键 1KEYS pattern按模式找键 KEYS k*→k1 k2SCAN cursor [MATCH p] [COUNT n]增量遍历 SCAN 0 MATCH sk* COUNT 10→ 游标0+sk1RANDOMKEY随机取一个键名 k2OBJECT ENCODING key看底层编码 实测:整数 int、短字符串embstr、列表/哈希/小有序集合listpack、纯整数集合intset、大有序集合skiplist、超阈值的大哈希hashtableSAVE/BGSAVE落 RDB 快照 OK/Background saving startedLASTSAVE上次快照的 Unix 时间戳 (integer) 1793731280(每次快照后变大,可用来确认BGSAVE真的完成了)FLUSHDB/FLUSHALL清空当前库 / 所有库 OKCOMMAND/COMMAND COUNT命令元信息 实测 COMMAND COUNT→267;COMMAND LIST有 397 条(含子命令,所以比 267 多);COMMAND LIST FILTERBY MODULE vectorset→ 12 条CONFIG GET/SET 参数读写配置 CONFIG GET databases→16CLIENT LIST当前连接列表 每行含 id、addr、db、sub、idle等字段KEYS与SCAN的取舍:KEYS *是单线程里的全库扫描,键多时会把整个实例卡住,生产环境基本禁用(可用 ACL 把keys收掉,实测受限用户跑KEYS '*'报NOPERM)。遍历请用SCAN,它返回「新游标 + 本批键」,游标回到0才算遍历完。EXISTS返回的是个数,所以IF EXISTS == 1这种判断在有多个键时会失效,用> 0。TTL的-1和-2是两个不同含义(活着但没设过期 / 键不存在),别混。SAVE是同步阻塞的(实测返回OK时已经写完),线上要用BGSAVE(fork 子进程去写)。
-
字符串(String)
字符串是 Redis 最基本的类型,值可以是文本、数字,也可以是任意二进制(序列化对象、JSON、图片字节都行)。
命令 使用描述 本机实测 SET key value设置值 OKGET key取值 value;不存在时是nil(显示空)DEL key删除键 1INCR key整数 +1 11DECR key整数 -1 15INCRBY key n加 n 16DECRBY key n减 n 13INCRBYFLOAT key f浮点增量 1.5加0.25→1.75APPEND key str追加,返回新长度 Hello+World→11STRLEN key值的字节长度 11MSET k1 v1 k2 v2批量设置 OKMGET k1 k2 ...批量取值 1 2 3(缺失的键位置是空)MSETNX ...全部不存在才设置 0(因为a已存在)SETEX key s v设置并给 TTL OK,TTL→60PSETEX key ms v毫秒版 OK,TTL→60SETNX key v不存在才设置 0(键已存在)GETSET key v设新值并返回旧值 返回 value,之后GET→newvalGETDEL key取值并删除(6.2+) 返回值本身(实测 gv),之后EXISTS→0GETEX key EX n取值同时改 TTL(6.2+) 返回值本身(实测 gv2),TTL变成指定秒数SET key v NX仅当不存在 首次 OK,重复时返回空(nil)SET key v XX仅当已存在 OKSET key v KEEPTTL覆盖值但保留 TTL OK,TTL仍是100SETRANGE key off str从下标覆盖,返回新长度 返回 11,值变HelloRedisdGETRANGE key start end取子串(含两端) 0 4→Hello;-5 -1→WorldSUBSTR key start end老版子串命令 Hello(已不推荐,用GETRANGE)LCS key1 key2最长公共子串(6.2+) LCS l1 l2(值为abcd与xycy)→bc;LCS l1 l2 LEN→2;LCS l1 l2 IDX→matches+ 下标数组- 自增的边界与报错,实测:
1
2INCR sN (sN 的值是 "abc")
(error) ERR value is not an integer or out of range SETRANGE是按下标覆盖而不是插入,所以Hello World从下标 5 写Redis得到HelloRedisd(原来的World被盖掉一部分)。STRALGO在 8.2 上已经彻底没有了:实测STRALGO LCS l1 l2报ERR unknown command 'STRALGO',COMMAND INFO STRALGO返回空。它是 6.2 被LCS取代的旧接口,老文章里的STRALGO LCS ...现在抄了就报错。SET key v NX失败时返回 nil 而不是 0,写脚本判断时要用「是否有回复」而不是「是否为 0」。- 单个值上限是 512 MB,实测依据是
CONFIG GET proto-max-bulk-len→536870912(正好 512×1024×1024)。超过就报ERR Protocol error: invalid bulk length。 - 键名没有长度硬上限,但要考虑内存与可读性;习惯上用
业务:实体:id这种分层命名,方便SCAN MATCH 业务:*。
- 自增的边界与报错,实测:
-
列表(List)
列表是按插入顺序排列的字符串序列,底层是双向链表结构(小列表用
listpack编码省内存,实测OBJECT ENCODING就是listpack),两端插入删除都是 O(1)。命令 使用描述 本机实测 RPUSH key v...尾部插入,返回新长度 3LPUSH key v...头部插入 head a b c→4LPOP key弹出头部 headRPOP key弹出尾部 cLLEN key长度 4LRANGE key start stop取区间, 0 -1是全部a bLINDEX key i按下标取 0→a;越界返回 nilLSET key i v按下标改 OK;越界报ERR index out of rangeLINSERT key BEFORE|AFTER pivot v在某元素前后插 4;pivot 不存在返回-1LREM key count v删等于 v 的元素 count=0删光 →3;count=1只删一个LTRIM key start stop原地裁剪 LTRIM tr 1 -1→b c d eRPOPLPUSH src dst原子搬运(旧) 把 s2从 src 移到 dstLMOVE src dst LEFT|RIGHT LEFT|RIGHT上面命令的替代(6.2+) s1BLPOP key... timeout阻塞式左弹 有数据返回 键 值;超时返回 nilBRPOP key... timeout阻塞式右弹 同上 BRPOPLPUSH src dst sec阻塞搬运(旧) 建议改 BLMOVELMPOP keynum key LEFT|RIGHT [COUNT n]一次弹多个(7.0+) 返回键名 + 元素 LPOS key element找元素下标 LPOS pos b→1LPOP key count一次弹多个 返回 first、bLREM的count语义要记牢:正数从头往尾找、负数从尾往头找、0是全删,绝对值是要删的个数。RPOPLPUSH、BRPOPLPUSH从 6.2 起被LMOVE/BLMOVE取代,仍然可用但属于旧写法。- 复杂度:
LPUSH/RPUSH/LPOP/RPOP是 O(1);LINDEX/LSET/LINSERT是 O(N),N 是到目标的距离,所以拿大列表当随机访问数组用会很慢。 BLPOP key 0是永久阻塞,脚本里几乎一定要给超时,否则连接会一直挂着。- 用 List 做队列的两种姿势:
LPUSH+BRPOP(先进先出);LPUSH+LPOP(栈)。要「可靠队列」用LMOVE配合一个 processing 列表,或者干脆上 Stream。
-
集合(Set)
集合是无序、成员唯一的字符串集合。重复添加不报错,但只算一个。
命令 使用描述 本机实测 SADD key m...添加,返回新增个数 SADD myset Hello Hello World→2SMEMBERS key全部成员 Hello WorldSISMEMBER key m是否成员 1/0SMISMEMBER key m...批量判断(6.0.6+) 1 0 0SCARD key成员数 2;键不存在 →0SREM key m...移除,返回删除数 1SPOP key [n]随机弹出 WorldSRANDMEMBER key [n]随机看但不删 c;n为负数时允许重复 →c b b d dSINTER k1 k2...交集 b cSUNION k1 k2...并集 a b d cSDIFF k1 k2...差集(在 k1 不在其余) aSINTERSTORE dst k1 k2...交集存到 dst 2,SMEMBERS SO→b cSUNIONSTORE dst k1 k2...并集存到 dst 4SDIFFSTORE dst k1 k2...差集存到 dst 1SINTERCARD numkeys k...只算交集大小(7.0+) 2SMOVE src dst m移动成员 1,src 变b c,dst 变b c d aSSCAN key cursor [MATCH p]增量遍历 SSCAN s2 0 COUNT 2→ 游标0+ 成员- 最容易踩的语法坑:
*STORE系列不带键数量参数,而*CARD系列要带。写错不报错,只会静悄悄给你 0:1
2
3SINTERSTORE SO SA SB -> (integer) 2 正确
SINTERSTORE SO 2 SA SB -> (integer) 0 错误:把名为 "2" 的键当成一个集合,它不存在,交集即为空
SINTERCARD 2 SA SB -> (integer) 2 正确:这里 2 是键个数- 我在这上面排查了很久(一度怀疑是自己编译的 Redis 有 bug),最后用
TYPE SO→none和EXISTS 2→0才定位到:那个2被当成了键名。记住「STORE 无 numkeys、CARD 有 numkeys」。
- 我在这上面排查了很久(一度怀疑是自己编译的 Redis 有 bug),最后用
- 交集为空时
SINTERSTORE不会创建目标键(实测TYPE是none);目标键已存在时会被结果整体覆盖。 - 集合是无序的,
SMEMBERS的返回顺序不保证稳定,别依赖它。 - 整数集合的编码阈值实测
set-max-intset-entries= 512,超过就退化成hashtable;列表的阈值参数名是list-max-listpack-size(实测-2,负数表示按hash-max-listpack-value反推),别去查不存在的list-max-listpack-entries。 - 大部分集合命令是 O(1)(
SADD/SISMEMBER/SPOP),但SMEMBERS、SUNION这类要返回全部成员的命令是 O(N),大集合上会阻塞主线程;跨集合运算优先用*STORE或SINTERCARD减少返回量。 - 典型用法:标签、共同好友(
SINTER)、抽奖去重(SADD返回值即「是否首次」)、随机抽人(SRANDMEMBER key -3允许重复时用SPOP)。
- 最容易踩的语法坑:
-
有序集合(Sorted Set)
有序集合每个成员带一个
float分数,按分数从小到大排序,成员仍然唯一。排行榜、延时队列、范围检索都靠它。命令 使用描述 本机实测 ZADD key score m [score m...]添加或更新分数 ZADD myz 1 one 2 two 3 three→3(新增数)ZSCORE key m取分数 1ZRANGE key start stop [WITHSCORES]按下标区间取 one two three;带分数时成对返回ZREVRANGE key start stop逆序 three two oneZRANGEBYSCORE key min max [WITHSCORES] [LIMIT o c]按分数区间(旧写法) 1 2→one two;(1表示开区间 →two threeZRANGE key min max BYSCORE上面命令的替代(6.2+) one twoZREVRANGEBYSCORE key max min分数区间逆序 参数是先大后小: +inf -inf→three two oneZRANGEBYLEX key min max分数相同时按字典序 [a[c→a b cZLEXCOUNT key min max字典区间成员数 3ZREM key m...删除成员 1ZINCRBY key increment m分数自增 2加2→4ZCARD key成员数 2;不存在 →0ZCOUNT key min max分数区间内的成员数 1 5→2ZRANK key m正序排名(从 0 开始) 1ZREVRANK key m逆序排名 0ZPOPMAX key/ZPOPMIN key弹出最高/最低 返回 成员 分数ZREMRANGEBYRANK key start stop按排名删 删 0 1→2,剩cZREMRANGEBYSCORE key min max按分数删 返回删除数 ZUNIONSTORE dst numkeys k... [WEIGHTS w...]并集存储(这里要 numkeys) 3,WEIGHTS 2 3时a的分数是1*2+10*3=32ZINTERSTORE dst numkeys k...交集存储 1,a→11ZADD key NX|XX|GT|LT [CH] score m条件更新 见下面说明 ZADD的四个条件标志实测:1
2
3
4ZADD myz NX 99 two -> 0 已存在就不新增,分数保持 4
ZADD myz XX 99 two -> 0 只更新已存在的,分数变 99
ZADD myz GT 1 two -> 0 新分数必须更大,1 < 99 所以不生效
ZADD myz GT CH 200 two -> 1 CH 让返回值统计「分数被改变的成员数」- 成员唯一,重复
ZADD同一个成员只改分数:ZADD zz 1 a再ZADD zz 5 a,结果只剩a 5。 - 分数是浮点,非数字直接报错:
ERR value is not a valid float。 ZREVRANGEBYSCORE的参数顺序是 max 在前、min 在后,这是最常被写错的一条(写反了会得到空结果而不是报错)。- 原笔记表里
ZLEXCOUNT的例子写成"["min" "["max",那是把占位符和引号混在一起了;正确写法是ZLEXCOUNT myz "[" "[~"这类字典区间([表示「从该字符串开始」,(表示「严格大于」,[str表示「以 str 开头的最大区间」)。 - 复杂度:
ZADD/ZREM/ZSCORE/ZRANK是 O(log N);ZRANGE、ZRANGEBYSCORE随返回条数增长,所以排行榜一定配LIMIT分页取。 - 小集合用
listpack编码(实测OBJECT ENCODING z1→listpack),超过zset-max-listpack-entries后转skiplist;实测本机zset-max-listpack-entries= 128、zset-max-listpack-value= 64。
-
哈希(Hash)
哈希是「一个键下面挂多个 field-value」,适合存对象,可以只读/只改其中某个字段。
命令 使用描述 本机实测 HSET key f v [f v...]设置字段,返回新建字段数 单字段 1,两字段2HGET key f取一个字段 Hello;不存在返回 nilHMSET key f v ...旧的多字段设置 OK(8.x 仍可用,但官方建议直接用HSET)HMGET key f...取多个字段 Hello World+ 缺失字段为空HGETALL key全部字段和值 f1 Hello f2 World f3 10 f4 20HKEYS key/HVALS key只取字段名 / 只取值 f1 f2 f3 f4/Hello World 10 20HLEN key字段数 4HEXISTS key f字段是否存在 1/0HDEL key f...删除字段 1HSTRLEN key f值的长度 5HSETNX key f v字段不存在才设 已存在 0,新字段1HINCRBY key f n字段整数自增 10加5→15HINCRBYFLOAT key f x浮点自增 15加0.5→15.5HGETDEL key FIELDS n f...取值并删字段(8.0+) v1,之后HEXISTS f1→0HGETEX key FIELDS n f... [EX s]取值并设过期(8.0+) v2HRANDFIELD key [n [WITHVALUES]]随机字段 f3;带值时成对返回HSCAN key cursor [MATCH p]增量遍历 HSCAN myhash 0→ 游标0+ 全部字段HGETDEL/HGETEX必须写FIELDS关键字,实测少写就报:1
2HGETDEL h f1 -> ERR wrong number of arguments for 'hgetdel' command
HGETDEL h FIELDS 1 f1 -> v1- 这两个是 8.0 新加的,网上多数资料还没写;它们和
GETEX/GETDEL的思路一样,只是作用在哈希字段上。
- 这两个是 8.0 新加的,网上多数资料还没写;它们和
HINCRBY对非数字值报错:ERR hash value is not an integer。- 字段和值都只能是字符串,想存数字再比较范围,用 ZSET 或把对象拆成多个键。
- 哈希在字段少时是
listpack编码(实测OBJECT ENCODING big→listpack),阈值实测hash-max-listpack-entries= 512、hash-max-listpack-value= 64;字段数超过它就转hashtable——实测连写 600 个字段后OBJECT ENCODING变成hashtable。 - 复杂度:
HSET/HGET/HDEL/HEXISTS是 O(1);HGETALL是 O(N),大哈希别反复全量取;HMGET是 O(N)(N 是请求字段数)。 - 一个常见误用:把整个用户对象存成 JSON 字符串塞进一个 string 键,改一个字段要整体读改写。用哈希就能只动一个 field,而且并发更安全(配合
WATCH或 Lua)。
-
其他值得知道的数据结构
- 位图(Bitmap):本质是 string,按位操作。
1
2
3
4SETBIT bm 7 1 -> 0 (返回该位原来的值)
GETBIT bm 7 -> 1
BITCOUNT bm -> 1
BITOP AND bc 2 ba bb -> 3 (返回结果长度)- 适合签到、在线状态、布尔标记,1 亿个标记只占约 12 MB。
- HyperLogLog:基数统计,固定 12 KB 左右,误差约 0.81%。
1
2PFADD pfc a b c d -> 1
PFCOUNT pfc -> 4 - GEO:基于 ZSET 的经纬度封装。
1
2GEOADD geo 116.4 39.9 beijing 121.5 31.2 shanghai -> 2
GEODIST geo beijing shanghai km -> 1071.5880 - Stream:追加型日志结构,支持消费者组、ACK、pending 查询,是 Redis 里唯一「消息有历史、可回溯」的类型。实测:
1
2
3
4
5
6XADD mystream * data hello name alice -> 1793719954991-0
XLEN mystream -> 1
XRANGE mystream - + -> 1793719954991-0 data hello name alice
XGROUP CREATE mystream g1 0 -> OK
XREADGROUP GROUP g1 c1 COUNT 1 STREAMS mystream > -> 该条目
XPENDING mystream g1 -> 1 条 pending,归属 c1- ID 格式是「毫秒时间戳-序列号」,
*让服务端生成,-/+是最小/最大,>表示「只读没投递过的」。
- ID 格式是「毫秒时间戳-序列号」,
- 发布/订阅:
SUBSCRIBE后PUBLISH能立刻收到,实测:1
2
3PUBLISH ch1 hello -> 1 (1 个订阅者收到)
订阅端输出 -> subscribe ch1 1 / message ch1 hello
PUBSUB CHANNELS -> ch1- 消息不落地:订阅者掉线期间的消息永久丢失。要可靠传递就用 Stream。
- vectorset(Redis 8 内置的向量集合):本机这份只编核心的实例里,
MODULE LIST只有一个内置模块vectorset(ver 1),它带 12 条命令,实测COMMAND LIST FILTERBY MODULE vectorset:1
VREM VGETATTR VCARD VSIM VADD VINFO VEMB VISMEMBER VSETATTR VLINKS VRANDMEMBER VDIM
- 里面没有
VRANGE,按ZADD/ZRANGE的习惯写VRANGE会直接报 unknown command。 - 8.2 的
VADD语法写在源码modules/vector-sets/README.md里:VADD key [REDUCE dim] FP32|VALUES vector element [CAS] [NOQUANT | Q8 | BIN],实测能跑的形式是:1
2
3
4
5
6
7
8
9VADD mykey VALUES 3 0.1 1.2 0.5 my-element -> (integer) 1
VADD mykey VALUES 3 0.1 1.1 0.5 second -> (integer) 1
VCARD mykey -> 3
VDIM mykey -> 3
VSIM mykey ELE my-element -> my-element second third
VSIM mykey ELE my-element WITHSCORES COUNT 2 -> my-element 1 second 1
VISMEMBER mykey second -> 1
VREM mykey third -> 1
VINFO mykey -> quant-type int8 hnsw-m 16 vector-dim 3 projection-input-dim 0 size 3 max-level 1 attributes-count 0 - 8.10 文档里那种
ALGORITHM ... dim ... label ...写法在 8.2 上报ERR invalid vector specification;FP32段要求的是打包好的二进制浮点串,写成FP32 3 0.1 1.2 0.5 e1同样报invalid vector specification,日常一律用VALUES。 - 属性与过滤都能用,实测(三个元素里只给
beta、gamma挂属性):注意1
2
3
4
5
6
7
8VSETATTR vv beta {"year":1950} -> (integer) 1
VSETATTR vv gamma {"year":2001} -> (integer) 1
VGETATTR vv beta -> {"year":1950}
VSIM vv ELE alpha -> alpha beta gamma
VSIM vv ELE alpha FILTER .year > 1900 -> beta gamma
VSIM vv ELE alpha FILTER .year > 1990 -> gamma
VSIM vv ELE alpha FILTER .year > 1900 WITHATTRIBS -> beta {"year":1950} gamma {"year":2001}
VSIM vv ELE alpha WITHATTRIBS -> alpha (空) beta {"year":1950} gamma {"year":2001}FILTER会把「没有该属性的元素」也排除掉(alpha没挂属性,所以两个过滤结果里都没有它),不是「属性不满足才排除」。表达式就是简单的算术/比较写法,不用 JSONPath 前缀也行。 - 踩坑记录(值得记住,因为它会让人误判功能不存在):我第一版测试把这条命令写成
... FILTER ".year > 1900"放在双引号的命令替换里,>被外层 shell 当成了重定向,输出跑到了一个名叫1900"的文件里,屏幕上什么都不显示,于是我在文章里写下「FILTER 实测返回空、别照抄」——那是测试脚本的 bug,不是 Redis 的问题。改用单引号包住表达式重测,结果如上。
- 里面没有
- JSON / 搜索 / 时序 / 概率型结构(Redis 8 的「模块包」):这些在官方源码树里是
modules/下的独立子项目(redisjson redistimeseries redisbloom redisearch),只有加BUILD_WITH_MODULES=yes才会编;本机按默认方式编出来的实例没有它们,实测:1
2
3
4
5
6
7JSON.SET -> ERR unknown command 'JSON.SET'
FT.CREATE -> ERR unknown command 'FT.CREATE'
TS.ADD -> ERR unknown command 'TS.ADD'
BF.ADD -> ERR unknown command 'BF.ADD'
CF.ADD -> ERR unknown command 'CF.ADD'
CMS.INITBYPROB -> ERR unknown command 'CMS.INITBYPROB'
TOPK.RESIZE -> ERR unknown command 'TOPK.RESIZE'- 要用它们就
make BUILD_WITH_MODULES=yes(得先有 Rust 工具链,modules/Makefile里有install-rust),或者直接用官方 Docker 镜像redis:8。看到「Redis 8 内置 JSON」这类说法时先COMMAND LIST FILTERBY MODULE <模块名>确认自己这套有没有。
- 要用它们就
- 位图(Bitmap):本质是 string,按位操作。
-
事务与脚本(TCL 在 Redis 里的样子)
MULTI把后续命令排进队列,EXEC一次性顺序执行,中间不会被别的客户端插入。- 但它不是关系数据库的事务,两种错误行为完全不同,实测:
- 入队时就有语法错误(命令不存在)→ 整个事务被丢弃:
1
2
3
4
5MULTI / INCR okcounter / NOSUCHCOMMAND x / EXEC
QUEUED
(error) ERR unknown command 'NOSUCHCOMMAND', with args beginning with: 'x'
(error) EXECABORT Transaction discarded because of previous errors.
GET okcounter -> nil 一条都没执行 - 运行期错误(类型不对)→ 只有那一条失败,其余照常执行:
1
2
3
4
5
6MULTI / INCR strkey / SET strkey world / EXEC
QUEUED
QUEUED
1) (error) ERR value is not an integer or out of range
2) OK
GET strkey -> world SET 生效了 - 所以 Redis 事务没有回滚。要「要么全成要么全不做」,得用 Lua 脚本。
- 入队时就有语法错误(命令不存在)→ 整个事务被丢弃:
WATCH key提供 CAS:被监视的键在EXEC前变了,事务就返回 nil 不执行;用完要UNWATCH或重连。- Lua 脚本整体原子执行,实测:
1
2EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 evalkey hello -> OK
GET evalkey -> helloEVAL的第一个参数是脚本,第二个是 key 个数,之后KEYS[...]、ARGV[...]。脚本里只能用redis.call/pcall,不能做循环无上限的扫描——它同样会卡住主线程。
- 分布式锁的最小可用写法:解锁要用 Lua 判断「值是自己才删」,否则可能删掉别人的锁。
1
SET lock:order:1001 <随机值> NX PX 30000
-
简介
- Python 侧最常用的是
redis-py(导入名是redis),它同时提供同步客户端redis.Redis和异步客户端redis.asyncio。 - 本机实测版本
redis-py 8.1.0,连的是127.0.0.1:6379上开了requirepass的 Redis 8.2.8。 - 另一个常见库
aioredis已经停止维护,异步用法直接改用redis.asyncio(同一个包里)。 - 最容易踩的第一个坑是返回的是 bytes:默认
decode_responses=False,所有值都是bytes。1
2
3
4
5import redis
r = redis.Redis(host='127.0.0.1', port=6379, db=0, password='你的密码')
r.set('b', '中文')
print(r.get('b')) # b'\xe4\xb8\xad\xe6\x96\x87'- 想直接拿到
str,在构造时加decode_responses=True:1
2
3r2 = redis.Redis(host='127.0.0.1', port=6379, db=0,
password='你的密码', decode_responses=True)
print(r2.get('b')) # 中文
- 想直接拿到
- Python 侧最常用的是
-
安装
1
pip install redis
- 需要异步能力时同一个包就有:
import redis.asyncio as aioredis。 - 网络慢时加镜像:
pip install -i https://mirrors.ustc.edu.cn/pypi/web/simple redis。
- 需要异步能力时同一个包就有:
-
连接到 Redis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37import redis
# 创建 Redis 对象(password 对应服务端的 requirepass)
r = redis.Redis(host='localhost', port=6379, db=0,
password='你的密码', decode_responses=True)
# 字符串
r.set('foo', 'bar')
print(r.get('foo')) # bar
# 带过期时间的写入:EX 是秒,PX 是毫秒,NX 是「只在不存在时写」
r.set('session:42', 'token', ex=60)
print(r.ttl('session:42')) # 60(紧接着读是 60,隔 1 秒再读就是 59)
print(r.set('session:42', 'x', nx=True)) # None(键已存在,没写进去)
# 哈希
r.hset('user:1', mapping={'name': 'Alice', 'age': 30})
print(r.hget('user:1', 'name')) # Alice
print(r.hgetall('user:1')) # {'name': 'Alice', 'age': '30'}
# 列表
r.lpush('numbers', 1, 2, 3)
print(r.rpop('numbers')) # 1
print(r.lrange('numbers', 0, -1)) # ['3', '2']
# 集合
r.sadd('fruits', 'apple', 'banana', 'cherry')
print(r.sismember('fruits', 'banana')) # 1(是整数,不是 True)
print(sorted(r.smembers('fruits'))) # ['apple', 'banana', 'cherry']
# 有序集合
r.zadd('scores', {'Alice': 50, 'Bob': 30, 'Charlie': 70})
print(r.zrange('scores', 0, -1, withscores=True))
# [('Bob', 30.0), ('Alice', 50.0), ('Charlie', 70.0)]
# 清理
r.delete('foo', 'session:42', 'user:1', 'numbers', 'fruits', 'scores')本机实跑输出(
smembers我改成排序后打印,其余逐字一致):1
2
3
4
5
6
7
8
9
10bar
60
None
Alice
{'name': 'Alice', 'age': '30'}
1
['3', '2']
1
['apple', 'banana', 'cherry']
[('Bob', 30.0), ('Alice', 50.0), ('Charlie', 70.0)]- 四个要点:
sismember返回的是1/0整数而不是布尔值——redis-py 8.1.0 里它的类型标注就是Literal[0] | Literal[1],实测type(r.sismember(...))是int。想当布尔用得自己bool(...)。hgetall里数字30回来是字符串'30'——Redis 存的就是字符串,需要自己转换。lpush('numbers', 1, 2, 3)是从头依次压入,所以列表变成[3, 2, 1],rpop弹出的是1。set(..., nx=True)失败时返回None而不是False,判空要用is None。
- 原笔记里
r.hmset(user_id, mapping)的写法:hmset在 redis-py 里已废弃,实测调用会告警:直接用1
DeprecationWarning: Call to deprecated hmset. (Use 'hset' instead.)
hset(name, mapping=...)即可(上面示例就是这么写的)。 - 密码错时的异常类型,实测:对非数字值执行
1
redis.exceptions.AuthenticationError: invalid username-password pair or user is disabled.
incr则是redis.exceptions.ResponseError: value is not an integer or out of range。异常层级都在redis.RedisError下面(ConnectionError、AuthenticationError、DataError、TimeoutError等),捕获时按RedisError兜底最省事。
- 四个要点:
-
连接池
1
2
3
4
5
6
7
8
9
10
11
12import redis
# 创建连接池
pool = redis.ConnectionPool(host='localhost', port=6379, db=0,
password='你的密码', decode_responses=True,
max_connections=10)
# 使用连接池创建 Redis 对象
r = redis.Redis(connection_pool=pool)
r.set('language', 'Python')
print(r.get('language')) # PythonConnectionPool的常用参数:max_connections(上限,超了会等或抛MaxConnectionsError)、timeout/socket_connect_timeout、socket_keepalive。- 多个
Redis实例共用同一个池时连接是复用的,实测两个实例共享一个max_connections=4的池:1
2
3
4
5a = redis.Redis(connection_pool=pool)
b = redis.Redis(connection_pool=pool)
a.set('shared', '1')
print(b.get('shared')) # 1
print(len(pool._available_connections)) # 1 - 实际项目里更常见的做法是不手写池对象,直接
redis.Redis(...):它内部本来就会建一个ConnectionPool,一个Redis实例就是一个池。真正需要显式传池的场景是「多个实例(比如带不同decode_responses)要共享同一批连接」。 - 连接池不需要手动关闭,程序退出时随之释放;要主动断开可以
pool.disconnect()(比如 fork 出子进程之后必须重建连接,否则会共用同一个 socket)。 - 常用构造参数一览(实测
inspect.signature的前几个):host、port、db、password、socket_timeout、socket_connect_timeout、socket_read_size、socket_keepalive、connection_pool、unix_socket_path、encoding、decode_responses、retry_on_timeout、retry、retry_on_error。
-
使用管道 (pipeline)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import redis
r = redis.Redis(host='localhost', port=6379, db=0,
password='你的密码', decode_responses=True)
# 创建管道对象
pipe = r.pipeline()
# 把多个命令攒起来
pipe.set('author', 'Hemingway')
pipe.get('author')
pipe.incr('books_count')
pipe.incr('books_count')
pipe.decr('books_count')
# execute() 一次性发送,返回值按命令顺序组成列表
results = pipe.execute()
print(results) # [True, 'Hemingway', 1, 2, 1]本机实跑输出:
1
[True, 'Hemingway', 1, 2, 1]
- 管道解决的是往返次数问题:五条命令本来要 5 次网络往返,用管道合成 1 次。它不保证原子性,除非用事务模式。
execute()之前,命令一条都没发出去;所以中途print是看不到结果的。- 事务模式(默认就是
transaction=True)会把命令包进MULTI/EXEC:1
2
3
4
5pipe = r.pipeline(transaction=True)
pipe.set('novel', 'For Whom the Bell Tolls')
pipe.get('novel')
pipe.incr('chapters_count')
print(pipe.execute())1
[True, 'For Whom the Bell Tolls', 1]
- 实测
transaction=False与transaction=True在成功路径上返回内容一样,区别在于True时这批命令之间不会被其他客户端插入。 - 空管道
execute()返回[],不报错。
- 实测
- 用完记得
pipe.reset()或让它自然回收;with r.pipeline() as pipe:是更稳的写法(异常时也会释放连接)。 - 想「读-改-写」的一致性用
WATCH:1
2
3
4
5
6
7
8
9with r.client() as conn:
conn.watch('books_count')
val = int(conn.get('books_count'))
pipe = conn.pipeline()
pipe.set('books_count', val + 1)
try:
pipe.execute() # 期间 books_count 被改过则返回空列表
except redis.WatchError:
... # 重试
-
异步客户端
1
2
3
4
5
6
7
8
9
10
11import asyncio
import redis.asyncio as aioredis
async def main():
r = aioredis.Redis(host='127.0.0.1', port=6379,
password='你的密码', decode_responses=True)
await r.set('k', 'v')
print(await r.get('k'))
await r.aclose()
asyncio.run(main())- 模块存在性实测:
redis.asyncio可导入且有Redis类(redis-py 8.1.0)。 - 注意异步客户端要
await,关闭用aclose();aioredis那个第三方包已经废弃,别再pip install aioredis。
- 模块存在性实测:
使用工具远程连接Redis
- 想用图形工具(Redis Insight、Another Redis Desktop Manager、Navicat 等)从另一台机器连过来,得改三处配置,只改
bind是不够的。 - 改配置文件
/etc/redis/redis.conf:1
2bind 0.0.0.0
protected-mode nobind从默认的127.0.0.1 -::1改成0.0.0.0才会监听所有网卡,实测本机默认值是:1
127.0.0.1 -::1
protected-mode yes的作用:没有设密码、也没有显式bind时,拒绝来自非回环地址的连接。所以要么关掉它,要么(更好)保留它并设requirepass。- 也可以运行时热改,但记得
CONFIG REWRITE固化:1
2redis-cli -a '你的密码' CONFIG SET bind "0.0.0.0"
redis-cli -a '你的密码' CONFIG REWRITE- 实测
CONFIG SET bind 0.0.0.0返回OK且立刻生效;改回127.0.0.1 -::1同样立即生效,ss -ltnp里能看到监听地址随之变化。
- 实测
- 改完重启服务:
1
sudo systemctl restart redis-server
- 服务名按安装方式而定(发行版包是
redis-server,自建单元可能叫redis)。
- 服务名按安装方式而定(发行版包是
- 验证监听是否已经放开:
1
ss -ltnp | grep 6379
1
LISTEN 0 511 0.0.0.0:6379 0.0.0.0:* users:(("redis-server",pid=11605,fd=8))
- 客户端侧连接:
1
redis-cli -h <服务器IP> -p 6379 -a '你的密码'
1
2<服务器IP>:6379> PING
PONG - 安全提醒(这条比配置本身重要):
- 不要为了省事把 Redis 裸露在公网。历史上大量 Redis 被入侵,攻击路径就是「公网 + 无密码」:用
CONFIG SET dir把 RDB 写到/var/spool/cron/或~/.ssh/authorized_keys,从而拿到主机的命令执行。 - 真需要远程访问时,按优先级选一种:
- 只绑内网地址 + 设
requirepass+ 用 ACL 限制命令和 key 范围。 - 走 SSH 隧道:
ssh -L 16379:127.0.0.1:6379 user@服务器,然后连本地16379,Redis 完全不用改bind。 - 上 TLS(需要编译时开启,本机这份
tls-port是0,说明没编进去)。
- 只绑内网地址 + 设
- 另外把危险命令收掉:
rename-command KEYS ""、rename-command CONFIG "",或者直接用 ACL 的+@read -@dangerous。
- 不要为了省事把 Redis 裸露在公网。历史上大量 Redis 被入侵,攻击路径就是「公网 + 无密码」:用
- 图形工具(可选):Redis Desktop Manager 的下载与激活说明见文末参考。