☰
Redis基础知识全解析:从数据结构到持久化与性能优化
2026/10/16 12:59:08 网站建设 项目流程

Redis 是很多人接触的第一款内存数据库,但大多数人一开始对它的理解都停在“把数据放内存,所以读写快”这个层面。我第一次把官方文档通读下来时其实挺懵的——它不只是缓存,还经常出现在分布式锁、排行榜、消息队列、计数的场景里。后来我陆续做过几个内部项目,回头看基础知识这块,发现真正理解它的定位、数据结构和底层机制,和“会用几个命令”之间差距很大。

这篇笔记是系列第一篇,写给两类读者:一类是刚接触 Redis 想系统梳理基础的人,一类是已经在用但主要是“复制粘贴命令、能跑就行”的朋友。我会把 Redis 的核心基础知识拆成六个部分,从它到底解决了什么问题开始,讲到五种数据类型的选型、性能来源、持久化机制、过期与淘汰策略,最后分享初学者第一个月最容易踩的坑。每个部分我都会解释“为什么”,而不是只丢几个命令让你背。

1. Redis到底是什么:一个内存键值仓库,但不只是“缓存”

1.1 为什么常说它是“数据库之上的数据库”

Redis 的官方定位是 in-memory data structure store,也就是“内存数据结构存储”。它把数据放在内存里,所以读写速度可以做到微秒到亚毫秒级别,远超传统磁盘数据库。这个特性决定了它最常见的用法:把热点数据放在 Redis 里,让请求先打到内存,而不是每次都在数据库里查一遍磁盘。

但你要注意,它和“数据库”的关系更像是互补,而不是替代。传统关系型数据库擅长事务、复杂查询、关系建模,数据持久放在磁盘上;Redis 擅长高速读写、简单数据类型、原子操作,数据主要活在内存里。用一个场景类比:关系型数据库是仓库,Redis 是前台货架。热门商品放货架上随时取,冷门商品去仓库搬。如果所有商品都堆在前台,货架会爆,成本也撑不住。

这决定了你在设计系统时要先问一个问题:这份数据真的是“热数据”吗?如果全量数据都塞进 Redis,内存成本会非常夸张,命中率也不一定高。正确思路是只把读多写少、访问频繁的数据放进去。

1.2 和Memcached、MySQL的定位差异:选型时先想清楚这三者

很多老项目早期用 Memcached 做缓存,后来逐步换到 Redis。两者都是内存键值存储,但差异非常明显:

  • 数据结构:Memcached 基本只有简单的 string,Redis 有 string、list、hash、set、zset 五种基础类型,还能做位图、HyperLogLog 这类扩展操作。
  • 持久化:Memcached 挂了数据就没了,Redis 可以通过 RDB 和 AOF 把数据恢复到磁盘。
  • 主从复制与高可用:Redis 有主从复制、哨兵、集群方案,Memcached 没有这么完整的体系。
  • 原子操作:Redis 的 INCR、SETNX 等命令天然支持原子自增、分布式锁,Memcached 需要自己拼逻辑。

MySQL 和 Redis 的区别更本质:MySQL 的核心是“关系模型 + 事务 + SQL 查询”,Redis 的核心是“内存 + 数据结构 + 原子命令”。你不可能用 Redis 替代 MySQL 存储复杂的订单关系数据,但可以用 Redis 缓存订单热点信息、加速计数统计、实现分布式锁。

我的建议很直接:新的系统里直接用 Redis 就行了,Memcached 除了极端简单的缓存场景,基本没有新项目选它的理由。而 MySQL 和 Redis 两者必须同时存在,Redis 负责扛流量和热点,MySQL 负责保底数据。

1.3 典型应用场景:缓存之外,Redis还常出现在这五个地方

