后端领域 Redis 数据结构大揭秘

后端领域 Redis 数据结构大揭秘

关键词:Redis、数据结构、缓存、键值存储、后端开发、内存数据库、应用场景

摘要:Redis 作为后端开发中最常用的内存数据库,其核心竞争力在于“灵活且高效的数据结构”。本文将以“便利店经营”的故事为线索,用通俗易懂的语言揭秘 Redis 最常用的 7 类数据结构(String、List、Hash、Set、ZSet、BitMap、HyperLogLog),结合生活类比、底层原理、代码示例和实战场景,帮你彻底搞懂“为什么 Redis 能成为后端开发的‘万能瑞士军刀’”。


背景介绍

目的和范围

本文面向后端开发者,系统讲解 Redis 核心数据结构的设计逻辑、底层实现和实际应用。内容覆盖基础数据结构(String/List/Hash/Set/ZSet)、扩展结构(BitMap/HyperLogLog),并结合电商、社交、游戏等真实业务场景,解决“如何根据需求选择正确数据结构”的核心问题。

预期读者

  • 有一定后端开发经验,用过 Redis 但不熟悉底层原理的开发者;
  • 想优化系统性能,需要选择合适 Redis 数据结构的工程师;
  • 准备面试,需要深入理解 Redis 核心机制的求职者。

文档结构概述

本文从“便利店经营”的故事引入,逐步拆解 Redis 各数据结构的“设计思路-底层实现-使用场景”,最后通过电商实战案例演示如何组合使用这些结构。

术语表

  • 内存数据库:数据存储在内存中,读写速度极快(Redis 读速约 11 万次/秒,写速 8.1 万次/秒);
  • 键值存储:数据以“键-值”形式存储(类似 Python 的字典);
  • SDS(Simple Dynamic String):Redis 自定义的字符串结构,比 C 语言原生字符串更高效;
  • 跳表(Skip List):一种通过“多层索引”实现快速查找的有序数据结构(ZSet 的核心实现)。

核心概念与联系

故事引入:小红的便利店难题

小红开了一家社区便利店,最近遇到了几个经营问题,她用 Redis 轻松解决了:

  1. 会员积分:需要快速查询用户“张三”的当前积分(用 String 存“user:1001:score”→“1500”);
  2. 每日订单:要按时间顺序记录今天的所有订单(用 List 存“order:20231001”→[“订单1”,“订单2”,…]);
  3. 商品属性:每个商品有“名称、价格、库存”等多个属性(用 Hash 存“product:666”→{“name”:“苹果”,“price”:“5”,“stock”:“100”});
  4. 热门商品:统计今天被加入购物车的所有不同商品(用 Set 存“cart:20231001”→{“苹果”,“香蕉”,“牛奶”});
  5. 畅销排行榜:按销量给商品排序(用 ZSet 存“sales:rank”→{“苹果”:100, “香蕉”:80, “牛奶”:120});
  6. 会员签到:记录用户连续 30 天是否签到(用 BitMap 存“sign:user:1001”→00011100…);
  7. 客流量统计:估算本月有多少不同用户访问(用 HyperLogLog 存“uv:202310”→约 1200 人)。

这些问题的解决方案,正是 Redis 最核心的 7 类数据结构!


核心概念解释(像给小学生讲故事一样)

1. String(字符串):便利店的“存钱罐”

String 是 Redis 最基础的数据结构,就像便利店的“存钱罐”——每个存钱罐(键)里只能装一样东西(值),但这个东西可以是数字、文本甚至图片(二进制数据)。
例子:用存钱罐“user:1001:score”存“张三的积分 1500”,需要时直接打开存钱罐取积分,速度极快(O(1) 时间复杂度)。

2. List(列表):排队的“取餐号”

List 像便利店取餐区的“排队号码条”,按顺序记录一系列事件。可以从队头(左边)或队尾(右边)添加/删除元素,适合需要“按时间顺序处理”的场景。
例子:用“order:20231001”记录今天的订单,新订单永远加在队尾,查最新订单时直接取队尾即可。

3. Hash(哈希):带抽屉的“储物柜”

