Redis

Intro

基础概念

https://github.com/redis/redis

对于构建 real-time data-driven applications 的开发者来说,Redis 是一个常用且性能很强的cache、data structure server、document 和 vector query engine。

Redis 是一个 key-value 内存数据库,key 是 binary-safe string,value 可以是 string、list、set、hash、zset、stream 等多种数据结构。

从使用者视角看:

key                             # 二进制安全字符串,通常按 UTF-8 文本来命名
value                           # Redis object,不只是简单 byte array

SET k "hello"                   # string value,可以存文本
SET img "\xff\xd8..."           # string value,也可以存二进制
LPUSH q a b c                   # list value
HSET user:1 name michael        # hash value

string 类型的 value 本身是 binary-safe 的,可以保存任意 bytes,不要求是 UTF-8,也不要求以 \0 结尾。但 list、set、hash、zset、stream 不是一整块原始二进制,而是 Redis 自己管理的结构化对象。

从源码视角看,value 外面统一包了一层 robj

struct redisObject {
    unsigned type:4;            // 逻辑类型:string / list / set / hash / zset / stream
    unsigned encoding:4;        // 内部编码:int / embstr / raw / listpack / quicklist / skiplist
    void *ptr;                  // 指向真实底层数据
};

所以 Redis 的核心模型更准确地说是:

key -> robj(type, encoding, ptr)

string -> int / embstr / raw              # 小整数、小字符串、大字符串不同编码
list   -> listpack / quicklist            # 紧凑数组 + 链式结构
hash   -> listpack / hashtable            # 小 hash 紧凑存,大 hash 转 dict
set    -> intset / hashtable              # 纯整数小集合用 intset
zset   -> listpack / skiplist + dict      # 排序和查找兼顾
stream -> rax + listpack                  # radix tree 索引 stream entry

也就是说,网络协议上传输的是 bytes,string value 可以是纯二进制;但 Redis 在内存里会根据 value的逻辑类型和大小选择不同 encoding,而不是所有 value 都按一块裸二进制保存。

多种语言SDK

Redis 有很多语言的 client library / SDK。它们通常不是 Redis server 的一部分,而是在应用进程里连接 redis-server,把本地 API 调用转换成 Redis 命令。

app code
    -> redis client library              # Python / Node.js / Java / Go ...
    -> RESP protocol                     # Redis Serialization Protocol
    -> redis-server                      # 真正执行命令、读写内存数据结构

常见选择:

Python      -> redis-py                  # pip install redis
Node.js     -> ioredis / node-redis      # npm package
Java        -> Jedis / Lettuce / Redisson
Go          -> go-redis
Rust        -> redis-rs
C           -> hiredis
C#          -> StackExchange.Redis
PHP         -> phpredis / predis
Ruby        -> redis-rb

使用时大体都是同一个模型:

import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)

r.set("name", "michael")                # 对应 Redis 命令:SET name michael
print(r.get("name"))                    # 对应 Redis 命令:GET name
r.incr("counter")                       # 对应 Redis 命令:INCR counter

所以学习 Redis 时,先理解 Redis 命令和数据结构,再看具体语言 SDK,会更容易建立统一模型。

使用场景

Redis 在很多场景中都很适合使用,例如:

  • Caching:支持多种 eviction policies、key expiration 和 hash-field expiration。

  • Distributed Session Store:支持灵活的 session 数据建模,例如 string、JSON、hash。

  • Data Structure Server:提供 strings、lists、sets、hashes、sorted sets、JSON 等底层数据结构,并在这些结构之上支持 counters、queues、leaderboards、rate limiters 等高层语义,同时支持transactions 和 scripting。

  • NoSQL Data Store:支持 key-value、document 和 time series 数据存储。

  • Search and Query Engine:可以为 hash/JSON documents 建索引,并通过 Redis Search 支持vector search、full-text search、geospatial queries、ranking 和 aggregations。

  • Event Store & Message Broker:可以用 lists 实现 queues,用 sorted sets 实现 priority queues,用 sets 做 event deduplication,也支持 streams 和 pub/sub。

  • Vector Store for GenAI:可以和 AI applications 集成,例如 LangGraph、mem0,用于 short-term memory、long-term memory、LLM response caching、semantic caching 和 retrieval augmented generation (RAG)。

  • Real-Time Analytics:可用于 personalization、recommendations、fraud detection 和 risk assessment。

