3. Redis 数据结构选型:String、List、Set、Sorted Set、Hash 在削峰场景下的适用性分析

在削峰填谷的业务场景中,Redis 数据结构的选择直接决定了系统的吞吐上限、资源消耗和代码复杂度。不同的数据结构在排队缓冲、去重过滤、优先级调度和局部更新等子场景下表现迥异。本节从内存效率、读写复杂度、原子性支持和典型削峰模式四个维度,逐一剖析五种核心数据结构的适用边界。

3.1 数据结构选型总览

数据结构 底层编码 核心操作时间复杂度 削峰典型模式 内存效率
String int / embstr / raw GET/SET O(1) 令牌桶、计数器、分布式锁 高(直接值存储)
List quicklist LPUSH/RPOP O(1) 任务队列、异步削峰 中(节点指针开销)
Set intset / hashtable SADD O(1), SMEMBERS O(N) 去重拦截、白名单 中低(哈希表额外内存)
Sorted Set ziplist / skiplist ZADD O(log N), ZRANGE O(log N+M) 优先级队列、延迟队列、滑动窗口 低(跳表+哈希表双结构)
Hash ziplist / hashtable HSET/HGET O(1) 局部计数器、用户维度削峰 中(字段元数据开销)
选型第一原则: 削峰场景优先关注 写入吞吐 和 原子性。List 的 LPUSH + RPOP 组合在纯排队场景下写入吞吐最高(单节点可达 10w+ QPS),而 Sorted Set 在需要排序和范围查询时不可替代,但写入性能约为 List 的 60%-70%。

3.2 各数据结构削峰适用性详解

3.2.1 String:计数器与令牌桶

String 是削峰中最基础、最轻量的数据结构。其 INCR/DECR 原子操作天然适合做全局计数器和令牌桶的底层存储。在秒杀场景中,使用 String 存储剩余库存,配合 DECR 原子扣减,可避免超卖。

// 令牌桶伪代码:每次请求尝试获取一个令牌
long remaining = redisTemplate.opsForValue().decrement("stock:seckill:1001");
if (remaining < 0) {
    // 回滚,拒绝请求
    redisTemplate.opsForValue().increment("stock:seckill:1001");
    return "已售罄";
}
// 继续处理订单
避坑: String 的 SETNX 实现分布式锁时,务必设置过期时间(SET key value NX EX 10),防止死锁。高并发下避免使用 GET + SET 非原子组合,应使用 Lua 脚本或 Redis 6.0+ 的 SET NX 原子命令。

3.2.2 List:异步削峰队列

List 是生产者-消费者模式在削峰中的标准实现。上游突发流量通过 LPUSH 写入队列,下游消费者按自身处理能力通过 BRPOP 阻塞拉取,实现流量整形。List 的 BLPOP/BRPOP 支持阻塞读取,避免空轮询浪费 CPU。

// 生产者:流量高峰时快速入队
redisTemplate.opsForList().leftPush("queue:order", orderJson);

// 消费者:后台线程持续拉取,处理速度可控
while (true) {
    String order = redisTemplate.opsForList().rightPop("queue:order", 5, TimeUnit.SECONDS);
    if (order != null) {
        processOrder(order); // 异步处理
    }
}
性能数据: List 在单节点上 LPUSH 吞吐可达 12w/s,RPOP 吞吐 10w/s。但需注意:List 不支持去重和优先级,纯先进先出模式。

3.2.3 Set:去重拦截与黑白名单

Set 的 SADD 原子去重特性,在削峰中常用于幂等拦截和限流白名单。例如,同一用户 1 秒内只能请求一次,可将 userId + 时间窗口 作为 Set 元素,SADD 返回 0 表示已存在,直接拒绝。

// 用户级别去重:1秒内重复请求直接拦截
String key = "idempotent:user:" + userId + ":" + currentSecond;
boolean isNew = redisTemplate.opsForSet().add(key, requestId) == 1;
if (!isNew) {
    return "请求过于频繁";
}
redisTemplate.expire(key, 2, TimeUnit.SECONDS); // 设置过期自动清理
适用边界: Set 适合元素数量可控(万级以内)的去重场景。当元素量超过百万时,内存膨胀明显(每个元素约 64 字节 + 哈希表指针开销),建议改用 Bloom Filter 或 HyperLogLog。

