乐于分享
好东西不私藏

交友 App 核心架构揭秘:几千万活跃用户是如何实现毫秒级随机匹配的?

交友 App 核心架构揭秘:几千万活跃用户是如何实现毫秒级随机匹配的?

关注我们,设为星标,每天7:30不见不散,每日java干货分享

案发场景:
你在开发一款类似 Soul 的社交 App,或者一个全网发放 1 亿个唯一验证码的系统。
你需要把这 1 亿个 ID 放进奖池里,供全网用户随时“随机抽取”一个进行匹配。
初级架构的毁灭:
你把 1 亿个 ID 全塞进了一个叫 pool:all_users 的 Set 里。
这个 Key 的体积膨胀到了 8GB
某天运维执行了一次数据清理 DEL pool:all_users。Redis 为了释放这 8GB 的连续内存,单线程直接卡死 15 秒。期间所有的登录、发消息请求全部超时,App 瞬间炸服。
极客的破局之道:
化整为零。把 1 个 8GB 的大水缸,砸碎成 1024 个 8MB 的小水桶。
抽奖时,先随机挑一个桶,再从桶里随机挑一个球。
这就是完美规避大 Key、并能无限水平扩展的分片打散与两次随机


1. 核心思想一:化整为零的 Hash 打散 (Sharding)

无论你是 1 亿还是 10 亿数据,绝不能放在同一个 Key 里。我们需要约定一个分片数 (Bucket Size),比如 1024。

写入规则:
当你要把用户 U10086 放入奖池时:

  1. 1. 计算 U10086 的 Hash 值(比如使用 CRC16 或简单的 hashCode())。
  2. 2. 将 Hash 值对 1024 取模:Hash("U10086") % 1024 = 42
  3. 3. 把这个用户存入对应的分片 Key 中:SADD pool:bucket:42 U10086

绝对收益:
原本 1 个包含 1 亿元素的超大 Set,变成了 1024 个只包含 10 万元素的安全小 Set。不仅彻底消灭了大 Key,如果配合 Redis Cluster 集群,这 1024 个 Key 还会被打散到不同的物理机器上,并发吞吐量直接翻了 N 倍!


2. 核心思想二:绝对公平的“两次随机” (Two-Step Random)

数据打散存进去了,那怎么“随机抽一个”出来呢?你总不能遍历 1024 个桶吧?

抽奖规则(两次随机法):

  1. 1. 第一步(随机选桶): 在应用层(Java 代码里),从 0 到 1023 中随机生成一个整数。比如生成了 88
  2. 2. 第二步(随机选球): 向 Redis 发起命令,从第 88 号桶里随机抽一个元素:SPOP pool:bucket:88 1

灵魂拷问:这样做真的公平吗?
只要你的 Hash 算法足够散列,每个桶里的元素数量是大致相等的(比如都在 9.5 万 ~ 10.5 万之间)。
先等概率(1/1024)选中桶,再等概率选中桶里的球。在概率学上,这无限趋近于从 1 亿个球里等概率盲抽!


3. 三大亿级高频实战场景

场景一:Soul 风格的“灵魂匹配 / 漂流瓶”

  • • 痛点: 几千万活跃用户同时在线,要求点击“匹配”后,毫秒级捞出一个性别匹配且不重复的陌生人。
  • • 实战: 按性别划分大池,按 Hash 分片为 match:female:bucket:{0~1023}。用户点击匹配,App 随机定位一个分片,执行 SRANDMEMBER 捞出 5 个候选人,在内存里过滤掉自己屏蔽过的人,最后呈现 1 个。

场景二:亿级独立优惠券码/激活码预生成分发

  • • 痛点: 运营提前生成了 1 亿个 16 位的随机充值卡密。用户兑换时必须随机给一个,且不能给重复的。
  • • 实战: 把 1 亿个卡密通过 Hash 路由,灌入 1024 个 coupon:bucket:{id} 分片中。用户兑换时,两次随机配合 SPOP 拿走卡密。即便每秒有 10 万人同时抢,Redis Cluster 的多节点也能轻松抗下这被打散的流量。

场景三:全网大促“集五福”卡片掉落

  • • 实战: “敬业福”非常稀缺。可以用带权重的分片思想。如果是抽普通福卡,路由到 1000 个分片;如果是“敬业福”的发放槽位,只设置 1 个极小容量的分片。通过控制“第一步随机选桶”的逻辑(比如 99% 的概率随机 0-999 号桶,1% 的概率指向敬业福桶),在极简架构下实现精确的概率掉落。

