1. Priority Queues#

堆分为最大最小堆, 插入和删除复杂度都是 O(log n), 通常通过数组实现, 平时的用法比如 heap sort, priority queues, 今天我们说的优先队列就是最小堆实现的, 一般优先队列就像array, list, hashmap 每个语言内都内置了, 平时用于开发系统级别, 比较 low-level 的系统会用到, 而 redis sorted sets 一般都是用在后端开发业务逻辑中, 而且本质上, 它也是优先队列, 这里对比一下, 毕竟都是和排序有关系

2.1. 默认排序规则(自然顺序)#

默认情况下,PriorityQueue 是一个最小堆,即:

  • 自然顺序要求元素实现 Comparable 接口,并在其 compareTo 方法中定义比较逻辑
  • 队列的队首(peek/poll 获取的元素)始终是最小的元素(根据 compareTo 的结果)
  • 例如,对于整数,PriorityQueue 会将最小的整数放在队首;对于字符串,会按字典序( lexicographical order)排序
import java.util.PriorityQueue;

public class Main {
    public static void main(String[] args) {
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        pq.offer(5);
        pq.offer(2);
        pq.offer(8);
        System.out.println(pq.poll()); // 输出: 2(最小值)
        System.out.println(pq.poll()); // 输出: 5
        System.out.println(pq.poll()); // 输出: 8
    }
}
  • 这里,Integer 实现了 ComparablecompareTo 按数值大小比较,因此 PriorityQueue 按升序排列,队首是最小值

2.2. 自定义排序规则(使用 Comparator)#

  • 你可以通过在构造 PriorityQueue 时传入一个 Comparator 对象来自定义排序规则

  • Comparator 的 compare 方法定义了元素的优先级顺序

  • 如果提供了 ComparatorPriorityQueue 会根据它来决定元素的顺序,而不是依赖 Comparable

  • 仍然默认是最小堆,队首是根据 Comparator 定义的“最小”元素

import java.util.PriorityQueue;
import java.util.Comparator;

public class Main {
    public static void main(String[] args) {
        // 自定义 Comparator,按降序排序(大的值优先)
        PriorityQueue<Integer> pq = new PriorityQueue<>(Comparator.reverseOrder());
        pq.offer(5);
        pq.offer(2);
        pq.offer(8);
        System.out.println(pq.poll()); // 输出: 8(最大值)
        System.out.println(pq.poll()); // 输出: 5
        System.out.println(pq.poll()); // 输出: 2
    }
}

2.3. 排队服务 任务调度系统#

  • 用户上传视频后,系统会排队转码(比如压缩、加字幕等)
  • 有的用户是普通用户,优先级低
  • 有的用户是 VIP,优先级高

建一个「优先队列」来排这些转码任务:

import java.util.PriorityQueue;

// 转码任务类
class TranscodeTask implements Comparable<TranscodeTask> {
    private String videoId;
    private int priority; // 优先级:数字越小优先级越高
    private boolean isVip;
    
    public TranscodeTask(String videoId, boolean isVip) {
        this.videoId = videoId;
        this.isVip = isVip;
        // VIP用户优先级为1,普通用户优先级为2
        this.priority = isVip ? 1 : 2;
    }
    
    // 实现Comparable接口,用于优先队列排序
    @Override
    public int compareTo(TranscodeTask other) {
        // 按优先级排序,数字小的在前
        return Integer.compare(this.priority, other.priority);
    }
    
    @Override
    public String toString() {
        return "视频: " + videoId + " [" + (isVip ? "VIP用户" : "普通用户") + 
               ", 优先级: " + priority + "]";
    }
}

// 视频转码服务
class TranscodingService {
    private PriorityQueue<TranscodeTask> queue;
    private int maxConcurrentTasks;
    
    public TranscodingService(int maxConcurrentTasks) {
        this.queue = new PriorityQueue<>();
        this.maxConcurrentTasks = maxConcurrentTasks;
    }
    
    // 添加任务到队列
    public void addTask(String videoId, boolean isVip) {
        TranscodeTask task = new TranscodeTask(videoId, isVip);
        queue.offer(task); // 添加到优先队列
        System.out.println("任务已添加: " + task);
    }
    
    // 处理队列中的任务
    public void processTasks() {
        System.out.println("\n开始处理队列中的任务,最多同时处理" + maxConcurrentTasks + "个任务");
        
        // 模拟处理maxConcurrentTasks个任务
        for (int i = 0; i < maxConcurrentTasks && !queue.isEmpty(); i++) {
            TranscodeTask task = queue.poll(); // 取出优先级最高的任务
            System.out.println("正在处理: " + task);
        }
        
        System.out.println("队列中剩余任务数: " + queue.size());
    }
}