光说理论容易空,我列几个实际项目中高频出现的使用方式:

  1. 缓存:缓存用户信息、商品详情、配置项。这是知名度最高的场景,也是新手的第一个练手项目。
  2. 计数器:文章阅读量、点赞数、接口调用次数。用 INCR 命令直接原子自增,不用先查再改,也不会并发覆盖。
  3. 分布式锁:多个实例同时抢资源时需要锁。用 SETNX + 过期时间实现一个简单锁,保证同一时刻只有一个进程执行关键操作。
  4. 排行榜:游戏积分排行、热销商品榜。ZSet 的分数排序天生适合这类需求,ZREVRANGE 一行命令拿到前 N 名。
  5. 简单的消息队列:用 List 的 LPUSH + BRPOP 做一个先进先出的任务队列。功能比不上专业消息中间件,但轻量、无额外依赖,很多小项目就这么跑起来的。

看到这里你应该能明白,Redis 的定位是“基础设施”,不是某个业务的专属工具。这也是为什么基础概念值得认真梳理一遍——数据类型之间的细微差别,直接影响你场景设计得好不好。

2. 五种基础数据结构:先学会给数据“分类”,再谈命令

2.1 String:不只是存字符串那么简单

String 是 Redis 里最基础、最通用的类型。你存 JSON 字符串、存图片 base64、存 session 都可以用它,但如果你想充分利用 Redis,String 还有一些“隐藏能力”。

先看最基本的读写:

SET user:1001 "tom" GET user:1001

真正有意思的是INCR / DECR原子自增。很多语言里做“先读后写”的计数器都有并发问题,Redis 把这些操作封装成了原子命令:

INCR page_view:2025:06:01 INCRBY login_count:1001 10

另外一个容易被忽略的是SETNX,也就是“不存在才设置”。它常用来实现分布式锁:

SET lock:order_paid 1001 NX EX 30

这条命令的意思是:只有当 key 不存在时才写入,并且 30 秒后自动过期。这样多个进程同时执行时,只有一个能成功拿到锁,其余进程直接失败或等一会儿重试。

String 的底层有多种编码方式,比如整数类型用的就是整数编码而非字符串,这一点在后面讲对象编码时细说。实际使用中,String 最适合做短小、高频修改的标量数据,比如状态、计数、token、标志位。

2.2 List:从队列到时间线,注意“头尾方向”

List 底层是双向链表。用 LPUSH 从左边推入,RPUSH 从右边推入,LPOP / RPOP 分别从两端弹出。它有两个常用姿势:

第一个是先进先出队列:

LPUSH task_queue "job_1001" RPOP task_queue

生产者把待处理任务 LPUSH 进去,消费者 RPOP 取出来,这就算完成了一个最简单的任务队列。如果消费者处理到一半挂了,任务会丢,这是它的局限。更可靠的做法是用 RPOPLPUSH 先把任务挪到一个 backup 队列里,处理成功后再真正删除,相当于带“备份”的队列。

第二个是时间线/最新消息列表:

LPUSH user:1001:newsfeed 10001 LPUSH user:1001:newsfeed 10002 LRANGE user:1001:newsfeed 0 9

每次新内容都往左推,LRANGE 取前 10 条,天然就是“最新的在前”。很多社交信息流、评论列表都是这么设计的。

使用 List 时最容易踩的坑是混淆头尾方向。LPUSH 和 RPUSH 不只是名字不同,它直接决定了列表顺序。写代码前先画一下:你希望“新元素排在前面”还是“排在后面”,再决定用哪个 push。否则你取出的顺序会莫名其妙反过来。

2.3 Hash:结构化的对象比 String 更省内存

Hash 是一个 key 下挂多个 field-value,非常適合存储“一个对象的多个字段”。比如用户信息:

HSET user:1001 name "tom" age 28 city "北京" HGET user:1001 name HGETALL user:1001 HINCRBY user:1001 points 50

