Redis 从入门到实战:高性能原理、数据结构、事务、Lua 与排行榜
Redis 是后端开发中最常见的基础设施之一。
很多人第一次接触 Redis,会把它简单理解成“一个很快的缓存”。这个理解不能说错,但明显低估了 Redis:它不仅能缓存字符串,还提供 Hash、List、Set、Sorted Set 等丰富的数据结构,并且通过事务、原子命令、Lua 脚本等能力,可以直接解决计数器、秒杀、抢票、排行榜等高并发场景中的核心问题。
本文结合几篇 Redis 学习资料,对其中的知识重新抽取和组织,尝试回答几个真正重要的问题:
- Redis 为什么快?
- Redis 的核心数据结构应该怎么选?
- Redis 的事务与传统关系型数据库事务有什么不同?
WATCH + MULTI为什么能解决并发抢票问题?- Lua 脚本解决了什么问题?
- 为什么排行榜几乎是 Sorted Set 的“标准答案”?
- Redis 和 MySQL 应该如何配合,而不是互相替代?
1. Redis 是什么?
Redis 全称可以理解为 Remote Dictionary Server,本质上是一种基于 Key-Value 模型的数据存储系统。
最简单的 Redis 数据可以写成:
1 | |
例如:
1 | |
但 Redis 与普通 Key-Value 数据库最大的区别之一,是它的 value 并不局限于一个字符串,而是可以使用多种数据结构。
因此,更准确地说,Redis 是一个:
以内存为核心、支持丰富数据结构、提供高性能读写与原子操作能力的数据存储系统。
在实际架构中,Redis 经常和 MySQL 等关系型数据库一起使用:
1 | |
Redis 擅长的是“快”和“实时”,关系型数据库擅长的是“可靠存储”和“复杂关系查询”。
两者并不是替代关系,而是互补关系。
2. Redis 为什么这么快?
资料中把 Redis 的高性能归纳为几个核心因素。
2.1 基于内存读写
传统数据库的大量性能成本来自磁盘 I/O。
而 Redis 的主要数据保存在内存中,大部分命令都直接对内存中的数据结构进行操作,因此绕开了磁盘随机 I/O 的巨大延迟。
粗略理解:
1 | |
所以 Redis 天生适合:
- 缓存
- Session
- 高频计数
- 排行榜
- 热点数据
- 临时状态
- 高并发读写
当然,“数据在内存中”也意味着 Redis 的容量和成本受到内存大小约束,它通常不应该被简单当成无限容量的主数据库。
2.2 数据结构针对场景进行了专门设计
Redis 并不是只提供一个简单的 HashMap。
它为不同场景提供不同的数据结构:
1 | |
当业务问题与数据结构高度匹配时,很多复杂逻辑可以直接由 Redis 命令完成。
例如排行榜如果放在普通关系型表中,通常需要:
1 | |
而 Redis Sorted Set 本身就在维护一个有序结构,因此“排序”不是每次查询时临时计算,而是数据结构本身提供的能力。
2.3 避免大量线程竞争和上下文切换
资料以经典 Redis 模型解释其性能:核心命令执行采用串行化思路,可以避免大量线程之间的:
- 锁竞争
- 上下文切换
- 临界区协调
- 死锁风险
- 共享状态同步
这让 Redis 的执行路径非常直接。
需要注意的是,现代 Redis 的网络 I/O、后台任务等实现已经随着版本发展发生变化,因此不要把“Redis 只有一个线程”理解成整个 Redis 进程内部永远只有一个线程。
真正应该抓住的是:
Redis 的核心命令执行模型尽可能保持简单、可预测,并避免让多线程锁竞争成为主要瓶颈。
2.4 I/O 多路复用
Redis 可以同时面对大量客户端连接,但不需要为每个连接都创建一个独立线程。
其关键思想是 I/O 多路复用:
1 | |
一个线程可以监听多个 Socket 的事件,只处理真正已经准备好的连接。
因此 Redis 能在减少线程开销的同时处理大量并发网络连接。
2.5 C 语言实现
Redis 使用 C 语言实现,运行时依赖较少,能够直接使用底层系统能力。
不过工程上不能简单得出“C 写的所以一定快”的结论。真正决定 Redis 性能的是:
内存访问 + 高效数据结构 + 简单执行模型 + I/O 多路复用 + 成熟工程实现。
3. Redis 最核心的五种数据类型
Redis 的重点不是背命令,而是建立:
业务问题 -> 数据结构
之间的映射。
4. String:最简单,也最常用
String 是 Redis 最基础的数据类型。
1 | |
除了存字符串,还经常用于数字:
1 | |
Redis 的 INCR、DECR 等操作具有原子性,因此对于单纯的计数问题,不一定需要事务。
典型场景:
- 缓存 JSON
- Token
- 验证码
- Session ID
- 页面访问量
- 库存计数
- 分布式序列的一部分
例如:
1 | |
5. Hash:适合表示对象
Hash 的结构可以理解为:
1 | |
例如用户:
1 | |
读取:
1 | |
逻辑结构:
1 | |
Hash 比较适合:
- 用户信息
- 商品属性
- 配置项
- 状态对象
它比把整个对象序列化成 JSON 字符串更灵活,因为可以单独修改某个字段。
6. List:天然的双端队列
List 可以从左右两侧插入数据。
1 | |
读取范围:
1 | |
从抽象上可以理解为:
1 | |
所以它适合:
- 简单消息队列
- 时间线
- 最新记录
- 双端队列
不过在现代大型消息系统中,如果真正需要消息确认、消费者组、消息重放等能力,更应该考虑 Redis Stream 或专门的 MQ,而不是只靠 List 硬凑。
7. Set:去重和集合运算
Set 是无序且元素唯一的集合。
1 | |
删除:
1 | |
判断元素是否存在:
1 | |
获取所有元素:
1 | |
Set 最重要的不是“存一堆不重复的数据”,而是集合语义。
它非常适合:
- 标签
- 用户关注关系
- 去重
- 黑名单
- 共同好友
- 权限集合
- 已处理 ID 集合
典型思路:
1 | |
8. Sorted Set:Redis 排行榜核心
Sorted Set,也叫 ZSet,可以理解成:
1 | |
例如:
1 | |
Redis 会按照 score 维护排序。
查看分数:
1 | |
倒序取前三名:
1 | |
它特别适合:
- 游戏排行榜
- 热度榜
- 销量榜
- 文章排行
- 积分榜
- 延迟队列
- 按时间或权重排序的数据
从业务角度看,可以把 Sorted Set 理解成:
1 | |
Redis 自动维护:
1 | |
这就是它构建实时排行榜的根本原因。
9. 其他 Redis 数据能力
除了五种经典类型,Redis 还提供过或持续扩展了更多能力,例如:
- Bitmap
- HyperLogLog
- Geospatial
- Stream
它们分别适合:
1 | |
这也是 Redis 和单纯 Memcached 类缓存系统相比的重要优势之一:
Redis 不只是缓存值,而是在服务端直接提供数据结构能力。
10. 为什么应用访问 Redis 要使用连接池?
资料中给出了 Python 直接连接 Redis 的方式:
1 | |
也给出了连接池:
1 | |
连接池解决的问题并不只属于 Redis。
如果每一次请求都:
1 | |
高并发下会产生大量额外开销。
连接池的思路是:
1 | |
连接使用完成后不是关闭,而是归还连接池。
核心收益:
- 减少连接创建开销
- 减少 TCP 建连成本
- 控制最大连接数
- 提升连接复用率
- 提高高并发下的稳定性
11. Redis 事务和 MySQL 事务不是一回事
关系型数据库中,我们经常强调 ACID:
1 | |
Redis 也有事务,但不要直接套用 MySQL 事务的全部语义。
Redis 的事务更接近:
把一组命令先进入队列,然后一次性按顺序执行。
核心命令:
1 | |
例如:
1 | |
MULTI 之后,命令不会立即执行,而是进入队列:
1 | |
12. Redis 事务为什么不提供传统 Rollback?
资料特别强调:
Redis 事务并不提供与关系型数据库相同的回滚机制。
这里要区分两类错误。
12.1 入队阶段就能发现的错误
例如命令本身语法错误。
这种问题可能导致整个事务无法正常执行。
12.2 真正执行时才出现的错误
例如对错误类型的 Key 使用了不匹配的命令。
这种情况下,Redis 不会像 MySQL 那样把前面已经执行成功的命令全部回滚,而是继续执行后续命令。
所以 Redis 事务的设计哲学更偏向:
1 | |
而不是实现复杂的数据库级回滚系统。
13. Redis 持久化:RDB 与 AOF
虽然 Redis 以内存为中心,但仍然提供持久化能力。
13.1 RDB
RDB 可以理解为:
对某一时刻 Redis 中的数据生成快照。
大致过程:
1 | |
优点:
- 文件紧凑
- 适合备份
- 恢复速度通常较快
问题:
- 快照之间存在时间窗口
- 宕机时可能丢失最近一段数据
13.2 AOF
AOF 的核心思想是记录写操作:
1 | |
恢复时可以重放这些写命令。
资料中提到常见同步策略:
1 | |
其中 everysec 是性能和数据安全之间常见的折中方式。
所以 Redis 的持久化不是一个简单的“有或没有”,而是:
性能、恢复速度和可接受数据丢失窗口之间的权衡。
14. 为什么 Redis 单线程执行命令,还需要并发控制?
这是 Redis 初学者非常容易困惑的问题。
假设 Redis 一次只执行一个命令:
1 | |
每条命令确实没有同时执行。
但是:
一个业务操作通常包含多条命令。
例如抢票:
1 | |
多个客户端的这些命令可以交叉:
1 | |
于是两个人都以为自己抢到了最后一张票。
所以:
单条 Redis 命令原子,不代表由多条命令组成的业务逻辑天然原子。
这就是 Redis 仍然需要事务、原子命令和 Lua 的原因。
15. WATCH + MULTI:Redis 的乐观锁
Redis 可以通过:
1 | |
实现一种乐观锁语义。
流程:
1 | |
如果 WATCH 之后、EXEC 之前,别的客户端修改了这个 Key,那么当前事务执行失败。
可以把它抽象成:
1 | |
它本质上是一种:
先假设不会冲突,真正提交时再检查冲突。
这就是乐观锁。
16. 抢票场景的核心逻辑
假设库存只有 1:
1 | |
两个客户端都开始:
1 | |
客户端 2 先执行:
1 | |
客户端 2 成功。
这时候客户端 1 再执行 EXEC,因为 ticket 已经发生修改,所以事务失败。
最终只会有一个客户端成功。
需要特别注意:
1 | |
不能:
1 | |
因为 WATCH 的目的就是监控事务正式提交之前 Key 是否被修改。
17. 抢票不一定必须使用事务
如果业务只是:
1 | |
Redis 本身提供原子命令:
1 | |
因此很多场景可以直接利用原子操作。
例如:
1 | |
返回:
1 | |
但真实业务通常还有:
1 | |
这时一个 DECR 就不够了。
于是 Lua 开始变得非常重要。
18. Lua:把多条 Redis 命令变成一个整体
Redis 支持执行 Lua 脚本。
基本命令:
1 | |
其中:
1 | |
用于访问 Key。
1 | |
用于访问脚本参数。
例如:
1 | |
Lua 最大的价值并不是“Redis 里还能写一种语言”。
真正重要的是:
可以把一组 Redis 操作封装成一个在服务端执行的整体。
这样有两个核心收益。
18.1 减少网络往返
原本:
1 | |
Lua:
1 | |
多次网络 RTT 变成一次。
18.2 多步操作不被其他命令插入
Lua 脚本执行时可以把多条 Redis 操作组合在一起完成。
这特别适合:
- 秒杀
- 库存扣减
- 限流
- 分布式锁释放
- 多 Key 状态检查
- 原子计数
- 排行榜批量操作
例如伪代码:
1 | |
比客户端写:
1 | |
更加适合要求原子语义的高并发场景。
19. 为什么排行榜特别适合 Redis?
假设现在有 10 万玩家:
1 | |
需求包括:
- 查询 Top N
- 查询我的排名
- 查询我的分数
- 修改积分
- 更新排名
- 查询我前后几名
- 玩家加入排行榜
- 玩家退出排行榜
如果全部使用 MySQL,最直观的查询是:
1 | |
MySQL 当然可以完成。
但是问题在于:
排行榜是一个高频动态排序问题,而关系型数据库的强项主要是持久化、关系和查询,不是把一个不断变化的大集合一直维护成实时排名。
而 Sorted Set 本身就为这个问题而生。
20. 使用 Sorted Set 构建排行榜
创建:
1 | |
结果逻辑上为:
1 | |
查询全部排行榜
1 | |
查询 Top 10
1 | |
查询某玩家分数
1 | |
查询某玩家排名
1 | |
注意排名从 0 开始。
如果业务显示排名从 1 开始:
1 | |
增加积分
1 | |
分数变化之后,Redis 会同步维护其有序位置。
查询某玩家前后 5 名
先查询排名:
1 | |
假设结果是:
1 | |
则:
1 | |
删除玩家
1 | |
新增玩家
1 | |
这套 API 几乎完整覆盖了实时排行榜。
21. 并列排行榜与严格排行榜
排行榜还有一个容易被忽略的问题:
两个人分数相同怎么办?
21.1 并列排行榜
例如:
1 | |
相同分数拥有相同业务排名。
这种业务规则通常需要应用层根据相同 score 进一步处理展示排名。
21.2 严格排行榜
严格排行榜要求:
1 | |
例如:
1 | |
即:
分数相同,注册更早的用户排名靠前。
MySQL 中非常直观:
1 | |
Redis Sorted Set 只有一个 score,因此资料采用了将多个排序维度编码进一个分数的办法:
1 | |
思路本质上是:
1 | |
不过生产环境中需要非常谨慎地设计这种编码,尤其要考虑:
- 浮点数精度
- 时间范围
- 主排序权重
- score 的安全整数范围
- 是否可能发生碰撞
不要仅仅为了“省一个字段”而把一个难以维护的浮点编码方案直接搬到生产环境。
22. Redis 排行榜与 MySQL 排行榜怎么选?
简单来说:
MySQL 更适合
1 | |
Redis 更适合
1 | |
一个常见组合:
1 | |
例如排行榜页面只需要:
1 | |
这些高频字段可以直接在 Redis 中准备好。
用户点击某个玩家进入详情页之后,再查询 MySQL 中更完整的数据。
23. Redis 中几种并发方案如何选择?
结合事务、原子命令和 Lua,可以形成一个很实用的判断方式。
场景一:一个 Key 的简单加减
优先:
1 | |
例如:
1 | |
场景二:读 -> 判断 -> 写
可以考虑:
1 | |
它属于乐观并发控制。
适合冲突概率不是特别高、逻辑比较简单的场景。
场景三:多个 Redis 操作必须作为整体
优先考虑:
1 | |
尤其是高并发核心路径。
场景四:业务一致性跨 Redis 与 MySQL
这时已经不是 Redis 单机原子性能够完整解决的问题,需要从系统层面考虑:
- 消息队列
- Outbox
- 最终一致性
- 幂等
- 状态机
- 补偿机制
不要试图用一个 Redis 事务解决整个分布式系统的一致性。
24. 从抢票案例真正应该学到什么?
抢票案例表面上是在学:
1 | |
真正应该学到的是:
并发问题通常发生在“读 -> 判断 -> 写”之间。
只要代码里出现:
1 | |
就应该立即警觉:
1 | |
这也是 CAS、乐观锁、版本号、数据库 UPDATE ... WHERE version = ?、Redis WATCH 等机制背后共同的思想。
25. 从排行榜案例真正应该学到什么?
排行榜案例最重要的不是记住:
1 | |
而是理解:
数据结构选对之后,很多业务问题会从“算法问题”变成“命令调用”。
如果使用普通数据库表思考排行榜,首先想到的是:
1 | |
如果用 Sorted Set 思考:
1 | |
问题一下子简单很多。
所以使用 Redis 的核心能力之一,就是:
用数据结构直接表达业务语义。
26. Redis 的工程使用原则
最后把前面的内容浓缩成几条工程经验。
26.1 Redis 不只是缓存
Redis 更准确的定位应该是:
1 | |
缓存只是它最常见的用途之一。
26.2 不要把所有数据都塞进 Redis
Redis 内存昂贵。
应该优先存:
- 热数据
- 高频数据
- 临时数据
- 实时状态
- 可以重建的数据
26.3 多条命令之间存在竞态条件
即使单条命令原子,也不能自动保证:
1 | |
整体原子。
26.4 优先使用 Redis 原子命令
如果一个命令能解决:
1 | |
就不要先上复杂事务。
26.5 多步原子逻辑优先考虑 Lua
客户端多次请求不仅有并发窗口,还有网络 RTT。
Lua 往往能够同时解决:
1 | |
26.6 Redis 与 MySQL 各自做擅长的事情
不要纠结:
1 | |
更好的问题是:
1 | |
如果核心需求是:
1 | |
Redis 很合适。
如果核心需求是:
1 | |
MySQL 更合适。
27. 总结
把 Redis 从入门到实战串起来,可以得到这样一条知识主线:
1 | |
真正掌握 Redis,不应该停留在:
1 | |
而应该逐渐建立三个层次的理解:
第一层是命令:
1 | |
第二层是数据结构:
1 | |
第三层是并发和系统设计:
1 | |
当你开始从第三层思考 Redis,它就不再只是一个“缓存工具”,而会真正成为系统架构中的高性能数据组件。