3.2.4 Sorted Set:优先级调度与滑动窗口

Sorted Set 是削峰场景中最灵活的数据结构。其 score 字段可存储时间戳或优先级权重,实现:

  • 延迟队列: score 存储任务执行时间戳,消费者通过 ZRANGEBYSCORE 拉取到期任务。
  • 滑动窗口限流: score 存储请求时间戳,通过 ZREMRANGEBYSCORE 移除窗口外数据,ZCARD 统计窗口内请求数。
  • 优先级调度: 高优先级任务设置低 score 值,消费者按 score 升序处理。
// 滑动窗口限流:1秒内最多允许100次请求
long now = System.currentTimeMillis();
long windowStart = now - 1000;
String key = "sliding:window:api:" + apiName;

// 移除窗口外数据
redisTemplate.opsForZSet().removeRangeByScore(key, 0, windowStart);
// 添加当前请求
redisTemplate.opsForZSet().add(key, String.valueOf(now), now);
// 统计窗口内请求数
Long count = redisTemplate.opsForZSet().zCard(key);
if (count > 100) {
    return "限流中";
}
性能注意: Sorted Set 的 ZADD 时间复杂度为 O(log N),当集合大小超过 10 万时,写入延迟会从微秒级上升到毫秒级。高吞吐削峰场景(>5w QPS)建议优先考虑 List 或 String 计数器。

3.2.5 Hash:用户维度局部计数器

Hash 在削峰中的独特价值在于字段级原子操作。当需要按用户、IP 或商品维度进行独立计数时,Hash 比 String 更节省内存(相同前缀共享 key 开销),且支持 HINCRBY 原子增减。

// 用户维度限流:每个用户每秒最多10次
String key = "user:rate:" + userId;
long count = redisTemplate.opsForHash().increment(key, String.valueOf(currentSecond), 1);
if (count == 1) {
    redisTemplate.expire(key, 2, TimeUnit.SECONDS); // 首次设置过期
}
if (count > 10) {
    return "用户限流";
}
内存对比: 存储 100 万个用户计数器,使用 String 需要约 128MB(每个 key 约 128 字节),使用 Hash 仅需约 32MB(共享 key 前缀 + 紧凑字段编码)。当字段数小于 512 且值较小时,Hash 使用 ziplist 编码,内存效率极高。

3.3 数据结构选型决策流程

Redis 削峰数据结构选型决策树 削峰需求入口 需要排队缓冲? 需要去重拦截? 需要优先级? List(异步队列) Set(去重拦截) Sorted Set 需要计数器? 需要滑动窗口? 延迟/优先级 String(计数器) Sorted Set(窗口) Sorted Set Hash(用户维度) + Lua 原子脚本 + 定时任务消费 决策依据:写入吞吐 > 内存效率 > 功能复杂度,优先选择最简单的数据结构

3.4 选型避坑清单

  1. List 做延迟队列? 不可行。List 不支持按时间戳范围查询,延迟队列必须使用 Sorted Set 或 Redisson 的 RDelayedQueue。
  2. Set 做滑动窗口? 不可行。Set 无法按时间范围删除元素,滑动窗口必须使用 Sorted Set 的 ZREMRANGEBYSCORE。
  3. String 做用户维度多字段计数? 不推荐。每个用户每个字段一个 key,内存膨胀严重,应使用 Hash 的 HINCRBY。
  4. Sorted Set 做纯排队队列? 不推荐。写入性能低于 List,且内存开销大,纯排队场景优先选择 List。
  5. Hash 做全局计数器? 不推荐。Hash 的字段操作虽然 O(1),但 key 的元数据开销比 String 大,全局计数器直接用 String。
实战总结: 80% 的削峰场景可以通过 List(排队)+ String(计数)+ Sorted Set(窗口) 三种数据结构组合覆盖。Hash 和 Set 在特定边界场景(用户维度限流、幂等拦截)中发挥关键作用,但不应作为首选通用方案。