每年最佳东方高峰论坛是平台的大活动,活动当天流量是平时的 5-10 倍,活动页、报名页、直播页都要扛住高并发。2022年高峰论坛活动系统做了专门的架构设计,今天把高并发下的缓存与降级策略分享出来。
一、流量预估与挑战#
- 活动当天 UV 50万+,PV 300万+
- 峰值 QPS 5000+(活动开始前 10 分钟和直播开始时)
- 核心页面:活动首页、报名页、议程页、直播页
- 挑战:瞬时流量大、报名操作有写操作、直播页有实时数据
二、多级缓存策略#
2.1 页面静态化#
活动首页、议程页等内容不常变的页面,提前生成静态 HTML,放 CDN:
1
2
3
4
5
6
| // 生成静态页面
$html = $this->renderActivityPage($activityId);
file_put_contents("/data/static/activity_{$activityId}.html", $html);
// 上传到 OSS + CDN
$this->oss->upload("static/activity_{$activityId}.html", $html);
|
用户访问 CDN 静态页,完全不经过源站,QPS 再高也不怕。
2.2 Redis 缓存#
需要动态数据的页面(如报名人数、实时观看人数),用 Redis 缓存:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| public function getActivityInfo($activityId)
{
$cacheKey = "activity:info:{$activityId}";
$cached = Yii::$app->redis->get($cacheKey);
if ($cached !== false) {
return json_decode($cached, true);
}
// 缓存击穿保护:分布式锁
$lockKey = "activity:lock:{$activityId}";
if (!Yii::$app->redis->set($lockKey, 1, 'NX', 'EX', 5)) {
usleep(100000); // 等100ms重试
return $this->getActivityInfo($activityId);
}
try {
$info = $this->queryFromDb($activityId);
Yii::$app->redis->setex($cacheKey, 300, json_encode($info)); // 5分钟
return $info;
} finally {
Yii::$app->redis->del($lockKey);
}
}
|
2.3 本地缓存#
热点数据用 APCu 本地缓存,减少 Redis 访问:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| public function getHotActivity()
{
$local = apcu_fetch('activity:hot');
if ($local !== false) return $local;
$redis = Yii::$app->redis->get('activity:hot');
if ($redis !== false) {
$data = json_decode($redis, true);
apcu_store('activity:hot', $data, 60); // 本地缓存1分钟
return $data;
}
$data = $this->queryFromDb();
Yii::$app->redis->setex('activity:hot', 300, json_encode($data));
apcu_store('activity:hot', $data, 60);
return $data;
}
|
三级缓存:CDN/静态页 → 本地缓存 → Redis → 数据库,越靠后访问越少。
三、限流与降级#
3.1 接口限流#
用 Redis 令牌桶限流:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public function checkRateLimit($key, $limit, $window = 60)
{
$redis = Yii::$app->redis;
$current = $redis->incr("rate:{$key}");
if ($current == 1) {
$redis->expire("rate:{$key}", $window);
}
if ($current > $limit) {
throw new TooManyRequestsException('请求过于频繁,请稍后再试');
}
return true;
}
// 报名接口限流:每分钟每用户最多5次
$this->checkRateLimit("signup:{$userId}", 5);
|
3.2 服务降级#
活动高峰期,非核心功能降级:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| public function actionActivityIndex($activityId)
{
$data = [];
// 核心功能:活动信息
$data['info'] = $this->getActivityInfo($activityId);
// 非核心:推荐活动(失败不影响主流程)
try {
$data['recommend'] = $this->getRecommendActivities();
} catch (Exception $e) {
$data['recommend'] = []; // 降级返回空
}
// 非核心:实时聊天(高峰期关闭)
if (!$this->isHighTraffic()) {
$data['chat'] = $this->getRecentChat($activityId);
} else {
$data['chat'] = ['status' => 'closed', 'msg' => '高峰期聊天功能暂不可用'];
}
return $data;
}
|
降级策略:
- 推荐活动:失败返回空
- 实时聊天:高峰期直接关闭
- 报名统计:显示近似值(缓存数据),不实时查
- 个人中心:高峰期隐藏非必要模块
3.3 报名操作优化#
报名是写操作,不能纯缓存。用异步队列削峰:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| public function actionSignup($activityId)
{
$userId = Yii::$app->user->id;
// 幂等检查
if (Yii::$app->redis->sismember("signup:{$activityId}", $userId)) {
return ['code' => 0, 'message' => '已报名'];
}
// 异步报名
Yii::$app->queue->push(new SignupJob([
'activity_id' => $activityId,
'user_id' => $userId,
]));
// 立即返回,后台异步处理
return ['code' => 0, 'message' => '报名成功,正在处理中'];
}
|
报名请求立即返回,后台异步处理写数据库。用户体验好,数据库压力小。
四、压测与预案#
4.1 压测#
活动前用 JMeter 压测,模拟峰值流量:
1
2
| # 压测活动首页,5000并发
jmeter -n -t activity_test.jmx -Jthreads=5000 -Jduration=300
|
压测发现的问题:
- 数据库连接池不够 → 调大连接池
- Redis 连接数不够 → 调大 maxclients
- 某个接口没缓存 → 加缓存
4.2 应急预案#
- CDN 故障:切换到备用 CDN 或源站
- Redis 故障:降级到数据库(限流保护)
- 数据库故障:只读模式,暂停报名等写操作
- 服务器扩容:提前准备好扩容脚本,5分钟内能加机器
五、效果#
活动当天数据:
- 峰值 QPS 6200,系统稳定运行
- 活动首页响应时间 < 50ms(CDN 静态页)
- 报名接口响应时间 < 100ms(异步队列)
- 数据库 QPS 峰值 800(平时 200,缓存扛了大部分)
- 0 宕机,0 数据丢失
六、总结#
高并发活动系统的核心经验:
- 能静态就静态:活动页提前生成静态 HTML 放 CDN,源站压力最小
- 多级缓存:CDN → 本地缓存 → Redis → 数据库,层层拦截
- 异步削峰:写操作用消息队列异步处理,请求立即返回
- 限流保护:核心接口限流,防止被打垮
- 降级预案:非核心功能高峰期降级,保证核心功能可用
- 压测验证:活动前充分压测,发现问题提前修复
- 应急预案:各种故障场景都有预案,出问题能快速恢复
高并发系统的设计思路是"能不查数据库就不查,能异步就异步,能降级就降级"。核心功能保证可用,非核心功能该砍就砍。活动系统不是日常系统,不需要所有功能都可用,高峰期保住核心功能就行。