和把整个用户对象序列化成 JSON 字符串存 String 相比,Hash 有几个好处:

  • 局部读改写:只改某个字段时,不需要把整个字符串拉出来反序列化再写回去。直接 HSET 或 HINCRBY 就行。
  • 省内存(部分场景):Redis 对小型 hash 做了 ziplist 压缩编码,字段少、值小的时候内存占用相当可观。
  • 结构清晰:业务上就是一个对象,代码读起来也自然。

Hash 的典型使用场景是购物车、用户资料、文章详情。注意不要滥用:如果字段很多且频繁写入,压缩编码会转成 hashtable,内存优势就没了,这时候还是要回到 String 序列化或分片存储。

2.4 Set 与 ZSet:去重、交并差运算和有序排行榜

Set 是无序字符串集合,主要特点是自动去重,而且提供集合运算。常用命令:

SADD user:1001:tags "java" "redis" SISMEMBER user:1001:tags "redis" # 判断是否存在 SPOP user:1001:tags # 随机弹出一个 SINTER user:1001:tags user:1002:tags # 交集:共同标签 SUNION user:1001:tags user:1002:tags # 并集 SDIFF user:1001:tags user:1002:tags # 差集

实际的常见场景包括:抽奖活动(SPOP 随机抽人)、点赞去重、UV 去重、社交关系里“共同好友”这种交并差计算。如果你需要“在大量元素中快速判断某个值是否存在”,Set 就非常合适。

ZSet(有序集合)在 Set 基础上给每个成员加了一个分数 score,按分数排序。它底层组合了哈希表和跳表,既支持 O(1) 查成员,又支持按分数排序。

ZADD leaderboard 1000 "player_a" ZADD leaderboard 1500 "player_b" ZINCRBY leaderboard 200 "player_a" ZREVRANGE leaderboard 0 9 WITHSCORES # 前十名 ZSCORE leaderboard "player_a" # 查询某人分数

ZSet 的经典场景是排行榜。每次比赛结束 ZINCRBY 加分,随时拿 ZREVRANGE 取前 N 名,分数还能作为时间戳实现“延迟队列”:按执行时间排序,定期取最前面的任务判断是否到期。

ZSet 要注意的是 score 支持浮点数,如果对精度敏感,建议统一存成整数(比如毫秒时间戳),避免浮点误差带来排序错乱。

2.5 一次真实的类型选型:从 JSON 大字段拆成 Hash 的收益

我在一个内部模拟项目里遇到过这种情况:业务方要把一整个用户扩展信息存进 Redis,最开始图省事直接存 JSON 字符串,key 是 user:1001:ext,value 是几百个字段序列化后的 JSON。上线后有两个问题:一是每次只要改动其中一个字段,就要把整个 JSON 拉出来反序列化再写回;二是内存占用比预期高,因为大字符串无法走压缩编码。

后来改成了 Hash 存储,把高频读写的字段单独拆出来,低频字段继续放 JSON 子串。结构变成:

HSET user:1001:ext level 5 vip 1 last_login "2025-06-01"

结果是:单次更新速度明显提升,内存占用也降下来一截。这个例子想说明的核心是:先判断业务对某个对象的读写模式,再选类型,而不是一律 SET JSON。

做个快速选择表:

需求推荐类型原因
计数器、token、短状态String原子操作、简单直接
任务队列、最新列表List天然先进先出,方向可控
对象多字段读改写Hash局部更新方便,小型时更省内存
去重、快速判存、交并差Set集合运算原生支持
排行榜、按分数排序ZSet分数维度天然排序
大 JSON、日志原文String不需要结构化解耦,直存最省事

3. 高性能的三个魔法:单线程、多路复用和高效数据结构

3.1 单线程为什么快:没有竞争,就没有锁的开销

Redis 的命令执行是单线程的,所有客户端请求最终都在一个主线程里串行处理。很多人第一反应是:单线程不是慢吗?这里要反过来想。