一个简单的场景如下:有个 web app,单机部署,是 node.js fastify + sqlite 服务,然后会有 PV/UV 等统计。如果每次都去读写 sqlite,性能可能不够好。这时可以进行缓存优化:

  • 不用 redis 时,直接在 node.js 里维护一个内存对象,定时把数据 dump 到 sqlite,进程优雅退出时 flush 一下。直接每次读写 sqlite 的话,可能会有明显的性能问题,尤其是高并发时。

  • 使用 Redis 时,把 PV/UV 这种高频计数先写到 Redis,定时或异步聚合后再落到 sqlite。例如 PV 可以用 INCR,UV 可以按天用 PFADD/PFCOUNT 做近似统计,或者用 SET 做精确去重。

那么适不适合可以按照下面几个维度来考虑:

  • Redis 的价值在于把高频、小粒度、可聚合的写请求从 sqlite 前面挡住,让 sqlite 只处理低频的批量写入。这样可以减少 sqlite 的写锁竞争、磁盘 IO 和事务开销。

  • 但单机服务不一定一开始就需要 Redis。进程内内存对象方案最简单,少一个外部依赖,适合数据允许短时间丢失、只有一个 node.js 进程、统计逻辑也比较简单的场景。

  • 当出现多进程部署、多个实例共享统计数据、需要独立重启 web app、希望统计数据有更好的恢复能力时,Redis 就比进程内对象更合适。

本质上,Redis 在这里不是替代 sqlite,而是作为 sqlite 前面的 write buffer / aggregation layer:请求路径里先做快速内存更新,后台再把聚合结果持久化。

使用步骤

本地启动:

cd ~/github/redis                 # Redis 源码目录
make -j$(nproc)                   # 编译

./src/redis-server redis.conf     # 启动 server,默认 127.0.0.1:6379
./src/redis-cli ping              # PONG

命令行试一下:

./src/redis-cli

SET name michael                  # 写
GET name                          # 读
INCR counter                      # 计数
EXPIRE counter 60                 # 60 秒过期
TTL counter                       # 看剩余时间

Python demo:

import redis

r = redis.Redis(host="127.0.0.1", port=6379, decode_responses=True)

r.set("name", "michael")          # string
print(r.get("name"))

r.incr("counter")                 # counter
print(r.get("counter"))

r.expire("counter", 60)           # TTL
print(r.ttl("counter"))

思路:

app -> Redis                      # 高频读写先进内存
Redis -> DB / file                # 后台定时持久化或聚合

代码导读

代码目录 layout

核心代码基本在 src/

src/
├── server.c / server.h                      # 进程启动、全局 server 状态、命令分发、核心结构定义
├── ae.c                                     # 事件循环抽象
├── ae_epoll.c / ae_kqueue.c / ae_select.c   # 不同 OS 的事件后端,Linux 下主要走 epoll
├── networking.c                             # client 连接、RESP 解析、请求 buffer、响应 buffer
├── commands.c / commands.def                # 命令表,把命令名映射到对应的 xxxCommand 函数
├── db.c                                     # keyspace 读写、过期、删除、rename、scan 等通用逻辑
├── object.c / object.h                      # robj 对象创建、编码、引用计数、释放
├── t_string.c / t_hash.c / t_list.c         # string、hash、list 命令实现
├── t_set.c / t_zset.c / t_stream.c          # set、zset、stream 命令实现
├── dict.c / sds.c / quicklist.c             # dict、SDS、quicklist 等基础数据结构
├── listpack.c / intset.c / rax.c            # listpack、intset、radix tree 等紧凑结构
└── rdb.c / aof.c / replication.c / cluster.c / module.c  # 持久化、复制、集群和 module 机制

从请求到命令执行

可以先把主路径看成下面这条链:

