1. 当有序集合遇上C#

在游戏排行榜、实时竞价系统、延时任务队列等场景中,我们常常需要处理带有权重的有序数据。Redis的SortedSet(有序集合)正是为此而生,它通过score数值进行自动排序的特性,就像给每个元素贴上了价格标签的货架。今天我们将用C#的StackExchange.Redis客户端,深入探索这个数据结构的妙用。

2. 环境搭建与基础准备

首先通过NuGet安装最新版StackExchange.Redis(当前最新为2.7.58):

Install-Package StackExchange.Redis -Version 2.7.58

建立Redis连接的推荐方式:

using StackExchange.Redis;

// 创建复用连接(重要!不要频繁创建新连接)
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost:6379");
IDatabase db = redis.GetDatabase();

// 测试连接是否正常
if (db.Ping().TotalMilliseconds < 1000) 
{
    Console.WriteLine("成功连接Redis!");
}

3. 核心操作全解析

3.1 基础数据写入

我们以游戏积分榜为例准备测试数据:

// 清空旧数据(测试环境使用)
db.KeyDelete("player_scores");

// 批量添加玩家得分(参数依次:键名,分数,成员)
var players = new[] 
{
    new SortedSetEntry("player_001", 1500),
    new SortedSetEntry("player_002", 3200),
    new SortedSetEntry("player_003", 2750)
};
db.SortedSetAdd("player_scores", players);

// 单个添加
db.SortedSetAdd("player_scores", "player_004", 4100);

3.2 范围查询三剑客

案例1:获取前3名玩家

// 参数说明:键名,起始排名(0表示第一个),结束排名,排序方式
RedisValue[] top3 = db.SortedSetRangeByRank(
    "player_scores", 
    start: 0, 
    stop: 2, 
    order: Order.Descending);

Console.WriteLine($"冠军:{top3[0]}"); // 输出:player_004

案例2:查询2000-3000分的玩家

var midPlayers = db.SortedSetRangeByScore(
    "player_scores", 
    min: 2000, 
    max: 3000, 
    order: Order.Descending);

// 输出:player_003(2750分)、player_001(1500分不满足)

案例3:带分页的查询

// 每页5条,获取第2页数据(跳过前5条)
var page2 = db.SortedSetRangeByScore(
    "player_scores",
    skip: 5,
    take: 5,
    order: Order.Descending);

3.3 高阶排序技巧

带分数返回的查询

var withScores = db.SortedSetRangeByRankWithScores(
    "player_scores", 
    start: 0, 
    stop: -1, 
    order: Order.Descending);

foreach (var item in withScores)
{
    Console.WriteLine($"{item.Element}: {item.Score}分");
}

组合条件查询

// 查询分数大于等于3000的玩家数量
long count = db.SortedSetCount("player_scores", min: 3000, max: double.PositiveInfinity);
Console.WriteLine($"高分玩家数量:{count}"); // 输出:2

4. 典型应用场景

4.1 实时排行榜系统

每小时更新电商商品热度值:

// 每次点击增加10点热度
db.SortedSetIncrement("product_hot", "product_123", 10);

// 获取Top10热门商品
var hotProducts = db.SortedSetRangeByRank("product_hot", 0, 9, Order.Descending);

4.2 延时任务队列

处理30分钟后到期的订单:

// 将订单ID和到期时间戳存入有序集合
double expireTime = DateTimeOffset.Now.AddMinutes(30).ToUnixTimeSeconds();
db.SortedSetAdd("delay_orders", "order_888", expireTime);

// 定时任务查询到期订单
var currentTime = DateTimeOffset.Now.ToUnixTimeSeconds();
var readyOrders = db.SortedSetRangeByScore("delay_orders", 0, currentTime);

5. 技术选型分析

优势亮点:

  • 内存级性能:10万级数据量查询可在毫秒级完成
  • 自动排序:无需额外代码维护排序逻辑
  • 灵活查询:支持分数范围、排名区间、分页等多种方式
  • 原子操作:ZADD、ZINCRBY等命令保证并发安全

潜在局限:

  • 内存消耗:超大集合需要合理设置过期时间
  • 分页性能:深度分页(如第1000页)效率较低
  • 精度问题:分数使用double类型存储,需注意精度控制

6. 避坑指南

6.1 键命名规范

建议采用业务:类型:ID的格式:

// 推荐写法
const string KEY = "game:rank:season2";

6.2 连接复用策略

创建连接池的正确方式:

// 使用Lazy创建线程安全连接
private static readonly Lazy<ConnectionMultiplexer> LazyConnection = 
    new Lazy<ConnectionMultiplexer>(() => 
        ConnectionMultiplexer.Connect("localhost:6379"));

public static ConnectionMultiplexer Connection => LazyConnection.Value;

6.3 大数据量优化

当处理百万级数据时:

// 使用SCAN迭代代替一次性获取
var allElements = new List<RedisValue>();
long cursor = 0;
do 
{
    var result = db.SortedSetScan("big_key", cursor: cursor);
    cursor = result.Cursor;
    allElements.AddRange(result.Items.Select(x => x.Element));
} while (cursor != 0);

7. 总结与展望

通过本文的实战演练,我们掌握了使用StackExchange.Redis操作有序集合的核心技巧。从基础的增删改查到复杂的分页、范围查询,Redis的有序集合展现了强大的排序能力。在后续开发中,可以结合管道(Pipeline)技术提升批量操作效率,或是尝试使用RediSearch实现更复杂的查询需求。记住,技术选型时要根据具体场景选择最合适的工具,就像在超市选购商品——有序集合是那个贴满价签的智能货架,而我们要做聪明的采购者。