对 Redis 来说,数据在内存里,一个简单 GET 命令耗时才几十纳秒到微秒级。此时的瓶颈根本不是 CPU 计算能力,而是网络 IO 和内存带宽。单线程最大的收益是不需要处理并发竞争:没有锁、没有线程切换、没有同步开销。多线程程序为了并发正确性付出的代价,往往比并发带来的收益还大,尤其当任务本身极快时。

打个比方:一个窗口办业务,每个人只需要 1 秒。如果十个窗口同时开,大家还要排队等叫号、协调资源,反而乱套;一个窗口顺滑地处理,吞吐反而稳定。

Redis 6.0 之后引入了多线程处理网络 IO 读写,但核心命令执行仍然是单线程。这意味着你写 Lua 脚本时也要保持原子性思维,脚本内所有操作按顺序执行,不会被其他命令插进来。

3.2 IO多路复用:一个进程如何“盯着”成千上万个连接

单线程要应对大量客户端连接,靠的是 IO 多路复用机制。通俗解释是:主线程只需要一个“接待员”,用 epoll 把成千上万个 socket 都交给内核监听。哪个连接有数据来了,内核就通知它去处理;没有数据,就用空闲时间去处理别的。

这个设计让 Redis 能够用很低的资源支撑大量连接。你用 redis-benchmark 可以看到它在本机压测下能跑到十万级 QPS,和生产环境网络瓶颈下表现完全不同。实际调优时,如果你发现 Redis 延迟升高,首先要排查的是网络延迟、客户端慢查询、big key 阻塞,而不是怀疑 Redis 单线程撑不住。

3.3 字符串底层 SDS 和对象编码:省下的时间都很可观

Redis 没有直接用 C 语言的 char 数组实现字符串,而是定义了自己的 SDS(简单动态字符串)结构。它额外记录了长度、分配空间等信息,所以获取字符串长度是 O(1),不用像 C 字符串那样从头扫到尾。修改字符串时还会做预分配,减少频繁 realloc 的次数。

对象编码对外透明,但了解后能帮助你理解内存。常见的字符串编码有:

  • int:字符串能解析成 64 位整数时,直接用整数存储。
  • embstr:短字符串,和 RedisObject 头一起分配在一块连续内存里,少一次内存分配。
  • raw:长字符串,用独立内存。

你可以用 OBJECT ENCODING 查看某个 key 的当前编码:

SET num 100 OBJECT ENCODING num # int SET name "hello" OBJECT ENCODING name # embstr SET long_str "a very long string worth of raw encoding..." OBJECT ENCODING long_str # raw

还有一个隐藏的优化是整数共享:0 到 9999 这种小整数在 Redis 内部是共享对象,不重复分配。所以如果你有很多小状态值,内存压力会小很多。这些细节不需要背,但排查内存问题时会非常有用。

4. 持久化:配置文件里最需要提前想明白的部分

4.1 RDB 快照的触发机制与“丢数据窗口”

Redis 默认的持久化方式是 RDB,它生成一个压缩的二进制快照文件 dump.rdb。触发方式有三种:手动执行 SAVE / BGSAVE、配置文件里的 save 规则、正常关闭时自动保存。

其中 SAVE 是同步执行,会阻塞主线程,生产环境千万不能用。BGSAVE 是 fork 一个子进程做快照,主线程继续服务。配置文件里默认的规则长这样:

save 900 1 save 300 10 save 60 10000

意思是:900 秒内有 1 次写操作就生成快照;300 秒内有 10 次写操作就生成快照;60 秒内有 10000 次写操作就生成快照。

RDB 的核心局限是丢数据窗口。比如你 60 秒内没攒够 10000 次写操作,最后一次快照之后所有数据都可能丢失。如果你做的项目能容忍几分钟数据丢失,RDB 够用;如果不行,就要开 AOF。

4.2 AOF 追加日志:三种 fsync 策略怎么选