client request
    -> networking.c                  # 读 socket,解析 RESP,请求参数放到 client.argv
    -> server.c:processCommand()     # 查命令表,检查权限、参数、server 状态
    -> server.c:call()               # 调用 redisCommand.proc
    -> t_string.c:setCommand()       # 具体命令实现,例如 SET
    -> db.c:setKey()                 # 更新 keyspace
    -> networking.c                  # 回复写入 client 输出 buffer,等 socket 可写时发送

Redis 启动入口在 src/server.cmain(),初始化配置、数据库、网络监听和事件循环后进入:

aeMain(server.el);                    // 进入主事件循环,server.el 是 aeEventLoop
while (!eventLoop->stop) {
    aeProcessEvents(eventLoop, AE_ALL_EVENTS |
                               AE_CALL_BEFORE_SLEEP |
                               AE_CALL_AFTER_SLEEP);  // 处理 fd 事件、timer、before/after sleep hook
}

核心结构

client 表示一个连接,定义在 server.h

typedef struct client {
    connection *conn;                 // 底层连接抽象,TCP / TLS / Unix socket
    sds querybuf;                     // 客户端请求 buffer
    int argc;                         // 当前命令参数个数
    robj **argv;                      // RESP 解析后的参数数组
    redisDb *db;                      // 当前 SELECT 的 DB
    struct redisCommand *cmd;         // 当前命令
    struct redisCommand *lastcmd;     // 上一个命令,便于命令查找复用
    struct redisCommand *realcmd;     // 原始命令,用于统计和错误归因
    list *reply;                      // 待发送响应
} client;

redisCommand 是命令描述符,命令表最终会落到这个结构:

struct redisCommand {
    const char *declared_name;        // 命令名,例如 "set"
    redisCommandProc *proc;           // 真正执行命令的函数指针,例如 setCommand
    int arity;                        // 参数个数约束,负数表示至少 N 个参数
    uint64_t flags;                   // 命令属性,例如 read-only / write / admin
    uint64_t acl_categories;          // ACL 分类
    long long microseconds, calls;    // 运行时统计
};

redisDb 是数据库实例。当前源码里 keyspace 用 kvstore 表示:

typedef struct redisDb {
    kvstore *keys;                    // 真正的 key -> value 存储
    kvstore *expires;                 // key 的过期时间
    dict *blocking_keys;              // BLPOP / XREAD 等阻塞命令等待的 key
    dict *ready_keys;                 // 已经 ready、需要唤醒 client 的 key
    dict *watched_keys;               // WATCH / MULTI / EXEC 的 CAS 状态
    int id;                           // DB 编号
} redisDb;

robj 是 Redis 值对象的统一外壳,定义在 object.h

struct redisObject {
    unsigned type:4;                  // 用户可见类型:string / list / hash / set / zset / stream
    unsigned encoding:4;              // 内部编码:int / embstr / raw / listpack / quicklist / skiplist
    unsigned refcount : OBJ_REFCOUNT_BITS;  // 引用计数
    unsigned iskvobj : 1;             // 是否作为 kvobj 使用
    unsigned metabits :8;             // key metadata / expiry 相关 bit
    unsigned lru:LRU_BITS;            // LRU 时间或 LFU 信息
    void *ptr;                        // 指向真实底层结构
};

数据结构映射

type 是用户看到的数据类型,encoding 是内部实现。同一个 Redis 类型会根据数据规模选择不同编码:

string   -> SDS / int / embstr              # 小整数、小字符串、大字符串会走不同编码
list     -> quicklist / listpack            # 面向两端 push/pop,减少小对象开销
hash     -> listpack / hashtable            # 小 hash 紧凑存储,大 hash 转 dict
set      -> intset / hashtable              # 纯整数小集合用 intset,否则用 dict
zset     -> listpack / skiplist + dict      # 小 zset 紧凑存储,大 zset 兼顾排序和查找
stream   -> radix tree (rax) + listpack     # rax 做 ID 索引,listpack 存 entry
keyspace -> kvstore / dict                  # DB 里的 key -> value 主索引

建议先从 GET / SET 读起:

t_string.c:getCommand() / setCommand()      # 命令入口
    -> object.c / object.h                  # 值对象 robj,编码和引用计数
    -> db.c:lookupKeyRead()                 # GET 读 keyspace
    -> db.c:setKey()                        # SET 写 keyspace
    -> networking.c:addReply*()             # 生成响应,等待事件循环写回 client