Hash 像便利店的“带抽屉储物柜”,每个大柜子(键)有多个小抽屉(字段),每个抽屉存不同东西。比如“product:666”柜子里,“name”抽屉存“苹果”,“price”抽屉存“5元”。
例子:要查商品价格,不用翻整个柜子,直接打开“price”抽屉就能拿到,比 String 存多个属性更高效(不用拼接/拆分字符串)。

4. Set(集合):玩具盒里的“不同玩具”

Set 像小朋友的“玩具盒”,里面装的玩具(元素)各不相同,而且打乱顺序随便放。适合需要“去重”或“快速判断是否存在”的场景。
例子:统计今天被加入购物车的商品,用 Set 存,重复添加“苹果”不会重复记录,查“是否加过苹果”时,看玩具盒里有没有就行。

5. ZSet(有序集合):带积分的“游戏排行榜”

ZSet 是“升级版的 Set”,每个元素(玩具)有一个“积分”(分数),玩具盒会根据积分自动排序。就像游戏里的“战斗力排行榜”,积分高的排前面。
例子:统计商品销量,“苹果”销量 100(分数 100),“牛奶”销量 120(分数 120),排行榜自动把“牛奶”放在“苹果”前面。

6. BitMap(位图):打卡用的“月历表”

BitMap 是一个“二进制位的数组”,每个位置(位)只能是 0 或 1,像便利店的“会员签到月历表”。比如 30 天的月历表,第 5 天签到就把第 5 位标为 1。
例子:用“sign:user:1001”存用户 10 月签到情况,第 1 天签到→第 1 位=1,第 2 天没签→第 2 位=0,月底统计“全勤”时,看所有位是否都是 1。

7. HyperLogLog(超日志):估算人数的“魔法盒子”

HyperLogLog 是一个“概率型数据结构”,像便利店门口的“客流量估算器”。不用记录每个用户是谁,通过数学算法就能估算出“今天有多少不同用户来过”,误差率约 0.81%。
例子:用“uv:202310”统计 10 月访问用户数,即使有 1000 万用户,只需要 12KB 内存就能存下估算结果。


核心概念之间的关系(用小学生能理解的比喻)

这些数据结构就像便利店的“经营工具箱”,不同工具解决不同问题:

  • String 和 Hash:String 是“单抽屉存钱罐”,Hash 是“多抽屉储物柜”。如果要存一个用户的多个属性(如姓名、年龄、积分),用 Hash 比用 String 拼接(如“张三:25:1500”)更方便,修改某个属性不用改整个字符串。
  • List 和 Set:List 是“排队的取餐号”(有序、可重复),Set 是“不重复的玩具盒”(无序、唯一)。如果需要记录“用户所有操作记录”(允许重复,按时间排序),用 List;如果需要“统计用户点击过的按钮”(不重复),用 Set。
  • Set 和 ZSet:Set 是“普通玩具盒”,ZSet 是“带积分的玩具盒”。如果要“找两个用户共同喜欢的商品”(交集),用 Set;如果要“按销量排序推荐商品”,用 ZSet。
  • BitMap 和 HyperLogLog:BitMap 是“精确的月历表”(适合小范围精确统计),HyperLogLog 是“估算的魔法盒子”(适合大范围近似统计)。统计“用户连续 30 天签到”用 BitMap(精确到每一天);统计“APP 月活用户数”用 HyperLogLog(不用存每个用户 ID)。

核心概念原理和架构的文本示意图

Redis 数据结构的底层实现依赖多种编码方式,以适应不同场景的性能需求:

  • String:小字符串用 int(整数)或 embstr(短字符串,≤44 字节),大字符串用 raw(动态字符串 SDS);
  • List:元素少用 ziplist(压缩列表,节省内存),元素多用 linkedlist(双向链表);
  • Hash:字段少用 ziplist,字段多用 hashtable(哈希表);
  • Set:元素少且是整数用 intset(整数集合),否则用 hashtable
  • ZSet:元素少用 ziplist,元素多用 skiplist(跳表)+ hashtable(双结构保证有序和快速查找)。

Mermaid 流程图:Redis 数据结构选择决策树

单个值
多个值
无序
需要
不需要
有序
需要
不需要
需求类型
需要存单个值还是多个值?
选 String
值需要有序吗?
值需要唯一吗?
选 Set
选 List
需要按分数排序吗?
选 ZSet
值是多个字段吗?
选 Hash
选 List
特殊统计
需要位操作?
选 BitMap
需要估算基数?
选 HyperLogLog