AOF(Append Only File)把每一条写命令追加到一个日志文件里。重启时重新执行这些命令来恢复数据,所以它最多只丢你最后一次 fsync 之后到宕机之间的数据。

关键配置是 appendfsync,三种策略:

策略行为特点
always每次写命令都执行 fsync最安全,但性能最差
everysec每秒执行一次 fsync性能与安全折中,默认推荐
no由操作系统决定何时落盘性能最好,但可能丢大量数据

实际生产中 unless 业务对数据一致性有硬性要求(比如支付流水),我一般用 everysec 就够,最坏丢一秒数据。always 每秒要 fsync 一次,会明显拖慢写性能,适合极端重要且写量不高的场景。

AOF 还有一个重写机制:文件越来越大时,需要压缩成最终状态。手动执行 BGREWRITEAOF,或者通过配置自动触发:

auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb

重写时 Redis 会 fork 子进程,根据当前内存数据生成最小命令集,清掉历史上大量冗余命令。你可以从 INFO 命令里看到 aof_rewrite 相关的统计信息。

4.3 混合持久化与我的备份建议

Redis 4.0 后支持混合持久化,在 AOF 重写时生成的文件开头包含一份 RDB 快照,后面继续追加增量写命令。这样重启恢复时,先加载 RDB 飞快回放,再补增量日志,兼顾恢复速度和数据完整性。

配置是:

aof-use-rdb-preamble yes

我的建议分两种情况:

  • 仅当缓存,可以丢数据:开 RDB 就够,恢复快,文件小。
  • 数据重要,不能丢:开 AOF,appendfsync everysec,同时打开混合持久化。
  • 两个都开时:Redis 会优先用 AOF 恢复,因为 AOF 完整度更高。

另外提醒一句:持久化不只是配置一把锁,还要定期做备份演练。把 dump.rdb 或 AOF 文件备份到独立存储,并实测一次“从备份恢复数据”的完整过程。如果你从来不知道备份文件能不能用,那这个“备份”等于没有。

5. 过期删除与内存淘汰:Redis 不会无限膨胀,靠的就是这套机制

5.1 惰性删除 + 定期删除:为什么不用定时器

给 key 设置过期时间是日常操作,比如验证码存 5 分钟、session 存 30 分钟:

SETEX captcha:user1001 300 "123456" EXPIRE product:1001:seckill_stock 60 TTL product:1001:seckill_stock

那过期时间一到,Redis 就会立刻把 key 删掉吗?不会。删除策略是惰性删除 + 定期删除的组合。

惰性删除的意思是,每次访问 key 时先检查是否过期,过期才删。好处是省 CPU,坏处是过期数据可能长时间占着内存。定期删除则是每 100ms 随机抽一批设置了过期时间的 key,删除其中过期的,防止大量过期 key 一直没人访问把内存占满。

为什么不直接用定时器每秒扫一遍?因为 key 量很大时,全量扫描会拖垮单线程。所以 Redis 采用“抽查”策略——这是性能和维护成本之间的平衡。

5.2 淘汰策略:8 种策略怎么选,近似 LRU 又是什么意思

就算有过期机制,内存也有膨胀风险。当内存达到 maxmemory 上限时,Redis 会按配置的策略淘汰 key。常见策略如下:

策略行为
noeviction不淘汰,写入直接报错
allkeys-lru所有 key 中淘汰最久未使用的
volatile-lru仅设了过期时间的 key 中淘汰最久未使用的
allkeys-random所有 key 随机淘汰
volatile-random仅有过期时间的 key 随机淘汰
volatile-ttl仅有过期时间,按剩余寿命从短到长淘汰

最常用的是 allkeys-lru。注意它叫近似 LRU,不是严格 LRU。因为 Redis 不会为每个 key 维护完整的一颗全量访问链表,而是采样一部分 key,从中选出最久没用的。默认采样数是 5,想让淘汰更精准可以调高采样数,但会消耗更多 CPU。

