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 特点

  1. 基于内存操作:数据常驻内存,读写是微秒级;也正因如此,容量受内存限制,数据集要能装进内存。
  2. 支持多种数据结构:字符串(string)、列表(list)、集合(set)、有序集合(zset)、哈希(hash)是五种基本类型;在此之上还有位图(bitmap,其实是 string)、HyperLogLog(也是 string)、地理空间(GEO,是 zset 的封装)、流(Stream);Redis 8 又把 JSON、搜索、时序、概率型结构等能力逐步并入。
  3. 数据持久化: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/)。
  4. 单线程命令执行:命令是一个一个串行执行的,所以单条命令天然原子,不需要加锁。这也是「Redis 快」的原因——省掉了锁竞争和上下文切换,但代价是一条慢命令会卡住所有人。
  5. 事件驱动 + IO 多线程:命令执行仍是单线程,但网络读写可以交给多个 IO 线程。实测本机 io-threads 默认是 1(等于不启用),要提速大流量场景才把它调到 2~8 之间;Redis 8 的 INFO 里还多了一个独立的 # Threads 统计段。
  6. 高度可配置:redis.conf 里有几百个参数,可以在持久性、内存上限、淘汰策略之间做权衡。
  7. 发布/订阅:SUBSCRIBE/PUBLISH 走的是「尽力而为、不存历史」的频道消息;要可靠消息用 Stream。
  8. 高可用与自动分片:Sentinel(哨兵)负责故障转移,Cluster 负责把 key 分到 16384 个哈希槽上。注意 Cluster 模式下只有 db0,SELECT 会被拒。
  9. 广泛的语言支持:官方与各社区驱动覆盖 Java、Python、Go、Node.js、C#、PHP 等。
  10. 事务:MULTI/EXEC/WATCH 把多条命令打包后顺序执行,中途不会被别的客户端插入;但它不是关系数据库那种事务——命令入队时的语法错误会让整个 EXEC 失败,而运行期错误(如对字符串执行 INCR)只会让那一条失败,其余照常执行。
  11. Lua 脚本:EVAL 把一段脚本作为整体原子执行,适合「读-判断-写」这种需要一致性的复合操作。
  12. 安全性:requirepass 做简单密码认证;6.0 起有 ACL,能按用户配「能跑哪些命令、能碰哪些 key」。原笔记写的「基于角色的访问控制(RBAC)」不够准确——Redis 的 ACL 是「用户 + 命令/键模式」授权模型,不是角色体系。TLS 需要编译时开启(本机这份没开,实测 tls-port 为 0)。
  13. 简单实用:装起来就一个二进制,跑起来就一个端口,命令直觉化,适合快速上手。
  • 顺带一个能体现「单线程」的实验:SETBIT/GETBIT/BITCOUNT 这类操作在 100 万条 INCR 压测下不会互相踩踏,因为所有写命令都在同一个线程里排队。实测 PFADD pfc a b c d → 1、PFCOUNT pfc → 4,HyperLogLog 的基数统计也是原子更新的。
  • Ubuntu / Debian(发行版自己的仓库)

    1
    2
    3
    4
    5
    sudo 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-server
    1
    2
    3
    4
    redis-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),这点在写部署脚本时容易踩。
  • 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
    4
    wget 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.8
    1
    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-server 19,215,112 字节、src/redis-cli 7,811,640 字节,make install 之后 /usr/local/bin 里是这六个名字(redis-check-aof/redis-check-rdb/redis-sentinel 只是指向 redis-server 的软链)。
      • make build redis 在 8.2 上根本不存在,照抄会报:
        1
        2
        make[1]: *** No rule to make target 'build'.  Stop.
        make: *** [Makefile:11: build] Error 2
        build 是 8.4 之后新构建脚本才有的目标,所以网上「Redis 8 编译必须用 make build」的文章只适用于 8.4+,写文章的人多半在 8.10 上试的。
      • 为什么默认不编模块,顶层 Makefile 全文就十几行,实测原文:
        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 $@; done
        模块是开关式的:要连 JSON/时序/搜索/布隆一起编得写 make 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)。
    • 装到系统里:
      1
      2
      sudo make install
      which redis-server redis-cli
      1
      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
