Linux 系统编程中的Redis
Redis 是什么?
从 Linux 系统编程的角度看,Redis 的本质是:
一个开源的、基于内存的、键值型数据结构存储系统。它通常被用作数据库、缓存和消息中间件。
我们可以从以下几个关键点来理解它:
-
一个网络服务器进程:Redis 不是一个静态的库(.so 或 .a 文件),而是一个独立运行的守护进程(redis-server)。你的应用程序(客户端)通过网络协议(TCP/IP 或 Unix Socket)与它进行通信。这意味着它可以在同一台机器上,也可以在远程服务器上。
-
基于内存:Redis 将其所有数据主要存储在内存(RAM)中。这使得它的读写速度极快(通常达到微秒级),远超基于磁盘的数据库(如 MySQL, PostgreSQL)。这是它高性能的根本原因。
-
键值存储:Redis 最基本的模型是 Key-Value。你可以通过一个唯一的 Key 来存储和检索一个 Value。
-
Key:通常是字符串。
-
Value:不仅仅是简单的字符串。这是 Redis 强大之处,它支持丰富的数据结构作为 Value。
-
-
支持持久化:虽然数据主要在内存中,但 Redis 提供了两种机制(RDB 快照和 AOF 日志)将内存中的数据异步写入硬盘,以防止服务器重启或宕机导致数据丢失。
-
使用标准协议:Redis 使用自己设计的 RESP (REdis Serialization Protocol) 协议与客户端通信。这个协议简单、高效,且是人类可读的。这也意味着你可以直接用 telnet 或 nc (netcat) 命令来与 Redis 服务器交互。
守护进程
守护进程(Daemon Process,在 Windows 中通常称为“服务”)是一种在后台运行的特殊进程,它独立于控制终端(Terminal)。它没有标准的输入输出(stdin, stdout, stderr),或者将其重定向到其他地方(如日志文件)。它通常在系统启动时开始运行,并在系统关闭时终止,为用户或其他程序提供持续的服务。
你可以把它理解为一个“后台服务员”,默默无闻地工作,你不需要直接和它交互(比如通过终端输入命令),但它一直在那里等待为你服务(比如响应网络请求)。
主要特点:
-
生命周期长:从系统启动开始,一直运行到系统关闭。
-
在后台运行:不与任何终端关联。即使你关闭了启动它的终端窗口,它也不会退出。
-
脱离控制终端:这是实现“后台运行”的关键。通常通过 fork() 创建子进程,然后让父进程退出,使得子进程被 init 或 systemd 这样的系统进程收养,从而脱离原终端。
-
通常以 root 或其他特殊用户权限运行:以便访问系统资源。
常见例子:
-
sshd:SSH 守护进程,监听 22 端口,处理你的远程登录请求。
-
nginx / apache:Web 服务器守护进程,监听 80/443 端口,处理 HTTP 请求。
-
redis-server:Redis 服务器守护进程,监听 6379 端口,处理数据库命令。
-
cron:定时任务守护进程,定期执行预设的命令。
所以,当我们说“启动 Redis”时,实际上就是在启动一个名为 redis-server 的守护进程。
RDB 快照和 AOF 日志的实现
这是 Redis 实现数据持久化(将内存中的数据保存到硬盘以防数据丢失)的两种主要策略。
RDB(Redis Database)快照
RDB 类似于给当前内存中的数据拍一张完整的快照,并保存到一个压缩的二进制文件(.rdb)中。
如何实现?
-
创建快照的时机:
-
手动触发:执行 SAVE(阻塞主进程,不常用)或 BGSAVE(后台异步执行,常用)命令。
-
自动触发:在配置文件中设置规则,如 save 900 1(在 900 秒内如果至少有 1 个 key 发生变化,则触发 BGSAVE)。
-
BGSAVE 的工作流程(核心):
-
主进程 fork() 一个子进程。这个子进程由操作系统创建,拥有与父进程(主进程)完全相同的内存数据副本。
-
主进程继续正常处理客户端请求。由于操作系统的 Copy-on-Write (COW,写时复制) 机制,如果主进程要修改某块数据,它会先复制一份副本再进行修改。这样就能保证子进程看到的内存数据是 fork() 那一瞬间的静止状态。
-
子进程负责将整个内存数据写入一个临时的 RDB 文件。
-
子进程完成写盘后,用新的 RDB 文件替换旧的 RDB 文件。
-
子进程退出。
优点:
-
性能好:fork() 子进程进行持久化,主进程几乎不受影响。
-
文件紧凑:二进制压缩文件,体积小,适合做灾难恢复(如备份后传输到远程机房)。
-
恢复速度快:恢复大数据集时比 AOF 快很多。
缺点:
-
可能丢失更多数据:如果 Redis 突然宕机,从上一次 RDB 快照到宕机之间的数据会全部丢失。
-
fork() 可能阻塞:如果数据集非常大,fork() 过程本身可能会耗时较长,导致主进程短暂阻塞。
AOF(Append Only File)日志
AOF 更像是记录所有写操作命令的日志。它通过追加(Append)的方式,将每一个会修改数据的命令写入一个日志文件。当 Redis 重启时,通过重放(Replay) 这个日志文件中的所有命令来重建数据。
如何实现?
-
工作流程:
-
命令追加(Append):主进程在执行完一个写命令后,会以协议的格式将该命令追加到 aof_buf 缓冲区。
-
文件写入(Write)和同步(Sync):根据配置的 appendfsync 策略,定期将缓冲区的内容写入到操作系统内核的页面缓存,并最终同步(fsync)到硬盘。
-
appendfsync always:每个命令都同步。数据最安全,性能最差。
-
appendfsync everysec:每秒同步一次。是性能和安全性的折中方案,默认推荐。最多丢失1秒数据。
-
appendfsync no:由操作系统决定何时同步。性能最好,但可能丢失大量数据。
-
-
-
AOF 重写(Rewrite):
-
问题:AOF 文件会不断变大,恢复时间变长,且包含大量冗余命令(如对一个 key 先后执行了 set a 1, set a 2, set a 3,其实只需要最后一条 set a 3)。
-
解决方案:Redis 会定期创建子进程,根据当前内存中的数据逆向生成一套最精简的命令集,写入一个新的 AOF 临时文件,最终替换掉旧的、庞大的 AOF 文件。
-
优点:
-
数据更安全:everysec 策略下最多丢失1秒数据。
-
可读性强:AOF 文件是文本格式,可以手动查看甚至修改。
缺点:
-
文件通常更大:即使经过重写,一般也比同数据的 RDB 文件大。
-
恢复速度较慢:需要逐条执行命令来重建数据,比 RDB 慢。
生产环境中,通常两者结合使用,用 RDB 做冷备,用 AOF 保证数据安全性。
在 Linux 系统编程中,Redis 的作用是什么?
在开发和部署应用程序时,Redis 就像一个高性能的“瑞士军刀”,解决了多种架构难题。其主要作用包括:
1.缓存(Cache) - 最核心、最常用的场景
-
问题:直接访问后端主数据库(如 MySQL)的读写速度较慢,尤其是在高并发场景下,频繁的数据库查询会成为系统瓶颈,导致响应变慢。
-
解决方案:将频繁访问且不常变动的“热点数据”(如用户信息、商品详情、页面片段)存储在 Redis 中。
-
工作流程(经典缓存模式):
-
应用程序需要读取数据。
-
首先查询 Redis 中是否存在该数据。
-
如果存在(缓存命中),直接从高速的 Redis 中返回数据。
-
如果不存在(缓存未命中),则从慢速的主数据库中查询,取得数据后并将其写入 Redis(以便下次快速读取),再返回给用户。
-
-
效果:极大减轻后端数据库的压力,显著提升应用程序的响应速度和处理能力。
2.消息队列(Message Broker)
-
问题:系统组件之间需要异步通信或解耦。例如,用户注册后需要发送邮件和短信,但不希望这些耗时操作阻塞主流程。
-
解决方案:利用 Redis 的 List(使用 LPUSH/BRPOP 命令)或 Pub/Sub(发布/订阅)功能实现简单的消息队列。
-
生产者将任务消息放入 Redis List。
-
消费者从 List 的另一端取出消息进行处理。
-
-
效果:实现异步处理、流量削峰和系统解耦。
3.会话存储(Session Store)
-
问题:在 Web 集群中,用户的登录会话(Session)如果存储在单台服务器的内存里,当用户的请求被负载均衡分配到其他服务器时,会话会丢失。
-
解决方案:使用 Redis 作为集中式的会话存储。所有 Web 服务器都从一个中央 Redis 服务中读写同一用户的 Session 信息。
-
效果:实现了分布式环境下的会话共享,支持应用的水平扩展。
4. 实时系统与排行榜
-
问题:需要实时更新和查询的数据,如游戏排行榜、实时计数(点赞、转发数)、热点新闻。
-
解决方案:利用 Redis 的 Sorted Set(有序集合)数据结构。可以非常高效地对成员按分数进行排序和范围查询。
-
效果:实现复杂的实时排序和计数功能,性能极高。
5. 分布式锁(Distributed Lock)
-
问题:在分布式系统中,多个进程或服务可能需要互斥地访问某个共享资源,防止数据冲突。
-
解决方案:使用 Redis 的 SET 命令的 NX(Not eXists)选项可以方便地实现一个简单的分布式锁。
-
效果:在分布式环境中协调多个进程的行为。
负载均衡(Load Balancing)
负载均衡是一种将网络流量或计算任务分布到多个服务器(或称为服务节点)上的技术。它的核心目标是优化资源使用、最大化吞吐量、最小化响应时间,并避免任何单一服务器过载,从而提高整个系统的可用性、可靠性和可扩展性。
想象一下一个热门餐厅只有一个服务员,他很快就会忙不过来。负载均衡就像是雇佣了一个接待员(负载均衡器),他的工作是根据每个服务员的忙碌情况,将新来的顾客引导到最空闲的服务员那里。
如何实现?
负载均衡通常由一个独立的硬件设备(如 F5)或软件(如 Nginx, HAProxy, LVS)来实现,这个设备/软件被称为负载均衡器(Load Balancer)。
基本工作流程:
-
客户端向负载均衡器(LB) 发送请求。
-
负载均衡器根据预设的算法,从后端的服务器池(Server Pool / Cluster) 中选择一个最合适的服务器。
-
负载均衡器将客户端的请求转发给选中的服务器。
-
服务器处理请求并将响应返回给负载均衡器。
-
负载均衡器最终将响应发回给客户端。
关键概念:
-
负载均衡算法:
-
轮询(Round Robin):依次将请求分发给每台服务器,循环往复。
-
加权轮询(Weighted Round Robin):给性能好的服务器分配更高的权重,让它处理更多请求。
-
最少连接(Least Connections):将新请求发给当前连接数最少的服务器。
-
IP 哈希(IP Hash):根据客户端的 IP 地址计算哈希值,将同一 IP 的请求总是发给同一台服务器(常用于保持会话)。
-
-
健康检查(Health Check):负载均衡器会定期检查后端服务器是否还“活着”(能否正常响应)。如果某台服务器宕机,LB 会自动将其从服务器池中移除,不再向其转发流量,从而保证服务的高可用性。
作用:
-
提高性能:通过分散请求,充分利用多台服务器的计算能力。
-
提高可用性(高可用):一台服务器宕机,其他服务器可以继续提供服务,用户无感知。
-
提高可扩展性(Scalability):当流量增加时,可以简单地通过向服务器池中添加新的服务器来水平扩展系统能力。
-
容错:自动屏蔽故障节点。
在 Redis 的语境中,我们谈论负载均衡,通常是指将客户端的请求分发到多个 Redis 从节点(Replica) 上进行读操作(读写分离),或者是在 Redis Cluster 模式中,将请求根据 key 的哈希值分发到不同的分片(Shard)上。
如何与 Redis 交互?
在 Linux 下用 C/C++ 编程时,你不需要自己实现 Redis 协议,通常使用官方提供的 hiredis 客户端库。
基本使用步骤:
-
安装 hiredis:通过包管理器安装,如 sudo apt-get install libhiredis-dev。
-
包含头文件:#include <hiredis/hiredis.h>。
-
连接数据库:使用 redisConnect 函数建立到 Redis 服务器的连接。
-
执行命令:使用 redisCommand 函数发送任何 Redis 命令(如 SET, GET, LPUSH)。
-
处理回复:检查命令返回的 redisReply 对象,从中提取数据。
-
释放连接和回复:使用 freeReplyObject 和 redisFree 来释放资源。
简单示例代码:
#include <stdio.h>
#include <hiredis/hiredis.h>int main() {// 1. 连接到本地Redis服务器,默认端口6379redisContext *c = redisConnect("127.0.0.1", 6379);if (c == NULL || c->err) {if (c) {printf("Connection error: %s\n", c->errstr);redisFree(c);} else {printf("Connection error: can't allocate redis context\n");}return 1;}// 2. 执行一个SET命令redisReply *reply = (redisReply *)redisCommand(c, "SET mykey \"Hello from C!\"");freeReplyObject(reply); // 对于简单的SET,我们不太关心回复,直接释放// 3. 执行一个GET命令reply = (redisReply *)redisCommand(c, "GET mykey");printf("Response from GET: %s\n", reply->str); // 打印获取到的值freeReplyObject(reply); // 释放回复对象// 4. 断开连接并清理redisFree(c);return 0;
}
编译:gcc -o example example.c -lhiredis
Redis内部数据结构
Redis 之所以如此快速和灵活,很大程度上归功于其精巧的内部数据结构设计。
简单来说,Redis 使用了一个名为 「哈希表」 的结构作为所有数据的全局索引,但这个索引的「值」并不仅仅是简单的数据,而是指向各种丰富底层数据结构的指针。这些底层数据结构是为了在不同场景下达到最优的性能和内存效率。
全局哈希表:一切的起点
你可以把整个 Redis 数据库想象成一个巨大的 字典(Dict),也就是哈希表(Hash Table)。
-
Key:就是你写入数据时用的字符串键,比如 user:1000:name。
-
Value:不是一个简单的字符串,而是一个叫 redisObject 的结构体指针。这个 redisObject 才是真正承载数据的地方。
这个全局哈希表让 Redis 可以在 O(1) 时间复杂度内根据 Key 找到对应的 redisObject,这是它速度快的基础。
RedisObject:值的通用包装器
Redis 中的所有数据值,无论是字符串、列表还是集合,都不是直接存储的,而是被包装在一个叫做 redisObject 的结构里。它的定义简化后如下:
typedef struct redisObject {unsigned type:4; // 数据类型(5大基础类型)unsigned encoding:4; // 底层使用的编码(具体数据结构)unsigned lru:24; // 最近一次访问时间/LRU信息(用于内存淘汰)int refcount; // 引用计数(用于内存回收)void *ptr; // 指向实际底层数据结构的指针
} robj;
这个结构体的两个最关键字段是:
-
type:表示这个对象对外展现的数据类型,也就是我们常说的 5 种基础类型:
-
REDIS_STRING
-
REDIS_LIST
-
REDIS_HASH
-
REDIS_SET
-
REDIS_ZSET
-
-
encoding:表示这个对象在内部真正使用的底层数据结构编码。这才是 Redis 设计的精妙之处:同一种 type 可以根据数据的大小和特征,采用不同的 encoding 来实现,以达到效率和空间的最优平衡。
ptr 指针则根据 encoding 的不同,指向不同的底层数据结构。
底层数据结构(encoding):真正的引擎
以下是 Redis 使用的几种核心底层数据结构,它们才是数据最终被存储的形式。
1.简单动态字符串 (SDS - Simple Dynamic String)
-
用于:所有字符串类型的 Key 和 Value(当它是字符串时),以及各种其他结构中的字符串元素。
-
特点:相比 C 原生字符串,SDS 具有:
-
O(1) 时间复杂度获取字符串长度。
-
避免缓冲区溢出。
-
减少修改字符串时带来的内存重分配次数(通过预分配空间和惰性空间释放)。
-
二进制安全,可以存储任何二进制数据,而不仅仅是文本。
-
2.字典 (Dict)
-
用于:全局哈希表、Hash 类型的数据结构。
-
特点:就是哈希表的实现。使用链地址法解决哈希冲突。包含两个哈希表,用于渐进式 Rehash(在扩容时避免服务阻塞)。
3.跳跃表 (SkipList)
-
用于:ZSET(有序集合)的底层实现之一。
-
特点:一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。平均查找复杂度为 O(log N)。可以高效地进行范围查询。
4.压缩列表 (ZipList)
-
用于:List、Hash、ZSET 在元素数量较少、元素值较小时使用的编码方式。
-
特点:是一块连续的内存空间,元素之间紧挨着存储,没有冗余指针,非常节省内存。但由于是连续空间,修改操作可能引发连锁更新,所以只适用于小数据量场景。
5.快速列表 (QuickList)
-
用于:List 类型的主要底层实现。
-
特点:是 Redis 3.2 之后引入的。它是 ZipList 和双向链表的结合体。一个 QuickList 是一个由多个 ZipList 节点组成的双向链表。它平衡了 ZipList 的内存效率和链表修改效率高的优点。
6.整数集合 (IntSet)
-
用于:Set 类型在元素全是整数且数量较少时的底层实现。
-
特点:是一块连续的内存空间,有序地存储所有的整数元素。查找效率高(二分查找),且非常节省内存。
数据类型与底层结构的映射关系
现在,我们把 type 和 encoding 联系起来,看看一个 SET mykey “Hello” 或 ZADD myzset 100 “member1” 在内部到底是如何存储的。
|
对外数据类型 (type) | 条件(大致) | 底层编码 (encoding) | 说明 |
---|---|---|---|
STRING (字符串) | 数值型字符串 | INT | 内部用长整型存储,减少空间 |
普通字符串,且长度 <= 44 字节 | EMBSTR | 字符串和 redisObject 分配在同一块连续内存中 | |
普通字符串,且长度 > 44 字节 | RAW | 典型的 SDS 动态字符串 | |
LIST (列表) | 老版本:元素小且少 | ZIPLIST | 使用压缩列表 |
新版本(3.2+)一律使用 | QUICKLIST | 使用快速列表(默认) | |
HASH (哈希) | 字段数量少(hash-max-ziplist-entries)且所有值长度小(hash-max-ziplist-value) | ZIPLIST | 所有字段和值在同一个压缩列表中紧挨着存储 |
不满足上述条件 | HT (Hashtable) | 使用字典(哈希表)存储 | |
SET (集合) | 元素全是整数,且数量少(set-max-intset-entries) | INTSET | 使用整数集合 |
不满足上述条件 | HT (Hashtable) | 使用字典(哈希表),但字典的 value 全部设为 NULL,只有 key 有用 | |
ZSET (有序集合) | 元素数量少(zset-max-ziplist-entries)且所有成员长度小(zset-max-ziplist-value) | ZIPLIST | 成员和分值在压缩列表中交替存储,并按分值排序 |
不满足上述条件 | SKIPLIST | 组合结构:同时使用跳跃表(用于范围查询和排序)和字典(用于 O(1) 分值查找) |
小结
-
全局哈希索引:保证根据 Key 的查找速度极快。
-
redisObject 抽象层:统一管理所有数据类型的内存分配、回收、共享等信息。
-
多编码适配:一种数据类型对应多种底层实现,根据数据的具体情况(大小、类型、数量)智能选择最节省内存或最高效的结构。这种空间换时间或时间换空间的权衡是 Redis 高效的关键。
-
高性能数据结构:大量使用如跳跃表、SDS、压缩列表等精心设计的数据结构。
正是这种复杂而精巧的内部分层设计,使得 Redis 能够对外提供简单易用的接口,同时 internally (在内部) 保持极致的性能和高效的内存利用。
因此,在 Linux 系统编程中,当你需要解决性能瓶颈、分布式协调或实时数据处理等问题时,Redis 是一个非常强大且常用的工具。它更像是一个构建复杂、高性能系统的“多功能组件”,而非一个传统的磁盘数据库。