4. 代码落地:Spring Boot 实战演示

下面是一段生产级别的防坑代码。必须考虑到一个边缘场景:如果随机选中的那个桶刚好被抽空了怎么办?

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.ThreadLocalRandom;

@Service
publicclassShardedLotteryService {

privatefinal StringRedisTemplate redisTemplate;
// 约定分片数量为 1024
privatestaticfinalintBUCKET_COUNT=1024;
privatestaticfinalStringPOOL_PREFIX="lottery:bucket:";

publicShardedLotteryService(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
    }

/**
     * 将 1 亿个元素打散存入分片
     */

publicvoidaddElementToShards(String elementId) {
// 1. 计算 Hash 值并取模定位桶
intbucketId= Math.abs(elementId.hashCode()) % BUCKET_COUNT;
StringbucketKey= POOL_PREFIX + bucketId;

// 2. 存入对应的 Redis Set 分片中
        redisTemplate.opsForSet().add(bucketKey, elementId);
    }

/**
     * 两次随机抽取核心逻辑 (SPOP 连拿带跑)
     */

public String drawPrizeWithTwoStepRandom() {
intretryCount=0;
// 设置最大重试次数,防止所有桶都空了导致死循环
intmaxRetries=10;

while (retryCount < maxRetries) {
// 第一步:在本地内存中随机选桶 (O(1) 且极快)
intrandomBucketId= ThreadLocalRandom.current().nextInt(BUCKET_COUNT);
StringbucketKey= POOL_PREFIX + randomBucketId;

// 第二步:从选中的 Redis 桶中随机弹出一个元素
Stringprize= redisTemplate.opsForSet().pop(bucketKey);

if (prize != null) {
return prize; // 抽中了,完美返回
            }

// 如果返回 null,说明这个桶已经被抽空了。
// retryCount++ 进入下一轮循环,重新随机一个新桶。
            retryCount++;
        }

// 超过重试次数依然没抽到,说明奖池大概率已经被彻底抽干了
return"PRIZE_POOL_EMPTY"
    }
}

5. 避坑指南:数据倾斜的克星

“两次随机”看似完美,但在极端情况下有一个“不绝对公平”的物理硬伤:数据倾斜 (Data Skew)

假设你的哈希算法不够散列,或者大奖已经被抽走了一半。

  • • 桶 0 里还有 100 个元素。
  • • 桶 1 里只剩下 1 个元素。
    如果此时你执行“第一步”,桶 0 和桶 1 被选中的概率依然各是 50%。
    但对于元素来说,桶 1 里的那个唯一元素,被抽中的概率高达 50%;而桶 0 里的元素,被抽中的概率只有 50% * (1/100) = 0.5%

实战解法:
对于绝大多数互联网抽奖、盲盒业务,这种程度的概率漂移是完全可以接受的(毕竟抽奖图的就是一个随机)。
但如果你的业务是极其严谨的算法派单,你必须在本地缓存维护一个 Bucket_Size_Array(记录每个桶剩余的实际数量),把第一步的“等概率随机选桶”升级为**“按桶剩余容量加权随机选桶”**。


总结

从单 Set 裸奔,到**“分片存储 + 两次随机”**,是从野生程序员走向资深架构师的必经之路。

它不仅彻底斩断了压垮 Redis 的“大 Key”梦魇,更利用了分布式的分而治之思想,把吞吐量提升了数个数量级。掌握了这个套路,无论是面对春晚级别的抽奖并发,还是千万级别的交友匹配,你都能稳坐钓鱼台。

推荐阅读点击标题可跳转

50个Java代码示例:全面掌握Lambda表达式与Stream API

16 个 Java 代码“痛点”大改造:“一般写法” VS “高级写法”终极对决,看完代码质量飙升!

为什么高级 Java 开发工程师喜爱用策略模式

精选Java代码片段:覆盖10个常见编程场景的更优写法

提升Java代码可靠性:5个异常处理最佳实践

为什么大佬的代码中几乎看不到 if-else,因为他们都用这个…

还在 Service 里疯狂注入其他 Service?你早就该用 Spring 的事件机制了

看完本文有收获?请转发分享给更多人

关注「java干货」加星标,提升java技能

❤️给个「推荐 」,是最大的支持❤️

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

.cls-1{fill:#001e36;}.cls-2{fill:#31a8ff;}

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 交友 App 核心架构揭秘:几千万活跃用户是如何实现毫秒级随机匹配的?

评论 抢沙发

1 + 5 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