2
3
brew install redis
brew services start redis
redis-cli ping
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 的新类型。
  • 推荐两条路:
    1. WSL(本文用的就是这条):在 WSL2 的 Ubuntu 里按上面 Linux 的方式装,Windows 侧通过 127.0.0.1:6379 直连(WSL2 会自动做 localhost 端口转发,实测 Windows 上 redis-cli -h 127.0.0.1 -p 6379 能通)。
    2. Docker:
      1
      2
      docker run -d --name redis -p 6379:6379 redis:8
      docker exec -it redis redis-cli ping
  • 只在本地学习用 .exe 的话,注意它和 Linux 版在持久化路径、权限模型上都不一样,别拿它验证生产行为。
  • 默认监听 127.0.0.1:6379,本机实测:

    1
    ss -ltnp | grep 6379
    1
    2
    LISTEN 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
    2
    127.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。
  • 非交互地跑一条命令(脚本里最常用):

    1
    2
    3
    redis-cli -h 127.0.0.1 -p 6379 PING
    redis-cli SET mykey "Hello"
    redis-cli GET mykey
    1
    2
    3
    PONG
    OK
    Hello
    • 注意 redis-cli SET mykey "Hello" 这种「一条命令一个进程」的方式每次都会新建连接,批量操作用 redis-cli -x、--pipe 或直接进交互模式。
  • 检查连通性的三个层次,实测都可用:

    1
    2
    3
    redis-cli PING                 # 服务活着
    redis-cli INFO server | head -3 # 能拿到运行时信息
    redis-cli --latency # 按住 Ctrl-C 结束,看往返延迟
    1
    2
    3
    4
    PONG
    # 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
    4
    127.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 密码

  • 设置密码

    1. 找到配置文件。本机是 /etc/redis/redis.conf(源码安装时也可以把配置放在安装目录里)。
    2. 找到 # requirepass foobared 这一行,去掉注释并换成强密码:
      1
      requirepass <你的强密码>
      • 密码强度由你自己保证,Redis 不会校验(requirepass "" 等于关闭认证)。
      • 注意:requirepass 在配置文件里是明文,CONFIG GET requirepass 也能读到它。所以配置文件权限要收紧(本机 /etc/redis/redis.conf 属主 redis),也别把它提交进仓库。
    3. 让改动生效——两种办法:
      1
      sudo systemctl restart redis-server
      或者不重启、运行时热改(只影响当前进程,重启后回到配置文件):
      1
      2
      redis-cli -a '旧密码' CONFIG SET requirepass '新密码'
      redis-cli -a '新密码' CONFIG REWRITE
      • CONFIG REWRITE 会把当前运行时配置写回配置文件,是「热改之后固化」的标准动作。
    4. 用 systemd 管理时,服务名按安装方式不同:发行版包是 redis-server,本机自建单元叫 redis。实测两者都可用:
      1
      2
      sudo systemctl restart redis-server
      sudo service redis-server restart
      • service xxx restart 在没有 systemd 的老系统上才走 /etc/init.d/;有 systemd 时它只是转发给 systemctl。
  • 通过密码连接

    • 命令行带 -a:
      1
      2
      redis-cli -a mySecurePassword
      redis-cli -h <hostname> -p <port> -a mySecurePassword
      • 每次都会提示一句安全告警,这是正常的:
        1
        Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
        因为密码会出现在 shell 历史和 ps 的进程列表里。脚本里加 --no-auth-warning 只是闭嘴,不解决泄露问题。
      • 更好的做法是用环境变量,ps 里看不到:
        1
        REDISCLI_AUTH='mySecurePassword' redis-cli PING
    • 不设 -a,先连再认证(原笔记推荐的方式,实测可行):
      1
      2
      127.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
    3
    redis-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
    3
    OK
    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
      3
      ERR 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 文件或 SSH authorized_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 探活 PONG
    SELECT n 切到第 n 个逻辑库 OK,提示符变 [(15)]
    DBSIZE 当前库的键数量 (integer) 0
    INFO [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 看值的类型 string
    EXISTS key [key...] 存在个数(不是布尔) 单个存在 1,EXISTS k1 k2 → 2,不存在 0
    DEL key [key...] 删除,返回删掉的个数 删掉 1,删不存在的键返回 0(不报错)
    UNLINK key 异步删除(后台释放内存) 1
    EXPIRE key seconds 设过期 1(成功)/ 0(键不存在)
    PEXPIRE key ms 毫秒版过期 1
    TTL key 剩余秒数 有 TTL → 60;无 TTL → -1;键不存在 → -2
    PERSIST key 取消过期 1,之后 TTL 变 -1
    RENAME old new 改名 OK
    MOVE key db 搬到另一个库 1
    COPY src dst 复制键 1
    KEYS pattern 按模式找键 KEYS k* → k1 k2
    SCAN cursor [MATCH p] [COUNT n] 增量遍历 SCAN 0 MATCH sk* COUNT 10 → 游标 0 + sk1
    RANDOMKEY 随机取一个键名 k2
    OBJECT ENCODING key 看底层编码 实测:整数 int、短字符串 embstr、列表/哈希/小有序集合 listpack、纯整数集合 intset、大有序集合 skiplist、超阈值的大哈希 hashtable
    SAVE / BGSAVE 落 RDB 快照 OK / Background saving started
    LASTSAVE 上次快照的 Unix 时间戳 (integer) 1793731280(每次快照后变大,可用来确认 BGSAVE 真的完成了)
    FLUSHDB / FLUSHALL 清空当前库 / 所有库 OK
    COMMAND / COMMAND COUNT 命令元信息 实测 COMMAND COUNT → 267;COMMAND LIST 有 397 条(含子命令,所以比 267 多);COMMAND LIST FILTERBY MODULE vectorset → 12 条
    CONFIG GET/SET 参数 读写配置 CONFIG GET databases → 16
    CLIENT 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 设置值 OK
    GET key 取值 value;不存在时是 nil(显示空)
    DEL key 删除键 1
    INCR key 整数 +1 11
    DECR key 整数 -1 15
    INCRBY key n 加 n 16
    DECRBY key n 减 n 13
    INCRBYFLOAT key f 浮点增量 1.5 加 0.25 → 1.75
    APPEND key str 追加,返回新长度 Hello + World → 11
    STRLEN key 值的字节长度 11
    MSET k1 v1 k2 v2 批量设置 OK
    MGET k1 k2 ... 批量取值 1 2 3(缺失的键位置是空)
    MSETNX ... 全部不存在才设置 0(因为 a 已存在)
    SETEX key s v 设置并给 TTL OK,TTL → 60
    PSETEX key ms v 毫秒版 OK,TTL → 60
    SETNX key v 不存在才设置 0(键已存在)
    GETSET key v 设新值并返回旧值 返回 value,之后 GET → newval
    GETDEL key 取值并删除(6.2+) 返回值本身(实测 gv),之后 EXISTS → 0
    GETEX key EX n 取值同时改 TTL(6.2+) 返回值本身(实测 gv2),TTL 变成指定秒数
    SET key v NX 仅当不存在 首次 OK,重复时返回空(nil)
    SET key v XX 仅当已存在 OK
    SET key v KEEPTTL 覆盖值但保留 TTL OK,TTL 仍是 100
    SETRANGE key off str 从下标覆盖,返回新长度 返回 11,值变 HelloRedisd
    GETRANGE key start end 取子串(含两端) 0 4 → Hello;-5 -1 → World
    SUBSTR 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
      2
      INCR 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... 尾部插入,返回新长度 3
    LPUSH key v... 头部插入 head a b c → 4
    LPOP key 弹出头部 head
    RPOP key 弹出尾部 c
    LLEN key 长度 4
    LRANGE key start stop 取区间,0 -1 是全部 a b
    LINDEX key i 按下标取 0 → a;越界返回 nil
    LSET key i v 按下标改 OK;越界报 ERR index out of range
    LINSERT key BEFORE|AFTER pivot v 在某元素前后插 4;pivot 不存在返回 -1
    LREM key count v 删等于 v 的元素 count=0 删光 → 3;count=1 只删一个
    LTRIM key start stop 原地裁剪 LTRIM tr 1 -1 → b c d e
    RPOPLPUSH src dst 原子搬运(旧) 把 s2 从 src 移到 dst
    LMOVE src dst LEFT|RIGHT LEFT|RIGHT 上面命令的替代(6.2+) s1
    BLPOP key... timeout 阻塞式左弹 有数据返回 键 值;超时返回 nil
    BRPOP key... timeout 阻塞式右弹 同上
    BRPOPLPUSH src dst sec 阻塞搬运(旧) 建议改 BLMOVE
    LMPOP keynum key LEFT|RIGHT [COUNT n] 一次弹多个(7.0+) 返回键名 + 元素
    LPOS key element 找元素下标 LPOS pos b → 1
    LPOP key count 一次弹多个 返回 first、b
    • LREM 的 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 → 2
    SMEMBERS key 全部成员 Hello World
    SISMEMBER key m 是否成员 1 / 0
    SMISMEMBER key m... 批量判断(6.0.6+) 1 0 0
    SCARD key 成员数 2;键不存在 → 0
    SREM key m... 移除,返回删除数 1
    SPOP key [n] 随机弹出 World
    SRANDMEMBER key [n] 随机看但不删 c;n 为负数时允许重复 → c b b d d
    SINTER k1 k2... 交集 b c
    SUNION k1 k2... 并集 a b d c
    SDIFF k1 k2... 差集(在 k1 不在其余) a
    SINTERSTORE dst k1 k2... 交集存到 dst 2,SMEMBERS SO → b c
    SUNIONSTORE dst k1 k2... 并集存到 dst 4
    SDIFFSTORE dst k1 k2... 差集存到 dst 1
    SINTERCARD numkeys k... 只算交集大小(7.0+) 2
    SMOVE src dst m 移动成员 1,src 变 b c,dst 变 b c d a
    SSCAN key cursor [MATCH p] 增量遍历 SSCAN s2 0 COUNT 2 → 游标 0 + 成员
    • 最容易踩的语法坑:*STORE 系列不带键数量参数,而 *CARD 系列要带。写错不报错,只会静悄悄给你 0:
      1
      2
      3
      SINTERSTORE 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」。
    • 交集为空时 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 取分数 1
    ZRANGE key start stop [WITHSCORES] 按下标区间取 one two three;带分数时成对返回
    ZREVRANGE key start stop 逆序 three two one
    ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT o c] 按分数区间(旧写法) 1 2 → one two;(1 表示开区间 → two three
    ZRANGE key min max BYSCORE 上面命令的替代(6.2+) one two
    ZREVRANGEBYSCORE key max min 分数区间逆序 参数是先大后小:+inf -inf → three two one
    ZRANGEBYLEX key min max 分数相同时按字典序 [a [c → a b c
    ZLEXCOUNT key min max 字典区间成员数 3
    ZREM key m... 删除成员 1
    ZINCRBY key increment m 分数自增 2 加 2 → 4
    ZCARD key 成员数 2;不存在 → 0
    ZCOUNT key min max 分数区间内的成员数 1 5 → 2
    ZRANK key m 正序排名(从 0 开始) 1
    ZREVRANK key m 逆序排名 0
    ZPOPMAX key / ZPOPMIN key 弹出最高/最低 返回 成员 分数
    ZREMRANGEBYRANK key start stop 按排名删 删 0 1 → 2,剩 c
    ZREMRANGEBYSCORE key min max 按分数删 返回删除数
    ZUNIONSTORE dst numkeys k... [WEIGHTS w...] 并集存储(这里要 numkeys) 3,WEIGHTS 2 3 时 a 的分数是 1*2+10*3=32
    ZINTERSTORE dst numkeys k... 交集存储 1,a → 11
    ZADD key NX|XX|GT|LT [CH] score m 条件更新 见下面说明
    • ZADD 的四个条件标志实测:
      1
      2
      3
      4
      ZADD 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,两字段 2
    HGET key f 取一个字段 Hello;不存在返回 nil
    HMSET key f v ... 旧的多字段设置 OK(8.x 仍可用,但官方建议直接用 HSET)
    HMGET key f... 取多个字段 Hello World + 缺失字段为空
    HGETALL key 全部字段和值 f1 Hello f2 World f3 10 f4 20
    HKEYS key / HVALS key 只取字段名 / 只取值 f1 f2 f3 f4 / Hello World 10 20
    HLEN key 字段数 4
    HEXISTS key f 字段是否存在 1 / 0
    HDEL key f... 删除字段 1
    HSTRLEN key f 值的长度 5
    HSETNX key f v 字段不存在才设 已存在 0,新字段 1
    HINCRBY key f n 字段整数自增 10 加 5 → 15
    HINCRBYFLOAT key f x 浮点自增 15 加 0.5 → 15.5
    HGETDEL key FIELDS n f... 取值并删字段(8.0+) v1,之后 HEXISTS f1 → 0
    HGETEX key FIELDS n f... [EX s] 取值并设过期(8.0+) v2
    HRANDFIELD key [n [WITHVALUES]] 随机字段 f3;带值时成对返回
    HSCAN key cursor [MATCH p] 增量遍历 HSCAN myhash 0 → 游标 0 + 全部字段
    • HGETDEL / HGETEX 必须写 FIELDS 关键字,实测少写就报:
      1
      2
      HGETDEL h f1        -> ERR wrong number of arguments for 'hgetdel' command
      HGETDEL h FIELDS 1 f1 -> v1
      • 这两个是 8.0 新加的,网上多数资料还没写;它们和 GETEX/GETDEL 的思路一样,只是作用在哈希字段上。
    • 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
      4
      SETBIT 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
      2
      PFADD pfc a b c d -> 1
      PFCOUNT pfc -> 4
    • GEO:基于 ZSET 的经纬度封装。
      1
      2
      GEOADD 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
      6
      XADD 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 格式是「毫秒时间戳-序列号」,* 让服务端生成,-/+ 是最小/最大,> 表示「只读没投递过的」。
    • 发布/订阅:SUBSCRIBE 后 PUBLISH 能立刻收到,实测:
      1
      2
      3
      PUBLISH 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
        9
        VADD 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
        8
        VSETATTR 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
      7
      JSON.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 <模块名> 确认自己这套有没有。
  • 事务与脚本(TCL 在 Redis 里的样子)

    • MULTI 把后续命令排进队列,EXEC 一次性顺序执行,中间不会被别的客户端插入。
    • 但它不是关系数据库的事务,两种错误行为完全不同,实测:
      • 入队时就有语法错误(命令不存在)→ 整个事务被丢弃:
        1
        2
        3
        4
        5
        MULTI / 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
        6
        MULTI / 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
      2
      EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 evalkey hello -> OK
      GET evalkey -> hello
      • EVAL 的第一个参数是脚本,第二个是 key 个数,之后 KEYS[...]、ARGV[...]。脚本里只能用 redis.call/pcall,不能做循环无上限的扫描——它同样会卡住主线程。
    • 分布式锁的最小可用写法:
      1
      SET lock:order:1001 <随机值> NX PX 30000
      解锁要用 Lua 判断「值是自己才删」,否则可能删掉别人的锁。
  • 简介

    • 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
      5
      import 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
        3
        r2 = redis.Redis(host='127.0.0.1', port=6379, db=0,
        password='你的密码', decode_responses=True)
        print(r2.get('b')) # 中文
  • 安装

    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
    37
    import 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
    10
    bar
    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
    12
    import 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')) # Python
    • ConnectionPool 的常用参数:max_connections(上限,超了会等或抛 MaxConnectionsError)、timeout/socket_connect_timeout、socket_keepalive。
    • 多个 Redis 实例共用同一个池时连接是复用的,实测两个实例共享一个 max_connections=4 的池:
      1
      2
      3
      4
      5
      a = 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
    18
    import 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
      5
      pipe = 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
      9
      with 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
    11
    import 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
    2
    bind 0.0.0.0
    protected-mode no
    • bind 从默认的 127.0.0.1 -::1 改成 0.0.0.0 才会监听所有网卡,实测本机默认值是:
      1
      127.0.0.1 -::1
    • protected-mode yes 的作用:没有设密码、也没有显式 bind 时,拒绝来自非回环地址的连接。所以要么关掉它,要么(更好)保留它并设 requirepass。
    • 也可以运行时热改,但记得 CONFIG REWRITE 固化:
      1
      2
      redis-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,从而拿到主机的命令执行。
    • 真需要远程访问时,按优先级选一种:
      1. 只绑内网地址 + 设 requirepass + 用 ACL 限制命令和 key 范围。
      2. 走 SSH 隧道:ssh -L 16379:127.0.0.1:6379 user@服务器,然后连本地 16379,Redis 完全不用改 bind。
      3. 上 TLS(需要编译时开启,本机这份 tls-port 是 0,说明没编进去)。
    • 另外把危险命令收掉:rename-command KEYS ""、rename-command CONFIG "",或者直接用 ACL 的 +@read -@dangerous。
  • 图形工具(可选):Redis Desktop Manager 的下载与激活说明见文末参考。

参考