Redis核心数据结构精讲:从String到Zset的选型与排行榜实战
先说说我为什么想写这篇东西。我见过太多人一开始玩 Redis把命令背得滚瓜烂熟SET、GET、LPUSH、SADD 张口就来可真到业务里要做选型的时候犹豫了用户信息到底放 String 还是 Hash消息队列能不能直接用 List积分排行榜除了 Zset 还有没有别的办法这些问题本质上不是命令不会敲而是对 Redis 核心数据结构的设计逻辑没吃透。这篇文章我就用“排行榜应用”这条主线把 String 到 Zset 五种核心数据结构从头到尾撸一遍不光讲命令和底层原理更重要的是讲清楚每个结构适合什么场景、不适合什么场景、踩过哪些坑。适合刚上手 Redis 的开发者也适合写了两三年 Redis 但没系统梳理过数据结构边界的同学。看完你应该能自己做选型而不是每次都得去搜“Redis 排行榜怎么实现”。1. 先弄明白Redis 为什么能这么快很多人以为 Redis 快是因为“数据在内存里”这话对了一半。内存存储只是基础真正让 Redis 在单线程模型下还能扛住高并发的原因是它的数据结构设计足够“聪明”。这里说的聪明不是用了什么黑科技而是针对不同的读写模式选择了实现复杂度最合理的那套方案。1.1 内存存储与单线程模型对数据结构设计的影响Redis 的命令执行核心是单线程的这一点哪怕到了 Redis 7 支持多线程 IO真正的命令执行逻辑依然是单线程。这意味着同一时刻只有一个命令在跑不存在多个线程同时修改数据的竞争问题也就不需要加锁。好处是简单、原子性强坏处是任何一个命令都不能太“重”否则会把后面所有请求都堵住。这就直接决定了 Redis 内部数据结构的设计取向。举例来说List 的结构用的是链表而不是数组因为链表在头部和尾部插入元素是 O(1)而数组在头部插入需要把整个数组往后挪O(N) 的成本在单线程模型下不可接受。Zset 用跳表而不是红黑树也不是随便选的红黑树虽然查找效率也很高但实现复杂而且做范围查询比如“取分数从 100 到 200 的所有成员”不如跳表直观、简单、高效。Redis 是追求极致性能和简洁实现的项目跳表能用几十行代码实现清楚红黑树做不到。明白了“单线程导致命令必须轻量”这个前提你就很容易理解后面所有关于数据结构的选型逻辑你能不能用这个结构存数据、怎么用核心就看你这个业务场景的读写模式够不够“轻”。1.2 五种结构一句话定位在深挖每个结构之前我先用一句话给它们做个定位后面展开的时候你们会有更具体的认知。String最朴素的 key-value适合存单值、计数器、简单缓存。Hash一个 key 对应多个字段适合存对象可以单独更新某个字段。List有序的字符串链表适合做消息队列、时间线列表。Set无序且自动去重的集合适合做标签、抽奖池、共同关注。Zset带分数的有序集合适合做排行榜、延迟队列、滑动窗口。你看这五种结构基本覆盖了程序里最常见的五种读写模式读一个值、读对象的多个字段、按顺序读一批值、判断某个值是否存在、按分数排序并读取。绝大多数业务场景都能归到这五种模式里这也是为什么 Redis 只有五种核心结构却能支撑起几乎整个互联网后端生态的原因。2. String 与 Hash把单个值和多个字段分清String 和 Hash 在缓存场景里经常被放在一起比较因为它们都适合存“一个东西”。区别在于你想存的是一个简单的值还是一个有多个属性的对象。这个选择看着简单但实际业务里我见过太多用错的案例。2.1 String 的底层类型与典型操作String 的底层并不是 C 语言里的标准字符串而是 Redis 自己封装的 SDSSimple Dynamic String。SDS 在 O(1) 时间内获取字符串长度并且能避免缓冲区溢出问题这也是 Redis 能安全处理大量字符串操作的基础。String 最经典的应用有三类。第一类是缓存把序列化后的 JSON 直接塞进去读取的时候反序列化回来典型的如商品详情、配置信息。第二类是计数器INCR、DECR、INCRBY 都是原子操作适合做秒杀库存扣减、点赞数、访问统计注意这里不需要先 GET 再 SET直接 INCR 即可天然防并发超卖。第三类是分布式锁用 SET key value NX PX 毫秒数 实现加锁用 Lua 脚本或 DEL 配合 value 校验实现解锁。关于分布式锁我想多说一句。网上很多教程教你 SETNX 加锁、DEL 解锁但一个常见的坑是如果锁的过期时间设置太短业务还没执行完锁就自动释放了另一个进程拿到锁就会出问题。我在项目里一般用 Redisson 之类的库它会自动续期不需要在业务代码里手动管续期逻辑。当然如果你只想用原生 Redis 命令手写那至少要把过期时间设成业务预估耗时的 5 到 10 倍并且加锁时带上唯一标识解锁前校验标识防止把别人持有的锁误删。String 虽然简单但用错了照样给你挖坑。2.2 Hash对象缓存的最佳选择Hash 结构一个 key 底下可以挂很多 field每个 field 又是一个独立的 value。如果你要缓存用户信息包含昵称、头像、等级、积分、最后登录时间这五个字段用 Hash 会很舒服HSET user:10086 nickname 张三 avatar /img/1.png level 5 score 3200 last_login 1700000000 HGET user:10086 nickname HINCRBY user:10086 score 100注意最后一条 HINCRBY这个操作在排行榜里非常常用因为它可以直接给某个用户的 score 字段增加分值而不需要先读出来、程序里加、再写回去。HINCR 系列是原子操作在并发场景下不会出错这个性质后面讲排行榜的时候还会用到。Hash 和 String 最大的区别是字段级更新能力。String 存 JSON 的话你想改其中一个字段得把整个 JSON 读出来、反序列化、改字段、再序列化、写回去这一整串动作在高频更新下既消耗网络又消耗 CPU。Hash 则可以直接针对某个字段做读写计数代价又小又原子。判断标准其实很简单你的数据是不是有多个属性、属性之间是不是独立更新如果是用 Hash如果只是一个孤零零的值比如验证码、token、计数器用 String。我用过的一个购物车场景就是典型的 Hash 应用以 用户ID 为 key商品ID 为 field商品数量为 value。用户加购、减购、改数量都是 HSET 或 HINCRBY 一下的事结算时 HGETALL 把整个购物车捞出来就行。比在关系型数据库里建购物车表轻量太多了。2.3 选型对照表什么时候用 String什么时候用 Hash为了让你一眼看明白我把它们放到一张表里对比场景特征推荐结构原因单值数据验证码、token、计数器、限流计数String命令简单INCR/DECR 原生原子简单缓存整个 JSON 一次性读、一次性写不拆字段String序列化一次到位代码简单多字段对象且经常更新其中一个字段HashHSET、HINCRBY 可单独更新字段省流量多字段对象但总是整体读写String相比 Hash 更节省内存小对象时 Hash 也有压缩优化需实测判断一个我个人的经验是如果你不确定数据未来会不会拆字段更新优先选 Hash。因为从 String 改成 Hash 涉及缓存 key 的数据迁移比较麻烦而从一开始就用 Hash哪怕后来用不上字段级更新损失也不大。这个思路在项目里帮我少踩了好几次坑。3. List 与 Set有序的队列与无序的集合List 和 Set 表面上都是“存一组数据”但一个保持插入顺序且允许重复一个完全不关心顺序且自动去重。这个差异决定了它们在业务里的定位完全不同。3.1 List 双向链表消息队列和最新列表List 在 Redis 里是双向链表实现支持从头部或尾部推入、弹出数据组合起来可以做几种常见的事。最经典的是做简单任务队列。生产者用 LPUSH 把任务塞进队列消费者用 BRPOP 阻塞弹出。BRPOP 和 RPOP 的区别在于RPOP 没数据时直接返回空你得自己写循环去轮询既浪费 CPU 又增加延迟BRPOP 可以指定阻塞超时时间没数据时就挂在那里等待一旦有数据立刻返回极大减少空轮询。我在项目里用这个模式做过异步通知队列效果很好几千上万的任务量完全扛得住没有引入额外中间件。List 还有一个很适合的场景最新列表。比如一个用户的最近操作记录、一篇帖子的最新评论。做法是 LPUSH 新数据然后 LTRIM 限制列表长度只保留最近 100 条。LTRIM key 0 99 这个命令会剪掉超出范围的部分相当于自动清理老数据防止列表无限增长。这是我个人很推荐的一个“有界列表”方案比定期删除干净利落得多。需要提醒的是List 的 LINDEX 和 LSET 这类随机访问操作是 O(N) 的如果数据量大还频繁做随机访问性能会很难看。另外如果队列积压数据量巨大几十万上百万List 占的内存会很大这时候就不太适合继续用 Redis 队列了建议上真正的消息中间件把数据生命周期的管控能力接过来。3.2 Set 去重集合标签、随机、抽奖Set 最核心的能力就是去重而且去重是由 Redis 内部完成的你不需要先查询存在性再插入。这个特性在做标签系统时非常自然给一篇文章打标签、给用户打兴趣标签用 SADD 反复添加同一个标签也不会重复。几个常用命令SADD user:10086:tags 游戏 篮球 音乐 SISMEMBER user:10086:tags 篮球 SCARD user:10086:tags SINTER user:10086:tags user:10087:tagsSINTER 可以取两个集合的交集这在做“共同关注”“相似用户推荐”时非常好用。还有 SRANDMEMBER 和 SPOP前者是随机返回一个成员但不删除适合做“随机抽一个用户展示”这类活动后者是随机返回并删除适合做成“奖池抽奖”保证一个用户只能抽到一次且奖品不会重复发放。我在一个活动系统里做过抽奖用 SPOP 把奖池里的优惠券 ID 弹出配合一个用户中奖记录 Set基本实现了零重复的抽奖逻辑。坑也要提醒SMEMBERS 会返回集合里全部成员如果一个 Set 里有几百万元素这个命令会造成 Redis 阻塞线上有可能会出事故。正确做法是业务里尽量用 SSCAN 分批迭代获取或者干脆用 SRANDMEMBER / SPOP 等只取部分成员的命令。这个坑在单个 Set 很大时是真实的阻塞源搞不好会把整个 Redis 拖垮。3.3 List 与 Set 的选择思路简单总结它们的取舍需求推荐结构关键命令按时间顺序排列、允许重复、需要读取最近 N 条ListLPUSH LTRIM先进先出队列、阻塞等待消费ListLPUSH BRPOP自动去重、判断存在性、交集并集运算SetSADD / SISMEMBER / SINTER / SUNION随机抽奖、奖池不重复、随机推荐SetSPOP / SRANDMEMBER一句话你需要“顺序”就选 List你需要“唯一性”就选 Set。如果既需要顺序、又需要去重、还需要带分数排序那就请看下一章 Zset。4. Zset 深挖跳表 排行榜实战Zset 是整篇文章的主角。它和 Set 一样是去重的但每个成员额外关联了一个 double 类型的分数scoreRedis 会按分数从小到大把成员排好序。这就让它成为做排行榜的天然数据结构。下面我先把它的底层实现讲透再上实战。4.1 结构本质与底层实现理解 Zset 的本质你只需要记住三个角色成员member、分数score、排名rank。成员是唯一的分数可以相同Redis 内部会维持一个按 score 升序排列的跳表同时还有一个哈希表记录 member 到 score 的映射。哈希表保证 O(1) 查到某个成员的分数跳表保证按分数范围查询和按排名查询都是 O(logN)。跳表是什么你可以把它想象成一个多层的高速公路。最底层是普通链表每个节点都指向下一个第二层开始每隔几个节点抽一个“快车道”直接跳过中间节点层数越高跳过的节点越多。查找的时候从最上层开始先走快车道大幅度向前然后逐层降级细化最后到底层定位目标。这个“多层索引”的构思很像二分查找但不需要像数组那样要求连续内存在链表上就能实现而且写起来比红黑树简单得多。Redis 为什么选跳表而不是红黑树两个主要原因一是跳表实现起来直观得多插入、删除、查找的代码量远小于红黑树二是跳表做区间查询有天然优势比如你想取 score 值在某个范围内的所有成员跳表按序走下去就行红黑树则要反复做中序遍历代码复杂度高不少。Redis 的设计哲学是“用简单实现解决 99% 的问题”Zset 用跳表就是这种哲学的最佳体现。另外补充一点Zset 在数据量比较小的时候底层并不是跳表而是用压缩列表ziplist新版本叫 listpack存储的。只有当元素数量超过 128 或者某个成员的长度超过 64 字节它才真正转成跳表加哈希表结构。这是 Redis 做小数据优化的一种手段保证小对象也能非常节省内存。4.2 Zset 常用命令盘点先过一遍最常用的命令防止后面实战代码你看着陌生。ZADD leaderboard 100 user_1 # 新增或更新user_1 的分数是 100 ZINCRBY leaderboard 10 user_1 # 给 user_1 加 10 分 ZSCORE leaderboard user_1 # 查询 user_1 当前分数 ZRANK leaderboard user_1 # 查询 user_1 分数从低到高排第几名从 0 开始 ZREVRANK leaderboard user_1 # 查询 user_1 分数从高到低排第几名从 0 开始 ZRANGE leaderboard 0 4 # 按分数从低到高取前 5 名 ZREVRANGE leaderboard 0 4 # 按分数从高到低取前 5 名 ZRANGEBYSCORE leaderboard 100 200 # 取分数在 100 到 200 之间的成员含端点 ZREMRANGEBYRANK leaderboard 0 99 # 删除排名最靠前的 100 个成员有几个细节值得注意。ZRANGE 和 ZREVRANGE 的下标从 0 开始所以取 Top10 就是 ZREVRANGE key 0 9。ZRANK 返回的排名从 0 开始如果你要展示给用户看“第 1 名”程序里要加 1。ZADD 不只是新增它还可以更新已存在成员的分数同一个命令重复执行没有问题。这些命令单独看都很简单组合起来就能做很多事情。4.3 排行榜业务场景积分总榜与同分排序的坑现在进入重头戏假设我们要做一个游戏积分排行榜玩家通过完成关卡获得积分需要展示全服前 100 名并且实时更新。最基础的版本很简单ZADD game_rank 0 user_1 ZINCRBY game_rank 50 user_1 # 玩家完成一关加 50 分 ZREVRANGE game_rank 0 99 WITHSCORES # 取前 100 名带分数展示每次玩家得分执行一次 ZINCRBY排行榜页面数据量大、要求实时性不高时可以每 10 秒把前 100 名同步到本地缓存或 CDN避免每次都打 Redis。这个基本流程不难但真正做过排行榜的人都知道最麻烦的问题是同分怎么排名Redis 默认对分数相同的成员按 member 的字典序排。也就是说分数一样时user_a 会排在 user_b 前面因为 a 比 b 大。这在业务上几乎没有意义。游戏行业的常见需求是“同分时先到先得”也就是谁先达到这个分数谁排前面。一个业界常用的解法是把时间戳编码进 scorescore 实际分数 * 一个足够大的权重 (一个大数 - 时间戳)。比如权重取 10 的 10 次方大数取 9999999999这样分数每增加 1保底拉开 100 亿的 score 差距同分的人再按“大数 - 时间戳”这部分的差异决定先后越早到达的人拿到更大的后缀排名自然更靠前。这里必须提醒精度问题。Zset 的 score 是 double 类型超过 2 的 53 次方约 9 千万亿之后就不能精确表示每个整数了。所以如果实际积分的数量级很大权重就不能选得太大。比如实际积分能达到 1 亿权重选 10 的 10 次方score 就达到了 10 的 19 次方直接超出精度范围排名会错乱。稳妥的做法是先把业务可能的积分上限估算出来再定权重保证 score 最大值远小于 2^53。我之前做过一个积分排行榜预计人均积分不超过 100 万权重选了 10 的 6 次方后缀取了 10 的 7 次方减时间戳整体 score 范围控制在 10 的 13 次方以内精度完全安全。还有几个常见问题要处理。比如只展示前 100 名的话底层数据不用全量展示但 Zset 里还是可以存所有玩家排名会用 ZRANK/ZREVRANK 查查询成本很低。比如玩家流失严重长期不活跃的人还占着 Zset 容量你可以定期用 ZREMRANGEBYRANK 删除分数最低的一部分人或者根据业务在每天凌晨跑一次冷数据清理。再比如高并发下频繁 ZINCRBY 会不会性能有问题ZINCRBY 是单个命令天然原子单线程执行每个操作 O(logN)在几百万成员的 Zset 上也就几十微秒级别完全扛得住一般业务量。4.4 扩展玩法同分并列的排行榜怎么实现上一节讲的是把时间戳编码进 score 实现先到先得这样每个人排名是唯一的。但有些业务希望同分的人显示相同名次比如分数一样都是第 2 名而不是一个第 2 一个第 3。这时候怎么处理实现思路很简单先用 ZREVRANK 拿到某个玩家的“物理排名”再取这个玩家的分数最后向前扫描和它分数相同的玩家把所有相同分数玩家的最小物理排名作为展示名次。用伪代码表示function showRank(userId): rank redis.ZREVRANK(key, userId) # 物理排名0 开始 score redis.ZSCORE(key, userId) # 用户分数 while rank 0: prev redis.ZREVRANGE(key, rank-1, rank-1) prevScore redis.ZSCORE(key, prev[0]) if prevScore ! score: break rank rank - 1 return rank 1 # 展示名次从 1 开始这个实现有一个性能点如果同分的人非常多向前扫描的次数会很多。不过实际排行榜里同分人数通常不会特别夸张而且这个逻辑可以加一层本地缓存或者干脆在服务端把前 100 名的名次一次性算好定期刷新用户查询时直接读缓存就不存在性能问题了。我把这个方法用在过一个社区积分榜上效果很稳定。再扩展一点如果你需要实时查看“我前一名是谁、后一名是谁”可以用 ZREVRANGE key rank-1 rank-1 和 ZREVRANGE key rank1 rank1 分别取前后玩家的 member再配合 ZSCORE 展示分数。这些操作都是 O(logN)在高并发下也能保持很低延迟。4.5 排行榜之外Zset 还能做延迟队列和滑动窗口排行榜是 Zset 最出名的应用但它本身的能力远不止于此。延迟队列就是另一个经典玩法member 是任务 IDscore 是任务要执行的时间戳。消费者定时用 ZRANGEBYSCORE key -inf now 取出所有已经到了执行时间的任务然后 ZREM 把它们删掉开始执行。这个方案实现简单、精度足够比在数据库里扫过期任务要高效很多。再比如滑动窗口限流score 存当前时间戳member 存请求 ID窗口大小比如 1 分钟。每来一个请求ZADD 当前时间戳然后 ZREMRANGEBYSCORE key -inf 60秒前 把窗口外的数据清掉再用 ZCARD 统计窗口内请求数超过阈值就限流。这个方案用 Zset 实现了灵活的滑动窗口比固定窗口限流平滑很多。我实际在接口限流里用过这个套路逻辑非常清爽维护成本极低。所以 Zset 不只是“排行榜神器”它本质上是“一个带分数的有序集合”凡是需要按某个指标排序、筛选、计数、范围读取的场景它都可以介入。你把它理解成“内存里的数据库索引”很多应用思路就打开了。5. 数据结构选型速查表和常见坑前面把每种结构都过了一遍最后我做一个汇总再集中讲几个贯穿所有结构的常见问题。这些坑大部分是我自己在项目里踩过或者在帮别人 review 代码时见过的希望你能直接绕过去。5.1 选型速查表一个比较通用的对照表适合贴在工位上业务需求推荐结构核心命令示例验证码、token、缓存单值、计数器StringSET / GET / INCR / EXPIRE用户信息、商品信息、对象属性缓存HashHSET / HGET / HINCRBY任务队列、简单消息队列、最新列表ListLPUSH / BRPOP / LTRIM标签、去重、抽奖、共同关注SetSADD / SISMEMBER / SINTER / SPOP排行榜、延迟队列、滑动窗口限流ZsetZINCRBY / ZREVRANGE / ZRANGEBYSCORE分布式锁StringSET NX PX / Lua 解锁这个表不复杂真正复杂的是业务里往往是多个结构组合使用。比如一个社区 Feed 流用户发帖时用 List 存时间线用 Zset 存热门度排序用 Set 存用户标签再用 String 做计数缓存。Redis 的五种结构不是互斥选项而是可以组合的积木取决于你愿意怎么去搭。5.2 大 key 与热 key 问题大 key 是我最想强调的坑。一个 Hash 有几十万字段、一个 Set 有几十万成员、一个 Zset 有几十万成员这些都会成为大 key。大 key 的危害不只是占内存多而是很多操作是 O(N) 的比如 HGETALL、SMEMBERS、ZRANGE 全量输出在高并发下直接拖垮 Redis 单线程。预防手段有两个方向。第一业务上尽量不要让单个 key 无限增长比如 List 用 LTRIM 限制长度Set 定期裁剪Zset 定期清理冷数据成员。第二在 Redis 5.0 以上可以用 OBJECT ENCODING 命令查看某个 key 的编码用 SCAN 扫描大 key 做监控或者干脆用 --bigkeys 参数定期找出大 key然后针对性地拆分或转移。我在一个项目里就是靠这个扫出一个 Hash 达到了几十万字段当时线上偶尔出现几秒的卡顿找了一圈最后定位到这个拆分之后立竿见影。热 key 是另一个问题。比如一个排行榜 Top1 的玩家所有人都要看他的分数这个 key 会被高频读取。解决办法一般是加一层本地缓存如本地内存缓存 TopN 数据比如 5 秒刷新一次或者引入 Redis 集群的从节点分担读流量再或者把热 key 加随机后缀分片到多个位置读的时候随机取一个。这部分看你的业务体量如果只是个人项目或者中小团队本地缓存基本就足够了。5.3 过期策略与内存淘汰Redis 的过期机制是惰性删除加定期删除配合惰性删除是访问 key 时发现过期才删定期删除是后台任务每 100ms 抽一部分 key 检查是否过期。大批 key 同一时间过期会造成短暂卡顿所以设置过期时间时最好加一个随机偏移量。但要注意一点排行榜这种场景不适合给 Zset 的 key 设置过期。如果你给整个排行榜设置了过期时间时间一到全部数据就没了玩家积分全部推翻这在业务上是不可接受的。排行榜要么不设过期要么做冷热分离把一个月前的数据定期清理掉。这就涉及到 Redis 的内存淘汰策略。如果你只是拿 Redis 当缓存可以配置 allkeys-lru但如果 Redis 里还存着排行榜这类“数据不容丢失”的业务就要小心了内存打满后 LRU 淘汰可能把你不想淘汰的数据也干掉。我一般建议把 Redis 分两类一类纯缓存可以允许淘汰一类存业务数据禁止淘汰noeviction靠容量监控和扩容解决内存问题。这个决策要提前做别等线上数据丢了再后悔。5.4 持久化对数据结构的影响Redis 的持久化分为 RDB 和 AOF对数据结构的影响容易被忽略。RDB 是定期把内存快照落盘AOF 是追加写命令日志。如果你的 Zset 很大比如千万级成员RDB 每次落盘时要遍历整个跳表完成序列化耗时可能会很长AOF 重写时也类似。所以排行榜这类大型有序集合在持久化配置不当时会出现写命令延迟。解决办法分两种。如果排行榜数据可以容忍丢失一段时间比如游戏周榜那可以只开 RDB且把 save 间隔调长一点减少快照频率。如果数据不能丢比如用户资产相关的排名那必须开 AOF同时设置合理的 rewritemin-size 和 auto-aof-rewrite-percentage控制重写的频率避免重写时 CPU 和磁盘 IO 冲高影响正常命令执行。这个方面没有放之四海皆准的配置但理解原理之后你至少能根据业务特点做出合理选择。写在最后的经验总结我个人在实际操作中的体会是Redis 数据结构学得好不好不取决于你背了多少命令而取决于你脑子里有没有“读写模式”这个概念。String 对应的是一次读写一个值Hash 对应的是一次读写一个对象的某些字段List 对应的是一次读写一批有序值Set 对应的是判断某个值在不在集合里Zset 对应的是按分数范围读取值。业务需求一旦被抽象成这些模式选型就水到渠成了。我曾经在维护一个老项目时吃过一次亏原开发把所有排行榜数据都存成了 String每次更新分数都先 GET 一个序列化的 JSON反序列化修改分数再序列化SET 回去。并发一高就出现覆盖写排名数据经常错乱。后来我把它改成 Zset用 ZINCRBY 一行命令解决了所有问题那段时间我真正理解了一个道理结构选对了代码量能少一个数量级线上故障也少一个数量级。最后再分享一个小技巧。如果你的排行榜需要批量更新分数别一个成员一个成员地发 ZINCRBY每个命令都有一次网络往返RTT批量场景下累计耗时很可观。可以把多个更新合并成一个 Lua 脚本在 Redis 服务端一次执行也可以在客户端用 pipeline 打包发送。我实测过100 个用户的分数更新用 pipeline 比逐个发送快了一个量级在高并发活动期间这个优化非常值得做。数据结构的掌握是基础但对命令的高效使用方式和常见坑的规避才是真正拉开差距的地方。