从 OS 视角看

Redis 经常被描述为『内存数据库』,但从 OS 视角看,它更像是一个对 Linux 内核能力使用得非常克制的高性能网络服务:主要工作线程用事件循环处理网络 IO 和命令执行,数据放在用户态内存中,持久化、复制、后台删除等工作则尽量拆到子进程或后台线程里,避免阻塞主事件循环。

进程与线程模型

Redis 的核心路径是单线程事件循环:

  • 一个主线程负责 accept/read/write、解析 RESP 协议、执行命令、返回结果。

  • 命令执行通常在主线程串行完成,所以单个 Redis 实例天然避免了复杂的锁竞争。

  • 后台线程用于处理部分慢操作,例如异步关闭连接、惰性释放大对象、部分 IO 辅助工作等。

  • BGSAVEBGREWRITEAOF 使用 fork() 派生子进程完成,父进程继续服务请求。

这个设计的关键点不是『完全单线程』,而是 用户请求的关键路径尽量单线程化 。这样 CPU cache 局部性更好,锁开销更少,性能更可预测。

事件驱动 IO

Redis 在 Linux 上通常基于 epoll 做 IO 多路复用。主线程维护一个事件循环:

  1. 等待 socket 可读/可写事件。

  2. 读取客户端请求并解析命令。

  3. 在内存数据结构上执行命令。

  4. 把响应写回 socket,写不完的部分挂到输出缓冲区,等下次可写事件继续发送。

这意味着 Redis 的吞吐瓶颈通常来自几个地方:

  • 单核 CPU 执行命令的能力。

  • 网络包处理和内核协议栈开销。

  • 大 key、大 value 导致的内存拷贝和输出缓冲区膨胀。

  • 慢命令阻塞事件循环,让后续客户端排队等待。

可以用下面命令观察 Redis 进程的系统调用行为:

strace -f -p $(pidof redis-server)

生产环境不要长时间挂 strace,它会明显影响性能。排障时更常用的是 perfssINFOSLOWLOG 等低侵入工具。

内存视角

Redis 把数据主要放在进程虚拟地址空间里,由 Linux 负责页表、物理页分配、回收和换页。从 OS 角度,Redis 性能高度依赖内存行为:

  • 热数据命中内存,命令执行路径短。

  • 如果发生 swap,访问延迟会从纳秒/微秒级恶化到毫秒级,Redis 会出现明显抖动。

  • 大量小对象会带来 allocator 元数据开销和内存碎片。

  • fork() 后父子进程共享物理页,依赖 copy-on-write 机制。

Redis 常见内存指标:

used_memory
used_memory_rss
mem_fragmentation_ratio
used_memory_peak

简单理解:

  • used_memory 是 Redis allocator 视角看到的已用内存。

  • used_memory_rss 是 OS 视角看到的常驻物理内存。

  • mem_fragmentation_ratio 明显偏高时,可能是内存碎片,也可能是刚释放完大量 key 后 RSS 还没归还 OS。

fork() 与写时复制

Redis 后台持久化常用 fork()

  • 父进程继续处理请求。

  • 子进程拿到 fork 时刻的数据快照。

  • 子进程把快照写到 RDB 或重写 AOF。

Linux 使用 copy-on-write,所以 fork() 本身不会立刻复制全部内存页。但如果后台保存期间父进程继续写数据,被修改的内存页就需要复制一份。这会带来两个影响:

  • 内存瞬时占用可能上升,极端情况下触发 OOM。

  • 页复制会增加 CPU 和内存带宽压力,导致延迟抖动。

所以大实例做 BGSAVE 或 AOF rewrite 时,要预留足够内存,不要只按 used_memory 估算。可以关注:

redis-cli INFO persistence
redis-cli INFO memory

持久化与文件系统

Redis 持久化主要有 RDB 和 AOF 两种:

  • RDB 是某个时间点的内存快照,适合全量备份和快速加载。

  • AOF 记录写命令,数据恢复粒度更细,但文件会不断增长,需要 rewrite。

