一、Sorted Set 是什么?
Redis Sorted Set,也叫 ZSet,是一种有序集合数据结构。
它和普通 Set 一样,成员不能重复;但不同的是,Sorted Set 中的每个成员都会关联一个 score 分数,Redis 会根据 score 对成员进行排序。
Sorted Set 非常适合处理以下类型的数据:
- 排行榜
- 延时队列
- 滑动窗口限流
- 按时间范围清理数据
- 热门数据统计
- 权重排序
- 范围查询
基本结构可以理解为:
key -> [
member1 -> score1,
member2 -> score2,
member3 -> score3
]
例如:
rank:user:score -> [
user:1001 -> 95,
user:1002 -> 88,
user:1003 -> 76
]
二、常用命令
1. 添加元素:ZADD
ZADD key score member
示例:
ZADD rank:user:score 95 user:1001
ZADD rank:user:score 88 user:1002
ZADD rank:user:score 76 user:1003
一次添加多个:
ZADD rank:user:score 95 user:1001 88 user:1002 76 user:1003
2. 查询元素分数:ZSCORE
ZSCORE rank:user:score user:1001
返回:
95
3. 获取排名:ZRANK / ZREVRANK
ZRANK 按照分数从小到大排名:
ZRANK rank:user:score user:1001
ZREVRANK 按照分数从大到小排名:
ZREVRANK rank:user:score user:1001
排行榜场景通常使用 ZREVRANK。
4. 获取指定排名范围:ZRANGE / ZREVRANGE
按照分数从小到大获取:
ZRANGE rank:user:score 0 9 WITHSCORES
按照分数从大到小获取:
ZREVRANGE rank:user:score 0 9 WITHSCORES
5. 按分数范围查询:ZRANGEBYSCORE
ZRANGEBYSCORE rank:user:score 80 100 WITHSCORES
查询分数在 80 到 100 之间的用户。
6. 删除指定元素:ZREM
ZREM rank:user:score user:1003
7. 按分数范围删除:ZREMRANGEBYSCORE
ZREMRANGEBYSCORE rank:user:score 0 60
删除分数在 0 到 60 之间的元素。
三、典型使用场景
场景一:排行榜系统
排行榜是 Sorted Set 最典型的使用场景。
例如游戏积分榜、文章热度榜、用户贡献榜、商品销量榜等,都可以使用 Sorted Set 实现。
业务需求
假设我们要实现一个用户积分排行榜:
- 用户积分越高,排名越靠前
- 可以查询 Top N 用户
- 可以查询某个用户的排名
- 可以更新用户积分
Redis 数据结构设计
key: rank:user:score
member: userId
score: 用户积分
例如:
rank:user:score -> [
1001 -> 950,
1002 -> 880,
1003 -> 760
]
Redis 操作示例
添加用户积分:
ZADD rank:user:score 950 1001
ZADD rank:user:score 880 1002
ZADD rank:user:score 760 1003
查询积分排行榜 Top 10:
ZREVRANGE rank:user:score 0 9 WITHSCORES
查询用户排名:
ZREVRANK rank:user:score 1001
注意:Redis 返回的排名从 0 开始,如果要展示给用户,需要 +1。
展示排名 = Redis排名 + 1
查询用户积分:
ZSCORE rank:user:score 1001
增加用户积分:
ZINCRBY rank:user:score 50 1001
删除用户:
ZREM rank:user:score 1003
Java 示例
使用 Spring Data Redis:
@Service
public class UserRankService {
private static final String USER_SCORE_RANK_KEY = "rank:user:score";
private final StringRedisTemplate stringRedisTemplate;
public UserRankService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 设置用户积分
*/
public void setScore(Long userId, double score) {
stringRedisTemplate.opsForZSet()
.add(USER_SCORE_RANK_KEY, String.valueOf(userId), score);
}
/**
* 增加用户积分
*/
public void incrementScore(Long userId, double delta) {
stringRedisTemplate.opsForZSet()
.incrementScore(USER_SCORE_RANK_KEY, String.valueOf(userId), delta);
}
/**
* 查询 Top N 用户
*/
public Set<ZSetOperations.TypedTuple<String>> topN(int n) {
return stringRedisTemplate.opsForZSet()
.reverseRangeWithScores(USER_SCORE_RANK_KEY, 0, n - 1);
}
/**
* 查询用户排名
*/
public Long getRank(Long userId) {
Long rank = stringRedisTemplate.opsForZSet()
.reverseRank(USER_SCORE_RANK_KEY, String.valueOf(userId));
if (rank == null) {
return null;
}
return rank + 1;
}
/**
* 查询用户积分
*/
public Double getScore(Long userId) {
return stringRedisTemplate.opsForZSet()
.score(USER_SCORE_RANK_KEY, String.valueOf(userId));
}
}
场景二:滑动窗口限流
Sorted Set 非常适合实现滑动窗口限流。
业务需求
假设接口要求:
同一个用户 60 秒内最多请求 100 次
可以使用当前时间戳作为 score,每次请求进来时:
- 删除窗口期之前的请求记录
- 统计当前窗口内的请求数量
- 判断是否超过限制
- 如果没有超过限制,则写入本次请求记录
Redis 数据结构设计
key: rate_limit:user:{userId}
member: requestId 或 timestamp + random
score: 当前请求时间戳
示例:
rate_limit:user:1001 -> [
request:001 -> 1710000000000,
request:002 -> 1710000000200,
request:003 -> 1710000000500
]
Redis 操作示例
删除 60 秒之前的请求:
ZREMRANGEBYSCORE rate_limit:user:1001 0 1710000000000
统计当前窗口内请求数:
ZCARD rate_limit:user:1001
添加当前请求:
ZADD rate_limit:user:1001 1710000060000 request:1710000060000
设置过期时间:
EXPIRE rate_limit:user:1001 120
Java 示例
@Service
public class SlidingWindowRateLimiter {
private final StringRedisTemplate stringRedisTemplate;
public SlidingWindowRateLimiter(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 滑动窗口限流
*
* @param userId 用户 ID
* @param windowSeconds 时间窗口,单位:秒
* @param maxRequests 最大请求次数
* @return 是否允许访问
*/
public boolean isAllowed(Long userId, int windowSeconds, int maxRequests) {
String key = "rate_limit:user:" + userId;
long now = System.currentTimeMillis();
long windowStart = now - windowSeconds * 1000L;
ZSetOperations<String, String> zSetOps = stringRedisTemplate.opsForZSet();
// 1. 删除窗口之前的请求记录
zSetOps.removeRangeByScore(key, 0, windowStart);
// 2. 统计当前窗口内的请求数
Long count = zSetOps.zCard(key);
if (count != null && count >= maxRequests) {
return false;
}
// 3. 添加当前请求记录
String requestId = now + ":" + UUID.randomUUID();
zSetOps.add(key, requestId, now);
// 4. 设置过期时间,避免 key 长期存在
stringRedisTemplate.expire(key, Duration.ofSeconds(windowSeconds * 2L));
return true;
}
}
注意事项
上面的实现存在多个 Redis 操作,在高并发场景下可能会有并发问题。
生产环境中建议使用 Lua 脚本保证原子性。
Lua 脚本版本
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window_start = tonumber(ARGV[2])
local max_requests = tonumber(ARGV[3])
local request_id = ARGV[4]
local expire_seconds = tonumber(ARGV[5])
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
local count = redis.call('ZCARD', key)
if count >= max_requests then
return 0
end
redis.call('ZADD', key, now, request_id)
redis.call('EXPIRE', key, expire_seconds)
return 1
Java 调用示例:
@Service
public class LuaRateLimiter {
private final StringRedisTemplate stringRedisTemplate;
public LuaRateLimiter(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public boolean isAllowed(Long userId, int windowSeconds, int maxRequests) {
String key = "rate_limit:user:" + userId;
long now = System.currentTimeMillis();
long windowStart = now - windowSeconds * 1000L;
String requestId = now + ":" + UUID.randomUUID();
String luaScript = """
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window_start = tonumber(ARGV[2])
local max_requests = tonumber(ARGV[3])
local request_id = ARGV[4]
local expire_seconds = tonumber(ARGV[5])
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
local count = redis.call('ZCARD', key)
if count >= max_requests then
return 0
end
redis.call('ZADD', key, now, request_id)
redis.call('EXPIRE', key, expire_seconds)
return 1
""";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(luaScript);
redisScript.setResultType(Long.class);
Long result = stringRedisTemplate.execute(
redisScript,
List.of(key),
String.valueOf(now),
String.valueOf(windowStart),
String.valueOf(maxRequests),
requestId,
String.valueOf(windowSeconds * 2L)
);
return result != null && result == 1L;
}
}
场景三:延时队列
Sorted Set 也可以用来实现一个简单的延时队列。
业务需求
例如订单创建后,如果 30 分钟内未支付,需要自动关闭订单。
可以将任务的执行时间作为 score,将任务 ID 或任务内容作为 member。
消费者不断查询:
score <= 当前时间戳
的任务,然后执行任务。
Redis 数据结构设计
key: delay:queue:order
member: orderId
score: 任务执行时间戳
示例:
delay:queue:order -> [
order:1001 -> 1710001800000,
order:1002 -> 1710001900000
]
Redis 操作示例
添加延时任务:
ZADD delay:queue:order 1710001800000 order:1001
查询到期任务:
ZRANGEBYSCORE delay:queue:order 0 1710001800000 LIMIT 0 10
删除已处理任务:
ZREM delay:queue:order order:1001
Java 示例
@Service
public class DelayQueueService {
private static final String ORDER_DELAY_QUEUE_KEY = "delay:queue:order";
private final StringRedisTemplate stringRedisTemplate;
public DelayQueueService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 添加延时任务
*/
public void addDelayTask(Long orderId, Duration delay) {
long executeTime = System.currentTimeMillis() + delay.toMillis();
stringRedisTemplate.opsForZSet()
.add(ORDER_DELAY_QUEUE_KEY, String.valueOf(orderId), executeTime);
}
/**
* 拉取到期任务
*/
public Set<String> pollDueTasks(int limit) {
long now = System.currentTimeMillis();
return stringRedisTemplate.opsForZSet()
.rangeByScore(ORDER_DELAY_QUEUE_KEY, 0, now, 0, limit);
}
/**
* 删除任务
*/
public void removeTask(Long orderId) {
stringRedisTemplate.opsForZSet()
.remove(ORDER_DELAY_QUEUE_KEY, String.valueOf(orderId));
}
}
消费者示例:
@Component
public class OrderDelayTaskConsumer {
private final DelayQueueService delayQueueService;
private final OrderService orderService;
public OrderDelayTaskConsumer(DelayQueueService delayQueueService,
OrderService orderService) {
this.delayQueueService = delayQueueService;
this.orderService = orderService;
}
@Scheduled(fixedDelay = 1000)
public void consume() {
Set<String> orderIds = delayQueueService.pollDueTasks(10);
if (orderIds == null || orderIds.isEmpty()) {
return;
}
for (String orderId : orderIds) {
boolean removed = removeTaskSafely(orderId);
if (!removed) {
continue;
}
orderService.closeOrderIfUnpaid(Long.valueOf(orderId));
}
}
private boolean removeTaskSafely(String orderId) {
// 简化示例,实际生产中建议使用 Lua 脚本实现“查询 + 删除”的原子性
delayQueueService.removeTask(Long.valueOf(orderId));
return true;
}
}
更安全的 Lua 删除方式
为了避免多个消费者重复消费同一个任务,可以使用 Lua 脚本实现:
- 查询到期任务
- 删除该任务
- 返回任务 ID
local key = KEYS[1]
local now = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local tasks = redis.call('ZRANGEBYSCORE', key, 0, now, 'LIMIT', 0, limit)
for i, task in ipairs(tasks) do
redis.call('ZREM', key, task)
end
return tasks
场景四:清理历史数据或过期数据
Sorted Set 适合保存带有时间属性的数据,例如:
- 用户积分变更记录
- 用户搜索记录
- 用户浏览历史
- 接口访问日志
- 最近登录记录
- 最近操作记录
如果只想保留最近一段时间的数据,可以用时间戳作为 score,然后定期删除过期数据。
业务需求
例如保留用户最近 7 天的浏览记录。
Redis 数据结构设计
key: user:browse:history:{userId}
member: 商品 ID 或文章 ID
score: 浏览时间戳
示例:
user:browse:history:1001 -> [
product:1 -> 1710000000000,
product:2 -> 1710000100000,
product:3 -> 1710000200000
]
Redis 操作示例
添加浏览记录:
ZADD user:browse:history:1001 1710000200000 product:3
删除 7 天前的数据:
ZREMRANGEBYSCORE user:browse:history:1001 0 1709395400000
查询最近浏览记录:
ZREVRANGE user:browse:history:1001 0 19 WITHSCORES
Java 示例
@Service
public class BrowseHistoryService {
private final StringRedisTemplate stringRedisTemplate;
public BrowseHistoryService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 添加浏览记录
*/
public void addBrowseRecord(Long userId, Long productId) {
String key = "user:browse:history:" + userId;
String member = "product:" + productId;
long now = System.currentTimeMillis();
stringRedisTemplate.opsForZSet().add(key, member, now);
// 保留最近 7 天的数据
long sevenDaysAgo = now - Duration.ofDays(7).toMillis();
stringRedisTemplate.opsForZSet()
.removeRangeByScore(key, 0, sevenDaysAgo);
// 设置过期时间,避免冷数据长期占用内存
stringRedisTemplate.expire(key, Duration.ofDays(8));
}
/**
* 查询最近浏览记录
*/
public Set<ZSetOperations.TypedTuple<String>> listRecentBrowseRecords(Long userId, int limit) {
String key = "user:browse:history:" + userId;
return stringRedisTemplate.opsForZSet()
.reverseRangeWithScores(key, 0, limit - 1);
}
}
场景五:热门内容统计
Sorted Set 可以用来统计文章、视频、帖子、商品等内容的热度。
业务需求
比如一个社区系统中,需要统计帖子热度:
- 浏览一次,加 1 分
- 点赞一次,加 5 分
- 评论一次,加 10 分
- 收藏一次,加 20 分
最后根据热度分数进行排序。
Redis 数据结构设计
key: hot:post:rank
member: postId
score: 热度分
Redis 操作示例
浏览加分:
ZINCRBY hot:post:rank 1 post:1001
点赞加分:
ZINCRBY hot:post:rank 5 post:1001
评论加分:
ZINCRBY hot:post:rank 10 post:1001
收藏加分:
ZINCRBY hot:post:rank 20 post:1001
查询热门帖子 Top 20:
ZREVRANGE hot:post:rank 0 19 WITHSCORES
Java 示例
@Service
public class PostHotRankService {
private static final String HOT_POST_RANK_KEY = "hot:post:rank";
private final StringRedisTemplate stringRedisTemplate;
public PostHotRankService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public void increaseViewScore(Long postId) {
incrementScore(postId, 1);
}
public void increaseLikeScore(Long postId) {
incrementScore(postId, 5);
}
public void increaseCommentScore(Long postId) {
incrementScore(postId, 10);
}
public void increaseFavoriteScore(Long postId) {
incrementScore(postId, 20);
}
private void incrementScore(Long postId, double score) {
stringRedisTemplate.opsForZSet()
.incrementScore(HOT_POST_RANK_KEY, "post:" + postId, score);
}
public Set<ZSetOperations.TypedTuple<String>> listHotPosts(int limit) {
return stringRedisTemplate.opsForZSet()
.reverseRangeWithScores(HOT_POST_RANK_KEY, 0, limit - 1);
}
}
场景六:最近联系人或最近会话列表
IM 系统、私信系统、客服系统中,经常需要展示最近会话列表。
Sorted Set 可以用来维护用户的最近会话。
业务需求
用户发送或接收消息后,将会话 ID 放入 Sorted Set,并使用最新消息时间作为 score。
这样查询最近会话时,只需要按照 score 倒序查询即可。
Redis 数据结构设计
key: user:recent:chat:{userId}
member: conversationId
score: 最新消息时间戳
示例:
user:recent:chat:1001 -> [
conversation:2001 -> 1710000200000,
conversation:2002 -> 1710000300000
]
Redis 操作示例
更新最近会话:
ZADD user:recent:chat:1001 1710000300000 conversation:2002
查询最近 20 个会话:
ZREVRANGE user:recent:chat:1001 0 19 WITHSCORES
删除某个会话:
ZREM user:recent:chat:1001 conversation:2002
Java 示例
@Service
public class RecentChatService {
private final StringRedisTemplate stringRedisTemplate;
public RecentChatService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 更新最近会话
*/
public void updateRecentChat(Long userId, Long conversationId) {
String key = "user:recent:chat:" + userId;
String member = "conversation:" + conversationId;
long now = System.currentTimeMillis();
stringRedisTemplate.opsForZSet()
.add(key, member, now);
}
/**
* 查询最近会话
*/
public Set<ZSetOperations.TypedTuple<String>> listRecentChats(Long userId, int limit) {
String key = "user:recent:chat:" + userId;
return stringRedisTemplate.opsForZSet()
.reverseRangeWithScores(key, 0, limit - 1);
}
/**
* 删除会话
*/
public void removeRecentChat(Long userId, Long conversationId) {
String key = "user:recent:chat:" + userId;
String member = "conversation:" + conversationId;
stringRedisTemplate.opsForZSet()
.remove(key, member);
}
}
场景七:按权重排序的推荐列表
在推荐系统中,可以将推荐分数作为 score,将内容 ID 作为 member。
例如:
- 商品推荐
- 文章推荐
- 视频推荐
- 用户推荐
- 搜索结果排序
Redis 数据结构设计
key: recommend:user:{userId}
member: itemId
score: 推荐分数
示例:
recommend:user:1001 -> [
item:1 -> 98.5,
item:2 -> 87.2,
item:3 -> 76.8
]
Redis 操作示例
写入推荐结果:
ZADD recommend:user:1001 98.5 item:1
ZADD recommend:user:1001 87.2 item:2
ZADD recommend:user:1001 76.8 item:3
查询推荐列表:
ZREVRANGE recommend:user:1001 0 9 WITHSCORES
移除低分推荐:
ZREMRANGEBYSCORE recommend:user:1001 0 60
Java 示例
@Service
public class RecommendService {
private final StringRedisTemplate stringRedisTemplate;
public RecommendService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 添加推荐内容
*/
public void addRecommendItem(Long userId, Long itemId, double score) {
String key = "recommend:user:" + userId;
String member = "item:" + itemId;
stringRedisTemplate.opsForZSet()
.add(key, member, score);
}
/**
* 查询推荐内容
*/
public Set<ZSetOperations.TypedTuple<String>> listRecommendItems(Long userId, int limit) {
String key = "recommend:user:" + userId;
return stringRedisTemplate.opsForZSet()
.reverseRangeWithScores(key, 0, limit - 1);
}
/**
* 移除低分推荐
*/
public void removeLowScoreItems(Long userId, double minScore) {
String key = "recommend:user:" + userId;
stringRedisTemplate.opsForZSet()
.removeRangeByScore(key, 0, minScore);
}
}
场景八:签到记录
Sorted Set 也可以用来记录用户签到历史。
不过需要注意,如果只是判断某一天是否签到,Bitmap 可能更节省空间;如果需要按照时间范围查询签到记录,Sorted Set 会更灵活。
Redis 数据结构设计
key: user:checkin:{userId}
member: yyyy-MM-dd
score: 签到时间戳
示例:
user:checkin:1001 -> [
2024-03-01 -> 1709222400000,
2024-03-02 -> 1709308800000
]
Redis 操作示例
添加签到记录:
ZADD user:checkin:1001 1709222400000 2024-03-01
判断某天是否签到:
ZSCORE user:checkin:1001 2024-03-01
查询最近签到记录:
ZREVRANGE user:checkin:1001 0 6 WITHSCORES
删除 1 年前的签到记录:
ZREMRANGEBYSCORE user:checkin:1001 0 1677686400000
Java 示例
@Service
public class CheckInService {
private final StringRedisTemplate stringRedisTemplate;
public CheckInService(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 签到
*/
public void checkIn(Long userId) {
String key = "user:checkin:" + userId;
LocalDate today = LocalDate.now();
long now = System.currentTimeMillis();
stringRedisTemplate.opsForZSet()
.add(key, today.toString(), now);
}
/**
* 判断某天是否签到
*/
public boolean hasCheckIn(Long userId, LocalDate date) {
String key = "user:checkin:" + userId;
Double score = stringRedisTemplate.opsForZSet()
.score(key, date.toString());
return score != null;
}
/**
* 查询最近签到记录
*/
public Set<ZSetOperations.TypedTuple<String>> listRecentCheckIn(Long userId, int limit) {
String key = "user:checkin:" + userId;
return stringRedisTemplate.opsForZSet()
.reverseRangeWithScores(key, 0, limit - 1);
}
}
四、Sorted Set 使用建议
1. score 的设计非常重要
Sorted Set 的排序能力来自 score,所以设计 score 时要结合业务场景。
常见设计方式:
| 场景 | score 设计 |
|---|---|
| 排行榜 | 积分、金额、数量 |
| 延时队列 | 执行时间戳 |
| 滑动窗口限流 | 请求时间戳 |
| 浏览历史 | 浏览时间戳 |
| 热门内容 | 热度分 |
| 推荐系统 | 推荐分数 |
| 最近会话 | 最新消息时间戳 |
2. member 要保证唯一性
Sorted Set 中的 member 是唯一的。
如果重复添加同一个 member,Redis 不会新增一条记录,而是更新它的 score。
例如:
ZADD rank:user:score 100 user:1001
ZADD rank:user:score 200 user:1001
最终只有一条记录:
user:1001 -> 200
如果需要记录多次请求、多次操作、多次事件,member 需要设计成唯一值。
例如滑动窗口限流中,不建议直接用时间戳作为 member,因为同一毫秒内可能有多个请求。
推荐写法:
String requestId = now + ":" + UUID.randomUUID();
3. 注意控制集合大小
Sorted Set 数据量过大会占用较多内存。
对于时间窗口、历史记录、浏览记录这类数据,建议定期清理。
例如:
ZREMRANGEBYSCORE user:browse:history:1001 0 1709395400000
也可以只保留最近 N 条:
ZREMRANGEBYRANK user:browse:history:1001 0 -101
这条命令的含义是:只保留分数最高的 100 条记录,删除其余更老的数据。
4. 高并发场景下使用 Lua 脚本保证原子性
如果一个业务逻辑需要多个 Redis 命令组合完成,就要考虑并发问题。
例如滑动窗口限流:
删除过期请求 -> 统计当前请求数 -> 写入当前请求
这三个操作如果不是原子的,在高并发下可能会导致限流不准确。
推荐使用 Lua 脚本:
redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
local count = redis.call('ZCARD', key)
if count >= max_requests then
return 0
end
redis.call('ZADD', key, now, request_id)
return 1
5. 根据访问模式设计 key
常见 key 设计:
rank:user:score
rate_limit:user:{userId}
delay:queue:order
user:browse:history:{userId}
hot:post:rank
user:recent:chat:{userId}
recommend:user:{userId}
user:checkin:{userId}
设计 key 时建议遵循:
业务名:模块名:维度:{id}
例如:
user:browse:history:1001
post:hot:rank
rate_limit:user:1001
五、Sorted Set 与其他 Redis 数据结构的选择
| 需求 | 推荐数据结构 | 原因 |
|---|---|---|
| 只需要去重 | Set | 更简单 |
| 需要排序 | Sorted Set | 支持 score 排序 |
| 需要排行榜 | Sorted Set | 天然支持排名 |
| 需要时间范围查询 | Sorted Set | 时间戳可作为 score |
| 需要简单队列 | List | 操作简单 |
| 需要延时队列 | Sorted Set | 执行时间可作为 score |
| 需要计数器 | String | 使用 INCR 即可 |
| 需要签到压缩存储 | Bitmap | 空间更省 |
| 需要字段对象 | Hash | 适合对象结构 |
六、总结
Redis Sorted Set 的核心能力是:
唯一 member + 可排序 score
它适合所有需要“排序”“排名”“范围查询”“按时间清理”的业务场景。
典型场景包括:
- 排行榜
- 滑动窗口限流
- 延时队列
- 历史数据清理
- 热门内容统计
- 最近会话列表
- 推荐列表
- 签到记录
在实际开发中,Sorted Set 最常见的两类 score 设计是:
1. 使用业务分数作为 score:排行榜、热度榜、推荐排序
2. 使用时间戳作为 score:限流、延时队列、历史记录、最近会话
如果业务中存在以下需求,就可以优先考虑 Sorted Set:
需要排序
需要排名
需要按分数范围查询
需要按时间范围查询
需要清理过期数据
需要查询 Top N 数据
Sorted Set 是 Redis 中非常实用的数据结构,掌握它可以很好地解决很多高频业务问题。