生产环境我会先把 maxmemory 设成物理内存的 60%~70% 左右,留出操作系统和持久化子进程的余量。如果空间很充足,就把淘汰策略配成 noeviction,让直接报错成为信号,尽早暴露容量问题,而不是靠淘汰静默丢数据。

5.3 缓存雪崩、击穿、穿透:从“基础知识”到“高频故障”的过渡

这三个概念属于“基础知识进阶”,但非常值得在这一篇里提一句,因为根源都来自过期和未命中。

  • 缓存雪崩:大量 key 同一时间过期,请求全部打到数据库,数据库被压垮。对策是给过期时间加随机抖动,比如 300 秒 ± 60 秒。
  • 缓存击穿:一个热点 key 过期瞬间,大量请求穿透到数据库。对策是热点 key 不过期,或用互斥锁保证只有一个请求去重建。
  • 缓存穿透:查询一个根本不存在的 key,缓存里没有,数据库里也没有,请求每次都穿透。对策是缓存空值,或者用布隆过滤器先挡一层。

这些内容放到后面“缓存设计”专题里展开,但在基础阶段至少要有这个意识:过期策略不只是清理数据,它直接影响线上稳定性。

6. 初学 Redis 第一个月,最容易踩的坑

6.1 big key:删除一个键,拖慢了整个实例

big key 指的是某个 key 对应的 value 特别大,比如一个 string 有几 MB,一个 list 里有几十万条数据,一个 hash 有上万个字段。

它的危害很隐蔽。表面上看存取都正常,但从删除、序列化、迁移时就能感受到问题。最典型的是 DEL 一个几百万成员的集合,主线程要花几秒清理内存,期间所有命令都被阻塞,线上就是断崖式超时。

处理方式:删除大 key 用 UNLINK 命令,它把释放内存放到后台线程去做,不阻塞主线程;或者用 SSCAN、LRANGE 分批删除。另外可以用 redis-cli 的 --bigkeys 参数扫描实例里最大的 key,提前发现风险:

redis-cli --bigkeys

我习惯把 big key 排查纳入周常巡检,尤其是业务快速迭代阶段,很容易不知不觉长出大 key。

6.2 keys * 与 scan:别再亲手制造线上事故

初学的时候最顺手的就是keys *,想看看都有哪些 key。但在生产环境执行它等同于自杀,因为 KEYS 会全量扫描所有键,单线程阻塞,几百万 key 的情况下直接把实例卡死几十秒。

正确做法是用 SCAN 命令,它每次只返回一部分 key,增量迭代:

SCAN 0 MATCH product:* COUNT 100 SCAN 1000 MATCH product:* COUNT 100

SCAN 支持游标方式分批遍历,不会阻塞主线程。即使目标是写脚本清理符合某种规则的 key,也应该用 SCAN + DEL 分批处理,而不是 KEYS 拿到全量再删。

6.3 学习建议:从 redis.conf 注释开始读起

最后一个建议可能和你想的不太一样:学 Redis 不要先刷命令列表,而是把 redis.conf 从头翻一遍。官方配置文件里的注释写得非常清楚,基本上把每个参数什么场景用、默认值多少、有什么坑都讲透了。我到现在排查问题时,还会经常打开 conf 看某个参数的官方说明。

配合 redis-cli 的 INFO 命令,你能看到内存、持久化、连接数、命中率等实时统计,这些才是真实运维的依据。如果遇到想不通的行为,就手动模拟一个场景,用 OBJECT ENCODING、TTL、MEMORY USAGE 观察真实状态,比背概念有效得多。

基础部分的笔记就写到这里。这个系列后续我打算继续整理过期策略实测、持久化恢复演练、主从与哨兵搭建、集群分片原理等内容。如果你在学习和排查中遇到了和这篇笔记相关的问题,可以在评论区把现象和配置发出来,我们一起看看是哪个基础原理没对上。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询