相关新闻

分布式事务设计实战:从最终一致性到七步落地法

分布式事务设计实战:从最终一致性到七步落地法

1. 为什么“分布式事务”不是加个注解就能解决的事“24.分布式事务设计”这个标题,乍看像教科书目录里一个平平无奇的章节编号,但凡在真实系统里踩过坑的人,看到它第一反应不是翻页,而是下意识摸了摸键盘右上角的Esc键——因为过去…

2026/10/15 5:41:36 阅读更多 →
Spring Boot 3迁移MyBatis-Plus:SqlSessionFactory缺失根因与修复

Spring Boot 3迁移MyBatis-Plus:SqlSessionFactory缺失根因与修复

前段时间把一个老服务从Spring Boot 2.7往3.2迁移,顺手把MyBatis也换成MyBatis-Plus,结果启动直接抛了一串异常,核心信息就是Error creating bean with name sqlSessionFactory。但更让人头疼的是,控制台给的提示并不统一——有时…

2026/10/16 10:02:21 阅读更多 →
基于Java和SpringBoot的旅游信息网站与景区门票管理系统设计与实现

基于Java和SpringBoot的旅游信息网站与景区门票管理系统设计与实现

毕业设计选这个题目,算是给自己选了一条比较稳的路。基于Java和SpringBoot的旅游信息网站兼旅游景区门票管理系统,这个题目最大的好处是业务边界清晰、技术栈主流、演示效果好,无论做课程设计还是毕业设计,都是一个容易出成果、也…

