一、Redis核心数据结构与热点场景
在秒杀、排行榜、Feed流等高并发场景中,Redis的数据结构选择直接决定了系统的吞吐上限与响应延迟。本章从五种核心数据结构出发,逐一剖析其底层实现、热点应用模式及性能调优要点。
1.1 String — 秒杀库存与分布式锁
String是Redis最基础的数据结构,在秒杀场景中承担库存扣减与分布式锁两大职责。其底层采用SDS(Simple Dynamic String),支持O(1)长度的原子增减操作。
秒杀库存扣减核心命令:
# 初始化库存
SET seckill:stock:1001 100
# 原子扣减(返回扣减后的值)
DECR seckill:stock:1001
# 带库存校验的扣减(Lua脚本保证原子性)
local stock = redis.call('GET', KEYS[1])
if stock and tonumber(stock) > 0 then
return redis.call('DECR', KEYS[1])
else
return -1
end
| 场景 | 数据结构 | 核心命令 | 时间复杂度 | 热点风险 |
|---|---|---|---|---|
| 库存扣减 | String | DECR / DECRBY | O(1) | 高(单Key竞争) |
| 分布式锁 | String | SET NX EX | O(1) | 中(锁超时) |
| 计数器 | String | INCR / INCRBY | O(1) | 低(分片后) |
性能调优要点:秒杀场景下String热点Key会导致单实例CPU打满。建议采用Key分片策略,将单个库存Key拆分为N个段(如stock:1001:0~stock:1001:9),扣减时随机选取一段,降低单Key竞争。
1.2 List — 秒杀异步队列与消息缓冲
List的LPUSH+BRPOP组合天然构成高性能生产者-消费者队列。在秒杀场景中,订单创建请求先入List缓冲,后端Worker异步消费,实现请求削峰。
秒杀异步队列实现:
// 生产者(秒杀接口)
LPUSH seckill:order:queue "{userId:1001, skuId:2001, time:1700000000}"
// 消费者(Worker进程)
BRPOP seckill:order:queue 5 // 阻塞5秒等待消息
| 操作 | 命令 | 时间复杂度 | 适用场景 |
|---|---|---|---|
| 入队 | LPUSH / RPUSH | O(1) | 请求缓冲 |
| 出队 | LPOP / RPOP | O(1) | 即时消费 |
| 阻塞出队 | BLPOP / BRPOP | O(1) | 避免空轮询 |
| 范围查询 | LRANGE | O(N) | 队列监控 |
避坑指南:List作为队列时,严禁使用LRANGE全量拉取再逐条删除,这会带来O(N)的复杂度。始终使用LPUSH+BRPOP或RPUSH+BLPOP的阻塞模式,单次操作复杂度O(1)。
1.3 Set — 秒杀去重与用户标签
Set的SADD/SISMEMBER操作在秒杀中用于一人一单校验,确保同一用户不能重复秒杀。其底层采用Hash表实现,SISMEMBER时间复杂度O(1)。
秒杀去重校验:
// 用户是否已秒杀
SISMEMBER seckill:users:1001 10001
// 记录秒杀用户
SADD seckill:users:1001 10001
// 获取秒杀用户总数
SCARD seckill:users:1001
| 场景 | 命令 | 数据量级 | 内存优化 |
|---|---|---|---|
| 用户去重 | SADD / SISMEMBER | 百万级 | 使用intset编码 |
| 标签系统 | SADD / SMEMBERS | 十万级 | 避免SMEMBERS全量 |
| 共同关注 | SINTER / SUNION | 万级 | 控制集合大小 |
1.4 ZSet — 排行榜与实时排序
ZSet(有序集合)是排行榜场景的最优解。每个元素关联一个score,Redis内部使用跳跃表(Skip List)+ Hash表实现,支持O(log N)的插入、更新和范围查询。
实时排行榜核心操作:
// 更新用户分数
ZINCRBY leaderboard:game:1001 10 "user_10001"
// 获取Top10
ZREVRANGE leaderboard:game:1001 0 9 WITHSCORES
// 获取用户排名
ZREVRANK leaderboard:game:1001 "user_10001"
// 获取用户分数
ZSCORE leaderboard:game:1001 "user_10001"
| 操作 | 命令 | 时间复杂度 | 热点场景 |
|---|---|---|---|
| 更新分数 | ZINCRBY | O(log N) | 游戏/直播打赏 |
| 获取TopN | ZREVRANGE | O(log N + M) | 榜单展示 |
| 获取排名 | ZREVRANK | O(log N) | 用户个人排名 |
| 范围删除 | ZREMRANGEBYRANK | O(log N + M) | 冷数据清理 |
性能调优要点:排行榜ZSet的最大风险是单个Key过大。当成员数超过1000万时,ZINCRBY延迟可能超过10ms。建议按时间分桶(如每日一个ZSet),或使用近似排行榜(只保留Top 10000,定期裁剪)。
1.5 Hash — Feed流与用户画像
Hash结构在Feed流场景中用于存储用户时间线或帖子元数据。每个用户维护一个Hash,field为帖子ID,value为时间戳或摘要,支持按需获取指定字段。
Feed流Hash存储模型:
// 存储用户Feed
HSET feed:user:10001 "post:20001" 1700000000
HSET feed:user:10001 "post:20002" 1700000001
// 批量获取Feed元数据
HMGET feed:user:10001 "post:20001" "post:20002"
// 获取所有Feed(慎用)
HGETALL feed:user:10001
| 场景 | 数据结构 | 优势 | 注意事项 |
|---|---|---|---|
| 用户时间线 | Hash | 按需获取字段 | 控制field数量 |
| 帖子元数据 | Hash | 内存紧凑 | 避免HGETALL |
| 用户画像 | Hash | 灵活扩展属性 | 使用ziplist编码 |
避坑指南:Hash的HGETALL命令会返回所有field-value,当field数量超过1000时,网络传输和序列化开销显著增加。Feed流场景建议使用HMGET按需获取,或配合List存储时间线顺序、Hash存储元数据。
1.6 数据结构选型对比总结
| 场景 | 推荐结构 | 核心指标 | 热点风险 | 调优方向 |
|---|---|---|---|---|
| 秒杀库存 | String | 原子性、O(1) | 高 | Key分片、Lua脚本 |
| 秒杀队列 | List | 阻塞出队、O(1) | 中 | 多消费者、确认机制 |
| 秒杀去重 | Set | SISMEMBER O(1) | 低 | intset编码、过期清理 |
| 排行榜 | ZSet | 排序、范围查询 | 中 | 时间分桶、近似排行 |
| Feed流 | Hash + List | 按需获取、顺序 | 低 | HMGET替代HGETALL |
架构师建议:热点场景的本质矛盾是单Key高并发与Redis单线程模型之间的冲突。解决思路有三:
1. Key分片:将热点Key拆散到多个实例
2. 本地缓存:热点数据在应用层缓存,减少Redis压力
3. 异步化:同步操作转为异步队列,削峰填谷
1. Key分片:将热点Key拆散到多个实例
2. 本地缓存:热点数据在应用层缓存,减少Redis压力
3. 异步化:同步操作转为异步队列,削峰填谷