核心算法原理 & 具体操作步骤

1. String:SDS(简单动态字符串)

Redis 的 String 底层用 SDS 实现,比 C 语言原生字符串更高效:

  • C 字符串缺点:获取长度需遍历(O(n)),容易溢出(修改时需手动分配内存);
  • SDS 结构struct sdshdr { int len; int free; char buf[]; }
    • len:当前字符串长度(O(1) 获取长度);
    • free:剩余空间(自动扩容,避免频繁内存分配);
    • buf:存储实际字符(末尾自动加 \0,兼容 C 函数)。

操作示例(Redis 命令)

# 存:设置用户积分
SET user:1001:score 1500
# 取:获取用户积分
GET user:1001:score  # 输出 "1500"
# 改:积分+100(原子操作,线程安全)
INCRBY user:1001:score 100  # 输出 1600

2. ZSet:跳表(Skip List)

ZSet 能快速排序(O(logn) 插入/删除),核心依赖 跳表 结构。跳表通过“多层索引”加速查找,类似“地铁的快线和慢线”:

  • 底层链表:存储所有元素(慢线,每站都停);
  • 上层索引:每隔几个元素建一个索引(快线,只停大站);
  • 查找时从最高层索引开始,快速跳过无关元素,再逐层向下细化。

跳表示意图

第 3 层索引: 10 ---------> 30 ---------> 50
第 2 层索引: 10 -----> 20 -----> 30 -----> 40 -----> 50
第 1 层索引: 10 -> 15 -> 20 -> 25 -> 30 -> 35 -> 40 -> 45 -> 50
底层链表:   10,15,20,25,30,35,40,45,50(按分数排序)

操作示例(Redis 命令)

# 存:添加商品销量(分数=销量)
ZADD sales:rank 100 苹果 80 香蕉 120 牛奶
# 取:查销量前 2 的商品(降序)
ZREVRANGE sales:rank 0 1 WITHSCORES  # 输出 "牛奶 120", "苹果 100"
# 改:苹果销量+20(原子操作)
ZINCRBY sales:rank 20 苹果  # 苹果分数变为 120

数学模型和公式 & 详细讲解 & 举例说明

1. HyperLogLog:基数估算的概率算法

HyperLogLog 用“伯努利过程”估算基数(不同元素的数量),核心公式:
估算值 = α m ⋅ m 2 ⋅ ∑ j = 1 m 2 − R j \text{估算值} = \alpha_m \cdot m^2 \cdot \sum_{j=1}^m 2^{-R_j} 估算值=αmm2j=1m2Rj
其中:

  • ( m ):分桶数(Redis 用 16384 个桶);
  • ( R_j ):第 ( j ) 个桶中“连续 0 的最大个数+1”;
  • ( \alpha_m ):修正因子(消除误差)。

举例:向 HyperLogLog 中添加元素“apple”“banana”“apple”,实际基数是 2。HyperLogLog 通过哈希函数将元素映射到二进制串,统计每个桶的 ( R_j ),最后用公式估算出基数≈2(误差≤1%)。

2. 跳表的时间复杂度:O(logn)

跳表的查找、插入、删除操作时间复杂度为 ( O(\log n) ),与平衡树(如红黑树)相当,但实现更简单。
数学推导:跳表的层数服从几何分布,平均层数为 ( O(\log n) ),每一层最多遍历 ( O(1) ) 个节点,总时间 ( O(\log n) )。


项目实战:电商系统中的 Redis 数据结构组合

场景描述

某电商系统需要实现以下功能:

  1. 商品详情页缓存(String);
  2. 用户购物车(Hash);
  3. 热门商品排行榜(ZSet);
  4. 秒杀库存扣减(String 原子操作);
  5. 用户连续签到统计(BitMap);
  6. 月活用户数估算(HyperLogLog)。

开发环境搭建

  • 安装 Redis(本地或云服务,如阿里云 Redis);
  • 客户端:Python 使用 redis-pypip install redis)。

源代码详细实现和代码解读

import redis

# 连接 Redis
r = redis.Redis(host='localhost', port=6379, db=0)

# 1. 商品详情页缓存(String)
def cache_product(product_id, detail):
    # 缓存 1 小时(3600 秒)
    r.set(f"product:detail:{product_id}", detail, ex=3600)