2026/10/15 5:40:35 阅读更多 →

最新新闻

贡献法与单调栈:O(n)求解所有子数组最小值之和

贡献法与单调栈:O(n)求解所有子数组最小值之和

如果你也在刷题社区追过《每日一题》这类栏目,应该知道最折磨人的地方在哪:每天放出一道看起来不算超纲的题,题解敲出来也就十几行,但你自己想破头就是想不到那个关键的转折点。四月底这期又出了一道这样的题,标题五个…

2026/10/16 12:03:05 阅读更多 →
图像锐化与边缘检测:从拉普拉斯算子到Canny的实战调参指南

图像锐化与边缘检测:从拉普拉斯算子到Canny的实战调参指南

1. 从“模糊”到“清晰”:图像锐化到底在解决什么问题很多人第一次接触数字图像处理,都是从“把一张模糊的照片变清楚”这个朴素需求开始的。我当年也一样,手里拿到一张拍虚了的文档照片,第一反应是找个“锐化”按钮按下去&#x…

2026/10/16 12:03:05 阅读更多 →
CKEditor 4 Emoji 下拉面板搜索框焦点轮廓(Outline)样式解析:从 Manual Test 到源码实现

CKEditor 4 Emoji 下拉面板搜索框焦点轮廓(Outline)样式解析:从 Manual Test 到源码实现

