1. 百万级点赞系统的核心挑战
每天处理百万级点赞请求的系统,远不是简单地在数据库里做UPDATE table SET likes=likes+1这么简单。去年我们团队接手一个直播平台的点赞系统重构,峰值时期每秒要处理超过2万次点赞请求,这个过程中踩过的坑让我深刻理解了高并发场景下的技术博弈。
最核心的矛盾在于:用户需要实时看到点赞数变化(强一致性),而系统又必须承受突发流量(高可用性)。这就像在早高峰的地铁站,既要保证每个乘客都能快速通过闸机(低延迟),又要避免人群在入口处堆积(防崩溃)。传统方案往往顾此失彼——用缓存可能导致计数不准,直接写数据库又扛不住流量。
2. 系统架构设计演进
2.1 第一代架构:数据库直写模式
初期采用最朴素的方案:
sql复制START TRANSACTION;
SELECT likes FROM items WHERE item_id=123;
UPDATE items SET likes=likes+1 WHERE item_id=123;
COMMIT;
这个方案在测试环境表现良好,但上线后立即暴露三个致命问题:
- 热门直播间的点赞操作导致数据库CPU飙升至100%
- 事务锁竞争造成大量请求超时(TP99超过3秒)
- 主从同步延迟导致用户看到的数据不一致
关键教训:关系型数据库的ACID特性在高并发场景反而成为瓶颈,特别是X锁的排他性会导致大量线程阻塞。
2.2 第二代架构:缓存计数+异步落库
引入Redis作为缓冲层,典型代码如下:
python复制def like(item_id, user_id):
redis.incr(f"counter:{item_id}")
redis.sadd(f"users:{item_id}", user_id)
async_task(commit_to_db, item_id) # 异步任务
优化效果立竿见影:
- 吞吐量从200QPS提升到15000QPS
- 响应时间从秒级降到毫秒级
但新问题随之而来:
- 突发流量导致Redis内存暴涨(每个item需要存储点赞用户ID)
- 异步任务堆积时可能丢失数据
- 缓存宕机导致计数完全不可用
2.3 第三代架构:分层削峰设计
最终我们采用分层处理架构:
code复制客户端 → 接入层 → 消息队列 → 计数服务 → 存储层
↑ ↓
实时展示 ← 缓存集群
具体实现要点:
-
接入层:使用Nginx+Lua做请求校验和限流
lua复制local tokens = ngx.shared.rate_limit if tokens:get(item_id) == nil then tokens:set(item_id, 100, 60) -- 60秒100次 end if tokens:incr(item_id, 1) > 100 then ngx.exit(429) end -
消息队列:选用Kafka做削峰填谷
java复制Properties props = new Properties(); props.put("linger.ms", 50); // 批量发送延迟 props.put("batch.size", 16384); // 16KB批量大小 producer.send(new ProducerRecord<>("likes", itemId, userId)); -
计数服务:采用分片Redis集群
bash复制# Redis配置关键参数 hz 10 # 提高过期键处理频率 maxmemory-policy allkeys-lru
3. 关键技术实现细节
3.1 精准去重设计
防止用户重复点赞需要解决两个问题:
- 内存效率:100万用户点赞1个视频,用Set存储需要约60MB内存
- 判断速度:布隆过滤器有误判可能,影响用户体验
最终方案:
python复制def is_liked(item_id, user_id):
# 第一层:布隆过滤器(内存高效)
if not bloom_filter.contains(f"{item_id}:{user_id}"):
return False
# 第二层:RoaringBitmap(精确判断)
bitmap_key = f"bitmap:{item_id}"
return redis.execute("GETBIT", bitmap_key, user_hash)
通过将用户ID哈希到固定范围,1亿用户仅需12MB存储空间,查询性能稳定在0.1ms内。
3.2 分布式计数器实现
单纯使用INCR命令会导致热点Key问题。我们采用分片计数方案:
code复制原始Key:counter:123
分片Key:counter:123:shard1 ~ counter:123:shard16
聚合计算时使用管道加速:
redis复制PIPELINE
GET counter:123:shard1
GET counter:123:shard2
...
GET counter:123:shard16
EXEC
实测显示,16个分片可以将单个计数器的吞吐量提升8倍。
3.3 数据一致性保障
采用双写队列确保缓存与数据库最终一致:
- 所有写操作先进入Kafka
- 消费者并行处理:
- 线程A更新Redis
- 线程B写入MySQL
- 定时对账服务修复差异
关键补偿逻辑:
go复制func reconcile() {
dbCount := sql.Query("SELECT likes FROM items WHERE id=?", itemID)
cacheCount := redis.Get(f"counter:{itemID}")
if math.Abs(dbCount - cacheCount) > threshold {
redis.Set(f"counter:{itemID}", dbCount)
}
}
4. 性能优化实战记录
4.1 缓存预热策略
通过历史数据分析发现:
- 80%的点赞集中在20%的内容上
- 热门内容在发布后5分钟内就会迎来流量高峰
因此实现智能预热:
sql复制-- 定时任务查询潜在热点
SELECT item_id FROM contents
WHERE publish_time > NOW() - INTERVAL 5 MINUTE
ORDER BY preview_count DESC
LIMIT 100;
4.2 JVM参数调优
计数服务GC优化配置:
bash复制-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:InitiatingHeapOccupancyPercent=45
-XX:ParallelGCThreads=8
调整后GC时间从平均500ms降至80ms,服务抖动显著减少。
4.3 网络传输优化
使用Protocol Buffers替代JSON:
protobuf复制message LikeEvent {
fixed64 item_id = 1;
fixed64 user_id = 2;
fixed32 timestamp = 3;
}
数据体积减少60%,Kafka带宽占用从1Gbps降至400Mbps。
5. 容灾与降级方案
5.1 多级熔断策略
在三个层面实施熔断:
- 接入层:单IP频率限制
- 服务层:基于错误率的熔断
java复制CircuitBreaker breaker = new CircuitBreaker() .withFailureThreshold(50, 1.0) // 1秒50次失败 .withSuccessThreshold(10); - 存储层:Redis只读模式降级
5.2 热点数据自动检测
实时监控TopN热点Key:
python复制def monitor_hotkeys():
while True:
# 采样统计
hotkeys = redis.execute("HOTKEYS", count=100)
for key in hotkeys:
migrate_to_special_cluster(key)
5.3 演练与复盘
每月进行全链路压测,模拟以下场景:
- Redis主节点宕机
- Kafka分区不可用
- 数据库主从切换
- 机房网络中断
每次演练后产出改进项,例如发现ZooKeeper会话超时设置不合理导致服务注册中心不可用。
6. 监控体系搭建
6.1 关键指标埋点
核心监控指标包括:
| 指标名称 | 采集频率 | 报警阈值 |
|---|---|---|
| 点赞QPS | 10s | >20000持续1分钟 |
| 写入延迟P99 | 1分钟 | >500ms |
| 缓存命中率 | 5分钟 | <90% |
| 消息积压量 | 30秒 | >10000 |
6.2 全链路追踪实现
基于OpenTelemetry的追踪方案:
go复制ctx, span := tracer.Start(ctx, "like_operation")
defer span.End()
span.SetAttributes(
attribute.String("item.id", itemID),
attribute.Int("user.id", userID),
)
6.3 日志规范化
采用结构化日志格式:
json复制{
"timestamp": "2023-07-20T14:32:45Z",
"trace_id": "abc123",
"level": "INFO",
"message": "like processed",
"item_id": 12345,
"user_id": 67890,
"latency_ms": 12
}
这套系统最终实现的效果:
- 峰值处理能力:32,000 QPS
- 端到端延迟:<100ms(P99)
- 数据一致性误差:<0.001%
- 服务器成本降低60%(相比初期方案)
在技术选型上走过的弯路让我明白:高并发系统没有银弹,必须根据业务特点在CAP理论中寻找平衡点。比如对于点赞这种场景,我们最终选择了最终一致性+高可用的方案,放弃了强一致性,这个决策使得系统能够承受春晚级别的流量冲击。