def get_cached_product(product_id):
    return r.get(f"product:detail:{product_id}")

# 2. 用户购物车(Hash)
def add_to_cart(user_id, product_id, quantity):
    # 购物车字段:product_id → 数量
    r.hincrby(f"cart:{user_id}", product_id, quantity)

def get_cart(user_id):
    # 获取所有商品及数量(返回字典)
    return r.hgetall(f"cart:{user_id}")

# 3. 热门商品排行榜(ZSet)
def update_sales_rank(product_id, sales_increment):
    # 销量增加(分数=总销量)
    r.zincrby("sales:rank", sales_increment, product_id)

def get_top_sales(limit=10):
    # 获取销量前 10 的商品(降序)
    return r.zrevrange("sales:rank", 0, limit-1, withscores=True)

# 4. 秒杀库存扣减(String 原子操作)
def seckill_stock(product_id, required=1):
    # 原子扣减库存(避免超卖)
    # 返回剩余库存,若 <0 则扣减失败
    return r.decrby(f"seckill:stock:{product_id}", required)

# 5. 用户连续签到统计(BitMap)
def sign_in(user_id, day_of_month):
    # 第 day_of_month 天签到(从 1 开始)
    r.setbit(f"sign:user:{user_id}", day_of_month-1, 1)

def check_sign(user_id, day_of_month):
    # 检查第 day_of_month 天是否签到
    return r.getbit(f"sign:user:{user_id}", day_of_month-1)

# 6. 月活用户数估算(HyperLogLog)
def record_uv(user_id, month):
    # 记录用户访问(自动去重)
    r.pfadd(f"uv:{month}", user_id)

def get_month_uv(month):
    # 估算月活用户数
    return r.pfcount(f"uv:{month}")

# 测试代码
if __name__ == "__main__":
    # 缓存商品详情
    cache_product(666, '{"name":"苹果","price":5}')
    print("缓存的商品详情:", get_cached_product(666))  # 输出缓存的 JSON

    # 添加购物车
    add_to_cart(1001, 666, 2)  # 用户 1001 买 2 个苹果
    print("购物车内容:", get_cart(1001))  # 输出 {b'666': b'2'}

    # 更新销量排行榜
    update_sales_rank(666, 2)  # 苹果销量+2
    update_sales_rank(888, 3)  # 香蕉销量+3
    print("销量前 2:", get_top_sales(2))  # 输出 [(b'888', 3.0), (b'666', 2.0)]

    # 秒杀库存扣减(初始库存设为 10)
    r.set("seckill:stock:666", 10)
    print("扣减后库存:", seckill_stock(666))  # 输出 9(剩余 9)

    # 签到(10 月 5 日)
    sign_in(1001, 5)
    print("10 月 5 日是否签到:", check_sign(1001, 5))  # 输出 1(是)

    # 记录月活
    record_uv(1001, "202310")
    record_uv(1002, "202310")
    print("10 月预估月活:", get_month_uv("202310"))  # 输出 2

代码解读与分析

  • String 缓存:用 set 存商品详情,设置过期时间避免内存泄漏;
  • Hash 购物车hincrby 原子增加商品数量,避免多线程并发问题;
  • ZSet 排行榜zincrby 原子更新销量,zrevrange 快速获取TopN;
  • String 秒杀decrby 原子扣减库存,保证“库存-1”操作的线程安全;
  • BitMap 签到setbit/getbit 按位操作,30 天签到仅需 4 字节内存;
  • HyperLogLog 月活pfadd 自动去重,pfcount 用 12KB 内存统计百万级用户。

实际应用场景