前端UI组件 【免费下载链接】ckeditor4 The best enterprise-grade WYSIWYG editor. Fully customizable with countless features and plugins. 项目地址: https://gitcode.com/gh_mirrors/ck/ckeditor4 点击查看 免费下载 导读 本文围绕 CKEditor 4 官方手动测试…

2026/10/16 12:03:05 阅读更多 →
Mac上安装Claude Code完整教程:从环境检查到常见报错

Mac上安装Claude Code完整教程:从环境检查到常见报错

如果你和我一样,每天都泡在终端里,那大概率已经听说了 Claude Code。简单说,它是一款跑在命令行里的 AI 编程助手,不用切网页、不用复制粘贴代码,直接在终端里让它读项目、改文件、写测试、解释报错。这篇图文教程记录…

2026/10/16 12:03:05 阅读更多 →
GPU容器化与AI训练平台实践:从Kubernetes调度到资源治理

GPU容器化与AI训练平台实践:从Kubernetes调度到资源治理

1. 从裸机乱局到容器化:我们为什么非要重建一套训练平台 我所在的团队负责为整个公司的算法部门提供 GPU 算力支撑。两年前的状态,说实话,和很多公司一样:几台 8 卡的 GPU 服务器散落在不同机柜,算法同学申请到一台机器…

2026/10/16 12:03:05 阅读更多 →
VMware精简磁盘vmdk只涨不缩?两种空间回收方案详解

VMware精简磁盘vmdk只涨不缩?两种空间回收方案详解

简介:面向VMware虚拟化运维人员与系统管理员,针对VMware精简磁盘(Thin)在VMFS5文件系统下无法自动回收已删除文件空间的问题,提供两种经过验证的手动回收方案。方案一借助微软SDelete工具在Windows虚拟机内擦除空闲扇区…

2026/10/16 12:02:04 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →