礼物打赏是很多社交App的核心变现方式。但一场直播中,几万人同时送礼物,怎么保证系统不崩?怎么保证礼物不丢?怎么保证数据准确?这篇文章拆解高并发场景下礼物系统的技术架构。
一、礼物系统的业务模型
1.1 核心步骤
用户A送礼物给主播B:
1. 扣减A的余额
2. 生成礼物记录
3. 增加B的收入
4. 全屏特效播放
5. 排行榜更新
1.2 关键业务规则
| 规则 | 说明 |
|---|---|
| 幸运礼物 | 随机倍数返还,有概率触发大奖 |
| 连击礼物 | 连续送N个礼物,触发连击特效 |
| 全服广播 | 昂贵礼物全服通知 |
| 排行榜 | 实时更新贡献榜、收到礼物榜 |
| 分成结算 | 平台抽成、主播分成 |
1.3 技术挑战
| 挑战 | 说明 |
|---|---|
| 高并发 | 热门主播,几万人同时送礼物 |
| 数据一致性 | 余额扣减、礼物记录、主播收入必须一致 |
| 实时性 | 礼物特效、排行榜要实时更新 |
| 准确性 | 每一笔礼物金额必须准确无误 |
二、架构设计
2.1 整体架构
2.2 服务拆分
| 服务 | 职责 | 技术要点 |
|---|---|---|
| 礼物服务 | 礼物发送、礼物查询 | 核心业务逻辑 |
| 账户服务 | 余额管理、扣款、充值 | 事务一致性 |
| 排行榜服务 | 实时排行、贡献榜 | Redis Sorted Set |
| 通知服务 | 全服广播、特效触发 | 消息推送 |
三、高并发设计
3.1 异步化处理
问题: 同步处理礼物请求,数据库压力大,响应慢。
解决方案: 核心流程同步,非核心流程异步。
同步流程(必须立即完成):
1. 扣减余额(Redis原子操作)
2. 生成礼物流水(返回成功)
3. 播放特效(客户端本地播放)
异步流程(可延迟处理):
1. 主播收入增加
2. 排行榜更新
3. 数据库持久化
4. 分成结算
3.2 消息队列削峰
队列设计:
礼物消息队列:
1. gift_queue:high # 高价值礼物,优先处理
2. gift_queue:normal # 普通礼物
3. gift_queue:low # 低价值礼物,批量处理
3.3 批量处理
连击礼物合并:
用户连续送100个礼物,不发送100次请求,合并为1次请求 。
{
gift_id: 1001,
count: 100,
total_amount: 100 * 单价
}
四、数据一致性设计
4.1 余额扣减
问题: 并发扣款时,可能扣成负数。
解决方案:Redis原子操作 + Lua脚本
-- 扣款Lua脚本
local balance = redis.call('GET', KEYS[1])
if not balance then
return -1 -- 用户不存在
end
local amount = tonumber(ARGV[1])
if tonumber(balance) < amount then
return -2 -- 余额不足
end
redis.call('DECRBY', KEYS[1], amount)
return 1 -- 成功
4.2 分布式事务
问题: 扣款成功、礼物记录成功、主播收入增加失败,数据不一致。
解决方案:本地消息表 + 最终一致性
1. 扣减用户余额(Redis + MySQL)
2. 写入礼物记录
3. 写入本地消息表(待处理状态)
4. 返回成功
异步任务:
5. 读取本地消息表
6. 增加主播收入
7. 更新消息表状态为已完成
消息表设计:
CREATE TABLE gift_message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
gift_id BIGINT NOT NULL,
from_user_id BIGINT NOT NULL,
to_user_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
status TINYINT DEFAULT 0 COMMENT '0-待处理 1-已完成 2-失败',
retry_count INT DEFAULT 0,
created_at DATETIME,
INDEX idx_status (status)
);
4.3 幂等性保证
问题: 网络重试导致礼物重复发送。
解决方案: 礼物请求唯一ID
# 礼物去重
gift_dedup:{user_id}:{request_id}
# 存在则返回已有结果
# 不存在则处理请求
五、排行榜设计
5.1 Redis Sorted Set
# 主播收礼物排行榜
rank:gift:daily:{date} # 日榜
rank:gift:weekly:{week} # 周榜
rank:gift:monthly:{month} # 月榜
rank:gift:total # 总榜
# 用户贡献榜
rank:contribution:{anchor_id} # 某主播的贡献榜
5.2 更新操作
# 增加贡献值
ZINCRBY rank:gift:daily:20240101 100 "anchor_123"
# 获取Top 100
ZREVRANGE rank:gift:daily:20240101 0 99 WITHSCORES
# 获取用户排名
ZREVRANK rank:gift:daily:20240101 "anchor_123"
5.3 排行榜优化
| 优化点 | 方案 |
|---|---|
| 冷数据清理 | 定时任务清理过期的排行榜数据 |
| 分页查询 | 只查询Top N,不查询全量 |
| 缓存热点 | 排行榜结果缓存,减少查询 |
| 异步更新 | 礼物发送后异步更新排行榜 |
六、幸运礼物设计
6.1 业务逻辑
用户送幸运礼物 :
- 扣减用户余额
- 随机计算返还倍数(1x-100x)
- 增加用户余额(返还部分)
- 播放特效
- 全服通知(如果大奖)
6.2 随机算法
def calculate_lucky_multiplier(gift_amount):
"""
根据礼物金额计算幸运倍数
返还金额 = 礼物金额 × 倍数
"""
random_value = random.random()
if random_value < 0.0001: # 0.01% 概率 100x
return 100
elif random_value < 0.001: # 0.1% 概率 50x
return 50
elif random_value < 0.01: # 1% 概率 10x
return 10
elif random_value < 0.1: # 10% 概率 2x
return 2
else: # 90% 概率 0x
return 0
6.3 公平性保证
问题: 如何保证随机结果公平,不能被操控?
解决方案:
· 服务端计算,客户端只展示结果
· 随机种子基于礼物ID + 时间戳,不可预测
· 记录所有随机结果,可审计
七、监控与告警
7.1 关键指标
| 指标 | 说明 | 告警阈值 |
|---|---|---|
| 礼物发送QPS | 每秒礼物请求数 | >10000 |
| 礼物成功率 | 成功/总数 | <99% |
| 扣款失败率 | 余额不足等 | >5% |
| 消息队列积压 | 待处理消息数 | >10000 |
| 排行榜更新延迟 | 更新耗时 | >1s |
7.2 数据对账
定期对账任务:
每小时执行:
- 统计礼物总金额
- 统计用户扣款总额
- 统计主播收入总额
- 校验三者是否一致,不一致 → 告警 + 人工处理
八、容灾设计
8.1 服务降级
| 场景 | 降级策略 |
|---|---|
| 消息队列积压 | 礼物记录先写Redis,异步同步 |
| 排行榜服务故障 | 排行榜暂停更新,显示缓存数据 |
| 数据库压力大 | 批量写入,减少单条写入 |
8.2 故障恢复
服务恢复后:
- 处理消息队列中的积压消息
- 同步Redis数据到MySQL
- 重建排行榜
- 执行数据对账
九、架构演进路径
阶段一:简单架构 单服务 + 同步处理 + 支持1000 QPS
阶段二:异步架构 服务拆分 + 消息队列 + 支持10000 QPS
阶段三:高并发架构 Redis原子操作 + 分库分表 + 多机房部署 + 支持50000+ QPS
下篇预告: 《社交推荐系统:从协同过滤到实时推荐》——让用户发现更多有趣的人和内容。
持续输出社交App开发实战经验,关注我,一起成长。