// 测试优先队列
public class VideoTranscodingDemo {
    public static void main(String[] args) {
        TranscodingService service = new TranscodingService(2); // 最多同时处理2个任务
        
        // 添加任务,普通用户和VIP用户混合
        service.addTask("video1", false); // 普通用户
        service.addTask("video2", true);  // VIP用户
        service.addTask("video3", false); // 普通用户
        service.addTask("video4", true);  // VIP用户
        service.addTask("video5", false); // 普通用户
        
        // 处理任务
        service.processTasks();
        
        // 再次处理剩余任务
        service.processTasks();
    }
}

2. Redis Sorted Set#

A Redis Sorted Set is a data type where each element is associated with a score, and elements are automatically sorted by that score. It behaves like a priority queue — but it’s persistent, distributed, and accessible over the network.

2.1. Redis Sorted Set 的特点#

  1. 每个元素都有一个关联的分数,用于排序
  2. 元素按分数从小到大排序(可以使用 ZREVRANGE 等命令反向获取)
  3. 相同分数的元素按字典序排序
  4. 支持范围查询、取 top-N 等操作
  5. 元素不可重复,但分数可以重复

3. 二者对比#

3.1. 实现原理#

特性 Redis Sorted Set 优先队列
数据结构 基于跳表实现 通常基于二叉堆、斐波那契堆等实现
元素唯一性 元素必须唯一 允许重复元素
排序依据 按分数(score)排序 按优先级排序
访问方式 可随机访问任意范围的元素 通常只能访问队头元素(最高/最低优先级)
操作复杂度 大多数操作 O(log(N)) 插入/删除通常为 O(log(N))
分布式支持 原生支持分布式环境 通常为单机内存数据结构
持久化 支持 通常不支持
内存占用 相对较高 相对较低
同分数/优先级处理 同分数按字典序排序 实现决定(通常不保证顺序)
范围查询 支持高效的范围查询 通常不支持

跳表(Skip List)是一种基于链表的数据结构, 通过在原始链表上构建多层索引来加速查找操作, 从而提供高效的动态集合实现, 它支持快速的查找、插入和删除操作, 在某些场景下性能可以媲美平衡树(如红黑树), 但实现上更为简单

跳表由以下几个部分组成:

  1. 原始链表:最底层是一个有序的链表,包含所有元素
  2. 索引层:在原始链表之上有多层索引,每一层是从下一层中随机选择部分节点构成的“稀疏”版本
  3. 头节点:每层都有一个头节点,用于从顶部开始查找

跳表的工作原理

  • 查找:从最高层的头节点开始, 沿着每一层的指针向右移动, 如果当前层的下一个节点键值大于目标键, 则下降到下一层继续查找, 直到找到目标元素或确认其不存在
  • 插入:先查找插入位置,然后随机决定新节点的层数,并在对应层中插入节点
  • 删除:先找到要删除的节点,然后在每一层中移除该节点

跳表的层数选择是随机的, 这种随机化保证了结构的平衡性, 使查找、插入和删除的平均时间复杂度达到 O(log n)

3.2. 使用场景#

场景 Redis Sorted Set 优先队列
排行榜系统 ✅ 适合:- 范围查询支持获取前N名- 分数更新自动重排序- 持久化保证数据不丢失 ❌ 不适合:- 无法高效获取范围数据- 通常无持久化能力
任务调度 ✅ 适合:- 分布式环境下多服务共享队列- score可用时间戳表示执行时间- 支持任务更新和取消 ✅ 适合:- 单机环境下内存效率高- 操作延迟低- 适合实时系统
延迟队列 ✅ 非常适合:- score设为执行时间- ZRANGEBYSCORE可查询该执行的任务- 分布式环境可靠 ⚠️ 有限支持:- 需要额外的定时器机制- 分布式环境需要额外协调
社交网络关系 ✅ 适合:- 可存储用户关系并按亲密度排序- 支持查询Top N好友- 支持范围查询 ❌ 不适合:- 无法进行复杂的关系查询
实时分析系统 ✅ 适合:- 可用于时间序列数据存储- 支持时间范围查询- 分布式环境下的数据共享 ⚠️ 部分适合:- 处理速度快但功能有限- 不支持复杂查询
图算法 ❌ 不适合:- 网络延迟影响性能- API不适合图算法需求 ✅ 非常适合:- Dijkstra等算法的核心组件- 快速获取最小/最大元素
地理位置应用 ✅ 适合:- 结合GEO命令可存储位置并按距离排序- 支持范围查询附近的POI ❌ 不适合:- 无地理位置特性支持
限流系统 ✅ 适合:- score设为时间戳- ZREMRANGEBYSCORE删除过期令牌- 原子操作保证一致性 ⚠️ 有限支持:- 需要额外逻辑处理过期
大数据处理 ✅ 支持:- 可处理大量数据(内存限制)- 支持集群扩展 ⚠️ 受限:- 受单机内存限制- 扩展性差
分布式应用 ✅ 原生支持:- 多客户端可并发访问- 支持主从复制和集群 ❌ 需要额外实现:- 需要自行处理分布式一致性- 需要额外的服务协调