从 OS 视角看,持久化性能受这些因素影响:

  • 页缓存:Redis 写文件通常先进入 Linux page cache,再由内核回写到磁盘。

  • fsync 策略:决定数据落盘频率,也决定延迟和可靠性的权衡。

  • 磁盘 IO:慢盘或 IO 拥塞会拖慢 AOF 刷盘和 RDB 保存。

  • 文件系统:ext4/xfs 等文件系统的日志和分配策略会影响尾延迟。

AOF 的 appendfsync 常见策略:

always
everysec
no

一般线上常见配置是 everysec:最多损失约 1 秒数据,换取更好的吞吐和延迟表现。always 更强一致,但每次写命令都可能等待刷盘,延迟代价很高。

网络栈与连接

Redis 是典型的 TCP 服务。客户端连接多、请求小、响应快时,OS 网络栈的细节会直接影响表现:

  • backlog 太小会导致高峰期连接排队不足。

  • 文件描述符上限太低会限制最大连接数。

  • 输出缓冲区过大可能挤占内存,尤其是慢客户端或 pub/sub 场景。

  • loopback、本机 Unix socket、跨机 TCP 的延迟差异很明显。

常用观察命令:

ss -lntp | grep redis
ss -antp | grep redis-server
lsof -p $(pidof redis-server) | wc -l

调度与 CPU

Redis 主线程通常只能吃满一个 CPU core。即使命令非常快,只要单核到达瓶颈,继续加客户端也只会增加排队。

几个实践点:

  • 单实例适合低延迟和简单运维,但吞吐上限受单核限制。

  • 多实例或 Redis Cluster 可以把 key 分片到多个进程,利用多核。

  • 避免把 Redis 和 CPU 密集型服务混部在同一组核心上。

  • 容器环境要注意 CPU quota,Redis 看到的 CPU 数量和实际可用时间片可能不一致。

如果怀疑 CPU 成为瓶颈,可以看:

top -H -p $(pidof redis-server)
perf top -p $(pidof redis-server)
redis-cli --latency

虚拟内存配置

Redis 常见 Linux 配置项:

sysctl vm.overcommit_memory
sysctl vm.swappiness
cat /sys/kernel/mm/transparent_hugepage/enabled

常见建议:

  • vm.overcommit_memory = 1:避免 fork() 时因为内核保守估算内存而失败。

  • 尽量关闭或降低 swap 影响:Redis 被换出后延迟会非常差。

  • 关闭 Transparent Huge Pages:THP 可能造成内存分配和 copy-on-write 延迟抖动。

这些配置不是 Redis 『业务逻辑』的一部分,但会直接影响 fork()、内存分配、延迟稳定性。

慢请求与阻塞

因为核心命令路径串行执行,一个慢命令会阻塞后面的所有客户端请求。常见风险:

  • KEYS * 扫描全量 key 空间。

  • 对大 list/set/zset/hash 做一次性大范围读取或删除。

  • Lua 脚本运行时间过长。

  • 删除大 key 触发大量内存释放。

  • AOF 刷盘或后台 rewrite 造成 IO 压力。

排查入口:

redis-cli SLOWLOG GET 20
redis-cli LATENCY DOCTOR
redis-cli INFO commandstats

大 key 删除可以优先考虑 UNLINK,它把真正的内存释放放到后台线程,减少主线程阻塞。

把 Redis 当成 OS 进程来调优

可以按下面路径建立排障思路:

  1. 延迟高:先看 SLOWLOGLATENCY DOCTOR,确认是不是慢命令。

  2. CPU 高:看单核是否打满,再看命令统计和热点 key。

  3. 内存高:看 INFO memory,区分数据量、碎片、输出缓冲区和 copy-on-write。

  4. IO 高:看 AOF/RDB 状态、磁盘延迟、appendfsync 策略。

  5. 连接异常:看 connected_clients、fd 限制、ss 队列和客户端输出缓冲区。

从这个角度看,Redis 的高性能来自三个约束:

  • 数据尽量在内存中完成。

  • 请求关键路径尽量少用锁、少切线程。

  • 慢 IO 和重任务尽量挪到后台,但仍要接受 OS 资源竞争的影响