Redis Sorted Set 的典型使用场景

一、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

查询分数在 80100 之间的用户。

6. 删除指定元素:ZREM

ZREM rank:user:score user:1003

7. 按分数范围删除:ZREMRANGEBYSCORE

ZREMRANGEBYSCORE rank:user:score 0 60

删除分数在 060 之间的元素。

三、典型使用场景

场景一:排行榜系统

排行榜是 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,每次请求进来时:

  1. 删除窗口期之前的请求记录
  2. 统计当前窗口内的请求数量
  3. 判断是否超过限制
  4. 如果没有超过限制,则写入本次请求记录

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 脚本实现:

  1. 查询到期任务
  2. 删除该任务
  3. 返回任务 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 中非常实用的数据结构,掌握它可以很好地解决很多高频业务问题。

使用 Hugo 构建
主题 StackJimmy 设计