数据结构 典型场景 优势
String 缓存(页面/API 结果)、计数器(点赞数/访问量)、分布式锁 原子操作(INCR/DECR)、支持二进制(存图片/序列化对象)
List 消息队列(生产者-消费者模型)、历史操作记录(最近 100 条日志) 支持从两端读写(LPUSH/RPOP)、阻塞操作(BRPOP 实现等待)
Hash 用户信息(姓名/年龄/积分)、商品属性(名称/价格/库存) 字段独立修改(HSET 单个字段)、节省内存(比 String 拼接更高效)
Set 标签系统(用户兴趣标签)、共同好友(交集运算)、去重统计(UV 临时存储) 快速集合操作(SINTER/SUNION)、自动去重
ZSet 排行榜(销量/战斗力)、时间线排序(按发布时间排序的动态) 按分数排序(ZREVRANGE)、支持范围查询(ZRANGEBYSCORE)
BitMap 用户签到(连续 30 天)、活跃状态(365 天是否登录)、性别统计(男/女) 内存极小(100 万用户签到用 122KB)、快速位运算(AND/OR 统计)
HyperLogLog 大基数统计(月活/日活)、广告点击统计(不同 IP 数) 内存固定(12KB 统计 2^64 元素)、误差率低(≤0.81%)

工具和资源推荐

  • 官方文档:Redis Documentation(权威的命令、数据结构说明);
  • 可视化工具:RedisInsight(图形化管理 Redis,支持查看键值、执行命令);
  • 监控工具:Prometheus + Grafana(监控 Redis 内存、QPS、连接数);
  • 学习书籍:《Redis 设计与实现》(黄健宏著,深入底层原理)、《Redis 实战》(Josiah L. Carlson 著,实战案例丰富)。

未来发展趋势与挑战

  • 内存优化:Redis 7.0 引入“Memory Optimization”模块,通过更紧凑的编码(如 listpack 替代 ziplist)减少内存占用;
  • 多线程支持:Redis 6.0+ 支持多线程 I/O,提升高并发场景下的性能;
  • 功能扩展:Redis 模块生态(如 RediSearch 全文搜索、RedisJSON 支持 JSON 操作)让 Redis 从“缓存”向“多功能数据库”进化;
  • 挑战:内存成本(大数据量时需考虑内存淘汰策略)、持久化延迟(RDB/AOF 对写入性能的影响)、分布式一致性(集群模式下的分片与同步)。

总结:学到了什么?

核心概念回顾

  • String:单值存储,适合缓存、计数器;
  • List:有序可重复,适合队列、历史记录;
  • Hash:多字段存储,适合对象属性;
  • Set:无序唯一,适合去重、集合运算;
  • ZSet:有序唯一(带分数),适合排行榜;
  • BitMap:位级操作,适合小范围精确统计;
  • HyperLogLog:概率基数统计,适合大范围近似估算。

概念关系回顾

Redis 数据结构是“按需设计”的工具箱:根据“是否需要有序、是否需要唯一、是否需要统计”等需求,选择最合适的结构。例如:

  • 统计“用户连续签到”→ 用 BitMap(位操作精确);
  • 统计“APP 月活”→ 用 HyperLogLog(内存小、误差可接受);
  • 实现“商品排行榜”→ 用 ZSet(按销量排序)。

思考题:动动小脑筋

  1. 如果你要设计一个“社交APP的消息通知”功能(需要按时间顺序展示,且允许用户删除某条通知),应该选 Redis 的哪种数据结构?为什么?
  2. 假设你需要统计“某视频的点赞用户”(需快速判断用户是否点赞,且能导出所有点赞用户),用 Set 还是 ZSet 更合适?为什么?
  3. HyperLogLog 为什么能只用 12KB 内存统计数亿用户?它的估算误差是如何产生的?

附录:常见问题与解答

Q1:Redis 数据结构的内存占用如何?
A:String 存储“1000”需约 40 字节(SDS 元数据+内容);Hash 存储 100 个字段比 String 拼接节省约 50% 内存;BitMap 存储 100 万用户签到仅需 122KB(1000000/8=125000 字节)。

Q2:如何选择 RDB 和 AOF 持久化?
A:RDB 适合“灾备”(文件小,恢复快),AOF 适合“数据安全”(记录每条写操作,误差≤1 秒)。生产环境建议同时启用。

Q3:Redis 集群如何分片?
A:Redis 集群用“哈希槽”(16384 个槽)分片,键通过 CRC16(key) % 16384 映射到槽,槽分布在不同节点。


扩展阅读 & 参考资料

  • 官方文档:Redis Data Types
  • 底层原理:《Redis 设计与实现》(黄健宏)第 2-7 章
  • 实战案例:《Redis 实战》(Josiah L. Carlson)第 3-6 章
  • 性能优化:Redis 内存优化指南

你可能感兴趣的:(redis,数据结构,数据库,ai)